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

Merge pull request #494 from haines/sti

Support STI in decorated associations
This commit is contained in:
Steve Klabnik 2013-03-11 12:14:30 -07:00
commit 07e19ccfbf
12 changed files with 55 additions and 137 deletions

View file

@ -8,10 +8,6 @@ module Draper
# {#initialize}.
attr_reader :decorator_class
# @return [Module] then namespace passed to each item if necessary to
# infer a decorator class, as set by {#initialize}
attr_reader :decorator_namespace
# @return [Hash] extra data to be used in user-defined methods, and passed
# to each item's decorator.
attr_accessor :context
@ -30,10 +26,9 @@ module Draper
# extra data to be stored in the collection decorator and used in
# user-defined methods, and passed to each item's decorator.
def initialize(source, options = {})
options.assert_valid_keys(:with, :namespace, :context)
options.assert_valid_keys(:with, :context)
@source = source
@decorator_class = options[:with]
@decorator_namespace = options[:namespace]
@context = options.fetch(:context, {})
end
@ -83,7 +78,7 @@ module Draper
# Decorates the given item.
def decorate_item(item)
item_decorator.call(item, namespace: decorator_namespace, context: context)
item_decorator.call(item, context: context)
end
private

View file

@ -15,13 +15,12 @@ module Draper
# @param [Hash] options
# see {Decorator#initialize}
def decorate(options = {})
namespace = options.delete(:namespace)
decorator_class(namespace).decorate(self, options)
decorator_class.decorate(self, options)
end
# (see ClassMethods#decorator_class)
def decorator_class(namespace=nil)
self.class.decorator_class(namespace)
def decorator_class
self.class.decorator_class
end
# The list of decorators that have been applied to the object.
@ -54,19 +53,16 @@ module Draper
# see {Decorator.decorate_collection}.
def decorate(options = {})
collection = Rails::VERSION::MAJOR >= 4 ? all : scoped
decorator_class(options[:namespace]).decorate_collection(collection, options.reverse_merge(with: nil))
decorator_class.decorate_collection(collection, options.reverse_merge(with: nil))
end
# Infers the decorator class to be used by {Decoratable#decorate} (e.g.
# `Product` maps to `ProductDecorator`).
#
# @return [Class] the inferred decorator class.
# @param [Module] namespace (nil)
# see {Decorator.decorate_collection}
def decorator_class(namespace=nil)
prefix = respond_to?(:model_name) ? model_name : name
decorator_name = [(namespace && namespace.name), "#{prefix}Decorator"].compact.join("::")
def decorator_class
prefix = respond_to?(:model_name) ? model_name : name
decorator_name = "#{prefix}Decorator"
decorator_name.constantize
rescue NameError => error
raise unless error.missing_name?(decorator_name)

View file

@ -3,7 +3,7 @@ module Draper
class DecoratedAssociation
def initialize(owner, association, options)
options.assert_valid_keys(:with, :namespace, :scope, :context)
options.assert_valid_keys(:with, :scope, :context)
@owner = owner
@association = association
@ -11,10 +11,8 @@ module Draper
@scope = options[:scope]
decorator_class = options[:with]
namespace = options[:namespace]
context = options.fetch(:context, ->(context){ context })
@factory = Draper::Factory.new(with: decorator_class, namespace: namespace, context: context)
@factory = Draper::Factory.new(with: decorator_class, context: context)
end
def call

View file

@ -22,10 +22,6 @@ module Draper
# @option options [Decorator, CollectionDecorator] :with (nil)
# decorator class to use. If nil, it is inferred from the instance
# variable.
# @option options [Module, nil] :namespace (nil)
# a namespace within which to look for inferred decorators (e.g. if
# +:namespace => API+, a model +Product+ would be decorated with
# +API::ProductDecorator+ (if defined)
# @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.

View file

