Extract Factory from DecoratedAssociation

This commit is contained in:
Andrew Haines 2013-02-06 23:45:35 +00:00
parent bbcc35cd8b
commit a6a60bcf92
4 changed files with 260 additions and 44 deletions

View File

@ -16,6 +16,7 @@ require 'draper/decorator'
require 'draper/helper_proxy'
require 'draper/lazy_helpers'
require 'draper/decoratable'
require 'draper/factory'
require 'draper/decorated_association'
require 'draper/helper_support'
require 'draper/view_context'

View File

@ -8,14 +8,15 @@ module Draper
@owner = owner
@association = association
@decorator_class = options[:with]
@scope = options[:scope]
@context = options.fetch(:context, owner.context)
@factory = Draper::Factory.new(options.slice(:with))
end
def call
return undecorated if undecorated.nil?
decorated
decorate unless defined?(@decorated)
@decorated
end
def context
@ -25,49 +26,13 @@ module Draper
private
attr_reader :owner, :association, :scope
attr_reader :factory, :owner, :association, :scope
def source
owner.source
end
def decorate
associated = owner.source.send(association)
associated = associated.send(scope) if scope
def undecorated
@undecorated ||= begin
associated = source.send(association)
associated = associated.send(scope) if scope
associated
end
end
def decorated
@decorated ||= decorator.call(undecorated, context: context)
end
def collection?
undecorated.respond_to?(:first)
end
def decorator
return collection_decorator if collection?
decorator_class.method(:decorate)
end
def collection_decorator
klass = decorator_class || Draper::CollectionDecorator
if klass.respond_to?(:decorate_collection)
klass.method(:decorate_collection)
else
klass.method(:decorate)
end
end
def decorator_class
@decorator_class || inferred_decorator_class
end
def inferred_decorator_class
undecorated.decorator_class if undecorated.respond_to?(:decorator_class)
@decorated = factory.decorate(associated, context: context)
end
end

76
lib/draper/factory.rb Normal file
View File

@ -0,0 +1,76 @@
module Draper
class Factory
# Creates a decorator factory.
#
# @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.
def initialize(options = {})
options.assert_valid_keys(:with, :context)
@decorator_class = options.delete(:with)
@default_options = options
end
# Decorates an object, inferring whether to create a singular or collection
# decorator from the type of object passed.
#
# @param [Object] source
# object to decorate.
# @option options [Hash] context
# extra data to be stored in the decorator. Overrides any context passed
# to the constructor.
# @return [Decorator, CollectionDecorator] the decorated object.
def decorate(source, options = {})
return nil if source.nil?
Worker.new(decorator_class, source).call(options.reverse_merge(default_options))
end
private
attr_reader :decorator_class, :default_options
class Worker
def initialize(decorator_class, source)
@decorator_class = decorator_class
@source = source
end
def call(options)
decorator.call(source, options)
end
def decorator
return collection_decorator if collection?
decorator_class.method(:decorate)
end
private
attr_reader :source
def collection_decorator
klass = decorator_class || Draper::CollectionDecorator
if klass.respond_to?(:decorate_collection)
klass.method(:decorate_collection)
else
klass.method(:decorate)
end
end
def collection?
source.respond_to?(:first)
end
def decorator_class
@decorator_class || source_decorator_class
end
def source_decorator_class
source.decorator_class if source.respond_to?(:decorator_class)
end
end
end
end

174
spec/draper/factory_spec.rb Normal file
View File

