Skip to content

Commit

Permalink
Merge pull request #478 from haines/context_lambda
Browse files Browse the repository at this point in the history
Accept a lambda for context in factory
  • Loading branch information
steveklabnik committed Feb 17, 2013
2 parents 9efda27 + 668661f commit 3ab3b35
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 134 deletions.
12 changes: 4 additions & 8 deletions lib/draper/decorated_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,17 @@ def initialize(owner, association, options)
@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
decorate unless defined?(@decorated)
@decorated
end

def context
return @context.call(owner.context) if @context.respond_to?(:call)
@context
end

private

attr_reader :factory, :owner, :association, :scope
Expand All @@ -32,7 +28,7 @@ def decorate
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
Expand Down
9 changes: 7 additions & 2 deletions lib/draper/decorates_assigned.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,12 @@ module DecoratesAssigned
# @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!)

Expand All @@ -29,7 +34,7 @@ def decorates_assigned(*variables)

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
Expand Down
17 changes: 14 additions & 3 deletions lib/draper/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -21,6 +23,8 @@ def initialize(options = {})
# @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?
Expand All @@ -31,13 +35,15 @@ def decorate(source, options = {})

attr_reader :decorator_class, :default_options

# @private
class Worker
def initialize(decorator_class, source)
@decorator_class = decorator_class
@source = source
end

def call(options)
update_context options
decorator.call(source, options)
end

Expand Down Expand Up @@ -71,6 +77,11 @@ def decorator_class
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
174 changes: 54 additions & 120 deletions spec/draper/decorated_association_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 "raises error on invalid options" do
expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/
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
end

describe "#call" do
let(:context) { {some: "context"} }
let(:options) { {} }

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
it "rejects invalid options" do
expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, foo: "bar")}.to raise_error ArgumentError, /Unknown key/
end

context "for a singular association" do
let(:associated) { Model.new }

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
it "creates a factory" do
options = {with: Decorator, context: {foo: "bar"}}

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(:new).with(options)
DecoratedAssociation.new(double, :association, options)
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
describe ":with option" do
it "defaults to nil" do
Factory.should_receive(:new).with(with: nil, context: anything())
DecoratedAssociation.new(double, :association, {})
end
end

context "with a scope" do
let(:options) { {scope: :foo} }
let(:associated) { double(foo: scoped) }
let(:scoped) { Product.new }

it "applies the scope before decoration" do
expect(decorated_association.call.source).to be scoped
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 "#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
describe "#call" do
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

factory.should_receive(:decorate).with(associated, context_args: owner_context).and_return(decorated)
expect(decorated_association.call).to be decorated
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 "memoizes" do
factory = double
Factory.stub new: factory
owner = double(source: double(association: double), context: {})
decorated_association = DecoratedAssociation.new(owner, :association, {})
decorated = double

it "returns the new context if the owner's context changes" do
new_context = {other: "context"}
owner.stub context: new_context
factory.should_receive(:decorate).once.and_return(decorated)
expect(decorated_association.call).to be decorated
expect(decorated_association.call).to be decorated
end

expect(decorated_association.context).to be new_context
context "when the :scope option was given" do
it "applies the scope before decoration" do
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

factory.should_receive(:decorate).with(scoped, anything()).and_return(decorated)
expect(decorated_association.call).to be decorated
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion spec/draper/decorates_assigned_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def self.helper_methods
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

Expand Down
42 changes: 42 additions & 0 deletions spec/draper/factory_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 3ab3b35

Please sign in to comment.