@ -27,7 +27,7 @@ module Draper
# extra data to be stored in the decorator and used in user-defined
# methods.
def initialize(source, options = {})
options.assert_valid_keys(:context, :namespace)
options.assert_valid_keys(:context)
@source = source
@context = options.fetch(:context, {})
handle_multiple_decoration(options) if source.instance_of?(self.class)
@ -91,10 +91,6 @@ module Draper
# name of the association to decorate (e.g. `:products`).
# @option options [Class] :with
# the decorator to apply to the association.
# @option options [Module, nil] :namespace (nil)
# a namespace within which to look for an inferred decorator (e.g. if
# +:namespace => API+, a model +Product+ would be decorated with
# +API::ProductDecorator+ (if defined)
# @option options [Symbol] :scope
# a scope to apply when fetching the association.
# @option options [Hash, #call] :context
@ -104,7 +100,7 @@ module Draper
# context and should return a new context hash for the association.
# @return [void]
def self.decorates_association(association, options = {})
options.assert_valid_keys(:with, :namespace, :scope, :context)
options.assert_valid_keys(:with, :scope, :context)
define_method(association) do
decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
decorated_associations[association].call
@ -135,15 +131,11 @@ module Draper
# @option options [Class, nil] :with (self)
# the decorator class used to decorate each item. When `nil`, it is
# inferred from each item.
# @option options [Module, nil] :namespace (nil)
# a namespace within which to look for an inferred decorator (e.g. if
# +:namespace => API+, a model +Product+ would be decorated with
# +API::ProductDecorator+ (if defined)
# @option options [Hash] :context
# extra data to be stored in the collection decorator.
def self.decorate_collection(source, options = {})
options.assert_valid_keys(:with, :namespace, :context)
collection_decorator_class(options[:namespace]).new(source, options.reverse_merge(with: self))
options.assert_valid_keys(:with, :context)
collection_decorator_class.new(source, options.reverse_merge(with: self))
end
# @return [Array<Class>] the list of decorators that have been applied to
@ -211,8 +203,8 @@ module Draper
singleton_class.delegate :model_name, to: :source_class
# @return [Class] the class created by {decorate_collection}.
def self.collection_decorator_class(namespace=nil)
name = collection_decorator_name(namespace)
def self.collection_decorator_class
name = collection_decorator_name
name.constantize
rescue NameError => error
raise if name && !error.missing_name?(name)
@ -234,10 +226,10 @@ module Draper
raise Draper::UninferrableSourceError.new(self)
end
def self.collection_decorator_name(namespace=nil)
def self.collection_decorator_name
plural = source_name.pluralize
raise NameError if plural == source_name
[(namespace && namespace.name), "#{plural}Decorator"].compact.join("::")
"#{plural}Decorator"
end
def handle_multiple_decoration(options)

View file

@ -5,16 +5,12 @@ module Draper
# @option options [Decorator, CollectionDecorator] :with (nil)
# decorator class to use. If nil, it is inferred from the object
# passed to {#decorate}.
# @option options [Module, nil] :namespace (nil)
# a namespace within which to look for an inferred decorator (e.g. if
# +:namespace => API+, a model +Product+ would be decorated with
# +API::ProductDecorator+ (if defined)
# @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, :namespace, :context)
options.assert_valid_keys(:with, :context)
@decorator_class = options.delete(:with)
@default_options = options
end
@ -48,22 +44,26 @@ module Draper
def call(options)
update_context options
decorator(options[:namespace]).call(source, options)
decorator.call(source, options)
end
def decorator(namespace=nil)
return collection_decorator(namespace) if collection?
decorator_class(namespace).method(:decorate)
def decorator
return decorator_method(decorator_class) if decorator_class
return source_decorator if decoratable?
return decorator_method(Draper::CollectionDecorator) if collection?
raise Draper::UninferrableDecoratorError.new(source.class)
end
private
attr_reader :source
attr_reader :decorator_class, :source
def collection_decorator(namespace=nil)
klass = decorator_class(namespace) || Draper::CollectionDecorator
def source_decorator
->(source, options) { source.decorate(options) }
end
if klass.respond_to?(:decorate_collection)
def decorator_method(klass)
if collection? && klass.respond_to?(:decorate_collection)
klass.method(:decorate_collection)
else
klass.method(:decorate)
@ -74,12 +74,8 @@ module Draper
source.respond_to?(:first)
end
def decorator_class(namespace=nil)
@decorator_class || source_decorator_class(namespace)
end
def source_decorator_class(namespace=nil)
source.decorator_class(namespace) if source.respond_to?(:decorator_class)
def decoratable?
source.respond_to?(:decorate)
end
def update_context(options)

View file

@ -37,22 +37,6 @@ module Draper
end
end
describe "with decorator namespace" do
it "stores the namespace itself" do
decorator = CollectionDecorator.new([], namespace: DecoratorNamespace)
expect(decorator.decorator_namespace).to be DecoratorNamespace
end
it "passes the namespace to the individual decorators" do
decorator = CollectionDecorator.new([Product.new, Product.new], namespace: DecoratorNamespace)
decorator.each do |item|
expect(item).to be_an_instance_of(DecoratorNamespace::ProductDecorator)
end
end
end
describe "#context=" do
it "updates the stored context" do
decorator = CollectionDecorator.new([], context: {some: "context"})

