mirror of
https://github.com/drapergem/draper
synced 2023-03-27 23:21:17 -04:00
Simplify decorator inferral
This commit is contained in:
parent
a84d958497
commit
a70bc5d098
6 changed files with 167 additions and 338 deletions
|
@ -3,20 +3,19 @@ module Draper
|
|||
include Enumerable
|
||||
include ViewHelpers
|
||||
|
||||
attr_accessor :source, :context, :decorator_class
|
||||
attr_accessor :source, :context
|
||||
alias_method :to_source, :source
|
||||
|
||||
delegate :as_json, *(Array.instance_methods - Object.instance_methods), to: :decorated_collection
|
||||
array_methods = Array.instance_methods - Object.instance_methods
|
||||
delegate :as_json, *array_methods, to: :decorated_collection
|
||||
|
||||
# @param source collection to decorate
|
||||
# @param [Hash] options (optional)
|
||||
# @option options [Class, Symbol] :with the class used to decorate
|
||||
# items, or `:infer` to call each item's `decorate` method instead
|
||||
# @option options [Class] :with the class used to decorate items
|
||||
# @option options [Hash] :context context available to each item's decorator
|
||||
def initialize(source, options = {})
|
||||
options.assert_valid_keys(:with, :context)
|
||||
@source = source
|
||||
@decorator_class = options.fetch(:with) { self.class.inferred_decorator_class }
|
||||
@decorator_class = options[:with]
|
||||
@context = options.fetch(:context, {})
|
||||
end
|
||||
|
||||
|
@ -62,14 +61,14 @@ module Draper
|
|||
each {|item| item.context = value } if @decorated_collection
|
||||
end
|
||||
|
||||
def decorator_class
|
||||
@decorator_class ||= self.class.inferred_decorator_class
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def decorate_item(item)
|
||||
if decorator_class == :infer
|
||||
item.decorate(context: context)
|
||||
else
|
||||
decorator_class.decorate(item, context: context)
|
||||
end
|
||||
item_decorator.call(item, context: context)
|
||||
end
|
||||
|
||||
def self.inferred_decorator_class
|
||||
|
@ -85,5 +84,15 @@ module Draper
|
|||
def self.decorator_uninferrable
|
||||
raise Draper::UninferrableDecoratorError.new(self)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def item_decorator
|
||||
@item_decorator ||= begin
|
||||
decorator_class.method(:decorate)
|
||||
rescue Draper::UninferrableDecoratorError
|
||||
->(item, options) { item.decorate(options) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
module Draper
|
||||
class DecoratedAssociation
|
||||
|
||||
attr_reader :base, :association, :options
|
||||
|
||||
def initialize(base, association, options)
|
||||
@base = base
|
||||
@association = association
|
||||
def initialize(owner, association, options)
|
||||
options.assert_valid_keys(:with, :scope, :context)
|
||||
@options = options
|
||||
|
||||
@owner = owner
|
||||
@association = association
|
||||
|
||||
@decorator_class = options[:with]
|
||||
@scope = options[:scope]
|
||||
@context = options.fetch(:context, owner.context)
|
||||
end
|
||||
|
||||
def call
|
||||
return undecorated if undecorated.nil?
|
||||
decorate
|
||||
decorated
|
||||
end
|
||||
|
||||
def source
|
||||
base.source
|
||||
def context
|
||||
return @context.call(owner.context) if @context.respond_to?(:call)
|
||||
@context
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :owner, :association, :decorator_class, :scope
|
||||
|
||||
def source
|
||||
owner.source
|
||||
end
|
||||
|
||||
def undecorated
|
||||
@undecorated ||= begin
|
||||
associated = source.send(association)
|
||||
associated = associated.send(options[:scope]) if options[:scope]
|
||||
associated = associated.send(scope) if scope
|
||||
associated
|
||||
end
|
||||
end
|
||||
|
||||
def decorate
|
||||
@decorated ||= decorator_class.send(decorate_method, undecorated, decorator_options)
|
||||
end
|
||||
|
||||
def decorate_method
|
||||
if collection? && decorator_class.respond_to?(:decorate_collection)
|
||||
:decorate_collection
|
||||
else
|
||||
:decorate
|
||||
end
|
||||
def decorated
|
||||
@decorated ||= decorator.call(undecorated, context: context)
|
||||
end
|
||||
|
||||
def collection?
|
||||
undecorated.respond_to?(:first)
|
||||
end
|
||||
|
||||
def decorator_class
|
||||
return options[:with] if options[:with]
|
||||
def decorator
|
||||
return collection_decorator if collection?
|
||||
|
||||
if collection?
|
||||
options[:with] = :infer
|
||||
Draper::CollectionDecorator
|
||||
if decorator_class
|
||||
decorator_class.method(:decorate)
|
||||
else
|
||||
undecorated.decorator_class
|
||||
->(item, options) { item.decorate(options) }
|
||||
end
|
||||
end
|
||||
|
||||
def decorator_options
|
||||
decorator_class # Ensures options[:with] = :infer for unspecified collections
|
||||
def collection_decorator
|
||||
klass = decorator_class || Draper::CollectionDecorator
|
||||
|
||||
dec_options = collection? ? options.slice(:with, :context) : options.slice(:context)
|
||||
dec_options[:context] = base.context unless dec_options.key?(:context)
|
||||
if dec_options[:context].respond_to?(:call)
|
||||
dec_options[:context] = dec_options[:context].call(base.context)
|
||||
if klass.respond_to?(:decorate_collection)
|
||||
klass.method(:decorate_collection)
|
||||
else
|
||||
klass.method(:decorate)
|
||||
end
|
||||
dec_options
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,7 +23,6 @@ module Draper
|
|||
# multiple places in the chain.
|
||||
#
|
||||
# @param [Object] source object to decorate
|
||||
# @param [Hash] options (optional)
|
||||
# @option options [Hash] :context context available to the decorator
|
||||
def initialize(source, options = {})
|
||||
options.assert_valid_keys(:context)
|
||||
|
@ -136,8 +135,8 @@ module Draper
|
|||
# @param [Object] source collection to decorate
|
||||
# @param [Hash] options passed to each item's decorator (except
|
||||
# for the keys listed below)
|
||||
# @option options [Class,Symbol] :with (self) the class used to decorate
|
||||
# items, or `:infer` to call each item's `decorate` method instead
|
||||
# @option options [Class] :with (self) the class used to decorate
|
||||
# items
|
||||
# @option options [Hash] :context context available to decorated items
|
||||
def self.decorate_collection(source, options = {})
|
||||
options.assert_valid_keys(:with, :context)
|
||||
|
|
|
@ -79,40 +79,6 @@ describe Draper::CollectionDecorator do
|
|||
expect { Draper::CollectionDecorator.new(source, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
|
||||
end
|
||||
end
|
||||
|
||||
context "when the :with option is given" do
|
||||
context "and the decorator can't be inferred from the class" do
|
||||
subject { Draper::CollectionDecorator.new(source, with: ProductDecorator) }
|
||||
|
||||
it "uses the :with option" do
|
||||
subject.decorator_class.should be ProductDecorator
|
||||
end
|
||||
end
|
||||
|
||||
context "and the decorator is inferrable from the class" do
|
||||
subject { ProductsDecorator.new(source, with: SpecificProductDecorator) }
|
||||
|
||||
it "uses the :with option" do
|
||||
subject.decorator_class.should be SpecificProductDecorator
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the :with option is not given" do
|
||||
context "and the decorator can't be inferred from the class" do
|
||||
it "raises an UninferrableDecoratorError" do
|
||||
expect{Draper::CollectionDecorator.new(source)}.to raise_error Draper::UninferrableDecoratorError
|
||||
end
|
||||
end
|
||||
|
||||
context "and the decorator is inferrable from the class" do
|
||||
subject { ProductsDecorator.new(source) }
|
||||
|
||||
it "infers the decorator" do
|
||||
subject.decorator_class.should be ProductDecorator
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#source" do
|
||||
|
@ -125,6 +91,52 @@ describe Draper::CollectionDecorator do
|
|||
end
|
||||
end
|
||||
|
||||
describe "item decoration" do
|
||||
subject { subject_class.new(source, options) }
|
||||
let(:decorator_classes) { subject.decorated_collection.map(&:class) }
|
||||
let(:source) { [Product.new, Widget.new] }
|
||||
|
||||
context "when the :with option was given" do
|
||||
let(:options) { {with: SpecificProductDecorator} }
|
||||
|
||||
context "and the decorator can't be inferred from the class" do
|
||||
let(:subject_class) { Draper::CollectionDecorator }
|
||||
|
||||
it "uses the :with option" do
|
||||
decorator_classes.should == [SpecificProductDecorator, SpecificProductDecorator]
|
||||
end
|
||||
end
|
||||
|
||||
context "and the decorator is inferrable from the class" do
|
||||
let(:subject_class) { ProductsDecorator }
|
||||
|
||||
it "uses the :with option" do
|
||||
decorator_classes.should == [SpecificProductDecorator, SpecificProductDecorator]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the :with option was not given" do
|
||||
let(:options) { {} }
|
||||
|
||||
context "and the decorator can't be inferred from the class" do
|
||||
let(:subject_class) { Draper::CollectionDecorator }
|
||||
|
||||
it "infers the decorator from each item" do
|
||||
decorator_classes.should == [ProductDecorator, WidgetDecorator]
|
||||
end
|
||||
end
|
||||
|
||||
context "and the decorator is inferrable from the class" do
|
||||
let(:subject_class) { ProductsDecorator}
|
||||
|
||||
it "infers the decorator" do
|
||||
decorator_classes.should == [ProductDecorator, ProductDecorator]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#find" do
|
||||
context "with a block" do
|
||||
it "decorates Enumerable#find" do
|
||||
|
|
|
@ -1,313 +1,131 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Draper::DecoratedAssociation do
|
||||
let(:decorated_association) { Draper::DecoratedAssociation.new(base, association, options) }
|
||||
let(:decorated_association) { Draper::DecoratedAssociation.new(owner, :association, options) }
|
||||
let(:source) { Product.new }
|
||||
let(:base) { source.decorate }
|
||||
let(:owner) { source.decorate }
|
||||
let(:options) { {} }
|
||||
|
||||
describe "#initialize" do
|
||||
describe "options validation" do
|
||||
let(:association) { :similar_products }
|
||||
let(:valid_options) { {with: ProductDecorator, scope: :foo, context: {}} }
|
||||
|
||||
it "does not raise error on valid options" do
|
||||
expect { Draper::DecoratedAssociation.new(base, association, valid_options) }.to_not raise_error
|
||||
expect { Draper::DecoratedAssociation.new(owner, :association, valid_options) }.to_not raise_error
|
||||
end
|
||||
|
||||
it "raises error on invalid options" do
|
||||
expect { Draper::DecoratedAssociation.new(base, association, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
|
||||
expect { Draper::DecoratedAssociation.new(owner, :association, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#base" do
|
||||
subject { decorated_association.base }
|
||||
let(:association) { :similar_products }
|
||||
|
||||
it "returns the base decorator" do
|
||||
should be base
|
||||
end
|
||||
|
||||
it "returns a Decorator" do
|
||||
subject.class.should == ProductDecorator
|
||||
end
|
||||
end
|
||||
|
||||
describe "#source" do
|
||||
subject { decorated_association.source }
|
||||
let(:association) { :similar_products }
|
||||
|
||||
it "returns the base decorator's source" do
|
||||
should be base.source
|
||||
end
|
||||
|
||||
it "returns a Model" do
|
||||
subject.class.should == Product
|
||||
end
|
||||
end
|
||||
|
||||
describe "#call" do
|
||||
subject { decorated_association.call }
|
||||
let(:context) { {foo: "bar"} }
|
||||
let(:expected_options) { {context: context} }
|
||||
|
||||
context "for an ActiveModel collection association" do
|
||||
let(:association) { :similar_products }
|
||||
before do
|
||||
source.stub association: associated
|
||||
decorated_association.stub context: context
|
||||
end
|
||||
|
||||
context "when the association is not empty" do
|
||||
it "decorates the collection" do
|
||||
subject.should be_a Draper::CollectionDecorator
|
||||
context "for a singular association" do
|
||||
let(:associated) { Product.new }
|
||||
|
||||
context "when :with option was given" do
|
||||
let(:options) { {with: decorator} }
|
||||
let(:decorator) { SpecificProductDecorator }
|
||||
|
||||
it "uses the specified decorator" do
|
||||
decorator.should_receive(:decorate).with(associated, expected_options).and_return(:decorated)
|
||||
decorated_association.call.should be :decorated
|
||||
end
|
||||
end
|
||||
|
||||
context "when :with option was not given" do
|
||||
it "infers the decorator" do
|
||||
subject.decorator_class.should be :infer
|
||||
end
|
||||
end
|
||||
|
||||
context "when the association is empty" do
|
||||
it "returns an empty collection decorator" do
|
||||
source.stub(:similar_products).and_return([])
|
||||
subject.should be_a Draper::CollectionDecorator
|
||||
subject.should be_empty
|
||||
subject.first.should be_nil
|
||||
associated.should_receive(:decorate).with(expected_options).and_return(:decorated)
|
||||
decorated_association.call.should be :decorated
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for non-ActiveModel collection associations" do
|
||||
let(:association) { :poro_similar_products }
|
||||
context "for a collection association" do
|
||||
let(:associated) { [Product.new, Widget.new] }
|
||||
|
||||
context "when the association is not empty" do
|
||||
it "decorates the collection" do
|
||||
subject.should be_a Draper::CollectionDecorator
|
||||
end
|
||||
context "when :with option is a collection decorator" do
|
||||
let(:options) { {with: collection_decorator} }
|
||||
let(:collection_decorator) { ProductsDecorator }
|
||||
|
||||
it "infers the decorator" do
|
||||
subject.decorator_class.should be :infer
|
||||
it "uses the specified decorator" do
|
||||
collection_decorator.should_receive(:decorate).with(associated, expected_options).and_return(:decorated_collection)
|
||||
decorated_association.call.should be :decorated_collection
|
||||
end
|
||||
end
|
||||
|
||||
context "when the association is empty" do
|
||||
it "returns an empty collection decorator" do
|
||||
source.stub(:poro_similar_products).and_return([])
|
||||
subject.should be_a Draper::CollectionDecorator
|
||||
subject.should be_empty
|
||||
subject.first.should be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
context "when :with option is a singular decorator" do
|
||||
let(:options) { {with: decorator} }
|
||||
let(:decorator) { SpecificProductDecorator }
|
||||
|
||||
context "for an ActiveModel singular association" do
|
||||
let(:association) { :previous_version }
|
||||
|
||||
context "when the association is present" do
|
||||
it "decorates the association" do
|
||||
subject.should be_decorated_with ProductDecorator
|
||||
it "uses a CollectionDecorator of the specified decorator" do
|
||||
decorator.should_receive(:decorate_collection).with(associated, expected_options).and_return(:decorated_collection)
|
||||
decorated_association.call.should be :decorated_collection
|
||||
end
|
||||
end
|
||||
|
||||
context "when the association is absent" do
|
||||
it "doesn't decorate the association" do
|
||||
source.stub(:previous_version).and_return(nil)
|
||||
subject.should be_nil
|
||||
context "when :with option was not given" do
|
||||
it "uses a CollectionDecorator of inferred decorators" do
|
||||
Draper::CollectionDecorator.should_receive(:decorate).with(associated, expected_options).and_return(:decorated_collection)
|
||||
decorated_association.call.should be :decorated_collection
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "for a non-ActiveModel singular association" do
|
||||
let(:association) { :poro_previous_version }
|
||||
|
||||
context "when the association is present" do
|
||||
it "decorates the association" do
|
||||
subject.should be_decorated_with ProductDecorator
|
||||
end
|
||||
end
|
||||
|
||||
context "when the association is absent" do
|
||||
it "doesn't decorate the association" do
|
||||
source.stub(:poro_previous_version).and_return(nil)
|
||||
subject.should be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when a decorator is specified" do
|
||||
let(:options) { {with: SpecificProductDecorator} }
|
||||
|
||||
context "for a singular association" do
|
||||
let(:association) { :previous_version }
|
||||
|
||||
it "decorates with the specified decorator" do
|
||||
subject.should be_decorated_with SpecificProductDecorator
|
||||
end
|
||||
end
|
||||
|
||||
context "for a collection association" do
|
||||
let(:association) { :similar_products}
|
||||
|
||||
it "decorates with a collection of the specifed decorators" do
|
||||
subject.should be_a Draper::CollectionDecorator
|
||||
subject.decorator_class.should be SpecificProductDecorator
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when a collection decorator is specified" do
|
||||
let(:association) { :similar_products }
|
||||
let(:options) { {with: ProductsDecorator} }
|
||||
|
||||
it "decorates with the specified decorator" do
|
||||
subject.should be_a ProductsDecorator
|
||||
end
|
||||
end
|
||||
|
||||
context "with a scope" do
|
||||
let(:association) { :thing }
|
||||
let(:associated) { [] }
|
||||
let(:options) { {scope: :foo} }
|
||||
|
||||
it "applies the scope before decoration" do
|
||||
scoped = [SomeThing.new]
|
||||
SomeThing.any_instance.should_receive(:foo).and_return(scoped)
|
||||
subject.source.should be scoped
|
||||
end
|
||||
end
|
||||
|
||||
context "base has context" do
|
||||
let(:association) { :similar_products }
|
||||
let(:base) { source.decorate(context: {some: 'context'}) }
|
||||
|
||||
context "when no context is specified" do
|
||||
it "it should inherit context from base" do
|
||||
subject.context.should == {some: 'context'}
|
||||
end
|
||||
|
||||
it "it should share context hash with base" do
|
||||
subject.context.should be base.context
|
||||
end
|
||||
end
|
||||
|
||||
context "when static context is specified" do
|
||||
let(:options) { {context: {other: 'context'}} }
|
||||
|
||||
it "it should get context from static option" do
|
||||
subject.context.should == {other: 'context'}
|
||||
end
|
||||
end
|
||||
|
||||
context "when lambda context is specified" do
|
||||
let(:options) { {context: lambda {|context| context.merge(other: 'protext')}} }
|
||||
|
||||
it "it should get generated context" do
|
||||
subject.context.should == {some: 'context', other: 'protext'}
|
||||
end
|
||||
scoped = [:scoped]
|
||||
associated.should_receive(:foo).and_return(scoped)
|
||||
decorated_association.call.source.should be scoped
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#decorator_options" do
|
||||
subject { decorated_association.send(:decorator_options) }
|
||||
describe "#context" do
|
||||
before { owner.stub context: :owner_context }
|
||||
|
||||
context "collection association" do
|
||||
let(:association) { :similar_products }
|
||||
context "when :context option was given" do
|
||||
let(:options) { {context: context} }
|
||||
|
||||
context "no options" do
|
||||
it "should return default options" do
|
||||
should == {with: :infer, context: {}}
|
||||
context "and is callable" do
|
||||
let(:context) { ->(*){ :dynamic_context } }
|
||||
|
||||
it "calls it with the owner's context" do
|
||||
context.should_receive(:call).with(:owner_context)
|
||||
decorated_association.context
|
||||
end
|
||||
|
||||
it "should set with: to :infer" do
|
||||
decorated_association.send(:options).should == options
|
||||
subject
|
||||
decorated_association.send(:options).should == {with: :infer}
|
||||
it "returns the lambda's return value" do
|
||||
decorated_association.context.should be :dynamic_context
|
||||
end
|
||||
end
|
||||
|
||||
context "option with: ProductDecorator" do
|
||||
let(:options) { {with: ProductDecorator} }
|
||||
it "should pass with: from options" do
|
||||
should == {with: ProductDecorator, context: {}}
|
||||
end
|
||||
end
|
||||
context "and is not callable" do
|
||||
let(:context) { :static_context }
|
||||
|
||||
context "option scope: :to_a" do
|
||||
let(:options) { {scope: :to_a} }
|
||||
it "should strip scope: from options" do
|
||||
decorated_association.send(:options).should == options
|
||||
should == {with: :infer, context: {}}
|
||||
end
|
||||
end
|
||||
|
||||
context "base has context" do
|
||||
let(:base) { source.decorate(context: {some: 'context'}) }
|
||||
|
||||
context "no options" do
|
||||
it "should return context from base" do
|
||||
should == {with: :infer, context: {some: 'context'}}
|
||||
end
|
||||
end
|
||||
|
||||
context "option context: {other: 'context'}" do
|
||||
let(:options) { {context: {other: 'context'}} }
|
||||
it "should return specified context" do
|
||||
should == {with: :infer, context: {other: 'context'}}
|
||||
end
|
||||
end
|
||||
|
||||
context "option context: lambda" do
|
||||
let(:options) { {context: lambda {|context| context.merge(other: 'protext')}} }
|
||||
it "should return specified context" do
|
||||
should == {with: :infer, context: {some: 'context', other: 'protext'}}
|
||||
end
|
||||
it "returns the specified value" do
|
||||
decorated_association.context.should be :static_context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "singular association" do
|
||||
let(:association) { :previous_version }
|
||||
|
||||
context "no options" do
|
||||
it "should return default options" do
|
||||
should == {context: {}}
|
||||
end
|
||||
end
|
||||
|
||||
context "option with: ProductDecorator" do
|
||||
let(:options) { {with: ProductDecorator} }
|
||||
it "should strip with: from options" do
|
||||
should == {context: {}}
|
||||
end
|
||||
end
|
||||
|
||||
context "option scope: :decorate" do
|
||||
let(:options) { {scope: :decorate} }
|
||||
it "should strip scope: from options" do
|
||||
decorated_association.send(:options).should == options
|
||||
should == {context: {}}
|
||||
end
|
||||
end
|
||||
|
||||
context "base has context" do
|
||||
let(:base) { source.decorate(context: {some: 'context'}) }
|
||||
|
||||
context "no options" do
|
||||
it "should return context from base" do
|
||||
should == {context: {some: 'context'}}
|
||||
end
|
||||
end
|
||||
|
||||
context "option context: {other: 'context'}" do
|
||||
let(:options) { {context: {other: 'context'}} }
|
||||
it "should return specified context" do
|
||||
should == {context: {other: 'context'}}
|
||||
end
|
||||
end
|
||||
|
||||
context "option context: lambda" do
|
||||
let(:options) { {context: lambda {|context| context.merge(other: 'protext')}} }
|
||||
it "should return specified context" do
|
||||
should == {context: {some: 'context', other: 'protext'}}
|
||||
end
|
||||
end
|
||||
context "when :context option was not given" do
|
||||
it "returns the owner's context" do
|
||||
decorated_association.context.should be :owner_context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -101,15 +101,6 @@ describe Draper::Decorator do
|
|||
subject.each {|item| item.should be_a ProductDecorator}
|
||||
end
|
||||
|
||||
context "when given :with => :infer" do
|
||||
subject { ProductDecorator.decorate_collection(source, with: :infer) }
|
||||
|
||||
it "infers the item decorators" do
|
||||
subject.first.should be_a ProductDecorator
|
||||
subject.last.should be_a WidgetDecorator
|
||||
end
|
||||
end
|
||||
|
||||
context "with context" do
|
||||
subject { ProductDecorator.decorate_collection(source, with: :infer, context: {some: 'context'}) }
|
||||
|
||||
|
|
Loading…
Reference in a new issue