Extract DecoratedAssociation

This commit is contained in:
Andrew Haines 2012-11-08 13:06:24 +00:00
parent 7fd06ea0c4
commit f0a1be3f73
5 changed files with 237 additions and 132 deletions

View File

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

View File

@ -0,0 +1,67 @@
module Draper
class DecoratedAssociation
attr_reader :source, :association, :options
def initialize(source, association, options)
@source = source
@association = association
@options = options
end
def call
return undecorated if undecorated.nil? || undecorated == []
decorate
end
private
def undecorated
@undecorated ||= begin
associated = source.send(association)
associated = associated.send(options[:scope]) if options[:scope]
associated
end
end
def decorate
@decorated ||= decorator_class.send(decorate_method, undecorated, options)
end
def decorate_method
if collection? && decorator_class.respond_to?(:decorate_collection)
:decorate_collection
else
:decorate
end
end
def collection?
undecorated.respond_to?(:first)
end
def decorator_class
return options[:with] if options[:with]
if collection?
options[:with] = :infer
Draper::CollectionDecorator
else
"#{association_class}Decorator".constantize
end
end
def association_class
if !options[:polymorphic] && association_reflection
association_reflection.klass
else
undecorated.class
end
end
def association_reflection
@reflection ||= source.class.reflect_on_association(association) if source.class.respond_to?(:reflect_on_association)
end
end
end

View File

@ -50,49 +50,25 @@ module Draper
# Typically called within a decorator definition, this method causes
# the assocation to be decorated when it is retrieved.
#
# @param [Symbol] association_symbol name of association to decorate, like `:products`
# @option options [Hash] :with The decorator to decorate the association with
# :scope The scope to apply to the association
def self.decorates_association(association_symbol, options = {})
define_method(association_symbol) do
orig_association = source.send(association_symbol)
return orig_association if orig_association.nil? || orig_association == []
return decorated_associations[association_symbol] if decorated_associations[association_symbol]
orig_association = orig_association.send(options[:scope]) if options[:scope]
return options[:with].decorate(orig_association) if options[:with]
collection = orig_association.respond_to?(:first)
klass = if options[:polymorphic]
orig_association.class
elsif association_reflection = find_association_reflection(association_symbol)
association_reflection.klass
elsif collection
orig_association.first.class
else
orig_association.class
end
decorator_class = "#{klass}Decorator".constantize
if collection
decorated_associations[association_symbol] = decorator_class.decorate_collection(orig_association, options)
else
decorated_associations[association_symbol] = decorator_class.decorate(orig_association, options)
end
# @param [Symbol] association name of association to decorate, like `:products`
# @option options [Class] :with the decorator to apply to the association
# @option options [Symbol] :scope a scope to apply when fetching the association
def self.decorates_association(association, options = {})
define_method(association) do
decorated_associations[association] ||= Draper::DecoratedAssociation.new(source, association, options)
decorated_associations[association].call
end
end
# A convenience method for decorating multiple associations. Calls
# decorates_association on each of the given symbols.
#
# @param [Symbols*] association_symbols name of associations to decorate
def self.decorates_associations(*association_symbols)
options = association_symbols.extract_options!
association_symbols.each{ |sym| decorates_association(sym, options) }
# @param [Symbols*] associations name of associations to decorate
def self.decorates_associations(*associations)
options = associations.extract_options!
associations.each do |association|
decorates_association(association, options)
end
end
# Specifies a black list of methods which may *not* be proxied to
@ -227,12 +203,6 @@ module Draper
end
end
def find_association_reflection(association)
if source.class.respond_to?(:reflect_on_association)
source.class.reflect_on_association(association)
end
end
def decorated_associations
@decorated_associations ||= {}
end

View File

