Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Merge pull request #385 from haines/context

Restore context
  • Loading branch information...
commit 9609156b997b3a469386eef3a5f043b24d8a2fba 2 parents 059ecbc + 7784564
@steveklabnik steveklabnik authored
View
2  lib/draper.rb
@@ -14,6 +14,8 @@
require 'draper/collection_decorator'
require 'draper/railtie' if defined?(Rails)
+require 'active_support/core_ext/hash/keys'
+
module Draper
def self.setup_action_controller(base)
base.class_eval do
View
23 lib/draper/collection_decorator.rb
@@ -3,20 +3,21 @@ class CollectionDecorator
include Enumerable
include ViewHelpers
- attr_accessor :source, :options, :decorator_class
+ attr_accessor :source, :context, :decorator_class
alias_method :to_source, :source
delegate :as_json, *(Array.instance_methods - Object.instance_methods), to: :decorated_collection
# @param source collection to decorate
- # @param options [Hash] passed to each item's decorator (except
- # for the keys listed below)
- # @option options [Class,Symbol] :with the class used 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 [Hash] :context context available to each item's decorator
def initialize(source, options = {})
+ options.assert_valid_keys(:with, :context)
@source = source
- @decorator_class = options.delete(:with) || self.class.inferred_decorator_class
- @options = options
+ @decorator_class = options.fetch(:with) { self.class.inferred_decorator_class }
+ @context = options.fetch(:context, {})
end
class << self
@@ -56,18 +57,18 @@ def to_s
"#<CollectionDecorator of #{decorator_class} for #{source.inspect}>"
end
- def options=(options)
- each {|item| item.options = options }
- @options = options
+ def context=(value)
+ @context = value
+ each {|item| item.context = value } if @decorated_collection
end
protected
def decorate_item(item)
if decorator_class == :infer
- item.decorate(options)
+ item.decorate(context: context)
else
- decorator_class.decorate(item, options)
+ decorator_class.decorate(item, context: context)
end
end
View
23 lib/draper/decorated_association.rb
@@ -1,11 +1,12 @@
module Draper
class DecoratedAssociation
- attr_reader :source, :association, :options
+ attr_reader :base, :association, :options
- def initialize(source, association, options)
- @source = source
+ def initialize(base, association, options)
+ @base = base
@association = association
+ options.assert_valid_keys(:with, :scope, :context)
@options = options
end
@@ -14,6 +15,10 @@ def call
decorate
end
+ def source
+ base.source
+ end
+
private
def undecorated
@@ -25,7 +30,7 @@ def undecorated
end
def decorate
- @decorated ||= decorator_class.send(decorate_method, undecorated, options)
+ @decorated ||= decorator_class.send(decorate_method, undecorated, decorator_options)
end
def decorate_method
@@ -51,5 +56,15 @@ def decorator_class
end
end
+ def decorator_options
+ decorator_class # Ensures options[:with] = :infer for unspecified collections
+
+ 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)
+ end
+ dec_options
+ end
end
end
View
28 lib/draper/decorator.rb
@@ -5,14 +5,15 @@ class Decorator
include Draper::ViewHelpers
include ActiveModel::Serialization if defined?(ActiveModel::Serialization)
- attr_accessor :source, :options
+ attr_accessor :source, :context
alias_method :model, :source
alias_method :to_source, :source
# Initialize a new decorator instance by passing in
# an instance of the source class. Pass in an optional
- # context inside the options hash is stored for later use.
+ # :context inside the options hash which is available
+ # for later use.
#
# A decorator cannot be applied to other instances of the
# same decorator and will instead result in a decorator
@@ -23,11 +24,13 @@ class Decorator
#
# @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)
source.to_a if source.respond_to?(:to_a) # forces evaluation of a lazy query from AR
@source = source
- @options = options
- handle_multiple_decoration if source.is_a?(Draper::Decorator)
+ @context = options.fetch(:context, {})
+ handle_multiple_decoration(options) if source.is_a?(Draper::Decorator)
end
class << self
@@ -72,9 +75,15 @@ def self.decorates_finders
# @param [Symbol] association name of association to decorate, like `:products`
# @option options [Class] :with the decorator to apply to the association
# @option options [Symbol] :scope a scope to apply when fetching the association
+ # @option options [Hash, #call] :context context available to decorated
+ # objects in collection. Passing a `lambda` or similar will result in that
+ # block being called when the association is evaluated. The block will be
+ # passed the base decorator's `context` Hash and should return the desired
+ # context Hash for the decorated items.
def self.decorates_association(association, options = {})
+ options.assert_valid_keys(:with, :scope, :context)
define_method(association) do
- decorated_associations[association] ||= Draper::DecoratedAssociation.new(source, association, options)
+ decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
decorated_associations[association].call
end
end
@@ -82,7 +91,8 @@ def self.decorates_association(association, options = {})
# A convenience method for decorating multiple associations. Calls
# decorates_association on each of the given symbols.
#
- # @param [Symbols*] associations name of associations to decorate
+ # @param [Symbols*] associations names of associations to decorate
+ # @param [Hash] options passed to `decorate_association`
def self.decorates_associations(*associations)
options = associations.extract_options!
associations.each do |association|
@@ -128,7 +138,9 @@ def self.allows(*methods)
# 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 [Hash] :context context available to decorated items
def self.decorate_collection(source, options = {})
+ options.assert_valid_keys(:with, :context)
Draper::CollectionDecorator.new(source, options.reverse_merge(with: self))
end
@@ -244,9 +256,9 @@ def allow?(method)
self.class.security.allow?(method)
end
- def handle_multiple_decoration
+ def handle_multiple_decoration(options)
if source.instance_of?(self.class)
- self.options = source.options if options.empty?
+ self.context = source.context unless options.has_key?(:context)
self.source = source.source
elsif source.decorated_with?(self.class)
warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}"
View
69 spec/draper/collection_decorator_spec.rb
@@ -16,33 +16,70 @@
subject.map{|item| item.source}.should == source
end
- context "with options" do
- subject { Draper::CollectionDecorator.new(source, with: ProductDecorator, some: "options") }
+ context "with context" do
+ subject { Draper::CollectionDecorator.new(source, with: ProductDecorator, context: {some: 'context'}) }
- its(:options) { should == {some: "options"} }
+ its(:context) { should == {some: 'context'} }
- it "passes options to the individual decorators" do
+ it "passes context to the individual decorators" do
subject.each do |item|
- item.options.should == {some: "options"}
+ item.context.should == {some: 'context'}
end
end
- describe "#options=" do
- it "updates the options on the collection decorator" do
- subject.options = {other: "options"}
- subject.options.should == {other: "options"}
+ it "does not tie the individual decorators' contexts together" do
+ subject.each do |item|
+ item.context.should == {some: 'context'}
+ item.context = {alt: 'context'}
+ item.context.should == {alt: 'context'}
+ end
+ end
+
+ describe "#context=" do
+ it "updates the collection decorator's context" do
+ subject.context = {other: 'context'}
+ subject.context.should == {other: 'context'}
end
- it "updates the options on the individual decorators" do
- subject.options = {other: "options"}
- subject.each do |item|
- item.options.should == {other: "options"}
+ context "when the collection is already decorated" do
+ it "updates the items' context" do
+ subject.decorated_collection
+ subject.context = {other: 'context'}
+ subject.each do |item|
+ item.context.should == {other: 'context'}
+ end
+ end
+ end
+
+ context "when the collection has not yet been decorated" do
+ it "does not trigger decoration" do
+ subject.should_not_receive(:decorated_collection)
+ subject.context = {other: 'context'}
+ end
+
+ it "sets context after decoration is triggered" do
+ subject.context = {other: 'context'}
+ subject.each do |item|
+ item.context.should == {other: 'context'}
+ end
end
end
end
end
describe "#initialize" do
+ describe "options validation" do
+ let(:valid_options) { {with: ProductDecorator, context: {}} }
+
+ it "does not raise error on valid options" do
+ expect { Draper::CollectionDecorator.new(source, valid_options) }.to_not raise_error
+ end
+
+ it "raises error on invalid options" 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) }
@@ -123,14 +160,14 @@
end
describe "#localize" do
- before { subject.helpers.should_receive(:localize).with(:an_object, {some: "options"}) }
+ before { subject.helpers.should_receive(:localize).with(:an_object, {some: 'parameter'}) }
it "delegates to helpers" do
- subject.localize(:an_object, some: "options")
+ subject.localize(:an_object, some: 'parameter')
end
it "is aliased to #l" do
- subject.l(:an_object, some: "options")
+ subject.l(:an_object, some: 'parameter')
end
end
View
12 spec/draper/decoratable_spec.rb
@@ -9,9 +9,9 @@
subject.decorate.source.should be subject
end
- it "accepts options" do
- decorator = subject.decorate(some: "options")
- decorator.options.should == {some: "options"}
+ it "accepts context" do
+ decorator = subject.decorate(context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
it "is not memoized" do
@@ -153,9 +153,9 @@
decorator.source.should be Product.scoped
end
- it "accepts options" do
- decorator = Product.decorate(some: "options")
- decorator.options.should == {some: "options"}
+ it "accepts context" do
+ decorator = Product.decorate(context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
it "is not memoized" do
View
183 spec/draper/decorated_association_spec.rb
@@ -1,10 +1,52 @@
require 'spec_helper'
describe Draper::DecoratedAssociation do
- let(:decorated_association) { Draper::DecoratedAssociation.new(source, association, options) }
+ let(:decorated_association) { Draper::DecoratedAssociation.new(base, association, options) }
let(:source) { Product.new }
+ let(:base) { 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
+ 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')
+ 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 }
@@ -128,5 +170,144 @@
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
+ end
+ end
+ end
+
+ describe "#decorator_options" do
+ subject { decorated_association.send(:decorator_options) }
+
+ context "collection association" do
+ let(:association) { :similar_products }
+
+ context "no options" do
+ it "should return default options" do
+ should == {with: :infer, context: {}}
+ end
+
+ it "should set with: to :infer" do
+ decorated_association.send(:options).should == options
+ subject
+ decorated_association.send(:options).should == {with: :infer}
+ 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 "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
+ 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
+ end
+ end
end
end
View
85 spec/draper/decorator_spec.rb
@@ -7,13 +7,25 @@
let(:source) { Product.new }
describe "#initialize" do
+ describe "options validation" do
+ let(:valid_options) { {context: {}} }
+
+ it "does not raise error on valid options" do
+ expect { decorator_class.new(source, valid_options) }.to_not raise_error
+ end
+
+ it "raises error on invalid options" do
+ expect { decorator_class.new(source, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
+ end
+ end
+
it "sets the source" do
subject.source.should be source
end
- it "stores options" do
- decorator = decorator_class.new(source, some: "options")
- decorator.options.should == {some: "options"}
+ it "stores context" do
+ decorator = decorator_class.new(source, context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
context "when decorating an instance of itself" do
@@ -23,16 +35,16 @@
end
context "when options are supplied" do
- it "overwrites existing options" do
- decorator = ProductDecorator.new(source, role: :admin)
- ProductDecorator.new(decorator, role: :user).options.should == {role: :user}
+ it "overwrites existing context" do
+ decorator = ProductDecorator.new(source, context: {role: :admin})
+ ProductDecorator.new(decorator, context: {role: :user}).context.should == {role: :user}
end
end
context "when no options are supplied" do
- it "preserves existing options" do
- decorator = ProductDecorator.new(source, role: :admin)
- ProductDecorator.new(decorator).options.should == {role: :admin}
+ it "preserves existing context" do
+ decorator = ProductDecorator.new(source, context: {role: :admin})
+ ProductDecorator.new(decorator).context.should == {role: :admin}
end
end
end
@@ -55,10 +67,31 @@
end
end
+ describe "#context=" do
+ it "modifies the context" do
+ decorator = decorator_class.new(source, context: {some: 'context'})
+ decorator.context = {some: 'other_context'}
+ decorator.context.should == {some: 'other_context'}
+ end
+ end
+
describe ".decorate_collection" do
subject { ProductDecorator.decorate_collection(source) }
let(:source) { [Product.new, Widget.new] }
+ describe "options validation" do
+ let(:valid_options) { {with: :infer, context: {}} }
+ before(:each) { Draper::CollectionDecorator.stub(:new) }
+
+ it "does not raise error on valid options" do
+ expect { ProductDecorator.decorate_collection(source, valid_options) }.to_not raise_error
+ end
+
+ it "raises error on invalid options" do
+ expect { ProductDecorator.decorate_collection(source, valid_options.merge(foo: 'bar')) }.to raise_error(ArgumentError, 'Unknown key: foo')
+ end
+ end
+
it "returns a collection decorator" do
subject.should be_a Draper::CollectionDecorator
subject.source.should be source
@@ -77,11 +110,11 @@
end
end
- context "with options" do
- subject { ProductDecorator.decorate_collection(source, with: :infer, some: "options") }
+ context "with context" do
+ subject { ProductDecorator.decorate_collection(source, with: :infer, context: {some: 'context'}) }
- it "passes the options to the collection decorator" do
- subject.options.should == {some: "options"}
+ it "passes the context to the collection decorator" do
+ subject.context.should == {some: 'context'}
end
end
end
@@ -104,14 +137,14 @@
end
describe "#localize" do
- before { subject.helpers.should_receive(:localize).with(:an_object, {some: "options"}) }
+ before { subject.helpers.should_receive(:localize).with(:an_object, {some: 'parameter'}) }
it "delegates to #helpers" do
- subject.localize(:an_object, some: "options")
+ subject.localize(:an_object, some: 'parameter')
end
it "is aliased to #l" do
- subject.l(:an_object, some: "options")
+ subject.l(:an_object, some: 'parameter')
end
end
@@ -223,8 +256,26 @@
describe "overridden association method" do
let(:decorated_association) { ->{} }
+ describe "options validation" do
+ let(:valid_options) { {with: ProductDecorator, scope: :foo, context: {}} }
+ before(:each) { Draper::DecoratedAssociation.stub(:new).and_return(decorated_association) }
+
+ it "does not raise error on valid options" do
+ expect { decorator_class.decorates_association :similar_products, valid_options }.to_not raise_error
+ end
+
+ it "raises error on invalid options" do
+ expect { decorator_class.decorates_association :similar_products, valid_options.merge(foo: 'bar') }.to raise_error(ArgumentError, 'Unknown key: foo')
+ end
+ end
+
it "creates a DecoratedAssociation" do
- Draper::DecoratedAssociation.should_receive(:new).with(source, :similar_products, {with: ProductDecorator}).and_return(decorated_association)
+ Draper::DecoratedAssociation.should_receive(:new).with(subject, :similar_products, {with: ProductDecorator}).and_return(decorated_association)
+ subject.similar_products
+ end
+
+ it "receives the Decorator" do
+ Draper::DecoratedAssociation.should_receive(:new).with(kind_of(decorator_class), :similar_products, {with: ProductDecorator}).and_return(decorated_association)
subject.similar_products
end
View
32 spec/draper/finders_spec.rb
@@ -15,9 +15,9 @@
decorator.source.should be found
end
- it "passes options to the decorator" do
- decorator = ProductDecorator.find(1, some: "options")
- decorator.options.should == {some: "options"}
+ it "passes context to the decorator" do
+ decorator = ProductDecorator.find(1, context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
end
@@ -55,10 +55,10 @@
ProductDecorator.find_or_create_by_name_and_size("apples", "large")
end
- it "passes options to the decorator" do
- Product.should_receive(:find_by_name_and_size).with("apples", "large", {some: "options"})
- decorator = ProductDecorator.find_by_name_and_size("apples", "large", some: "options")
- decorator.options.should == {some: "options"}
+ it "passes context to the decorator" do
+ Product.should_receive(:find_by_name_and_size).with("apples", "large", context: {some: 'context'})
+ decorator = ProductDecorator.find_by_name_and_size("apples", "large", context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
end
@@ -84,9 +84,9 @@
collection.first.should be_a ProductDecorator
end
- it "passes options to the collection decorator" do
- collection = ProductDecorator.all(some: "options")
- collection.options.should == {some: "options"}
+ it "passes context to the collection decorator" do
+ collection = ProductDecorator.all(context: {some: 'context'})
+ collection.context.should == {some: 'context'}
end
end
@@ -104,9 +104,9 @@
decorator.source.should be first
end
- it "passes options to the decorator" do
- decorator = ProductDecorator.first(some: "options")
- decorator.options.should == {some: "options"}
+ it "passes context to the decorator" do
+ decorator = ProductDecorator.first(context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
end
@@ -124,9 +124,9 @@
decorator.source.should be last
end
- it "passes options to the decorator" do
- decorator = ProductDecorator.last(some: "options")
- decorator.options.should == {some: "options"}
+ it "passes context to the decorator" do
+ decorator = ProductDecorator.last(context: {some: 'context'})
+ decorator.context.should == {some: 'context'}
end
end
Please sign in to comment.
Something went wrong with that request. Please try again.