Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add :namespace option to decorate #899

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 8 additions & 1 deletion lib/draper/collection_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class CollectionDecorator
# to each item's decorator.
attr_accessor :context

# @return [String, nil] decorator namespace to be used to instantiate decorator, and passed
# to each item's decorator.
attr_accessor :namespace

array_methods = Array.instance_methods - Object.instance_methods
delegate :==, :as_json, *array_methods, to: :decorated_collection

Expand All @@ -27,11 +31,14 @@ class CollectionDecorator
# @option options [Hash] :context ({})
# extra data to be stored in the collection decorator and used in
# user-defined methods, and passed to each item's decorator.
# @option options [String, nil] :namespace (nil)
# decorator namespace
def initialize(object, options = {})
options.assert_valid_keys(:with, :context)
options.assert_valid_keys(:with, :context, :namespace)
@object = object
@decorator_class = options[:with]
@context = options.fetch(:context, {})
@namespace = options.delete(:namespace)
end

class << self
Expand Down
14 changes: 8 additions & 6 deletions lib/draper/decoratable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ module Decoratable
# @param [Hash] options
# see {Decorator#initialize}
def decorate(options = {})
decorator_class.decorate(self, options)
decorator_class(namespace: options.delete(: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: namespace)
end

def decorator_class?
Expand Down Expand Up @@ -55,7 +55,7 @@ module ClassMethods
# @param [Hash] options
# see {Decorator.decorate_collection}.
def decorate(options = {})
decorator_class.decorate_collection(all, options.reverse_merge(with: nil))
decorator_class(namespace: options[:namespace]).decorate_collection(all, options.reverse_merge(with: nil))
end

def decorator_class?
Expand All @@ -68,9 +68,11 @@ def decorator_class?
# `Product` maps to `ProductDecorator`).
#
# @return [Class] the inferred decorator class.
def decorator_class(called_on = self)
def decorator_class(called_on = self, namespace: nil)
prefix = respond_to?(:model_name) ? model_name : name
decorator_name = "#{prefix}Decorator"
namespace = "#{namespace}::" if namespace.present?

decorator_name = "#{namespace}#{prefix}Decorator"
decorator_name_constant = decorator_name.safe_constantize
return decorator_name_constant unless decorator_name_constant.nil?

Expand Down
5 changes: 3 additions & 2 deletions lib/draper/decorated_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module Draper
# @private
class DecoratedAssociation
def initialize(owner, association, options)
options.assert_valid_keys(:with, :scope, :context)
options.assert_valid_keys(:with, :scope, :context, :namespace)

@owner = owner
@association = association
Expand All @@ -11,7 +11,8 @@ def initialize(owner, association, options)

decorator_class = options[:with]
context = options.fetch(:context, ->(context){ context })
@factory = Draper::Factory.new(with: decorator_class, context: context)
namespace = options[:namespace] || owner.namespace
@factory = Draper::Factory.new(with: decorator_class, context: context, namespace: namespace)
end

def call
Expand Down
12 changes: 9 additions & 3 deletions lib/draper/decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,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, :scope, :context, :namespace)
define_method(association) do
decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
decorated_associations[association].call
Expand Down Expand Up @@ -138,8 +138,9 @@ def self.decorates_associations(*associations)
# @option options [Hash] :context
# extra data to be stored in the collection decorator.
def self.decorate_collection(object, options = {})
options.assert_valid_keys(:with, :context)
collection_decorator_class.new(object, options.reverse_merge(with: self))
options.assert_valid_keys(:with, :context, :namespace)
options[:with] ||= self
collection_decorator_class.new(object, options)
end

# @return [Array<Class>] the list of decorators that have been applied to
Expand Down Expand Up @@ -218,6 +219,10 @@ def attributes
object.attributes.select {|attribute, _| respond_to?(attribute) }
end

def namespace
self.class.to_s.deconstantize.presence
end

# ActiveModel compatibility
delegate :to_param, :to_partial_path

Expand Down Expand Up @@ -250,6 +255,7 @@ def self.object_class_name

def self.inferred_object_class
name = object_class_name
name = name.split('::').last unless name.nil?
name_constant = name&.safe_constantize
return name_constant unless name_constant.nil?

Expand Down
16 changes: 9 additions & 7 deletions lib/draper/factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ class Factory
# 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, :context, :namespace)
@decorator_class = options.delete(:with)
@namespace = options.delete(:namespace)
@default_options = options
end

Expand All @@ -28,18 +29,19 @@ def initialize(options = {})
# @return [Decorator, CollectionDecorator] the decorated object.
def decorate(object, options = {})
return nil if object.nil?
Worker.new(decorator_class, object).call(options.reverse_merge(default_options))
Worker.new(decorator_class, object, namespace).call(options.reverse_merge(default_options))
end

