1
0
Fork 0
mirror of https://github.com/drapergem/draper synced 2023-03-27 23:21:17 -04:00

Simplify decorator inferral

This commit is contained in:
Andrew Haines 2012-12-19 21:45:22 +00:00
parent a84d958497
commit a70bc5d098
6 changed files with 167 additions and 338 deletions

View file

@ -3,20 +3,19 @@ module Draper
include Enumerable
include ViewHelpers
attr_accessor :source, :context, :decorator_class
attr_accessor :source, :context
alias_method :to_source, :source
delegate :as_json, *(Array.instance_methods - Object.instance_methods), to: :decorated_collection
array_methods = Array.instance_methods - Object.instance_methods
delegate :as_json, *array_methods, to: :decorated_collection
# @param source collection to decorate
# @param [Hash] options (optional)
# @option options [Class, Symbol] :with the class used to decorate
# items, or `:infer` to call each item's `decorate` method instead
# @option options [Class] :with the class used to decorate items
# @option options [Hash] :context context available to each item's decorator
def initialize(source, options = {})
options.assert_valid_keys(:with, :context)
@source = source
@decorator_class = options.fetch(:with) { self.class.inferred_decorator_class }
@decorator_class = options[:with]
@context = options.fetch(:context, {})
end
@ -62,14 +61,14 @@ module Draper
each {|item| item.context = value } if @decorated_collection
end
def decorator_class
@decorator_class ||= self.class.inferred_decorator_class
end
protected
def decorate_item(item)
if decorator_class == :infer
item.decorate(context: context)
else
decorator_class.decorate(item, context: context)
end
item_decorator.call(item, context: context)
end
def self.inferred_decorator_class
@ -85,5 +84,15 @@ module Draper
def self.decorator_uninferrable
raise Draper::UninferrableDecoratorError.new(self)
end
private
def item_decorator
@item_decorator ||= begin
decorator_class.method(:decorate)
rescue Draper::UninferrableDecoratorError
->(item, options) { item.decorate(options) }
end
end
end
end

View file

@ -1,70 +1,70 @@
module Draper
class DecoratedAssociation
attr_reader :base, :association, :options
def initialize(base, association, options)
@base = base
@association = association
def initialize(owner, association, options)
options.assert_valid_keys(:with, :scope, :context)
@options = options
@owner = owner
@association = association
@decorator_class = options[:with]
@scope = options[:scope]
@context = options.fetch(:context, owner.context)
end
def call
return undecorated if undecorated.nil?
decorate
decorated
end
def source
base.source
def context
return @context.call(owner.context) if @context.respond_to?(:call)
@context
end
private
attr_reader :owner, :association, :decorator_class, :scope
def source
owner.source
end
def undecorated
@undecorated ||= begin
associated = source.send(association)
associated = associated.send(options[:scope]) if options[:scope]
associated = associated.send(scope) if scope
associated
end
end
def decorate
@decorated ||= decorator_class.send(decorate_method, undecorated, decorator_options)
end
def decorate_method
if collection? && decorator_class.respond_to?(:decorate_collection)
:decorate_collection
else
:decorate
end
def decorated
@decorated ||= decorator.call(undecorated, context: context)
end
def collection?
undecorated.respond_to?(:first)
end
def decorator_class
return options[:with] if options[:with]
def decorator
return collection_decorator if collection?
if collection?
options[:with] = :infer
Draper::CollectionDecorator
if decorator_class
decorator_class.method(:decorate)
else
undecorated.decorator_class
->(item, options) { item.decorate(options) }
end
end
def decorator_options
decorator_class # Ensures options[:with] = :infer for unspecified collections
def collection_decorator
klass = decorator_class || Draper::CollectionDecorator
dec_options = collection? ? options.slice(:with, :context) : options.slice(:context)
dec_options[:context] = base.context unless dec_options.key?(:context)
if dec_options[:context].respond_to?(:call)
dec_options[:context] = dec_options[:context].call(base.context)
if klass.respond_to?(:decorate_collection)
klass.method(:decorate_collection)
else
klass.method(:decorate)
end
dec_options
end
end
end

View file

