Skip to content

Commit

Permalink
use current module's namespace for decorates_association
Browse files Browse the repository at this point in the history
  • Loading branch information
Ivan Denisov authored and Alexander-Senko committed Aug 30, 2024
1 parent 9daf3ac commit c028da1
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 39 deletions.
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
2 changes: 1 addition & 1 deletion lib/draper/decorated_association.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def initialize(owner, association, options)

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

def call
Expand Down
7 changes: 6 additions & 1 deletion lib/draper/decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ def self.decorates_associations(*associations)
# 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[: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
12 changes: 7 additions & 5 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,7 +29,7 @@ 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
Expand All @@ -37,9 +38,10 @@ def decorate(object, 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 @@ -60,9 +62,9 @@ def decorator

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
15 changes: 15 additions & 0 deletions spec/draper/decorator_spec.rb
Original file line number Diff line number Diff line change
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
36 changes: 18 additions & 18 deletions spec/draper/factory_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ module Draper
factory = Factory.new
object = double

expect(Factory::Worker).to receive(:new).with(anything(), object).and_return(->(*){})
expect(Factory::Worker).to receive(:new).with(anything(), object, anything()).and_return(->(*){})
factory.decorate(object)
end

Expand All @@ -43,7 +43,7 @@ module Draper
decorator_class = double
factory = Factory.new(with: decorator_class)

expect(Factory::Worker).to receive(:new).with(decorator_class, anything()).and_return(->(*){})
expect(Factory::Worker).to receive(:new).with(decorator_class, anything(), anything()).and_return(->(*){})
factory.decorate(double)
end
end
Expand All @@ -52,7 +52,7 @@ module Draper
it "passes nil to the worker" do
factory = Factory.new

expect(Factory::Worker).to receive(:new).with(nil, anything()).and_return(->(*){})
expect(Factory::Worker).to receive(:new).with(nil, anything(), anything()).and_return(->(*){})
factory.decorate(double)
end
end
Expand Down Expand Up @@ -94,7 +94,7 @@ module Draper
it "calls the decorator method" do
object = double
options = {foo: "bar"}
worker = Factory::Worker.new(double, object)
worker = Factory::Worker.new(double, object, nil)
decorator = ->(*){}
allow(worker).to receive(:decorator){ decorator }

Expand All @@ -104,7 +104,7 @@ module Draper

context "when the :context option is callable" do
it "calls it" do
worker = Factory::Worker.new(double, double)
worker = Factory::Worker.new(double, double, nil)
decorator = ->(*){}
allow(worker).to receive_messages decorator: decorator
context = {foo: "bar"}
Expand All @@ -114,7 +114,7 @@ module Draper
end

it "receives arguments from the :context_args option" do
worker = Factory::Worker.new(double, double)
worker = Factory::Worker.new(double, double, nil)
allow(worker).to receive_messages decorator: ->(*){}
context = ->{}

Expand All @@ -123,7 +123,7 @@ module Draper
end

it "wraps non-arrays passed to :context_args" do
worker = Factory::Worker.new(double, double)
worker = Factory::Worker.new(double, double, nil)
allow(worker).to receive_messages decorator: ->(*){}
context = ->{}
hash = {foo: "bar"}
Expand All @@ -135,7 +135,7 @@ module Draper

context "when the :context option is not callable" do
it "doesn't call it" do
worker = Factory::Worker.new(double, double)
worker = Factory::Worker.new(double, double, nil)
decorator = ->(*){}
allow(worker).to receive_messages decorator: decorator
context = {foo: "bar"}
Expand All @@ -146,7 +146,7 @@ module Draper
end

it "does not pass the :context_args option to the decorator" do
worker = Factory::Worker.new(double, double)
worker = Factory::Worker.new(double, double, nil)
decorator = ->(*){}
allow(worker).to receive_messages decorator: decorator

Expand All @@ -160,7 +160,7 @@ module Draper
context "when decorator_class is specified" do
it "returns the .decorate method from the decorator" do
decorator_class = Class.new(Decorator)
worker = Factory::Worker.new(decorator_class, double)
worker = Factory::Worker.new(decorator_class, double, nil)

expect(worker.decorator).to eq decorator_class.method(:decorate)
end
Expand All @@ -171,17 +171,17 @@ module Draper
it "returns the object's #decorate method" do
object = double
options = {foo: "bar"}
worker = Factory::Worker.new(nil, object)
worker = Factory::Worker.new(nil, object, nil)

expect(object).to receive(:decorate).with(options).and_return(:decorated)
expect(object).to receive(:decorate).with(options.merge(namespace: nil)).and_return(:decorated)
expect(worker.decorator.call(object, options)).to be :decorated
end
end

context "and the object is not decoratable" do
it "raises an error" do
object = double
worker = Factory::Worker.new(nil, object)
worker = Factory::Worker.new(nil, object, nil)

expect{worker.decorator}.to raise_error UninferrableDecoratorError
end
Expand All @@ -193,7 +193,7 @@ module Draper
object = Struct.new(:stuff).new("things")

decorator_class = Class.new(Decorator)
worker = Factory::Worker.new(decorator_class, object)
worker = Factory::Worker.new(decorator_class, object, nil)

expect(worker.decorator).to eq decorator_class.method(:decorate)
end
Expand All @@ -204,7 +204,7 @@ module Draper
context "when decorator_class is a CollectionDecorator" do
it "returns the .decorate method from the collection decorator" do
decorator_class = Class.new(CollectionDecorator)
worker = Factory::Worker.new(decorator_class, [])
worker = Factory::Worker.new(decorator_class, [], nil)

expect(worker.decorator).to eq decorator_class.method(:decorate)
end
Expand All @@ -213,7 +213,7 @@ module Draper
context "when decorator_class is a Decorator" do
it "returns the .decorate_collection method from the decorator" do
decorator_class = Class.new(Decorator)
worker = Factory::Worker.new(decorator_class, [])
worker = Factory::Worker.new(decorator_class, [], nil)

expect(worker.decorator).to eq decorator_class.method(:decorate_collection)
end
Expand All @@ -226,7 +226,7 @@ module Draper
decorator_class = Class.new(Decorator)
allow(object).to receive(:decorator_class){ decorator_class }
allow(object).to receive(:decorate){ nil }
worker = Factory::Worker.new(nil, object)
worker = Factory::Worker.new(nil, object, nil)

expect(decorator_class).to receive(:decorate_collection).with(object, {foo: "bar", with: nil}).and_return(:decorated)
expect(worker.decorator.call(object, foo: "bar")).to be :decorated
Expand All @@ -235,7 +235,7 @@ module Draper

context "and the object is not decoratable" do
it "returns the .decorate method from CollectionDecorator" do
worker = Factory::Worker.new(nil, [])
worker = Factory::Worker.new(nil, [], nil)

expect(worker.decorator).to eq CollectionDecorator.method(:decorate)
end
Expand Down

0 comments on commit c028da1

Please sign in to comment.