private

attr_reader :decorator_class, :default_options
attr_reader :decorator_class, :namespace, :default_options

# @private
class Worker
def initialize(decorator_class, object)
def initialize(decorator_class, object, namespace)
@decorator_class = decorator_class
@object = object
@namespace = namespace
end

def call(options)
Expand All @@ -56,13 +58,13 @@ def decorator

private

attr_reader :decorator_class, :object
attr_reader :decorator_class, :object, :namespace

def object_decorator
if collection?
->(object, options) { object.decorator_class.decorate_collection(object, options.reverse_merge(with: nil))}
->(object, options) { object.decorator_class(namespace: namespace).decorate_collection(object, options.reverse_merge(with: nil))}
else
->(object, options) { object.decorate(options) }
->(object, options) { object.decorate(options.merge(namespace: namespace)) }
end
end

Expand Down
6 changes: 6 additions & 0 deletions spec/draper/decoratable_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,12 @@ module Draper
end
end

context "when namespace is passed explicitly" do
it "returns namespaced decorator class" do
expect(Product.decorator_class(namespace: Namespaced)).to be Namespaced::ProductDecorator
end
end

context "when the decorator contains name error" do
it "throws an NameError" do
# We imitate ActiveSupport::Autoload behavior here in order to cause lazy NameError exception raising
Expand Down
16 changes: 8 additions & 8 deletions spec/draper/decorated_association_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ module Draper
it "creates a factory" do
options = {with: Decorator, context: {foo: "bar"}}

expect(Factory).to receive(:new).with(options)
DecoratedAssociation.new(double, :association, options)
expect(Factory).to receive(:new).with(options.merge(namespace: nil))
DecoratedAssociation.new(double(namespace: nil), :association, options)
end

describe ":with option" do
it "defaults to nil" do
expect(Factory).to receive(:new).with(with: nil, context: anything())
DecoratedAssociation.new(double, :association, {})
expect(Factory).to receive(:new).with(with: nil, context: anything(), namespace: nil)
DecoratedAssociation.new(double(namespace: nil), :association, {})
end
end

Expand All @@ -31,7 +31,7 @@ module Draper
expect(Factory).to receive(:new) do |options|
options[:context].call(:anything) == :anything
end
DecoratedAssociation.new(double, :association, {})
DecoratedAssociation.new(double(namespace: nil), :association, {})
end
end
end
Expand All @@ -43,7 +43,7 @@ module Draper
associated = double
owner_context = {foo: "bar"}
object = double(association: associated)
owner = double(object: object, context: owner_context)
owner = double(object: object, context: owner_context, namespace: nil)
decorated_association = DecoratedAssociation.new(owner, :association, {})
decorated = double

Expand All @@ -54,7 +54,7 @@ module Draper
it "memoizes" do
factory = double
allow(Factory).to receive_messages(new: factory)
owner = double(object: double(association: double), context: {})
owner = double(object: double(association: double), context: {}, namespace: nil)
decorated_association = DecoratedAssociation.new(owner, :association, {})
decorated = double

Expand All @@ -69,7 +69,7 @@ module Draper
allow(Factory).to receive_messages(new: factory)
scoped = double
object = double(association: double(applied_scope: scoped))
owner = double(object: object, context: {})
owner = double(object: object, context: {}, namespace: nil)
decorated_association = DecoratedAssociation.new(owner, :association, scope: :applied_scope)
decorated = double

Expand Down
19 changes: 17 additions & 2 deletions spec/draper/decorator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ module Draper
before { allow(CollectionDecorator).to receive(:new) }

it "does not raise error on valid options" do
valid_options = {with: OtherDecorator, context: {}}
valid_options = {with: OtherDecorator, context: {}, namespace: nil}
expect{Decorator.decorate_collection([], valid_options)}.not_to raise_error
end

Expand Down Expand Up @@ -197,7 +197,7 @@ module Draper
end

it "infers the object class for namespaced decorators" do
expect(Namespaced::ProductDecorator.object_class).to be Namespaced::Product
expect(Namespaced::ProductDecorator.object_class).to be Product
end

context "when an unrelated NameError is thrown" do
Expand Down Expand Up @@ -471,6 +471,21 @@ module Draper
end
end

describe "#namespace" do
it "returns own module nesting" do
decorator = Namespaced::ProductDecorator.new(double)
expect(decorator.namespace).to eq("Namespaced")
end

context "when class has no nesting" do
it "returns nil" do
::TopLevelDecorator = Class.new(Draper::Decorator)
decorator = TopLevelDecorator.new(double)
expect(decorator.namespace).to eq(nil)
end
end
end

describe ".model_name" do
it "delegates to the object class" do
allow(Decorator).to receive(:object_class).and_return(double(model_name: :delegated))
Expand Down