Skip to content

Loading…

Add option to specify namespace for decorator lookup #480

Merged
merged 1 commit into from

4 participants

@urbanautomaton

Hi,

In our app, we use decorators in a number of different contexts - e.g. constructing an API response, rendering HTML templates, and so forth. We keep separate decorators for each context, and namespace them to keep them organised, so our Activity model has a root-level decorator, but also API::ActivityDecorator and HTML::ActivityDecorator.

This patch adds a :namespace option to the various decoration methods, so that users can do things like this:

> Activity.last.decorate
=> #<ActivityDecorator:0x000001030d0ad0
  @context={},
  @source=#<Activity id:1>>
> Activity.last.decorate(:namespace => HTML)
=> #<HTML::ActivityDecorator:0x000001030d0ad0
  @context={},
  @source=#<Activity id:1>>

The option can also be passed to Decorator.decorate_collection, .decorates_association and the like:

module API
  class ActivityDecorator < Draper::Decorator
    decorates_association :comments, :namespace => API
  end
end
> Activity.last.decorate(:namespace => API).comments.first
=> #<API::CommentDecorator:0x000001030d0ad0
  @context={},
  @source=#<Comment id:1>>

Is this a feature you'd be interested in including? The ability to organise our decorators in this manner has certainly helped us a lot, and we think others might find it useful too...

Cheers,
Simon

@urbanautomaton urbanautomaton 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>
b1d66ca
@steveklabnik
drapergem member

Hmm. I am wary to write code that messes with Ruby's normal lookup semantics.

@urbanautomaton

Are we doing that? It adjusts how the constant name that's eventually looked up is put together, but I don't think we're changing the actual lookup semantics...

Edit: put another way, is prepending "SomeNamespace::" to a class name any different to appending "Decorator" to it?

@steveklabnik
drapergem member

You're right: I caught this PR before Travis was done building, so I didn't even look at your implementation. ;)

Seems fine. Let's do it!

@steveklabnik steveklabnik merged commit f93542c into drapergem:master

1 check passed

Details default The Travis build passed
@urbanautomaton

Nice one, thanks very much. :-)

@haines haines added a commit to haines/draper that referenced this pull request
@haines haines Revert "Merge pull request #480 from urbanautomaton/decorator-namespa…
…ce-option"

This reverts commit f93542c, reversing
changes made to f4dd9e0.
2444c09
@artempartos artempartos added a commit to artempartos/draper that referenced this pull request
@haines haines Revert "Merge pull request #480 from urbanautomaton/decorator-namespa…
…ce-option"

This reverts commit f93542c, reversing
changes made to f4dd9e0.
b6bf39f
@kryzhovnik

Why revert? Is this feature was rejected?

I was going to use it to separate "admin area" (Admin::ProductDecorator) decorators and public one (ProductDecorator). I think it is very useful feature.

@haines
drapergem member

@kryzhovnik Nope, as I noted on #494, I really like the feature too, but I think we need to find a way to do this without propagating the namespace option throughout the codebase.

@urbanautomaton urbanautomaton added a commit to urbanautomaton/draper that referenced this pull request
@urbanautomaton urbanautomaton 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>

Second attempt, following #480 and #494.
315ee51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 19, 2013
  1. @urbanautomaton

    Add option to specify namespace for decorator lookup

    urbanautomaton committed
    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>
View
9 lib/draper/collection_decorator.rb
@@ -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
19 lib/draper/decoratable.rb
@@ -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
View
6 lib/draper/decorated_association.rb
@@ -3,7 +3,7 @@ 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
@@ -11,8 +11,10 @@ def initialize(owner, association, options)
@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
View
4 lib/draper/decorates_assigned.rb
@@ -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
24 lib/draper/decorator.rb
@@ -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
26 lib/draper/factory.rb
@@ -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)
View
16 spec/draper/collection_decorator_spec.rb
@@ -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"})
View
16 spec/draper/decoratable_spec.rb
@@ -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
View
13 spec/draper/decorated_association_spec.rb
@@ -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,7 +14,7 @@ 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)
@@ -22,7 +22,14 @@ module Draper
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
View
9 spec/draper/decorator_spec.rb
@@ -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
View
12 spec/draper/factory_spec.rb
@@ -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
7 spec/spec_helper.rb
@@ -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) }
Something went wrong with that request. Please try again.