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:
commit
07e19ccfbf
12 changed files with 55 additions and 137 deletions
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
|
Loading…
Reference in a new issue