@ -0,0 +1,174 @@
require 'spec_helper'
module Draper
describe Factory do
describe "#initialize" do
it "accepts valid options" do
valid_options = {with: Decorator, context: {foo: "bar"}}
expect{Factory.new(valid_options)}.not_to raise_error
end
it "rejects invalid options" do
expect{Factory.new(foo: "bar")}.to raise_error ArgumentError, /Unknown key/
end
end
describe "#decorate" do
context "when source is nil" do
it "returns nil" do
factory = Factory.new
expect(factory.decorate(nil)).to be_nil
end
end
it "calls a worker" do
factory = Factory.new
worker = ->(*){ :decorated }
Factory::Worker.should_receive(:new).and_return(worker)
expect(factory.decorate(double)).to be :decorated
end
it "passes the source to the worker" do
factory = Factory.new
source = double
Factory::Worker.should_receive(:new).with(anything(), source).and_return(->(*){})
factory.decorate(source)
end
context "when the :with option was given" do
it "passes the decorator class to the worker" do
decorator_class = double
factory = Factory.new(with: decorator_class)
Factory::Worker.should_receive(:new).with(decorator_class, anything()).and_return(->(*){})
factory.decorate(double)
end
end
context "when the :with option was omitted" do
it "passes nil to the worker" do
factory = Factory.new
Factory::Worker.should_receive(:new).with(nil, anything()).and_return(->(*){})
factory.decorate(double)
end
end
it "passes options to the call" do
factory = Factory.new
worker = ->(*){}
Factory::Worker.stub new: worker
options = {foo: "bar"}
worker.should_receive(:call).with(options)
factory.decorate(double, options)
end
context "when the :context option was given" do
it "sets the passed context" do
factory = Factory.new(context: {foo: "bar"})
worker = ->(*){}
Factory::Worker.stub new: worker
worker.should_receive(:call).with(baz: "qux", context: {foo: "bar"})
factory.decorate(double, {baz: "qux"})
end
it "is overridden by explicitly-specified context" do
factory = Factory.new(context: {foo: "bar"})
worker = ->(*){}
Factory::Worker.stub new: worker
worker.should_receive(:call).with(context: {baz: "qux"})
factory.decorate(double, context: {baz: "qux"})
end
end
end
end
describe Factory::Worker do
describe "#call" do
it "calls the decorator method" do
source = double
options = {foo: "bar"}
worker = Factory::Worker.new(double, source)
decorator = ->(*){}
worker.stub decorator: decorator
decorator.should_receive(:call).with(source, options).and_return(:decorated)
expect(worker.call(options)).to be :decorated
end
end
describe "#decorator" do
context "for a singular source" do
context "when decorator_class is specified" do
it "returns the .decorate method from the decorator" do
decorator_class = Class.new(Decorator)
worker = Factory::Worker.new(decorator_class, double)
expect(worker.decorator).to eq decorator_class.method(:decorate)
end
end
context "when decorator_class is unspecified" do
it "returns the .decorate method from the source's decorator" do
decorator_class = Class.new(Decorator)
source = double(decorator_class: decorator_class)
worker = Factory::Worker.new(nil, source)
expect(worker.decorator).to eq decorator_class.method(:decorate)
end
end
end
context "for a collection source" do
context "when decorator_class is a CollectionDecorator" do
it "returns the .decorate method from the collection decorator" do
decorator_class = Class.new(CollectionDecorator)
worker = Factory::Worker.new(decorator_class, [])
expect(worker.decorator).to eq decorator_class.method(:decorate)
end
end
context "when decorator_class is a Decorator" do
it "returns the .decorate_collection method from the decorator" do
decorator_class = Class.new(Decorator)
worker = Factory::Worker.new(decorator_class, [])
expect(worker.decorator).to eq decorator_class.method(:decorate_collection)
end
end
context "when decorator_class is unspecified" do
context "and the source is decoratable" do
it "returns the .decorate method from the source's decorator" do
decorator_class = Class.new(CollectionDecorator)
source = []
source.stub decorator_class: decorator_class
worker = Factory::Worker.new(nil, source)
expect(worker.decorator).to eq decorator_class.method(:decorate)
end
end
context "and the source is not decoratable" do
it "returns the .decorate method from CollectionDecorator" do
worker = Factory::Worker.new(nil, [])
expect(worker.decorator).to eq CollectionDecorator.method(:decorate)
end
end
end
end
end
end
end