@ -0,0 +1,141 @@
require 'spec_helper'
describe Draper::DecoratedAssociation do
let(:decorated_association) { Draper::DecoratedAssociation.new(source, association, options) }
let(:source) { Product.new }
let(:options) { {} }
describe "#call" do
subject { decorated_association.call }
context "for an ActiveModel collection association" do
let(:association) { :similar_products }
context "when the association is not empty" do
it "decorates the collection" do
subject.should be_a Draper::CollectionDecorator
end
it "infers the decorator" do
subject.decorator_class.should be :infer
end
end
context "when the association is empty" do
it "doesn't decorate the collection" do
source.stub(:similar_products).and_return([])
subject.should_not be_a Draper::CollectionDecorator
subject.should be_empty
end
end
end
context "for non-ActiveModel collection associations" do
let(:association) { :poro_similar_products }
context "when the association is not empty" do
it "decorates the collection" do
subject.should be_a Draper::CollectionDecorator
end
it "infers the decorator" do
subject.decorator_class.should be :infer
end
end
context "when the association is empty" do
it "doesn't decorate the collection" do
source.stub(:poro_similar_products).and_return([])
subject.should_not be_a Draper::CollectionDecorator
subject.should be_empty
end
end
end
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
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
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(: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 "for a polymorphic association" do
let(:association) { :thing }
let(:options) { {polymorphic: true} }
it "makes the association return the right decorator" do
subject.should be_decorated_with SomeThingDecorator
end
end
end
end

View File

@ -126,100 +126,26 @@ describe Draper::Decorator do
end
describe ".decorates_association" do
context "for ActiveModel collection associations" do
before { subject.class.decorates_association :similar_products }
before { subject.class.decorates_association :similar_products, with: ProductDecorator }
context "when the association is not empty" do
it "decorates the collection" do
subject.similar_products.should be_a Draper::CollectionDecorator
subject.similar_products.each {|item| item.should be_decorated_with ProductDecorator }
end
describe "overridden association method" do
let(:decorated_association) { ->{} }
it "creates a DecoratedAssociation" do
Draper::DecoratedAssociation.should_receive(:new).with(source, :similar_products, {with: ProductDecorator}).and_return(decorated_association)
subject.similar_products
end
context "when the association is empty" do
it "doesn't decorate the collection" do
source.stub(:similar_products).and_return([])
subject.similar_products.should_not be_a Draper::CollectionDecorator
subject.similar_products.should be_empty
end
end
end
context "for Plain Old Ruby Object collection associations" do
before { subject.class.decorates_association :poro_similar_products }
context "when the association is not empty" do
it "decorates the collection" do
subject.poro_similar_products.should be_a Draper::CollectionDecorator
subject.poro_similar_products.each {|item| item.should be_decorated_with ProductDecorator }
end
it "memoizes the DecoratedAssociation" do
Draper::DecoratedAssociation.should_receive(:new).once.and_return(decorated_association)
subject.similar_products
subject.similar_products
end
context "when the association is empty" do
it "doesn't decorate the collection" do
source.stub(:poro_similar_products).and_return([])
subject.poro_similar_products.should_not be_a Draper::CollectionDecorator
subject.poro_similar_products.should be_empty
end
end
end
context "for an ActiveModel singular association" do
before { subject.class.decorates_association :previous_version }
context "when the association is present" do
it "decorates the association" do
subject.previous_version.should be_decorated_with ProductDecorator
end
end
context "when the association is absent" do
it "doesn't decorate the association" do
source.stub(:previous_version).and_return(nil)
subject.previous_version.should be_nil
end
end
end
context "for an ActiveModel singular association" do
before { subject.class.decorates_association :poro_previous_version }
context "when the association is present" do
it "decorates the association" do
subject.poro_previous_version.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.poro_previous_version.should be_nil
end
end
end
context "when a decorator is specified" do
before { subject.class.decorates_association :previous_version, with: SpecificProductDecorator }
it "decorates with the specified decorator" do
subject.previous_version.should be_decorated_with SpecificProductDecorator
end
end
context "with a scope" do
before { subject.class.decorates_association :thing, scope: :foo }
it "applies the scope before decoration" do
SomeThing.any_instance.should_receive(:foo).and_return(:bar)
subject.thing.model.should == :bar
end
end
context "for a polymorphic association" do
before { subject.class.decorates_association :thing, polymorphic: true }
it "makes the association return the right decorator" do
subject.thing.should be_decorated_with SomeThingDecorator
it "calls the DecoratedAssociation" do
Draper::DecoratedAssociation.stub(:new).and_return(decorated_association)
decorated_association.should_receive(:call).and_return(:decorated)
subject.similar_products.should be :decorated
end
end
end