Permalink
Browse files

Add option to specify namespace for decorator lookup

To use different decorators in different contexts (e.g. HTML rendering
vs. API representation), the ability to specify a decorator namespace is
added. For example:

    > Product.new.decorate
    => #<ProductDecorator:0x0000010b6e47e8>
    > Product.new.decorate(:namespace => HTML)
    => #<HTML::ProductDecorator:0x0000010b6e47e9>
  • Loading branch information...
1 parent f4dd9e0 commit b1d66ca15c076e7ddd0f13230d4d25b099395c70 @urbanautomaton urbanautomaton committed Feb 18, 2013
@@ -8,6 +8,10 @@ class CollectionDecorator
# {#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
@@ -26,9 +30,10 @@ class CollectionDecorator
# 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, :context)
+ options.assert_valid_keys(:with, :namespace, :context)
@source = source
@decorator_class = options[:with]
+ @decorator_namespace = options[:namespace]
@context = options.fetch(:context, {})
end
@@ -73,7 +78,7 @@ def decorated?
# Decorates the given item.
def decorate_item(item)
- item_decorator.call(item, context: context)
+ item_decorator.call(item, namespace: decorator_namespace, context: context)
end
private
View
@@ -15,12 +15,13 @@ module Decoratable
# @param [Hash] options
# see {Decorator#initialize}
def decorate(options = {})
- decorator_class.decorate(self, options)
+ namespace = options.delete(:namespace)
+ decorator_class(namespace).decorate(self, options)
end
# (see ClassMethods#decorator_class)
- def decorator_class
- self.class.decorator_class
+ def decorator_class(namespace=nil)
+ self.class.decorator_class(namespace)
end
# The list of decorators that have been applied to the object.
@@ -52,16 +53,20 @@ module ClassMethods
# @param [Hash] options
# see {Decorator.decorate_collection}.
def decorate(options = {})
- decorator_class.decorate_collection(scoped, options.reverse_merge(with: nil))
+ decorator_class(options[:namespace]).decorate_collection(scoped, 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.
- def decorator_class
- prefix = respond_to?(:model_name) ? model_name : name
- "#{prefix}Decorator".constantize
+ # @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("::")
+
+ decorator_name.constantize
rescue NameError
raise Draper::UninferrableDecoratorError.new(self)
end
@@ -3,16 +3,18 @@ module Draper
class DecoratedAssociation
def initialize(owner, association, options)
- options.assert_valid_keys(:with, :scope, :context)
+ options.assert_valid_keys(:with, :namespace, :scope, :context)
@owner = owner
@association = association
@scope = options[:scope]
decorator_class = options[:with]
+ namespace = options[:namespace]
context = options.fetch(:context, ->(context){ context })
- @factory = Draper::Factory.new(with: decorator_class, context: context)
+
+ @factory = Draper::Factory.new(with: decorator_class, namespace: namespace, context: context)
end
def call
@@ -22,6 +22,10 @@ module DecoratesAssigned
# @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
@@ -27,7 +27,7 @@ class Decorator
# extra data to be stored in the decorator and used in user-defined
# methods.
def initialize(source, options = {})
- options.assert_valid_keys(:context)
+ options.assert_valid_keys(:context, :namespace)
@source = source
@context = options.fetch(:context, {})
handle_multiple_decoration(options) if source.instance_of?(self.class)
@@ -91,6 +91,10 @@ def self.decorates_finders
# 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
@@ -100,7 +104,7 @@ def self.decorates_finders
# context and should return a new context hash for the association.
# @return [void]
def self.decorates_association(association, options = {})
- options.assert_valid_keys(:with, :scope, :context)
+ options.assert_valid_keys(:with, :namespace, :scope, :context)
define_method(association) do
decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
decorated_associations[association].call
@@ -131,11 +135,15 @@ def self.decorates_associations(*associations)
# @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, :context)
- collection_decorator_class.new(source, options.reverse_merge(with: self))
+ options.assert_valid_keys(:with, :namespace, :context)
+ collection_decorator_class(options[:namespace]).new(source, options.reverse_merge(with: self))
end
# @return [Array<Class>] the list of decorators that have been applied to
@@ -203,8 +211,8 @@ def attributes
singleton_class.delegate :model_name, to: :source_class
# @return [Class] the class created by {decorate_collection}.
- def self.collection_decorator_class
- collection_decorator_name.constantize
+ def self.collection_decorator_class(namespace=nil)
+ collection_decorator_name(namespace).constantize
rescue NameError
Draper::CollectionDecorator
end
@@ -222,10 +230,10 @@ def self.inferred_source_class
raise Draper::UninferrableSourceError.new(self)
end
- def self.collection_decorator_name
+ def self.collection_decorator_name(namespace=nil)
plural = source_name.pluralize
raise NameError if plural == source_name
- "#{plural}Decorator"
+ [(namespace && namespace.name), "#{plural}Decorator"].compact.join("::")
end
def handle_multiple_decoration(options)
View
@@ -5,12 +5,16 @@ class Factory
# @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, :context)
+ options.assert_valid_keys(:with, :namespace, :context)
@decorator_class = options.delete(:with)
@default_options = options
end
@@ -44,20 +48,20 @@ def initialize(decorator_class, source)
def call(options)
update_context options
- decorator.call(source, options)
+ decorator(options[:namespace]).call(source, options)
end
- def decorator
- return collection_decorator if collection?
- decorator_class.method(:decorate)
+ def decorator(namespace=nil)
+ return collection_decorator(namespace) if collection?
+ decorator_class(namespace).method(:decorate)
end
private
attr_reader :source
- def collection_decorator
- klass = decorator_class || Draper::CollectionDecorator
+ def collection_decorator(namespace=nil)
+ klass = decorator_class(namespace) || Draper::CollectionDecorator
if klass.respond_to?(:decorate_collection)
klass.method(:decorate_collection)
@@ -70,12 +74,12 @@ def collection?
source.respond_to?(:first)
end
- def decorator_class
- @decorator_class || source_decorator_class
+ def decorator_class(namespace=nil)
+ @decorator_class || source_decorator_class(namespace)
end
- def source_decorator_class
- source.decorator_class if source.respond_to?(:decorator_class)
+ def source_decorator_class(namespace=nil)
+ source.decorator_class(namespace) if source.respond_to?(:decorator_class)
end
def update_context(options)
@@ -37,6 +37,22 @@ 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"})
@@ -156,6 +156,22 @@ 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, context: {}}
+ valid_options = {with: Decorator, scope: :foo, namespace: DecoratorNamespace, context: {}}
expect{DecoratedAssociation.new(Decorator.new(Model.new), :association, valid_options)}.not_to raise_error
end
@@ -14,15 +14,22 @@ module Draper
end
it "creates a factory" do
- options = {with: Decorator, context: {foo: "bar"}}
+ options = {with: Decorator, namespace: nil, context: {foo: "bar"}}
Factory.should_receive(:new).with(options)
DecoratedAssociation.new(double, :association, options)
end
describe ":with option" do
it "defaults to nil" do
- Factory.should_receive(:new).with(with: nil, context: anything())
+ 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())
DecoratedAssociation.new(double, :association, {})
end
end
@@ -145,6 +145,15 @@ module Draper
ProductDecorator.decorate_collection([], options)
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
end
describe ".decorates" do
@@ -178,6 +178,18 @@ module Draper
expect(worker.decorator).to eq decorator_class.method(:decorate)
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)
+
+ source.should_receive(:decorator_class).with(namespace)
+ expect(worker.decorator(namespace)).to eq decorator_class.method(:decorate)
+ end
+ end
end
context "for a collection source" do
View
@@ -26,6 +26,13 @@ class ProductDecorator < Draper::Decorator; end
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) }

0 comments on commit b1d66ca

Please sign in to comment.