View file

@ -158,22 +158,6 @@ module Draper
end
end
context "when a namespace is supplied" do
context "for classes" do
it "infers the decorator from the class and provided namespace" do
expect(Product.decorator_class(DecoratorNamespace)).to be DecoratorNamespace::ProductDecorator
end
end
context "for ActiveModel classes" do
it "infers the decorator from the model name and provided namespace" do
Product.stub(:model_name).and_return("Other")
expect(Product.decorator_class(DecoratorNamespace)).to be DecoratorNamespace::OtherDecorator
end
end
end
context "when the decorator can't be inferred" do
it "throws an UninferrableDecoratorError" do
expect{Model.decorator_class}.to raise_error UninferrableDecoratorError

View file

@ -5,7 +5,7 @@ module Draper
describe "#initialize" do
it "accepts valid options" do
valid_options = {with: Decorator, scope: :foo, namespace: DecoratorNamespace, context: {}}
valid_options = {with: Decorator, scope: :foo, context: {}}
expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, valid_options)}.not_to raise_error
end
@ -14,7 +14,7 @@ module Draper
end
it "creates a factory" do
options = {with: Decorator, namespace: nil, context: {foo: "bar"}}
options = {with: Decorator, context: {foo: "bar"}}
Factory.should_receive(:new).with(options)
DecoratedAssociation.new(double, :association, options)
@ -22,14 +22,7 @@ module Draper
describe ":with option" do
it "defaults to nil" do
Factory.should_receive(:new).with(with: nil, namespace: anything(), context: anything())
DecoratedAssociation.new(double, :association, {})
end
end
describe ":namespace option" do
it "defaults to nil" do
Factory.should_receive(:new).with(with: anything(), namespace: nil, context: anything())
Factory.should_receive(:new).with(with: nil, context: anything())
DecoratedAssociation.new(double, :association, {})
end
end

View file

@ -146,15 +146,6 @@ module Draper
end
end
context "with a custom decorator namespace" do
it "passes the namespace option to the collection decorator" do
source = [Model.new]
CollectionDecorator.should_receive(:new).with(source, with: nil, namespace: DecoratorNamespace)
Decorator.decorate_collection(source, with: nil, namespace: DecoratorNamespace)
end
end
context "when a NameError is thrown" do
it "re-raises that error" do
String.any_instance.stub(:constantize).and_return{Draper::DecoratedEnumerableProxy}

View file

@ -170,24 +170,24 @@ module Draper
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)
context "and the source is decoratable" do
it "returns the source's #decorate method" do
source = double
options = {foo: "bar"}
worker = Factory::Worker.new(nil, source)
expect(worker.decorator).to eq decorator_class.method(:decorate)
source.should_receive(:decorate).with(options).and_return(:decorated)
expect(worker.decorator.call(source, options)).to be :decorated
end
end
end
context "when a decorator namespace is supplied" do
it "passes the namespace option to the source when finding the decorator" do
decorator_class = Class.new(Decorator)
namespace = Module.new
source = double(decorator_class: decorator_class)
worker = Factory::Worker.new(nil, source)
context "and the source is not decoratable" do
it "raises an error" do
source = double
worker = Factory::Worker.new(nil, source)
source.should_receive(:decorator_class).with(namespace)
expect(worker.decorator(namespace)).to eq decorator_class.method(:decorate)
expect{worker.decorator}.to raise_error UninferrableDecoratorError
end
end
end
end
@ -213,13 +213,13 @@ module Draper
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)
it "returns the source's #decorate method" do
source = []
source.stub decorator_class: decorator_class
options = {foo: "bar"}
worker = Factory::Worker.new(nil, source)
expect(worker.decorator).to eq decorator_class.method(:decorate)
source.should_receive(:decorate).with(options).and_return(:decorated)
expect(worker.decorator.call(source, options)).to be :decorated
end
end

View file

@ -27,13 +27,6 @@ module Namespaced
class OtherDecorator < Draper::Decorator; end
end
module DecoratorNamespace
class ProductDecorator < Draper::Decorator; end
class ProductsDecorator < Draper::CollectionDecorator; end
class OtherDecorator < Draper::Decorator; end
end
# After each example, revert changes made to the class
def protect_class(klass)
before { stub_const klass.name, Class.new(klass) }