diff --git a/lib/draper.rb b/lib/draper.rb index 74fd196..ecff734 100644 --- a/lib/draper.rb +++ b/lib/draper.rb @@ -4,6 +4,7 @@ require 'draper/version' require 'draper/system' require 'draper/active_model_support' require 'draper/view_helpers' +require 'draper/finders' require 'draper/decorator' require 'draper/helper_proxy' require 'draper/lazy_helpers' diff --git a/lib/draper/decorator.rb b/lib/draper/decorator.rb index a55ce12..962eba0 100755 --- a/lib/draper/decorator.rb +++ b/lib/draper/decorator.rb @@ -1,4 +1,3 @@ -require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/array/extract_options' module Draper @@ -6,7 +5,6 @@ module Draper include ActiveModelSupport include Draper::ViewHelpers - class_attribute :model_class attr_accessor :model, :options # Initialize a new decorator instance by passing in @@ -24,39 +22,23 @@ module Draper # @param [Hash] options (optional) def initialize(input, options = {}) input.to_a if input.respond_to?(:to_a) # forces evaluation of a lazy query from AR - self.class.model_class = input.class if model_class.nil? self.model = input self.options = options handle_multiple_decoration if input.is_a?(Draper::Decorator) end - # Proxies to the class specified by `decorates` to automatically - # lookup an object in the database and decorate it. + # Adds ActiveRecord finder methods to the decorator class. The + # methods return decorated models, so that you can use + # `ProductDecorator.find(id)` instead of + # `ProductDecorator.decorate(Product.find(id))`. # - # @param [Symbol or String] id id to lookup - # @param [Hash] options additional options to the find method of the model - # @return [Object] instance of this decorator class - def self.find(id, options = {}) - self.new(model_class.find(id), options) - end - - # Typically called within a decorator definition, this method - # specifies the name of the wrapped object class. + # If the `:for` option is not supplied, the model class will be + # inferred from the decorator class. # - # For instance, a `ProductDecorator` class might call `decorates :product` - # - # But they don't have to match in name, so a `EmployeeDecorator` - # class could call `decorates :person` to wrap instances of `Person` - # - # This is primarilly set so the `.find` method knows which class - # to query. - # - # @param [Symbol] class_name snakecase name of the decorated class, like `:product` - def self.decorates(class_name, options = {}) - self.model_class = options[:class] || options[:class_name] || class_name.to_s.camelize - self.model_class = model_class.constantize if model_class.respond_to?(:constantize) - model_class.send :include, Draper::Decoratable - define_method(class_name) { @model } + # @option options [Class, Symbol] :for The model class to find + def self.add_finders(options = {}) + extend Draper::Finders + self.finder_class = options[:for] || name.chomp("Decorator") end # Typically called within a decorator definition, this method causes @@ -156,22 +138,6 @@ module Draper end end - # Fetch all instances of the decorated class and decorate them. - # - # @param [Hash] options (optional) - # @return [Draper::CollectionDecorator] - def self.all(options = {}) - Draper::CollectionDecorator.new(model_class.all, self, options) - end - - def self.first(options = {}) - decorate(model_class.first, options) - end - - def self.last(options = {}) - decorate(model_class.last, options) - end - # Get the chain of decorators applied to the object. # # @return [Array] list of decorator classes @@ -238,18 +204,6 @@ module Draper raise no_method_error end - def self.method_missing(method, *args, &block) - if method.to_s.match(/^find_((all_|last_)?by_|or_(initialize|create)_by_).*/) - self.decorate(model_class.send(method, *args, &block), :context => args.dup.extract_options!) - else - model_class.send(method, *args, &block) - end - end - - def self.respond_to?(method, include_private = false) - super || model_class.respond_to?(method) - end - def context options.fetch(:context, {}) end diff --git a/lib/draper/finders.rb b/lib/draper/finders.rb new file mode 100644 index 0000000..d364ac5 --- /dev/null +++ b/lib/draper/finders.rb @@ -0,0 +1,38 @@ +module Draper + module Finders + + attr_reader :finder_class + def finder_class=(klass) + @finder_class = klass.to_s.camelize.constantize + end + + def find(id, options = {}) + decorate(finder_class.find(id), options) + end + + def all(options = {}) + decorate(finder_class.all, options) + end + + def first(options = {}) + decorate(finder_class.first, options) + end + + def last(options = {}) + decorate(finder_class.last, options) + end + + def method_missing(method, *args, &block) + if method.to_s.match(/^find_((all_|last_)?by_|or_(initialize|create)_by_).*/) + decorate(finder_class.send(method, *args, &block), context: args.dup.extract_options!) + else + finder_class.send(method, *args, &block) + end + end + + def respond_to?(method, include_private = false) + super || finder_class.respond_to?(method) + end + + end +end diff --git a/spec/draper/decorator_spec.rb b/spec/draper/decorator_spec.rb index e189f91..d92d161 100755 --- a/spec/draper/decorator_spec.rb +++ b/spec/draper/decorator_spec.rb @@ -7,10 +7,6 @@ describe Draper::Decorator do let(:non_active_model_source){ NonActiveModelProduct.new } describe "#initialize" do - it "sets the model class for the decorator" do - ProductDecorator.new(source).model_class.should == Product - end - it "does not re-apply on instances of itself" do product_decorator = ProductDecorator.new(source) ProductDecorator.new(product_decorator).model.should be_instance_of Product @@ -168,20 +164,6 @@ describe Draper::Decorator do end end - describe "proxying class methods" do - it "pass missing class method calls on to the wrapped class" do - subject.class.sample_class_method.should == "sample class method" - end - - it "respond_to a wrapped class method" do - subject.class.should respond_to(:sample_class_method) - end - - it "still respond_to its own class methods" do - subject.class.should respond_to(:own_class_method) - end - end - describe "#helpers" do it "returns a HelperProxy" do subject.helpers.should be_a Draper::HelperProxy @@ -221,75 +203,6 @@ describe Draper::Decorator do end end - context(".decorates") do - it "handle plural-like words properly'" do - class Business; end - expect do - class BusinessDecorator < Draper::Decorator - decorates:business - end - BusinessDecorator.model_class.should == Business - end.to_not raise_error - end - - context("accepts ActiveRecord like :class_name option too") do - it "accepts constants for :class" do - expect do - class CustomDecorator < Draper::Decorator - decorates :product, :class => Product - end - CustomDecorator.model_class.should == Product - end.to_not raise_error - end - - it "accepts constants for :class_name" do - expect do - class CustomDecorator < Draper::Decorator - decorates :product, :class_name => Product - end - CustomDecorator.model_class.should == Product - end.to_not raise_error - end - - it "accepts strings for :class" do - expect do - class CustomDecorator < Draper::Decorator - decorates :product, :class => 'Product' - end - CustomDecorator.model_class.should == Product - end.to_not raise_error - end - - it "accepts strings for :class_name" do - expect do - class CustomDecorator < Draper::Decorator - decorates :product, :class_name => 'Product' - end - CustomDecorator.model_class.should == Product - end.to_not raise_error - end - end - - it "creates a named accessor for the wrapped model" do - pd = ProductDecorator.new(source) - pd.send(:product).should == source - end - - context("namespaced model supporting") do - let(:source){ Namespace::Product.new } - - it "sets the model class for the decorator" do - decorator = Namespace::ProductDecorator.new(source) - decorator.model_class.should == Namespace::Product - end - - it "creates a named accessor for the wrapped model" do - pd = Namespace::ProductDecorator.new(source) - pd.send(:product).should == source - end - end - end - describe ".decorates_association" do context "for ActiveModel collection associations" do before { subject.class.decorates_association :similar_products } @@ -515,72 +428,6 @@ describe Draper::Decorator do subject.block{"marker"}.should == "marker" end - context ".find" do - it "lookup the associated model when passed an integer" do - pd = ProductDecorator.find(1) - pd.should be_instance_of(ProductDecorator) - pd.model.should be_instance_of(Product) - end - - it "lookup the associated model when passed a string" do - pd = ProductDecorator.find("1") - pd.should be_instance_of(ProductDecorator) - pd.model.should be_instance_of(Product) - end - - it "accept and store a context" do - pd = ProductDecorator.find(1, :context => :admin) - pd.context.should == :admin - end - end - - context ".find_by_(x)" do - it "runs the similarly named finder" do - Product.should_receive(:find_by_name) - ProductDecorator.find_by_name("apples") - end - - it "returns a decorated result" do - ProductDecorator.find_by_name("apples").should be_kind_of(ProductDecorator) - end - - it "runs complex finders" do - Product.should_receive(:find_by_name_and_size) - ProductDecorator.find_by_name_and_size("apples", "large") - end - - it "runs find_all_by_(x) finders" do - Product.should_receive(:find_all_by_name_and_size) - ProductDecorator.find_all_by_name_and_size("apples", "large") - end - - it "runs find_last_by_(x) finders" do - Product.should_receive(:find_last_by_name_and_size) - ProductDecorator.find_last_by_name_and_size("apples", "large") - end - - it "runs find_or_initialize_by_(x) finders" do - Product.should_receive(:find_or_initialize_by_name_and_size) - ProductDecorator.find_or_initialize_by_name_and_size("apples", "large") - end - - it "runs find_or_create_by_(x) finders" do - Product.should_receive(:find_or_create_by_name_and_size) - ProductDecorator.find_or_create_by_name_and_size("apples", "large") - end - - it "accepts an options hash" do - Product.should_receive(:find_by_name_and_size).with("apples", "large", {:role => :admin}) - ProductDecorator.find_by_name_and_size("apples", "large", {:role => :admin}) - end - - it "uses the options hash in the decorator instantiation" do - Product.should_receive(:find_by_name_and_size).with("apples", "large", {:role => :admin}) - pd = ProductDecorator.find_by_name_and_size("apples", "large", {:role => :admin}) - pd.context[:role].should == :admin - end - end - describe "#==" do it "compares the decorated models" do other = Draper::Decorator.new(source) @@ -599,24 +446,6 @@ describe Draper::Decorator do end end - context 'position accessors' do - [:first, :last].each do |method| - context "##{method}" do - it "return a decorated instance" do - ProductDecorator.send(method).should be_instance_of ProductDecorator - end - - it "return the #{method} instance of the wrapped class" do - ProductDecorator.send(method).model.should == Product.send(method) - end - - it "accept an optional context" do - ProductDecorator.send(method, :context => :admin).context.should == :admin - end - end - end - end - describe "method security" do subject(:decorator_class) { Draper::Decorator } let(:security) { stub } @@ -720,4 +549,44 @@ describe Draper::Decorator do subject.kind_of?(subject.class).should be_true subject.is_a?(subject.class).should be_true end + + describe ".add_finders" do + it "extends the Finders module" do + ProductDecorator.should be_a_kind_of Draper::Finders + end + + context "with no options" do + it "infers the finder class" do + ProductDecorator.finder_class.should be Product + end + + context "for a namespaced model" do + it "infers the finder class" do + Namespace::ProductDecorator.finder_class.should be Namespace::Product + end + end + end + + context "with for: symbol" do + it "sets the finder class" do + FinderDecorator.add_finders for: :product + FinderDecorator.finder_class.should be Product + end + end + + context "with for: string" do + it "sets the finder class" do + FinderDecorator.add_finders for: "some_thing" + FinderDecorator.finder_class.should be SomeThing + end + end + + context "with for: class" do + it "sets the finder_class" do + FinderDecorator.add_finders for: Namespace::Product + FinderDecorator.finder_class.should be Namespace::Product + end + end + end + end diff --git a/spec/draper/finders_spec.rb b/spec/draper/finders_spec.rb new file mode 100644 index 0000000..e683d45 --- /dev/null +++ b/spec/draper/finders_spec.rb @@ -0,0 +1,151 @@ +require 'spec_helper' + +describe Draper::Finders do + describe ".find" do + it "proxies to the model class" do + Product.should_receive(:find).with(1) + ProductDecorator.find(1) + end + + it "decorates the result" do + found = Product.new + Product.stub(:find).and_return(found) + decorator = ProductDecorator.find(1) + decorator.should be_a ProductDecorator + decorator.source.should be found + end + + it "accepts a context" do + decorator = ProductDecorator.find(1, context: :admin) + decorator.context.should == :admin + end + end + + describe ".find_by_(x)" do + it "proxies to the model class" do + Product.should_receive(:find_by_name).with("apples") + ProductDecorator.find_by_name("apples") + end + + it "decorates the result" do + found = Product.new + Product.stub(:find_by_name).and_return(found) + decorator = ProductDecorator.find_by_name("apples") + decorator.should be_a ProductDecorator + decorator.source.should be found + end + + it "proxies complex finders" do + Product.should_receive(:find_by_name_and_size).with("apples", "large") + ProductDecorator.find_by_name_and_size("apples", "large") + end + + it "proxies find_all_by_(x) finders" do + Product.should_receive(:find_all_by_name_and_size).with("apples", "large") + ProductDecorator.find_all_by_name_and_size("apples", "large") + end + + it "proxies find_last_by_(x) finders" do + Product.should_receive(:find_last_by_name_and_size).with("apples", "large") + ProductDecorator.find_last_by_name_and_size("apples", "large") + end + + it "proxies find_or_initialize_by_(x) finders" do + Product.should_receive(:find_or_initialize_by_name_and_size).with("apples", "large") + ProductDecorator.find_or_initialize_by_name_and_size("apples", "large") + end + + it "proxies find_or_create_by_(x) finders" do + Product.should_receive(:find_or_create_by_name_and_size).with("apples", "large") + ProductDecorator.find_or_create_by_name_and_size("apples", "large") + end + + it "accepts options" do + Product.should_receive(:find_by_name_and_size).with("apples", "large", {role: :admin}) + ProductDecorator.find_by_name_and_size("apples", "large", role: :admin) + end + + it "sets the context to the options" do + Product.should_receive(:find_by_name_and_size).with("apples", "large", {role: :admin}) + decorator = ProductDecorator.find_by_name_and_size("apples", "large", role: :admin) + decorator.context.should == {role: :admin} + end + end + + describe ".all" do + it "returns a decorated collection" do + collection = ProductDecorator.all + collection.should be_a Draper::CollectionDecorator + collection.first.should be_a ProductDecorator + end + + it "accepts a context" do + collection = ProductDecorator.all(context: :admin) + collection.first.context.should == :admin + end + end + + describe ".first" do + it "proxies to the model class" do + Product.should_receive(:first) + ProductDecorator.first + end + + it "decorates the result" do + first = Product.new + Product.stub(:first).and_return(first) + decorator = ProductDecorator.first + decorator.should be_a ProductDecorator + decorator.source.should be first + end + + it "accepts a context" do + decorator = ProductDecorator.first(context: :admin) + decorator.context.should == :admin + end + end + + describe ".last" do + it "proxies to the model class" do + Product.should_receive(:last) + ProductDecorator.last + end + + it "decorates the result" do + last = Product.new + Product.stub(:last).and_return(last) + decorator = ProductDecorator.last + decorator.should be_a ProductDecorator + decorator.source.should be last + end + + it "accepts a context" do + decorator = ProductDecorator.last(context: :admin) + decorator.context.should == :admin + end + end + + describe "scopes" do + it "proxies to the model class" do + Product.should_receive(:where).with({name: "apples"}) + ProductDecorator.where(name: "apples") + end + + it "doesn't decorate the result" do + found = [Product.new] + Product.stub(:where).and_return(found) + ProductDecorator.where(name: "apples").should be found + end + end + + describe ".respond_to?" do + it "responds to the model's class methods" do + ProductDecorator.should respond_to :sample_class_method + end + + it "responds to its own methods" do + ProductDecorator.should respond_to :my_class_method + end + end + +end diff --git a/spec/dummy/app/controllers/posts_controller.rb b/spec/dummy/app/controllers/posts_controller.rb index 88e34e1..ddc929c 100644 --- a/spec/dummy/app/controllers/posts_controller.rb +++ b/spec/dummy/app/controllers/posts_controller.rb @@ -1,5 +1,5 @@ class PostsController < ApplicationController def show - @post = PostDecorator.find(params[:id]) + @post = Post.find(params[:id]).decorate end end diff --git a/spec/dummy/app/decorators/post_decorator.rb b/spec/dummy/app/decorators/post_decorator.rb index f172bae..110c3e8 100644 --- a/spec/dummy/app/decorators/post_decorator.rb +++ b/spec/dummy/app/decorators/post_decorator.rb @@ -1,6 +1,4 @@ class PostDecorator < Draper::Decorator - decorates :post - def posted_date if created_at.to_date == Date.today "Today" diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cbc03ca..5d2a055 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,6 +14,7 @@ require './spec/support/samples/decorator_with_denies' require './spec/support/samples/decorator_with_denies_all' require './spec/support/samples/decorator_with_special_methods' require './spec/support/samples/enumerable_proxy' +require './spec/support/samples/finder_decorator' require './spec/support/samples/namespaced_product' require './spec/support/samples/namespaced_product_decorator' require './spec/support/samples/non_active_model_product' diff --git a/spec/support/samples/finder_decorator.rb b/spec/support/samples/finder_decorator.rb new file mode 100644 index 0000000..d02fcde --- /dev/null +++ b/spec/support/samples/finder_decorator.rb @@ -0,0 +1,2 @@ +class FinderDecorator < Draper::Decorator +end diff --git a/spec/support/samples/namespaced_product_decorator.rb b/spec/support/samples/namespaced_product_decorator.rb index 8796dab..6f3ed49 100644 --- a/spec/support/samples/namespaced_product_decorator.rb +++ b/spec/support/samples/namespaced_product_decorator.rb @@ -2,6 +2,6 @@ require './spec/support/samples/namespaced_product' module Namespace class ProductDecorator < Draper::Decorator - decorates :product, :class => Namespace::Product + add_finders end end diff --git a/spec/support/samples/product_decorator.rb b/spec/support/samples/product_decorator.rb index d59ec53..9e86081 100644 --- a/spec/support/samples/product_decorator.rb +++ b/spec/support/samples/product_decorator.rb @@ -1,7 +1,10 @@ class ProductDecorator < Draper::Decorator - decorates :product + add_finders def awesome_title "Awesome Title" end + + def self.my_class_method + end end diff --git a/spec/support/samples/some_thing_decorator.rb b/spec/support/samples/some_thing_decorator.rb index b3ad763..a27c4fc 100644 --- a/spec/support/samples/some_thing_decorator.rb +++ b/spec/support/samples/some_thing_decorator.rb @@ -1,3 +1,2 @@ class SomeThingDecorator < Draper::Decorator - decorates :some_thing end