diff --git a/.yardopts b/.yardopts index 5d7fba3..c181336 100644 --- a/.yardopts +++ b/.yardopts @@ -1 +1 @@ -yardoc 'lib/draper/**/*.rb' -m markdown \ No newline at end of file +yardoc 'lib/draper/**/*.rb' -m markdown --no-private diff --git a/lib/draper/collection_decorator.rb b/lib/draper/collection_decorator.rb index 2b3b45f..4c6d750 100644 --- a/lib/draper/collection_decorator.rb +++ b/lib/draper/collection_decorator.rb @@ -3,14 +3,23 @@ module Draper include Enumerable include ViewHelpers + # @return [Hash] extra data to be used in user-defined methods, and passed + # to each item's decorator. attr_accessor :context array_methods = Array.instance_methods - Object.instance_methods delegate :==, :as_json, *array_methods, to: :decorated_collection - # @param source collection to decorate - # @option options [Class] :with the class used to decorate items - # @option options [Hash] :context context available to each item's decorator + # @param [Enumerable] source + # collection to decorate. + # @option options [Class, nil] :with (nil) + # the decorator class used to decorate each item. When `nil`, it is + # inferred from the collection decorator class if possible (e.g. + # `ProductsDecorator` maps to `ProductDecorator`), otherwise each item's + # {Decoratable#decorate decorate} method will be used. + # @option options [Hash] :context ({}) + # extra data to be stored in the collection decorator and used in + # user-defined methods, and passed to each item's decorator. def initialize(source, options = {}) options.assert_valid_keys(:with, :context) @source = source @@ -22,10 +31,14 @@ module Draper alias_method :decorate, :new end + # @return [Array] the decorated items. def decorated_collection @decorated_collection ||= source.map{|item| decorate_item(item)} end + # Delegated to the decorated collection when using the block form + # (`Enumerable#find`) or to the decorator class if not + # (`ActiveRecord::FinderMethods#find`) def find(*args, &block) if block_given? decorated_collection.find(*args, &block) @@ -49,18 +62,25 @@ module Draper each {|item| item.context = value } if @decorated_collection end + # @return [Class] the decorator class used to decorate each item, as set by + # {#initialize} or as inferred from the collection decorator class (e.g. + # `ProductsDecorator` maps to `ProductDecorator`). def decorator_class @decorator_class ||= self.class.inferred_decorator_class end protected + # @return the collection being decorated. attr_reader :source + # Decorates the given item. def decorate_item(item) item_decorator.call(item, context: context) end + private + def self.inferred_decorator_class decorator_name = "#{name.chomp("Decorator").singularize}Decorator" decorator_uninferrable if decorator_name == name @@ -75,8 +95,6 @@ module Draper raise Draper::UninferrableDecoratorError.new(self) end - private - def item_decorator @item_decorator ||= begin decorator_class.method(:decorate) diff --git a/lib/draper/decoratable.rb b/lib/draper/decoratable.rb index a8e71b7..56536b5 100644 --- a/lib/draper/decoratable.rb +++ b/lib/draper/decoratable.rb @@ -1,44 +1,83 @@ -module Draper::Decoratable - extend ActiveSupport::Concern +module Draper + # Provides shortcuts to decorate objects directly, so you can do + # `@product.decorate` instead of `ProductDecorator.new(@product)`. + # + # This module is included by default into `ActiveRecord::Base` and + # `Mongoid::Document`, but you're using another ORM, or want to decorate + # plain old Ruby objects, you can include it manually. + module Decoratable + extend ActiveSupport::Concern - def decorate(options = {}) - decorator_class.decorate(self, options) - end - - def decorator_class - self.class.decorator_class - end - - def applied_decorators - [] - end - - def decorated_with?(decorator_class) - false - end - - def decorated? - false - end - - def ==(other) - super || (other.respond_to?(:source) && self == other.source) - end - - module ClassMethods + # Decorates the object using the inferred {#decorator_class}. + # @param [Hash] options + # see {Decorator#initialize} def decorate(options = {}) - decorator_class.decorate_collection(self.scoped, options) + decorator_class.decorate(self, options) end + # (see ClassMethods#decorator_class) def decorator_class - prefix = respond_to?(:model_name) ? model_name : name - "#{prefix}Decorator".constantize - rescue NameError - raise Draper::UninferrableDecoratorError.new(self) + self.class.decorator_class end - def ===(other) - super || (other.respond_to?(:source) && super(other.source)) + # The list of decorators that have been applied to the object. + # + # @return [Array] `[]` + def applied_decorators + [] end + + # (see Decorator#decorated_with?) + # @return [false] + def decorated_with?(decorator_class) + false + end + + # Checks if this object is decorated. + # + # @return [false] + def decorated? + false + end + + # Compares with possibly-decorated objects. + # + # @return [Boolean] + def ==(other) + super || (other.respond_to?(:source) && self == other.source) + end + + module ClassMethods + + # Decorates a collection of objects. Used at the end of a scope chain. + # + # @example + # Product.popular.decorate + # @param [Hash] options + # see {Decorator.decorate_collection}. + def decorate(options = {}) + decorator_class.decorate_collection(self.scoped, options) + end + + # Infers the decorator class to be used by {Decoratable#decorate} (e.g. + # `Product` maps to `ProductDecorator`). + # + # @return [Class] the inferred decorator class. + def decorator_class + prefix = respond_to?(:model_name) ? model_name : name + "#{prefix}Decorator".constantize + rescue NameError + raise Draper::UninferrableDecoratorError.new(self) + end + + # Compares with possibly-decorated objects. + # + # @return [Boolean] + def ===(other) + super || (other.respond_to?(:source) && super(other.source)) + end + + end + end end diff --git a/lib/draper/decorated_association.rb b/lib/draper/decorated_association.rb index 207e26c..7ad15bd 100644 --- a/lib/draper/decorated_association.rb +++ b/lib/draper/decorated_association.rb @@ -1,4 +1,5 @@ module Draper + # @private class DecoratedAssociation def initialize(owner, association, options) diff --git a/lib/draper/decorator.rb b/lib/draper/decorator.rb index 78bc839..ef889bb 100755 --- a/lib/draper/decorator.rb +++ b/lib/draper/decorator.rb @@ -5,26 +5,25 @@ module Draper include Draper::ViewHelpers include ActiveModel::Serialization if defined?(ActiveModel::Serialization) + # @return the object being decorated. attr_reader :source alias_method :model, :source alias_method :to_source, :source + # @return [Hash] extra data to be used in user-defined methods. attr_accessor :context - # 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. + # Wraps an object in a new instance of the decorator. # - # 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. + # Decorators may be applied to other decorators. However, applying a + # decorator to an instance of itself will create a decorator with the same + # source as the original, rather than redecorating the other instance. # - # @param [Object] source object to decorate - # @option options [Hash] :context context available to the decorator + # @param [Object] source + # object to decorate. + # @option options [Hash] :context ({}) + # extra data to be stored in the decorator and used in user-defined + # methods. 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 @@ -37,49 +36,60 @@ module Draper alias_method :decorate, :new end - # Specify the class that this class decorates. + # Sets the source class corresponding to the decorator class. # - # @param [String, Symbol, Class] Class or name of class to decorate. - def self.decorates(klass) - @source_class = klass.to_s.camelize.constantize + # @note This is only necessary if you wish to proxy class methods to the + # source (including when using {decorates_finders}), and the source class + # cannot be inferred from the decorator class (e.g. `ProductDecorator` + # maps to `Product`). + # @param [String, Symbol, Class] source_class + # source class (or class name) that corresponds to this decorator. + # @return [void] + def self.decorates(source_class) + @source_class = source_class.to_s.camelize.constantize end - # @return [Class] The source class corresponding to this - # decorator class + # Returns the source class corresponding to the decorator class, as set by + # {decorates}, or as inferred from the decorator class name (e.g. + # `ProductDecorator` maps to `Product`). + # + # @return [Class] the source class that corresponds to this decorator. def self.source_class @source_class ||= inferred_source_class end - # Checks whether this decorator class has a corresponding - # source class + # 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 + # 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. + # Finder methods are applied to the {source_class}. # + # @return [void] 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. + # Automatically decorate an association. # - # @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. + # @param [Symbol] association + # name of the association to decorate (e.g. `: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 + # extra data to be stored in the associated decorator. If omitted, the + # associated decorator's context will be the same as the parent + # decorator's. If a Proc is given, it will be called with the parent's + # context and should return a new context hash for the association. + # @return [void] def self.decorates_association(association, options = {}) options.assert_valid_keys(:with, :scope, :context) define_method(association) do @@ -88,11 +98,13 @@ module Draper 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` + # @overload decorates_associations(*associations, options = {}) + # Automatically decorate multiple associations. + # @param [Symbols*] associations + # names of the associations to decorate. + # @param [Hash] options + # see {decorates_association}. + # @return [void] def self.decorates_associations(*associations) options = associations.extract_options! associations.each do |association| @@ -100,99 +112,106 @@ module Draper end end - # Specifies a black list of methods which may *not* be proxied to - # the wrapped object. + # Specifies a blacklist of methods which are not to be automatically + # proxied to the source 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` + # @note Use only one of {allows}, {denies}, and {denies_all}. + # @param [Symbols*] methods + # list of methods not to be automatically proxied. + # @return [void] def self.denies(*methods) security.denies(*methods) end - # Specifies that all methods may *not* be proxied to the wrapped object. + # Prevents all methods from being automatically proxied to the source + # object. # - # Do not use `.allows` and `.denies` in combination with '.denies_all' + # @note (see denies) + # @return [void] 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. + # Specifies a whitelist of methods which are to be automatically proxied to + # the source 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 allow like `:find, :find_by_name` + # @note (see denies) + # @param [Symbols*] methods + # list of methods to be automatically proxied. + # @return [void] def self.allows(*methods) security.allows(*methods) end - # Creates a new CollectionDecorator for the given collection. + # Decorates a collection of objects. The class of the collection decorator + # is inferred from the decorator class if possible (e.g. `ProductDecorator` + # maps to `ProductsDecorator`), but otherwise defaults to + # {Draper::CollectionDecorator}. # - # @param [Object] source collection to decorate - # @param [Hash] options passed to each item's decorator (except - # for the keys listed below) - # @option options [Class] :with (self) the class used to decorate - # items - # @option options [Hash] :context context available to decorated items + # @param [Object] source + # collection to decorate. + # @option options [Class, nil] :with (self) + # the decorator class used to decorate each item. When `nil`, it is + # inferred from each item. + # @option options [Hash] :context + # extra data to be stored in the collection decorator. def self.decorate_collection(source, options = {}) options.assert_valid_keys(:with, :context) collection_decorator_class.new(source, options.reverse_merge(with: self)) end - # Get the chain of decorators applied to the object. - # - # @return [Array] list of decorator classes + # @return [Array] the list of decorators that have been applied to + # the object. def applied_decorators chain = source.respond_to?(:applied_decorators) ? source.applied_decorators : [] chain << self.class end - # Checks if a given decorator has been applied. + # Checks if a given decorator has been applied to the object. # # @param [Class] decorator_class def decorated_with?(decorator_class) applied_decorators.include?(decorator_class) end + # Checks if this object is decorated. + # + # @return [true] def decorated? true end - # Delegates == to the decorated models + # Delegated to the source object. # - # @return [Boolean] true if other's model == self's model + # @return [Boolean] def ==(other) source == (other.respond_to?(:source) ? other.source : other) end + # @overload kind_of?(class) + # Checks if `self.kind_of?(class)` or `source.kind_of?(class)` 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. + # Delegated to the source object, in case it is `nil`. def present? source.present? end - # For ActiveModel compatibilty + # For ActiveModel compatibility. + # @return [self] def to_model self end - # For ActiveModel compatibility + # Delegated to the source object for ActiveModel compatibility. def to_param source.to_param end + # Proxies missing instance methods to the source object. def method_missing(method, *args, &block) if delegatable_method?(method) self.class.define_proxy(method) @@ -202,10 +221,13 @@ module Draper end end + # Checks if the decorator responds to an instance method, or is able to + # proxy it to the source object. def respond_to?(method, include_private = false) super || delegatable_method?(method) end + # Proxies missing class methods to the {source_class}. def self.method_missing(method, *args, &block) if delegatable_method?(method) source_class.send(method, *args, &block) @@ -214,12 +236,13 @@ module Draper end end + # Checks if the decorator responds to a class method, or is able to proxy + # it to the {source_class}. def self.respond_to?(method, include_private = false) super || delegatable_method?(method) end - protected - + # @return [Class] the class created by {decorate_collection}. def self.collection_decorator_class collection_decorator_name.constantize rescue NameError diff --git a/lib/draper/finders.rb b/lib/draper/finders.rb index e89a234..1278295 100755 --- a/lib/draper/finders.rb +++ b/lib/draper/finders.rb @@ -1,4 +1,7 @@ module Draper + # Provides automatically-decorated finder methods for your decorators. You + # do not have to extend this module directly; it is extended by + # {Decorator.decorates_finders}. module Finders def find(id, options = {}) @@ -17,6 +20,7 @@ module Draper decorate(source_class.last, options) end + # Decorates dynamic finder methods (`find_all_by_` and friends). def method_missing(method, *args, &block) result = super options = args.extract_options! diff --git a/lib/draper/helper_proxy.rb b/lib/draper/helper_proxy.rb index 9a7edf7..5a67164 100644 --- a/lib/draper/helper_proxy.rb +++ b/lib/draper/helper_proxy.rb @@ -1,8 +1,9 @@ module Draper + # Provides access to helper methods - both Rails built-in helpers, and those + # defined in your application. class HelperProxy - # Some helpers are private, for example html_escape... as a workaround - # we are wrapping the helpers in a delegator that passes the methods - # along through a send, which will ignore private/public distinctions + + # Sends helper methods to the view context. def method_missing(method, *args, &block) view_context.send(method, *args, &block) end diff --git a/lib/draper/lazy_helpers.rb b/lib/draper/lazy_helpers.rb index 09215f5..2e8d9a0 100644 --- a/lib/draper/lazy_helpers.rb +++ b/lib/draper/lazy_helpers.rb @@ -1,11 +1,15 @@ module Draper + # Include this module in your decorators to get direct access to the helpers + # so that you can stop typing `h.` everywhere, at the cost of mixing in a + # bazillion methods. module LazyHelpers - def method_missing(method_name, *args, &block) - begin - helpers.send method_name, *args, &block - rescue NoMethodError - super - end + + # Sends missing methods to the {HelperProxy}. + def method_missing(method, *args, &block) + helpers.send(method, *args, &block) + rescue NoMethodError + super end + end end diff --git a/lib/draper/security.rb b/lib/draper/security.rb index fe39b34..70524c1 100644 --- a/lib/draper/security.rb +++ b/lib/draper/security.rb @@ -1,4 +1,5 @@ module Draper + # @private class Security def initialize @methods = [] diff --git a/lib/draper/view_helpers.rb b/lib/draper/view_helpers.rb index f6a66b7..13c3242 100644 --- a/lib/draper/view_helpers.rb +++ b/lib/draper/view_helpers.rb @@ -1,4 +1,6 @@ module Draper + # Provides the {#helpers} method used in {Decorator} and {CollectionDecorator} + # to call the Rails helpers. module ViewHelpers extend ActiveSupport::Concern @@ -24,9 +26,8 @@ module Draper end alias_method :h, :helpers - # Localize is something that's used quite often. Even though - # it's available through helpers, that's annoying. Aliased - # to `l` for convenience. + # Alias for `helpers.localize`, since localize is something that's used + # quite often. Further aliased to `l` for convenience. def localize(*args) helpers.localize(*args) end