Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept a lambda for context in factory #478

Merged
merged 1 commit into from
Feb 17, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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