diff --git a/lib/draper/decorated_association.rb b/lib/draper/decorated_association.rb index 21aec91..b2e8a1b 100644 --- a/lib/draper/decorated_association.rb +++ b/lib/draper/decorated_association.rb @@ -9,9 +9,10 @@ module Draper @association = association @scope = options[:scope] - @context = options.fetch(:context, ->(context){ context }) - @factory = Draper::Factory.new(options.slice(:with)) + decorator_class = options[:with] + context = options.fetch(:context, ->(context){ context }) + @factory = Draper::Factory.new(with: decorator_class, context: context) end def call @@ -19,11 +20,6 @@ module Draper @decorated end - def context - return @context.call(owner.context) if @context.respond_to?(:call) - @context - end - private attr_reader :factory, :owner, :association, :scope @@ -32,7 +28,7 @@ module Draper associated = owner.source.send(association) associated = associated.send(scope) if scope - @decorated = factory.decorate(associated, context: context) + @decorated = factory.decorate(associated, context_args: owner.context) end end diff --git a/lib/draper/decorates_assigned.rb b/lib/draper/decorates_assigned.rb index b6f604b..636ac8c 100644 --- a/lib/draper/decorates_assigned.rb +++ b/lib/draper/decorates_assigned.rb @@ -19,7 +19,12 @@ module Draper # @param [Symbols*] variables # names of the instance variables to decorate (without the `@`). # @param [Hash] options - # see {Factory#initialize} + # @option options [Decorator, CollectionDecorator] :with (nil) + # decorator class to use. If nil, it is inferred from the instance + # variable. + # @option options [Hash, #call] :context + # extra data to be stored in the decorator. If a Proc is given, it will + # be passed the controller and should return a new context hash. def decorates_assigned(*variables) factory = Draper::Factory.new(variables.extract_options!) @@ -29,7 +34,7 @@ module Draper define_method variable do return instance_variable_get(decorated) if instance_variable_defined?(decorated) - instance_variable_set decorated, factory.decorate(instance_variable_get(undecorated)) + instance_variable_set decorated, factory.decorate(instance_variable_get(undecorated), context_args: self) end helper_method variable diff --git a/lib/draper/factory.rb b/lib/draper/factory.rb index 475633a..4879110 100644 --- a/lib/draper/factory.rb +++ b/lib/draper/factory.rb @@ -2,11 +2,13 @@ module Draper class Factory # Creates a decorator factory. # - # @option options [Decorator,CollectionDecorator] :with (nil) + # @option options [Decorator, CollectionDecorator] :with (nil) # decorator class to use. If nil, it is inferred from the object # passed to {#decorate}. - # @option options [Hash] context - # extra data to be stored in created decorators. + # @option options [Hash, #call] context + # extra data to be stored in created decorators. If a proc is given, it + # will be called each time {#decorate} is called and its return value + # will be used as the context. def initialize(options = {}) options.assert_valid_keys(:with, :context) @decorator_class = options.delete(:with) @@ -21,6 +23,8 @@ module Draper # @option options [Hash] context # extra data to be stored in the decorator. Overrides any context passed # to the constructor. + # @option options [Object, Array] context_args (nil) + # argument(s) to be passed to the context proc. # @return [Decorator, CollectionDecorator] the decorated object. def decorate(source, options = {}) return nil if source.nil? @@ -31,6 +35,7 @@ module Draper attr_reader :decorator_class, :default_options + # @private class Worker def initialize(decorator_class, source) @decorator_class = decorator_class @@ -38,6 +43,7 @@ module Draper end def call(options) + update_context options decorator.call(source, options) end @@ -71,6 +77,11 @@ module Draper def source_decorator_class source.decorator_class if source.respond_to?(:decorator_class) end + + def update_context(options) + args = options.delete(:context_args) + options[:context] = options[:context].call(*args) if options[:context].respond_to?(:call) + end end end end diff --git a/spec/draper/decorated_association_spec.rb b/spec/draper/decorated_association_spec.rb index 7876ef4..62b4fed 100644 --- a/spec/draper/decorated_association_spec.rb +++ b/spec/draper/decorated_association_spec.rb @@ -4,144 +4,78 @@ module Draper describe DecoratedAssociation do describe "#initialize" do - describe "options validation" do - it "does not raise error on valid options" do - valid_options = {with: Decorator, scope: :foo, context: {}} - expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, valid_options)}.not_to raise_error - end + it "accepts valid options" do + valid_options = {with: Decorator, scope: :foo, context: {}} + expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, valid_options)}.not_to raise_error + end - it "raises error on invalid options" do - expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/ + it "rejects invalid options" do + expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/ + end + + it "creates a factory" do + options = {with: Decorator, context: {foo: "bar"}} + + Factory.should_receive(:new).with(options) + DecoratedAssociation.new(double, :association, options) + end + + describe ":with option" do + it "defaults to nil" do + Factory.should_receive(:new).with(with: nil, context: anything()) + DecoratedAssociation.new(double, :association, {}) + end + end + + describe ":context option" do + it "defaults to the identity function" do + Factory.should_receive(:new).with do |options| + options[:context].call(:anything) == :anything + end + DecoratedAssociation.new(double, :association, {}) end end end describe "#call" do - let(:context) { {some: "context"} } - let(:options) { {} } + it "calls the factory" do + factory = double + Factory.stub new: factory + associated = double + owner_context = {foo: "bar"} + source = double(association: associated) + owner = double(source: source, context: owner_context) + decorated_association = DecoratedAssociation.new(owner, :association, {}) + decorated = double - let(:decorated_association) do - owner = double(context: nil, source: double(association: associated)) - - DecoratedAssociation.new(owner, :association, options).tap do |decorated_association| - decorated_association.stub context: context - end + factory.should_receive(:decorate).with(associated, context_args: owner_context).and_return(decorated) + expect(decorated_association.call).to be decorated end - context "for a singular association" do - let(:associated) { Model.new } + it "memoizes" do + factory = double + Factory.stub new: factory + owner = double(source: double(association: double), context: {}) + decorated_association = DecoratedAssociation.new(owner, :association, {}) + decorated = double - context "when :with option was given" do - let(:options) { {with: Decorator} } - - it "uses the specified decorator" do - Decorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated) - expect(decorated_association.call).to be :decorated - end - end - - context "when :with option was not given" do - it "infers the decorator" do - associated.stub decorator_class: OtherDecorator - - OtherDecorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated) - expect(decorated_association.call).to be :decorated - end - end + factory.should_receive(:decorate).once.and_return(decorated) + expect(decorated_association.call).to be decorated + expect(decorated_association.call).to be decorated end - context "for a collection association" do - let(:associated) { [] } - - context "when :with option is a collection decorator" do - let(:options) { {with: ProductsDecorator} } - - it "uses the specified decorator" do - ProductsDecorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated_collection) - expect(decorated_association.call).to be :decorated_collection - end - end - - context "when :with option is a singular decorator" do - let(:options) { {with: ProductDecorator} } - - it "uses a CollectionDecorator of the specified decorator" do - ProductDecorator.should_receive(:decorate_collection).with(associated, context: context).and_return(:decorated_collection) - expect(decorated_association.call).to be :decorated_collection - end - end - - context "when :with option was not given" do - context "when the collection itself is decoratable" do - before { associated.stub decorator_class: ProductsDecorator } - - it "infers the decorator" do - ProductsDecorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated_collection) - expect(decorated_association.call).to be :decorated_collection - end - end - - context "when the collection is not decoratable" do - it "uses a CollectionDecorator of inferred decorators" do - CollectionDecorator.should_receive(:decorate).with(associated, context: context).and_return(:decorated_collection) - expect(decorated_association.call).to be :decorated_collection - end - end - end - end - - context "with a scope" do - let(:options) { {scope: :foo} } - let(:associated) { double(foo: scoped) } - let(:scoped) { Product.new } - + context "when the :scope option was given" do it "applies the scope before decoration" do - expect(decorated_association.call.source).to be scoped - end - end - end + factory = double + Factory.stub new: factory + scoped = double + source = double(association: double(applied_scope: scoped)) + owner = double(source: source, context: {}) + decorated_association = DecoratedAssociation.new(owner, :association, scope: :applied_scope) + decorated = double - describe "#context" do - let(:owner_context) { {some: "context"} } - let(:options) { {} } - let(:owner) { double(context: owner_context) } - let(:decorated_association) { DecoratedAssociation.new(owner, :association, options) } - - context "when :context option was given" do - let(:options) { {context: context} } - - context "and is callable" do - let(:context) { ->(*){ :dynamic_context } } - - it "calls it with the owner's context" do - context.should_receive(:call).with(owner_context) - decorated_association.context - end - - it "returns the lambda's return value" do - expect(decorated_association.context).to be :dynamic_context - end - end - - context "and is not callable" do - let(:context) { {other: "context"} } - - it "returns the specified value" do - expect(decorated_association.context).to be context - end - end - end - - context "when :context option was not given" do - it "returns the owner's context" do - expect(decorated_association.context).to be owner_context - end - - it "returns the new context if the owner's context changes" do - new_context = {other: "context"} - owner.stub context: new_context - - expect(decorated_association.context).to be new_context + factory.should_receive(:decorate).with(scoped, anything()).and_return(decorated) + expect(decorated_association.call).to be decorated end end end diff --git a/spec/draper/decorates_assigned_spec.rb b/spec/draper/decorates_assigned_spec.rb index 6c964fa..76c4d10 100644 --- a/spec/draper/decorates_assigned_spec.rb +++ b/spec/draper/decorates_assigned_spec.rb @@ -49,7 +49,7 @@ module Draper controller = controller_class.new controller.instance_variable_set "@article", source - factory.should_receive(:decorate).with(source).and_return(:decorated) + factory.should_receive(:decorate).with(source, context_args: controller).and_return(:decorated) expect(controller.article).to be :decorated end diff --git a/spec/draper/factory_spec.rb b/spec/draper/factory_spec.rb index 39d5ac0..86f4124 100644 --- a/spec/draper/factory_spec.rb +++ b/spec/draper/factory_spec.rb @@ -104,6 +104,48 @@ module Draper decorator.should_receive(:call).with(source, options).and_return(:decorated) expect(worker.call(options)).to be :decorated end + + context "when the :context option is callable" do + it "calls it" do + worker = Factory::Worker.new(double, double) + decorator = ->(*){} + worker.stub decorator: decorator + context = {foo: "bar"} + + decorator.should_receive(:call).with(anything(), context: context) + worker.call(context: ->{ context }) + end + + it "receives arguments from the :context_args option" do + worker = Factory::Worker.new(double, double) + worker.stub decorator: ->(*){} + context = ->{} + + context.should_receive(:call).with(:foo, :bar) + worker.call(context: context, context_args: [:foo, :bar]) + end + end + + context "when the :context option is not callable" do + it "doesn't call it" do + worker = Factory::Worker.new(double, double) + decorator = ->(*){} + worker.stub decorator: decorator + context = {foo: "bar"} + + decorator.should_receive(:call).with(anything(), context: context) + worker.call(context: context) + end + end + + it "does not pass the :context_args option to the decorator" do + worker = Factory::Worker.new(double, double) + decorator = ->(*){} + worker.stub decorator: decorator + + decorator.should_receive(:call).with(anything(), foo: "bar") + worker.call(foo: "bar", context_args: []) + end end describe "#decorator" do