Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

executable file 273 lines (234 sloc) 8.747 kb
require 'active_support/core_ext/array/extract_options'
module Draper
class Decorator
include Draper::ViewHelpers
include ActiveModel::Serialization if defined?(ActiveModel::Serialization)
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 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
# with the same target as the original.
# You can, however, apply several decorators in a chain but
# you will get a warning if the same decorator appears at
# 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)
source.to_a if source.respond_to?(:to_a) # forces evaluation of a lazy query from AR
@source = source
@context = options.fetch(:context, {})
handle_multiple_decoration(options) if source.is_a?(Draper::Decorator)
end
class << self
alias_method :decorate, :new
end
# Specify the class that this class decorates.
#
# @param [String, Symbol, Class] Class or name of class to decorate.
def self.decorates(klass)
@source_class = klass.to_s.classify.constantize
end
# @return [Class] The source class corresponding to this
# decorator class
def self.source_class
@source_class ||= inferred_source_class
end
# Checks whether this decorator class has a corresponding
# source class
def self.source_class?
source_class
rescue Draper::UninferrableSourceError
false
end
# Automatically decorates ActiveRecord finder methods, so that
# you can use `ProductDecorator.find(id)` instead of
# `ProductDecorator.decorate(Product.find(id))`.
#
# The model class to be found is defined by `decorates` or
# inferred from the decorator class name.
#
def self.decorates_finders
extend Draper::Finders
end
# Typically called within a decorator definition, this method causes
# the assocation to be decorated when it is retrieved.
#
# @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(self, association, options)
decorated_associations[association].call
end
end
# A convenience method for decorating multiple associations. Calls
# decorates_association on each of the given symbols.
#
# @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|
decorates_association(association, options)
end
end
# Specifies a black list of methods which may *not* be proxied to
# the wrapped object.
#
# Do not use both `.allows` and `.denies` together, either write
# a whitelist with `.allows` or a blacklist with `.denies`
#
# @param [Symbols*] methods methods to deny like `:find, :find_by_name`
def self.denies(*methods)
security.denies(*methods)
end
# Specifies that all methods may *not* be proxied to the wrapped object.
#
# Do not use `.allows` and `.denies` in combination with '.denies_all'
def self.denies_all
security.denies_all
end
# Specifies a white list of methods which *may* be proxied to
# the wrapped object. When `allows` is used, only the listed
# methods and methods defined in the decorator itself will be
# available.
#
# Do not use both `.allows` and `.denies` together, either write
# a whitelist with `.allows` or a blacklist with `.denies`
#
# @param [Symbols*] methods methods to allow like `:find, :find_by_name`
def self.allows(*methods)
security.allows(*methods)
end
# Creates a new CollectionDecorator for the given collection.
#
# @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 [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
# Get the chain of decorators applied to the object.
#
# @return [Array] list of decorator classes
def applied_decorators
chain = source.respond_to?(:applied_decorators) ? source.applied_decorators : []
chain << self.class
end
# Checks if a given decorator has been applied.
#
# @param [Class] decorator_class
def decorated_with?(decorator_class)
applied_decorators.include?(decorator_class)
end
def decorated?
true
end
# Delegates == to the decorated models
#
# @return [Boolean] true if other's model == self's model
def ==(other)
source == (other.respond_to?(:source) ? other.source : other)
end
def kind_of?(klass)
super || source.kind_of?(klass)
end
alias_method :is_a?, :kind_of?
# We always want to delegate present, in case we decorate a nil object.
#
# I don't like the idea of decorating a nil object, but we'll deal with
# that later.
def present?
source.present?
end
# For ActiveModel compatibilty
def to_model
self
end
# For ActiveModel compatibility
def to_param
source.to_param
end
def method_missing(method, *args, &block)
if delegatable_method?(method)
self.class.define_proxy(method)
send(method, *args, &block)
else
super
end
end
def respond_to?(method, include_private = false)
super || delegatable_method?(method)
end
def self.method_missing(method, *args, &block)
if delegatable_method?(method)
source_class.send(method, *args, &block)
else
super
end
end
def self.respond_to?(method, include_private = false)
super || delegatable_method?(method)
end
private
def delegatable_method?(method)
allow?(method) && source.respond_to?(method)
end
def self.delegatable_method?(method)
source_class? && source_class.respond_to?(method)
end
def self.inferred_source_class
uninferrable_source if name.nil? || name.demodulize !~ /.+Decorator$/
begin
name.chomp("Decorator").constantize
rescue NameError
uninferrable_source
end
end
def self.uninferrable_source
raise Draper::UninferrableSourceError.new(self)
end
def self.define_proxy(method)
define_method(method) do |*args, &block|
source.send(method, *args, &block)
end
end
def self.security
@security ||= Security.new
end
def allow?(method)
self.class.security.allow?(method)
end
def handle_multiple_decoration(options)
if source.instance_of?(self.class)
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")}"
end
end
def decorated_associations
@decorated_associations ||= {}
end
end
end
Jump to Line
Something went wrong with that request. Please try again.