219 lines
7.3 KiB
Ruby
219 lines
7.3 KiB
Ruby
module Draper
|
|
class Base
|
|
require 'active_support/core_ext/class/attribute'
|
|
class_attribute :denied, :allowed, :model_class
|
|
attr_accessor :context, :model
|
|
|
|
DEFAULT_DENIED = Object.new.methods << :method_missing
|
|
FORCED_PROXY = [:to_param, :id]
|
|
FORCED_PROXY.each do |method|
|
|
define_method method do |*args, &block|
|
|
model.send method, *args, &block
|
|
end
|
|
end
|
|
self.denied = DEFAULT_DENIED
|
|
|
|
# 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.
|
|
#
|
|
# @param [Object] instance to wrap
|
|
# @param [Hash] options (optional)
|
|
def initialize(input, options = {})
|
|
input.inspect # forces evaluation of a lazy query from AR
|
|
self.class.model_class = input.class if model_class.nil?
|
|
@model = input
|
|
self.context = options.fetch(:context, {})
|
|
end
|
|
|
|
# Proxies to the class specified by `decorates` to automatically
|
|
# lookup an object in the database and decorate it.
|
|
#
|
|
# @param [Symbol or String] id to lookup
|
|
# @return [Object] instance of this decorator class
|
|
def self.find(input, options = {})
|
|
self.new(model_class.find(input), options)
|
|
end
|
|
|
|
# Typically called within a decorator definition, this method
|
|
# specifies the name of the wrapped object class.
|
|
#
|
|
# For instance, a `ProductDecorator` class might call `decorates :product`
|
|
#
|
|
# But they don't have to match in name, so a `EmployeeDecorator`
|
|
# class could call `decorates :person` to wrap instances of `Person`
|
|
#
|
|
# This is primarilly set so the `.find` method knows which class
|
|
# to query.
|
|
#
|
|
# @param [Symbol] class_name snakecase name of the decorated class, like `:product`
|
|
def self.decorates(input, options = {})
|
|
self.model_class = options[:class] || input.to_s.camelize.constantize
|
|
model_class.send :include, Draper::ModelSupport
|
|
define_method(input){ @model }
|
|
end
|
|
|
|
# Typically called within a decorator definition, this method causes
|
|
# the assocation to be decorated when it is retrieved.
|
|
#
|
|
# @param [Symbol] name of association to decorate, like `:products`
|
|
# @option opts [Class] :with The decorator to decorate the association with
|
|
def self.decorates_association(association_symbol, options = {})
|
|
define_method(association_symbol) do
|
|
orig_association = model.send(association_symbol)
|
|
return orig_association if orig_association.nil?
|
|
if options[:with]
|
|
options[:with].decorate(orig_association)
|
|
else
|
|
reflection = model.class.reflect_on_association(association_symbol)
|
|
"#{reflection.klass}Decorator".constantize.decorate(orig_association)
|
|
end
|
|
end
|
|
end
|
|
|
|
# A convenience method for decorating multiple associations. Calls
|
|
# decorates_association on each of the given symbols.
|
|
#
|
|
# @param [Symbols*] name of associations to decorate
|
|
def self.decorates_associations(*association_symbols)
|
|
association_symbols.each{ |sym| decorates_association(sym) }
|
|
end
|
|
|
|
# Specifies a black list of methods which may *not* be proxied to
|
|
# 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 to deny like `:find, :find_by_name`
|
|
def self.denies(*input_denied)
|
|
raise ArgumentError, "Specify at least one method (as a symbol) to exclude when using denies" if input_denied.empty?
|
|
raise ArgumentError, "Use either 'allows' or 'denies', but not both." if self.allowed?
|
|
self.denied += input_denied
|
|
end
|
|
|
|
# Specifies a white list of methods which *may* be proxied to
|
|
# 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 to allow like `:find, :find_by_name`
|
|
def self.allows(*input_allows)
|
|
raise ArgumentError, "Specify at least one method (as a symbol) to allow when using allows" if input_allows.empty?
|
|
raise ArgumentError, "Use either 'allows' or 'denies', but not both." unless (self.denied == DEFAULT_DENIED)
|
|
self.allowed = input_allows
|
|
end
|
|
|
|
# Initialize a new decorator instance by passing in
|
|
# an instance of the source class. Pass in an optional
|
|
# context into the options hash is stored for later use.
|
|
#
|
|
# When passing in a single object, using `.decorate` is
|
|
# identical to calling `.new`. However, `.decorate` can
|
|
# also accept a collection and return a collection of
|
|
# individually decorated objects.
|
|
#
|
|
# @param [Object] instance(s) to wrap
|
|
# @param [Hash] options (optional)
|
|
def self.decorate(input, options = {})
|
|
if input.respond_to?(:each)
|
|
Draper::DecoratedEnumerableProxy.new(input, self, options)
|
|
elsif options[:infer]
|
|
input.decorator(options)
|
|
else
|
|
new(input, options)
|
|
end
|
|
end
|
|
|
|
# Fetch all instances of the decorated class and decorate them.
|
|
#
|
|
# @param [Hash] options (optional)
|
|
# @return [Draper::DecoratedEnumerableProxy]
|
|
def self.all(options = {})
|
|
Draper::DecoratedEnumerableProxy.new(model_class.all, self, options)
|
|
end
|
|
|
|
def self.first(options = {})
|
|
decorate(model_class.first, options)
|
|
end
|
|
|
|
def self.last(options = {})
|
|
decorate(model_class.last, options)
|
|
end
|
|
|
|
# Access the helpers proxy to call built-in and user-defined
|
|
# Rails helpers. Aliased to `.h` for convinience.
|
|
#
|
|
# @return [Object] proxy
|
|
def helpers
|
|
self.class.helpers
|
|
end
|
|
alias :h :helpers
|
|
|
|
# Access the helpers proxy to call built-in and user-defined
|
|
# Rails helpers from a class context.
|
|
#
|
|
# @return [Object] proxy
|
|
class << self
|
|
def helpers
|
|
Draper::ViewContext.current
|
|
end
|
|
alias :h :helpers
|
|
end
|
|
|
|
# Fetch the original wrapped model.
|
|
#
|
|
# @return [Object] original_model
|
|
def to_model
|
|
@model
|
|
end
|
|
|
|
# Delegates == to the decorated models
|
|
#
|
|
# @return [Boolean] true if other's model == self's model
|
|
def ==(other)
|
|
@model == (other.respond_to?(:model) ? other.model : other)
|
|
end
|
|
|
|
def kind_of?(klass)
|
|
super || model.kind_of?(klass)
|
|
end
|
|
alias :is_a? :kind_of?
|
|
|
|
def respond_to?(method, include_private = false)
|
|
super || (allow?(method) && model.respond_to?(method))
|
|
end
|
|
|
|
def method_missing(method, *args, &block)
|
|
if allow?(method)
|
|
begin
|
|
self.class.send :define_method, method do |*args, &block|
|
|
model.send(method, *args, &block)
|
|
end
|
|
self.send(method, *args, &block)
|
|
rescue NoMethodError
|
|
super
|
|
end
|
|
else
|
|
super
|
|
end
|
|
end
|
|
|
|
def self.method_missing(method, *args, &block)
|
|
model_class.send(method, *args, &block)
|
|
end
|
|
|
|
def self.respond_to?(method, include_private = false)
|
|
super || model_class.respond_to?(method)
|
|
end
|
|
|
|
private
|
|
def allow?(method)
|
|
(!allowed? || allowed.include?(method) || FORCED_PROXY.include?(method)) && !denied.include?(method)
|
|
end
|
|
end
|
|
end
|