@ -23,7 +23,6 @@ module Draper
# multiple places in the chain.
#
# @param [Object] source object to decorate
# @param [Hash] options (optional)
# @option options [Hash] :context context available to the decorator
def initialize(source, options = {})
options.assert_valid_keys(:context)
@ -136,8 +135,8 @@ module Draper
# @param [Object] source collection to decorate
# @param [Hash] options passed to each item's decorator (except
# for the keys listed below)
# @option options [Class,Symbol] :with (self) the class used to decorate
# items, or `:infer` to call each item's `decorate` method instead
# @option options [Class] :with (self) the class used to decorate
# items
# @option options [Hash] :context context available to decorated items
def self.decorate_collection(source, options = {})
options.assert_valid_keys(:with, :context)

View file

@ -79,40 +79,6 @@ describe Draper::CollectionDecorator do
expect { Draper::CollectionDecorator.new(source, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
end
end
context "when the :with option is given" do
context "and the decorator can't be inferred from the class" do
subject { Draper::CollectionDecorator.new(source, with: ProductDecorator) }
it "uses the :with option" do
subject.decorator_class.should be ProductDecorator
end
end
context "and the decorator is inferrable from the class" do
subject { ProductsDecorator.new(source, with: SpecificProductDecorator) }
it "uses the :with option" do
subject.decorator_class.should be SpecificProductDecorator
end
end
end
context "when the :with option is not given" do
context "and the decorator can't be inferred from the class" do
it "raises an UninferrableDecoratorError" do
expect{Draper::CollectionDecorator.new(source)}.to raise_error Draper::UninferrableDecoratorError
end
end
context "and the decorator is inferrable from the class" do
subject { ProductsDecorator.new(source) }
it "infers the decorator" do
subject.decorator_class.should be ProductDecorator
end
end
end
end
describe "#source" do
@ -125,6 +91,52 @@ describe Draper::CollectionDecorator do
end
end
describe "item decoration" do
subject { subject_class.new(source, options) }
let(:decorator_classes) { subject.decorated_collection.map(&:class) }
let(:source) { [Product.new, Widget.new] }
context "when the :with option was given" do
let(:options) { {with: SpecificProductDecorator} }
context "and the decorator can't be inferred from the class" do
let(:subject_class) { Draper::CollectionDecorator }
it "uses the :with option" do
decorator_classes.should == [SpecificProductDecorator, SpecificProductDecorator]
end
end
context "and the decorator is inferrable from the class" do
let(:subject_class) { ProductsDecorator }
it "uses the :with option" do
decorator_classes.should == [SpecificProductDecorator, SpecificProductDecorator]
end
end
end
context "when the :with option was not given" do
let(:options) { {} }
context "and the decorator can't be inferred from the class" do
let(:subject_class) { Draper::CollectionDecorator }
it "infers the decorator from each item" do
decorator_classes.should == [ProductDecorator, WidgetDecorator]
end
end
context "and the decorator is inferrable from the class" do
let(:subject_class) { ProductsDecorator}
it "infers the decorator" do
decorator_classes.should == [ProductDecorator, ProductDecorator]
end
end
end
end
describe "#find" do
context "with a block" do
it "decorates Enumerable#find" do

View file

@ -1,313 +1,131 @@
require 'spec_helper'
describe Draper::DecoratedAssociation do
let(:decorated_association) { Draper::DecoratedAssociation.new(base, association, options) }
let(:decorated_association) { Draper::DecoratedAssociation.new(owner, :association, options) }
let(:source) { Product.new }
let(:base) { source.decorate }
let(:owner) { source.decorate }
let(:options) { {} }
describe "#initialize" do
describe "options validation" do
let(:association) { :similar_products }
let(:valid_options) { {with: ProductDecorator, scope: :foo, context: {}} }
it "does not raise error on valid options" do
expect { Draper::DecoratedAssociation.new(base, association, valid_options) }.to_not raise_error
expect { Draper::DecoratedAssociation.new(owner, :association, valid_options) }.to_not raise_error
end
it "raises error on invalid options" do
expect { Draper::DecoratedAssociation.new(base, association, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
expect { Draper::DecoratedAssociation.new(owner, :association, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
end
end
end
describe "#base" do
subject { decorated_association.base }
let(:association) { :similar_products }
it "returns the base decorator" do
should be base
end
it "returns a Decorator" do
subject.class.should == ProductDecorator
end
end
describe "#source" do
subject { decorated_association.source }
let(:association) { :similar_products }
it "returns the base decorator's source" do
should be base.source
end
it "returns a Model" do
subject.class.should == Product
end
end
describe "#call" do
subject { decorated_association.call }
let(:context) { {foo: "bar"} }
let(:expected_options) { {context: context} }
context "for an ActiveModel collection association" do
let(:association) { :similar_products }
before do
source.stub association: associated
decorated_association.stub context: context
end
context "when the association is not empty" do
it "decorates the collection" do
subject.should be_a Draper::CollectionDecorator
context "for a singular association" do
let(:associated) { Product.new }
context "when :with option was given" do
let(:options) { {with: decorator} }
let(:decorator) { SpecificProductDecorator }
it "uses the specified decorator" do
decorator.should_receive(:decorate).with(associated, expected_options).and_return(:decorated)
decorated_association.call.should be :decorated
end
end
context "when :with option was not given" do
it "infers the decorator" do
subject.decorator_class.should be :infer
end
end
context "when the association is empty" do
it "returns an empty collection decorator" do
source.stub(:similar_products).and_return([])
subject.should be_a Draper::CollectionDecorator
subject.should be_empty
subject.first.should be_nil
associated.should_receive(:decorate).with(expected_options).and_return(:decorated)
decorated_association.call.should be :decorated
end
end
end
context "for non-ActiveModel collection associations" do
let(:association) { :poro_similar_products }
context "for a collection association" do
let(:associated) { [Product.new, Widget.new] }
context "when the association is not empty" do
it "decorates the collection" do
subject.should be_a Draper::CollectionDecorator
end
context "when :with option is a collection decorator" do
let(:options) { {with: collection_decorator} }
let(:collection_decorator) { ProductsDecorator }
it "infers the decorator" do
subject.decorator_class.should be :infer
it "uses the specified decorator" do
collection_decorator.should_receive(:decorate).with(associated, expected_options).and_return(:decorated_collection)
decorated_association.call.should be :decorated_collection
end
end
context "when the association is empty" do
it "returns an empty collection decorator" do
source.stub(:poro_similar_products).and_return([])
subject.should be_a Draper::CollectionDecorator
subject.should be_empty
subject.first.should be_nil
end
end
end
context "when :with option is a singular decorator" do
let(:options) { {with: decorator} }
let(:decorator) { SpecificProductDecorator }
context "for an ActiveModel singular association" do
let(:association) { :previous_version }
context "when the association is present" do
it "decorates the association" do
subject.should be_decorated_with ProductDecorator
it "uses a CollectionDecorator of the specified decorator" do
decorator.should_receive(:decorate_collection).with(associated, expected_options).and_return(:decorated_collection)
decorated_association.call.should be :decorated_collection
end
end
context "when the association is absent" do
it "doesn't decorate the association" do
source.stub(:previous_version).and_return(nil)
subject.should be_nil
context "when :with option was not given" do
it "uses a CollectionDecorator of inferred decorators" do
Draper::CollectionDecorator.should_receive(:decorate).with(associated, expected_options).and_return(:decorated_collection)
decorated_association.call.should be :decorated_collection
end
end
end
context "for a non-ActiveModel singular association" do
let(:association) { :poro_previous_version }
context "when the association is present" do
it "decorates the association" do
subject.should be_decorated_with ProductDecorator
end
end
context "when the association is absent" do
it "doesn't decorate the association" do
source.stub(:poro_previous_version).and_return(nil)
subject.should be_nil
end
end
end
context "when a decorator is specified" do
let(:options) { {with: SpecificProductDecorator} }
context "for a singular association" do
let(:association) { :previous_version }
it "decorates with the specified decorator" do
subject.should be_decorated_with SpecificProductDecorator
end
end
context "for a collection association" do
let(:association) { :similar_products}
it "decorates with a collection of the specifed decorators" do
subject.should be_a Draper::CollectionDecorator
subject.decorator_class.should be SpecificProductDecorator
end
end
end
context "when a collection decorator is specified" do
let(:association) { :similar_products }
let(:options) { {with: ProductsDecorator} }
it "decorates with the specified decorator" do
subject.should be_a ProductsDecorator
end
end
context "with a scope" do
let(:association) { :thing }
let(:associated) { [] }
let(:options) { {scope: :foo} }
it "applies the scope before decoration" do
scoped = [SomeThing.new]
SomeThing.any_instance.should_receive(:foo).and_return(scoped)
subject.source.should be scoped
end
end
context "base has context" do
let(:association) { :similar_products }
let(:base) { source.decorate(context: {some: 'context'}) }
context "when no context is specified" do
it "it should inherit context from base" do
subject.context.should == {some: 'context'}
end
it "it should share context hash with base" do
subject.context.should be base.context
end
end
context "when static context is specified" do
let(:options) { {context: {other: 'context'}} }
it "it should get context from static option" do
subject.context.should == {other: 'context'}
end
end
context "when lambda context is specified" do
let(:options) { {context: lambda {|context| context.merge(other: 'protext')}} }
it "it should get generated context" do
subject.context.should == {some: 'context', other: 'protext'}
end
scoped = [:scoped]
associated.should_receive(:foo).and_return(scoped)
decorated_association.call.source.should be scoped
end
end
end
describe "#decorator_options" do
subject { decorated_association.send(:decorator_options) }
describe "#context" do
before { owner.stub context: :owner_context }
context "collection association" do
let(:association) { :similar_products }
context "when :context option was given" do
let(:options) { {context: context} }
context "no options" do
it "should return default options" do
should == {with: :infer, 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 "should set with: to :infer" do
decorated_association.send(:options).should == options
subject
decorated_association.send(:options).should == {with: :infer}
it "returns the lambda's return value" do
decorated_association.context.should be :dynamic_context
end
end
context "option with: ProductDecorator" do
let(:options) { {with: ProductDecorator} }
it "should pass with: from options" do
should == {with: ProductDecorator, context: {}}
end
end
context "and is not callable" do
let(:context) { :static_context }
context "option scope: :to_a" do
let(:options) { {scope: :to_a} }
it "should strip scope: from options" do
decorated_association.send(:options).should == options
should == {with: :infer, context: {}}
end
end
context "base has context" do
let(:base) { source.decorate(context: {some: 'context'}) }
context "no options" do
it "should return context from base" do
should == {with: :infer, context: {some: 'context'}}
end
end
context "option context: {other: 'context'}" do
let(:options) { {context: {other: 'context'}} }
it "should return specified context" do
should == {with: :infer, context: {other: 'context'}}
end
end
context "option context: lambda" do
let(:options) { {context: lambda {|context| context.merge(other: 'protext')}} }
it "should return specified context" do
should == {with: :infer, context: {some: 'context', other: 'protext'}}
end
it "returns the specified value" do
decorated_association.context.should be :static_context
end
end
end
context "singular association" do
let(:association) { :previous_version }
context "no options" do
it "should return default options" do
should == {context: {}}
end
end
context "option with: ProductDecorator" do
let(:options) { {with: ProductDecorator} }
it "should strip with: from options" do
should == {context: {}}
end
end
context "option scope: :decorate" do
let(:options) { {scope: :decorate} }
it "should strip scope: from options" do
decorated_association.send(:options).should == options
should == {context: {}}
end
end
context "base has context" do
let(:base) { source.decorate(context: {some: 'context'}) }
context "no options" do
it "should return context from base" do
should == {context: {some: 'context'}}
end
end
context "option context: {other: 'context'}" do
let(:options) { {context: {other: 'context'}} }
it "should return specified context" do
should == {context: {other: 'context'}}
end
end
context "option context: lambda" do
let(:options) { {context: lambda {|context| context.merge(other: 'protext')}} }
it "should return specified context" do
should == {context: {some: 'context', other: 'protext'}}
end
end
context "when :context option was not given" do
it "returns the owner's context" do
decorated_association.context.should be :owner_context
end
end
end
end

View file

@ -101,15 +101,6 @@ describe Draper::Decorator do
subject.each {|item| item.should be_a ProductDecorator}
end
context "when given :with => :infer" do
subject { ProductDecorator.decorate_collection(source, with: :infer) }
it "infers the item decorators" do
subject.first.should be_a ProductDecorator
subject.last.should be_a WidgetDecorator
end
end
context "with context" do
subject { ProductDecorator.decorate_collection(source, with: :infer, context: {some: 'context'}) }