Accept a lambda for context

Closes #467
This commit is contained in:
Andrew Haines 2013-02-17 16:56:49 +00:00
parent 0799cd4beb
commit 668661f13a
6 changed files with 126 additions and 138 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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