diff --git a/lib/draper/decorator.rb b/lib/draper/decorator.rb index d7ce452..fb8e75d 100755 --- a/lib/draper/decorator.rb +++ b/lib/draper/decorator.rb @@ -157,21 +157,12 @@ module Draper end def method_missing(method, *args, &block) - super unless allow?(method) - - if source.respond_to?(method) - self.class.send :define_method, method do |*args, &blokk| - source.send method, *args, &blokk - end - - send method, *args, &block + if allow?(method) && source.respond_to?(method) + self.class.define_proxy(method) + send(method, *args, &block) else super end - - rescue NoMethodError => no_method_error - super if no_method_error.name == method - raise no_method_error end # For ActiveModel compatibilty @@ -184,7 +175,13 @@ module Draper source.to_param end - private + private + + def self.define_proxy(method) + define_method(method) do |*args, &block| + source.send(method, *args, &block) + end + end def self.security @security ||= Security.new diff --git a/spec/draper/collection_decorator_spec.rb b/spec/draper/collection_decorator_spec.rb index 6f683ce..09ebd9e 100644 --- a/spec/draper/collection_decorator_spec.rb +++ b/spec/draper/collection_decorator_spec.rb @@ -136,11 +136,11 @@ describe Draper::CollectionDecorator do describe ".helpers" do it "returns a HelperProxy" do - Decorator.helpers.should be_a Draper::HelperProxy + subject.class.helpers.should be_a Draper::HelperProxy end it "is aliased to .h" do - Decorator.h.should be Decorator.helpers + subject.class.h.should be subject.class.helpers end end diff --git a/spec/draper/decorator_spec.rb b/spec/draper/decorator_spec.rb index fc9b986..218c19f 100755 --- a/spec/draper/decorator_spec.rb +++ b/spec/draper/decorator_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Draper::Decorator do before { ApplicationController.new.view_context } - subject { Decorator.new(source) } - let(:source){ Product.new } - let(:non_active_model_source){ NonActiveModelProduct.new } + subject { decorator_class.new(source) } + let(:decorator_class) { Draper::Decorator } + let(:source) { Product.new } describe "#initialize" do it "sets the source" do @@ -12,7 +12,7 @@ describe Draper::Decorator do end it "stores options" do - decorator = Decorator.new(source, some: "options") + decorator = decorator_class.new(source, some: "options") decorator.options.should == {some: "options"} end @@ -106,7 +106,7 @@ describe Draper::Decorator do describe "#localize" do before { subject.helpers.should_receive(:localize).with(:an_object, {some: "options"}) } - it "delegates to helpers" do + it "delegates to #helpers" do subject.localize(:an_object, some: "options") end @@ -117,16 +117,17 @@ describe Draper::Decorator do describe ".helpers" do it "returns a HelperProxy" do - Decorator.helpers.should be_a Draper::HelperProxy + subject.class.helpers.should be_a Draper::HelperProxy end it "is aliased to .h" do - Decorator.h.should be Decorator.helpers + subject.class.h.should be subject.class.helpers end end describe ".decorates_association" do - before { subject.class.decorates_association :similar_products, with: ProductDecorator } + let(:decorator_class) { Class.new(ProductDecorator) } + before { decorator_class.decorates_association :similar_products, with: ProductDecorator } describe "overridden association method" do let(:decorated_association) { ->{} } @@ -151,7 +152,7 @@ describe Draper::Decorator do end describe ".decorates_associations" do - subject { Decorator } + subject { decorator_class } it "decorates each of the associations" do subject.should_receive(:decorates_association).with(:similar_products, {}) @@ -210,56 +211,11 @@ describe Draper::Decorator do end end - describe "method selection" do - it "echos the methods of the wrapped class" do - source.methods.each do |method| - subject.respond_to?(method.to_sym, true).should be_true - end + describe "#to_param" do + it "proxies to the source" do + source.stub(:to_param).and_return(42) + subject.to_param.should == 42 end - - it "not override a defined method with a source method" do - DecoratorWithApplicationHelper.new(source).length.should == "overridden" - end - - it "not copy the .class, .inspect, or other existing methods" do - source.class.should_not == subject.class - source.inspect.should_not == subject.inspect - source.to_s.should_not == subject.to_s - end - - context "when an ActiveModel descendant" do - it "always proxy to_param if it is not defined on the decorator itself" do - source.stub(:to_param).and_return(1) - Draper::Decorator.new(source).to_param.should == 1 - end - - it "always proxy id if it is not defined on the decorator itself" do - source.stub(:id).and_return(123456789) - Draper::Decorator.new(source).id.should == 123456789 - end - - it "always proxy errors if it is not defined on the decorator itself" do - Draper::Decorator.new(source).errors.should be_an_instance_of ActiveModel::Errors - end - - it "never proxy to_param if it is defined on the decorator itself" do - source.stub(:to_param).and_return(1) - DecoratorWithSpecialMethods.new(source).to_param.should == "foo" - end - - it "never proxy id if it is defined on the decorator itself" do - source.stub(:id).and_return(123456789) - DecoratorWithSpecialMethods.new(source).id.should == 1337 - end - - it "never proxy errors if it is defined on the decorator itself" do - DecoratorWithSpecialMethods.new(source).errors.should be_an_instance_of Array - end - end - end - - it "wrap source methods so they still accept blocks" do - subject.block{"marker"}.should == "marker" end describe "#==" do @@ -270,13 +226,104 @@ describe Draper::Decorator do end describe "#respond_to?" do - # respond_to? is called by some proxies (id, to_param, errors). - # This is, why I stub it this way. - it "delegates to the decorated model" do - other = Draper::Decorator.new(source) - source.stub(:respond_to?).and_return(false) - source.should_receive(:respond_to?).with(:whatever, true).once.and_return("mocked") - subject.respond_to?(:whatever, true).should == "mocked" + let(:decorator_class) { Class.new(ProductDecorator) } + + it "returns true for its own methods" do + subject.should respond_to :awesome_title + end + + it "returns true for the source's methods" do + subject.should respond_to :title + end + + context "with include_private" do + it "returns true for its own private methods" do + subject.respond_to?(:awesome_private_title, true).should be_true + end + + it "returns true for the source's private methods" do + subject.respond_to?(:private_title, true).should be_true + end + end + + context "with method security" do + it "respects allows" do + subject.class.allows :hello_world + + subject.should respond_to :hello_world + subject.should_not respond_to :goodnight_moon + end + + it "respects denies" do + subject.class.denies :goodnight_moon + + subject.should respond_to :hello_world + subject.should_not respond_to :goodnight_moon + end + + it "respects denies_all" do + subject.class.denies_all + + subject.should_not respond_to :hello_world + subject.should_not respond_to :goodnight_moon + end + end + end + + describe "method proxying" do + let(:decorator_class) { Class.new(ProductDecorator) } + + it "does not proxy methods that are defined on the decorator" do + subject.overridable.should be :overridden + end + + it "does not proxy methods inherited from Object" do + subject.inspect.should_not be source.inspect + end + + it "proxies missing methods that exist on the source" do + source.stub(:hello_world).and_return(:proxied) + subject.hello_world.should be :proxied + end + + it "adds proxied methods to the decorator when they are used" do + subject.methods.should_not include :hello_world + subject.hello_world + subject.methods.should include :hello_world + end + + it "passes blocks to proxied methods" do + subject.block{"marker"}.should == "marker" + end + + it "does not confuse Kernel#Array" do + Array(subject).should be_a Array + end + + context "with method security" do + it "respects allows" do + source.stub(:hello_world, :goodnight_moon).and_return(:proxied) + subject.class.allows :hello_world + + subject.hello_world.should be :proxied + expect{subject.goodnight_moon}.to raise_error NameError + end + + it "respects denies" do + source.stub(:hello_world, :goodnight_moon).and_return(:proxied) + subject.class.denies :goodnight_moon + + subject.hello_world.should be :proxied + expect{subject.goodnight_moon}.to raise_error NameError + end + + it "respects denies_all" do + source.stub(:hello_world, :goodnight_moon).and_return(:proxied) + subject.class.denies_all + + expect{subject.hello_world}.to raise_error NameError + expect{subject.goodnight_moon}.to raise_error NameError + end end end @@ -302,64 +349,26 @@ describe Draper::Decorator do end context "in a Rails application" do - let(:decorator){ DecoratorWithApplicationHelper.decorate(Object.new) } + let(:decorator_class) { DecoratorWithApplicationHelper } it "has access to ApplicationHelper helpers" do - decorator.uses_hello_world.should == "Hello, World!" + subject.uses_hello_world.should == "Hello, World!" end it "is able to use the content_tag helper" do - decorator.sample_content.to_s.should == "Hello, World!" + subject.sample_content.to_s.should == "Hello, World!" end it "is able to use the link_to helper" do - decorator.sample_link.should == "Hello" + subject.sample_link.should == "Hello" end - it "is able to use the pluralize helper" do - decorator.sample_truncate.should == "Once..." + it "is able to use the truncate helper" do + subject.sample_truncate.should == "Once..." end it "is able to access html_escape, a private method" do - decorator.sample_html_escaped_text.should == '<script>danger</script>' - end - end - - describe "#method_missing" do - context "with an isolated decorator class" do - let(:decorator_class) { Class.new(Decorator) } - subject{ decorator_class.new(source) } - - context "when #hello_world is called again" do - it "proxies method directly after first hit" do - subject.methods.should_not include(:hello_world) - subject.hello_world - subject.methods.should include(:hello_world) - end - end - - context "when #hello_world is called for the first time" do - it "hits method missing" do - subject.should_receive(:method_missing) - subject.hello_world - end - end - end - - context "when the delegated method calls a non-existant method" do - it "should not try to delegate to non-existant methods to not confuse Kernel#Array" do - Array(subject).should be_kind_of(Array) - end - - it "raises the correct NoMethodError" do - begin - subject.some_action - rescue NoMethodError => e - e.name.should_not == :some_action - else - fail("No exception raised") - end - end + subject.sample_html_escaped_text.should == '<script>danger</script>' end end @@ -390,24 +399,28 @@ describe Draper::Decorator do end end - context "with for: symbol" do - it "sets the finder class" do - FinderDecorator.has_finders for: :product - FinderDecorator.finder_class.should be Product - end - end + context "with :for option" do + subject { Class.new(Draper::Decorator) } - context "with for: string" do - it "sets the finder class" do - FinderDecorator.has_finders for: "some_thing" - FinderDecorator.finder_class.should be SomeThing + context "with a symbol" do + it "sets the finder class" do + subject.has_finders for: :product + subject.finder_class.should be Product + end end - end - context "with for: class" do - it "sets the finder_class" do - FinderDecorator.has_finders for: Namespace::Product - FinderDecorator.finder_class.should be Namespace::Product + context "with a string" do + it "sets the finder class" do + subject.has_finders for: "some_thing" + subject.finder_class.should be SomeThing + end + end + + context "with a class" do + it "sets the finder_class" do + subject.has_finders for: Namespace::Product + subject.finder_class.should be Namespace::Product + end end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1bdfa8e..9a7007b 100755 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,49 +3,29 @@ require 'ammeter/init' require 'rails' require 'action_view' -Bundler.require - -require './spec/support/samples/active_record' -require './spec/support/samples/decorator' -require './spec/support/samples/decorator_with_application_helper' -require './spec/support/samples/decorator_with_special_methods' -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' -require './spec/support/samples/non_active_model_product_decorator' -require './spec/support/samples/product' -require './spec/support/samples/product_decorator' -require './spec/support/samples/products_decorator' -require './spec/support/samples/sequel_product' -require './spec/support/samples/specific_product_decorator' -require './spec/support/samples/some_thing' -require './spec/support/samples/some_thing_decorator' -require './spec/support/samples/uninferrable_decorator_model' -require './spec/support/samples/widget' -require './spec/support/samples/widget_decorator' - require 'action_controller' require 'action_controller/test_case' -module ActionController - class Base - Draper::System.setup_action_controller(self) - end -end +Bundler.require -module ActiveRecord - class Relation - end -end +require './spec/support/active_record' +require './spec/support/action_controller' -class ApplicationController < ActionController::Base - def hello_world - "Hello, World!" - end - helper_method :hello_world -end +require './spec/support/models/product' +require './spec/support/models/namespaced_product' +require './spec/support/models/non_active_model_product' +require './spec/support/models/widget' +require './spec/support/models/some_thing' +require './spec/support/models/uninferrable_decorator_model' +require './spec/support/decorators/product_decorator' +require './spec/support/decorators/namespaced_product_decorator' +require './spec/support/decorators/non_active_model_product_decorator' +require './spec/support/decorators/widget_decorator' +require './spec/support/decorators/specific_product_decorator' +require './spec/support/decorators/products_decorator' +require './spec/support/decorators/some_thing_decorator' +require './spec/support/decorators/decorator_with_application_helper' class << Rails undef application # Avoid silly Rails bug: https://github.com/rails/rails/pull/6429 diff --git a/spec/support/action_controller.rb b/spec/support/action_controller.rb new file mode 100644 index 0000000..3146904 --- /dev/null +++ b/spec/support/action_controller.rb @@ -0,0 +1,12 @@ +module ActionController + class Base + Draper::System.setup_action_controller(self) + end +end + +class ApplicationController < ActionController::Base + def hello_world + "Hello, World!" + end + helper_method :hello_world +end diff --git a/spec/support/samples/active_record.rb b/spec/support/active_record.rb similarity index 84% rename from spec/support/samples/active_record.rb rename to spec/support/active_record.rb index 928463b..b581423 100644 --- a/spec/support/samples/active_record.rb +++ b/spec/support/active_record.rb @@ -15,3 +15,8 @@ module ActiveRecord end end + +module ActiveRecord + class Relation + end +end diff --git a/spec/support/samples/decorator_with_application_helper.rb b/spec/support/decorators/decorator_with_application_helper.rb similarity index 100% rename from spec/support/samples/decorator_with_application_helper.rb rename to spec/support/decorators/decorator_with_application_helper.rb diff --git a/spec/support/samples/namespaced_product_decorator.rb b/spec/support/decorators/namespaced_product_decorator.rb similarity index 62% rename from spec/support/samples/namespaced_product_decorator.rb rename to spec/support/decorators/namespaced_product_decorator.rb index e89fa5e..fba3363 100644 --- a/spec/support/samples/namespaced_product_decorator.rb +++ b/spec/support/decorators/namespaced_product_decorator.rb @@ -1,5 +1,3 @@ -require './spec/support/samples/namespaced_product' - module Namespace class ProductDecorator < Draper::Decorator has_finders diff --git a/spec/support/samples/non_active_model_product_decorator.rb b/spec/support/decorators/non_active_model_product_decorator.rb similarity index 100% rename from spec/support/samples/non_active_model_product_decorator.rb rename to spec/support/decorators/non_active_model_product_decorator.rb diff --git a/spec/support/samples/product_decorator.rb b/spec/support/decorators/product_decorator.rb similarity index 62% rename from spec/support/samples/product_decorator.rb rename to spec/support/decorators/product_decorator.rb index 2dd1a8f..1019877 100644 --- a/spec/support/samples/product_decorator.rb +++ b/spec/support/decorators/product_decorator.rb @@ -5,6 +5,15 @@ class ProductDecorator < Draper::Decorator "Awesome Title" end + def overridable + :overridden + end + def self.my_class_method end + + private + + def awesome_private_title + end end diff --git a/spec/support/samples/products_decorator.rb b/spec/support/decorators/products_decorator.rb similarity index 100% rename from spec/support/samples/products_decorator.rb rename to spec/support/decorators/products_decorator.rb diff --git a/spec/support/samples/some_thing_decorator.rb b/spec/support/decorators/some_thing_decorator.rb similarity index 100% rename from spec/support/samples/some_thing_decorator.rb rename to spec/support/decorators/some_thing_decorator.rb diff --git a/spec/support/samples/specific_product_decorator.rb b/spec/support/decorators/specific_product_decorator.rb similarity index 50% rename from spec/support/samples/specific_product_decorator.rb rename to spec/support/decorators/specific_product_decorator.rb index 54fd7c4..1acfec1 100644 --- a/spec/support/samples/specific_product_decorator.rb +++ b/spec/support/decorators/specific_product_decorator.rb @@ -1,4 +1,2 @@ -require './spec/support/samples/product_decorator' - class SpecificProductDecorator < ProductDecorator end diff --git a/spec/support/samples/widget_decorator.rb b/spec/support/decorators/widget_decorator.rb similarity index 100% rename from spec/support/samples/widget_decorator.rb rename to spec/support/decorators/widget_decorator.rb diff --git a/spec/support/samples/namespaced_product.rb b/spec/support/models/namespaced_product.rb similarity index 94% rename from spec/support/samples/namespaced_product.rb rename to spec/support/models/namespaced_product.rb index 29d576c..1aa3214 100644 --- a/spec/support/samples/namespaced_product.rb +++ b/spec/support/models/namespaced_product.rb @@ -1,5 +1,3 @@ -require './spec/support/samples/product' - module Namespace class Product < ActiveRecord::Base include Draper::Decoratable diff --git a/spec/support/samples/non_active_model_product.rb b/spec/support/models/non_active_model_product.rb similarity index 100% rename from spec/support/samples/non_active_model_product.rb rename to spec/support/models/non_active_model_product.rb diff --git a/spec/support/samples/product.rb b/spec/support/models/product.rb similarity index 93% rename from spec/support/samples/product.rb rename to spec/support/models/product.rb index a9ad562..a613723 100644 --- a/spec/support/samples/product.rb +++ b/spec/support/models/product.rb @@ -77,4 +77,12 @@ class Product < ActiveRecord::Base Product.new end + def overridable + :overridable + end + + private + + def private_title + end end diff --git a/spec/support/samples/some_thing.rb b/spec/support/models/some_thing.rb similarity index 100% rename from spec/support/samples/some_thing.rb rename to spec/support/models/some_thing.rb diff --git a/spec/support/samples/uninferrable_decorator_model.rb b/spec/support/models/uninferrable_decorator_model.rb similarity index 100% rename from spec/support/samples/uninferrable_decorator_model.rb rename to spec/support/models/uninferrable_decorator_model.rb diff --git a/spec/support/samples/widget.rb b/spec/support/models/widget.rb similarity index 100% rename from spec/support/samples/widget.rb rename to spec/support/models/widget.rb diff --git a/spec/support/samples/decorator.rb b/spec/support/samples/decorator.rb deleted file mode 100644 index 26142e2..0000000 --- a/spec/support/samples/decorator.rb +++ /dev/null @@ -1,5 +0,0 @@ -class Decorator < Draper::Decorator - def self.own_class_method - "own class method" - end -end diff --git a/spec/support/samples/decorator_with_special_methods.rb b/spec/support/samples/decorator_with_special_methods.rb deleted file mode 100755 index 542bc94..0000000 --- a/spec/support/samples/decorator_with_special_methods.rb +++ /dev/null @@ -1,13 +0,0 @@ -class DecoratorWithSpecialMethods < Draper::Decorator - def to_param - "foo" - end - - def id - 1337 - end - - def errors - ["omg errors!"] - end -end diff --git a/spec/support/samples/finder_decorator.rb b/spec/support/samples/finder_decorator.rb deleted file mode 100644 index d02fcde..0000000 --- a/spec/support/samples/finder_decorator.rb +++ /dev/null @@ -1,2 +0,0 @@ -class FinderDecorator < Draper::Decorator -end diff --git a/spec/support/samples/sequel_product.rb b/spec/support/samples/sequel_product.rb deleted file mode 100644 index b610051..0000000 --- a/spec/support/samples/sequel_product.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Sequel - class Model - def each - end - end -end - -class SequelProduct < Sequel::Model - def some_attribute - "hello" - end -end -