drape 1.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.codeclimate.yml +27 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1159 -0
- data/.travis.yml +14 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +230 -0
- data/CONTRIBUTING.md +20 -0
- data/Gemfile +16 -0
- data/Guardfile +26 -0
- data/LICENSE +7 -0
- data/README.md +592 -0
- data/Rakefile +76 -0
- data/drape.gemspec +31 -0
- data/gemfiles/5.0.gemfile +8 -0
- data/lib/drape.rb +63 -0
- data/lib/drape/automatic_delegation.rb +55 -0
- data/lib/drape/collection_decorator.rb +102 -0
- data/lib/drape/decoratable.rb +93 -0
- data/lib/drape/decoratable/equality.rb +26 -0
- data/lib/drape/decorated_association.rb +33 -0
- data/lib/drape/decorates_assigned.rb +44 -0
- data/lib/drape/decorator.rb +282 -0
- data/lib/drape/delegation.rb +13 -0
- data/lib/drape/factory.rb +91 -0
- data/lib/drape/finders.rb +36 -0
- data/lib/drape/helper_proxy.rb +43 -0
- data/lib/drape/helper_support.rb +5 -0
- data/lib/drape/lazy_helpers.rb +13 -0
- data/lib/drape/railtie.rb +69 -0
- data/lib/drape/tasks/test.rake +9 -0
- data/lib/drape/test/devise_helper.rb +30 -0
- data/lib/drape/test/minitest_integration.rb +6 -0
- data/lib/drape/test/rspec_integration.rb +20 -0
- data/lib/drape/test_case.rb +42 -0
- data/lib/drape/undecorate.rb +9 -0
- data/lib/drape/version.rb +3 -0
- data/lib/drape/view_context.rb +104 -0
- data/lib/drape/view_context/build_strategy.rb +46 -0
- data/lib/drape/view_helpers.rb +34 -0
- data/lib/generators/controller_override.rb +17 -0
- data/lib/generators/mini_test/decorator_generator.rb +20 -0
- data/lib/generators/mini_test/templates/decorator_spec.rb +4 -0
- data/lib/generators/mini_test/templates/decorator_test.rb +4 -0
- data/lib/generators/rails/decorator_generator.rb +36 -0
- data/lib/generators/rails/templates/decorator.rb +19 -0
- data/lib/generators/rspec/decorator_generator.rb +9 -0
- data/lib/generators/rspec/templates/decorator_spec.rb +4 -0
- data/lib/generators/test_unit/decorator_generator.rb +9 -0
- data/lib/generators/test_unit/templates/decorator_test.rb +4 -0
- data/spec/draper/collection_decorator_spec.rb +305 -0
- data/spec/draper/decoratable/equality_spec.rb +10 -0
- data/spec/draper/decoratable_spec.rb +203 -0
- data/spec/draper/decorated_association_spec.rb +85 -0
- data/spec/draper/decorates_assigned_spec.rb +70 -0
- data/spec/draper/decorator_spec.rb +828 -0
- data/spec/draper/factory_spec.rb +250 -0
- data/spec/draper/finders_spec.rb +165 -0
- data/spec/draper/helper_proxy_spec.rb +61 -0
- data/spec/draper/lazy_helpers_spec.rb +21 -0
- data/spec/draper/undecorate_spec.rb +19 -0
- data/spec/draper/view_context/build_strategy_spec.rb +116 -0
- data/spec/draper/view_context_spec.rb +160 -0
- data/spec/draper/view_helpers_spec.rb +8 -0
- data/spec/dummy/.gitignore +21 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +3 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +16 -0
- data/spec/dummy/app/assets/javascripts/cable.js +13 -0
- data/spec/dummy/app/assets/javascripts/channels/.keep +0 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +5 -0
- data/spec/dummy/app/controllers/application_controller.rb +5 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/controllers/posts_controller.rb +21 -0
- data/spec/dummy/app/decorators/mongoid_post_decorator.rb +4 -0
- data/spec/dummy/app/decorators/post_decorator.rb +59 -0
- data/spec/dummy/app/helpers/application_helper.rb +5 -0
- data/spec/dummy/app/jobs/application_job.rb +2 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/mailers/post_mailer.rb +18 -0
- data/spec/dummy/app/models/admin.rb +5 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/mongoid_post.rb +5 -0
- data/spec/dummy/app/models/post.rb +3 -0
- data/spec/dummy/app/models/user.rb +5 -0
- data/spec/dummy/app/views/layouts/application.html.erb +9 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/app/views/post_mailer/decorated_email.html.erb +31 -0
- data/spec/dummy/app/views/posts/_post.html.erb +40 -0
- data/spec/dummy/app/views/posts/show.html.erb +1 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +9 -0
- data/spec/dummy/bin/rake +9 -0
- data/spec/dummy/bin/setup +34 -0
- data/spec/dummy/bin/spring +15 -0
- data/spec/dummy/bin/update +29 -0
- data/spec/dummy/config.ru +5 -0
- data/spec/dummy/config/application.rb +21 -0
- data/spec/dummy/config/boot.rb +2 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +51 -0
- data/spec/dummy/config/environments/production.rb +91 -0
- data/spec/dummy/config/environments/test.rb +42 -0
- data/spec/dummy/config/initializers/active_record_belongs_to_required_by_default.rb +6 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +6 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/callback_terminator.rb +6 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
- data/spec/dummy/config/initializers/devise.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/per_form_csrf_tokens.rb +4 -0
- data/spec/dummy/config/initializers/request_forgery_protection.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/ssl_options.rb +4 -0
- data/spec/dummy/config/initializers/to_time_preserves_timezone.rb +10 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/mongoid.yml +16 -0
- data/spec/dummy/config/puma.rb +47 -0
- data/spec/dummy/config/routes.rb +8 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/config/spring.rb +6 -0
- data/spec/dummy/db/migrate/20121019115657_create_posts.rb +7 -0
- data/spec/dummy/db/schema.rb +19 -0
- data/spec/dummy/db/seeds.rb +2 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/lib/tasks/.keep +0 -0
- data/spec/dummy/lib/tasks/test.rake +16 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
- data/spec/dummy/public/apple-touch-icon.png +0 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/public/robots.txt +5 -0
- data/spec/dummy/spec/decorators/active_model_serializers_spec.rb +12 -0
- data/spec/dummy/spec/decorators/devise_spec.rb +64 -0
- data/spec/dummy/spec/decorators/helpers_spec.rb +21 -0
- data/spec/dummy/spec/decorators/post_decorator_spec.rb +66 -0
- data/spec/dummy/spec/decorators/spec_type_spec.rb +7 -0
- data/spec/dummy/spec/decorators/view_context_spec.rb +22 -0
- data/spec/dummy/spec/mailers/post_mailer_spec.rb +33 -0
- data/spec/dummy/spec/models/mongoid_post_spec.rb +8 -0
- data/spec/dummy/spec/models/post_spec.rb +6 -0
- data/spec/dummy/spec/shared_examples/decoratable.rb +26 -0
- data/spec/dummy/spec/spec_helper.rb +8 -0
- data/spec/dummy/test/controllers/.keep +0 -0
- data/spec/dummy/test/fixtures/.keep +0 -0
- data/spec/dummy/test/fixtures/files/.keep +0 -0
- data/spec/dummy/test/helpers/.keep +0 -0
- data/spec/dummy/test/integration/.keep +0 -0
- data/spec/dummy/test/mailers/.keep +0 -0
- data/spec/dummy/test/models/.keep +0 -0
- data/spec/dummy/test/test_helper.rb +10 -0
- data/spec/dummy/vendor/assets/javascripts/.keep +0 -0
- data/spec/dummy/vendor/assets/stylesheets/.keep +0 -0
- data/spec/generators/controller/controller_generator_spec.rb +24 -0
- data/spec/generators/decorator/decorator_generator_spec.rb +94 -0
- data/spec/integration/integration_spec.rb +68 -0
- data/spec/performance/active_record.rb +4 -0
- data/spec/performance/benchmark.rb +57 -0
- data/spec/performance/decorators.rb +41 -0
- data/spec/performance/models.rb +20 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/support/dummy_app.rb +88 -0
- data/spec/support/matchers/have_text.rb +50 -0
- data/spec/support/shared_examples/decoratable_equality.rb +40 -0
- data/spec/support/shared_examples/view_helpers.rb +39 -0
- metadata +497 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Drape
|
|
2
|
+
# @private
|
|
3
|
+
class DecoratedAssociation
|
|
4
|
+
def initialize(owner, association, options)
|
|
5
|
+
options.assert_valid_keys(:with, :scope, :context)
|
|
6
|
+
|
|
7
|
+
@owner = owner
|
|
8
|
+
@association = association
|
|
9
|
+
|
|
10
|
+
@scope = options[:scope]
|
|
11
|
+
|
|
12
|
+
decorator_class = options[:with]
|
|
13
|
+
context = options.fetch(:context, -> (inner_context) { inner_context })
|
|
14
|
+
@factory = Drape::Factory.new(with: decorator_class, context: context)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def call
|
|
18
|
+
decorate unless defined?(@decorated)
|
|
19
|
+
@decorated
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :factory, :owner, :association, :scope
|
|
25
|
+
|
|
26
|
+
def decorate
|
|
27
|
+
associated = owner.object.send(association)
|
|
28
|
+
associated = associated.send(scope) if scope
|
|
29
|
+
|
|
30
|
+
@decorated = factory.decorate(associated, context_args: owner.context)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Drape
|
|
2
|
+
module DecoratesAssigned
|
|
3
|
+
# @overload decorates_assigned(*variables, options = {})
|
|
4
|
+
# Defines a helper method to access decorated instance variables.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# # app/controllers/articles_controller.rb
|
|
8
|
+
# class ArticlesController < ApplicationController
|
|
9
|
+
# decorates_assigned :article
|
|
10
|
+
#
|
|
11
|
+
# def show
|
|
12
|
+
# @article = Article.find(params[:id])
|
|
13
|
+
# end
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # app/views/articles/show.html.erb
|
|
17
|
+
# <%= article.decorated_title %>
|
|
18
|
+
#
|
|
19
|
+
# @param [Symbols*] variables
|
|
20
|
+
# names of the instance variables to decorate (without the `@`).
|
|
21
|
+
# @param [Hash] options
|
|
22
|
+
# @option options [Decorator, CollectionDecorator] :with (nil)
|
|
23
|
+
# decorator class to use. If nil, it is inferred from the instance
|
|
24
|
+
# variable.
|
|
25
|
+
# @option options [Hash, #call] :context
|
|
26
|
+
# extra data to be stored in the decorator. If a Proc is given, it will
|
|
27
|
+
# be passed the controller and should return a new context hash.
|
|
28
|
+
def decorates_assigned(*variables)
|
|
29
|
+
factory = Drape::Factory.new(variables.extract_options!)
|
|
30
|
+
|
|
31
|
+
variables.each do |variable|
|
|
32
|
+
undecorated = "@#{variable}"
|
|
33
|
+
decorated = "@decorated_#{variable}"
|
|
34
|
+
|
|
35
|
+
define_method variable do
|
|
36
|
+
return instance_variable_get(decorated) if instance_variable_defined?(decorated)
|
|
37
|
+
instance_variable_set decorated, factory.decorate(instance_variable_get(undecorated), context_args: self)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
helper_method variable
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
module Drape
|
|
2
|
+
class Decorator
|
|
3
|
+
include Drape::ViewHelpers
|
|
4
|
+
extend Drape::Delegation
|
|
5
|
+
|
|
6
|
+
include ActiveModel::Serialization
|
|
7
|
+
include ActiveModel::Serializers::JSON
|
|
8
|
+
include ActiveModel::Serializers::Xml
|
|
9
|
+
|
|
10
|
+
# @return the object being decorated.
|
|
11
|
+
attr_reader :object
|
|
12
|
+
alias model object
|
|
13
|
+
alias source object # TODO: deprecate this
|
|
14
|
+
alias to_source object # TODO: deprecate this
|
|
15
|
+
|
|
16
|
+
# @return [Hash] extra data to be used in user-defined methods.
|
|
17
|
+
attr_accessor :context
|
|
18
|
+
|
|
19
|
+
# Wraps an object in a new instance of the decorator.
|
|
20
|
+
#
|
|
21
|
+
# Decorators may be applied to other decorators. However, applying a
|
|
22
|
+
# decorator to an instance of itself will create a decorator with the same
|
|
23
|
+
# source as the original, rather than redecorating the other instance.
|
|
24
|
+
#
|
|
25
|
+
# @param [Object] object
|
|
26
|
+
# object to decorate.
|
|
27
|
+
# @option options [Hash] :context ({})
|
|
28
|
+
# extra data to be stored in the decorator and used in user-defined
|
|
29
|
+
# methods.
|
|
30
|
+
def initialize(object, options = {})
|
|
31
|
+
options.assert_valid_keys(:context)
|
|
32
|
+
@object = object
|
|
33
|
+
@context = options.fetch(:context, {})
|
|
34
|
+
handle_multiple_decoration(options) if object.instance_of?(self.class)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
alias decorate new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Automatically delegates instance methods to the source object. Class
|
|
42
|
+
# methods will be delegated to the {object_class}, if it is set.
|
|
43
|
+
#
|
|
44
|
+
# @return [void]
|
|
45
|
+
def self.delegate_all
|
|
46
|
+
include Drape::AutomaticDelegation
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Sets the source class corresponding to the decorator class.
|
|
50
|
+
#
|
|
51
|
+
# @note This is only necessary if you wish to proxy class methods to the
|
|
52
|
+
# source (including when using {decorates_finders}), and the source class
|
|
53
|
+
# cannot be inferred from the decorator class (e.g. `ProductDecorator`
|
|
54
|
+
# maps to `Product`).
|
|
55
|
+
# @param [String, Symbol, Class] object_class
|
|
56
|
+
# source class (or class name) that corresponds to this decorator.
|
|
57
|
+
# @return [void]
|
|
58
|
+
def self.decorates(object_class)
|
|
59
|
+
@object_class = object_class.to_s.camelize.constantize
|
|
60
|
+
alias_object_to_object_class_name
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the source class corresponding to the decorator class, as set by
|
|
64
|
+
# {decorates}, or as inferred from the decorator class name (e.g.
|
|
65
|
+
# `ProductDecorator` maps to `Product`).
|
|
66
|
+
#
|
|
67
|
+
# @return [Class] the source class that corresponds to this decorator.
|
|
68
|
+
def self.object_class
|
|
69
|
+
@object_class ||= inferred_object_class
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Checks whether this decorator class has a corresponding {object_class}.
|
|
73
|
+
def self.object_class?
|
|
74
|
+
object_class
|
|
75
|
+
rescue Drape::UninferrableSourceError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class << self # TODO deprecate this
|
|
80
|
+
alias source_class object_class
|
|
81
|
+
alias source_class? object_class?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Automatically decorates ActiveRecord finder methods, so that you can use
|
|
85
|
+
# `ProductDecorator.find(id)` instead of
|
|
86
|
+
# `ProductDecorator.decorate(Product.find(id))`.
|
|
87
|
+
#
|
|
88
|
+
# Finder methods are applied to the {object_class}.
|
|
89
|
+
#
|
|
90
|
+
# @return [void]
|
|
91
|
+
def self.decorates_finders
|
|
92
|
+
extend Drape::Finders
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Automatically decorate an association.
|
|
96
|
+
#
|
|
97
|
+
# @param [Symbol] association
|
|
98
|
+
# name of the association to decorate (e.g. `:products`).
|
|
99
|
+
# @option options [Class] :with
|
|
100
|
+
# the decorator to apply to the association.
|
|
101
|
+
# @option options [Symbol] :scope
|
|
102
|
+
# a scope to apply when fetching the association.
|
|
103
|
+
# @option options [Hash, #call] :context
|
|
104
|
+
# extra data to be stored in the associated decorator. If omitted, the
|
|
105
|
+
# associated decorator's context will be the same as the parent
|
|
106
|
+
# decorator's. If a Proc is given, it will be called with the parent's
|
|
107
|
+
# context and should return a new context hash for the association.
|
|
108
|
+
# @return [void]
|
|
109
|
+
def self.decorates_association(association, options = {})
|
|
110
|
+
options.assert_valid_keys(:with, :scope, :context)
|
|
111
|
+
define_method(association) do
|
|
112
|
+
decorated_associations[association] ||= Drape::DecoratedAssociation.new(self, association, options)
|
|
113
|
+
decorated_associations[association].call
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @overload decorates_associations(*associations, options = {})
|
|
118
|
+
# Automatically decorate multiple associations.
|
|
119
|
+
# @param [Symbols*] associations
|
|
120
|
+
# names of the associations to decorate.
|
|
121
|
+
# @param [Hash] options
|
|
122
|
+
# see {decorates_association}.
|
|
123
|
+
# @return [void]
|
|
124
|
+
def self.decorates_associations(*associations)
|
|
125
|
+
options = associations.extract_options!
|
|
126
|
+
associations.each do |association|
|
|
127
|
+
decorates_association(association, options)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Decorates a collection of objects. The class of the collection decorator
|
|
132
|
+
# is inferred from the decorator class if possible (e.g. `ProductDecorator`
|
|
133
|
+
# maps to `ProductsDecorator`), but otherwise defaults to
|
|
134
|
+
# {Drape::CollectionDecorator}.
|
|
135
|
+
#
|
|
136
|
+
# @param [Object] object
|
|
137
|
+
# collection to decorate.
|
|
138
|
+
# @option options [Class, nil] :with (self)
|
|
139
|
+
# the decorator class used to decorate each item. When `nil`, it is
|
|
140
|
+
# inferred from each item.
|
|
141
|
+
# @option options [Hash] :context
|
|
142
|
+
# extra data to be stored in the collection decorator.
|
|
143
|
+
def self.decorate_collection(object, options = {})
|
|
144
|
+
options.assert_valid_keys(:with, :context)
|
|
145
|
+
collection_decorator_class.new(object, options.reverse_merge(with: self))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# @return [Array<Class>] the list of decorators that have been applied to
|
|
149
|
+
# the object.
|
|
150
|
+
def applied_decorators
|
|
151
|
+
chain = object.respond_to?(:applied_decorators) ? object.applied_decorators : []
|
|
152
|
+
chain << self.class
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Checks if a given decorator has been applied to the object.
|
|
156
|
+
#
|
|
157
|
+
# @param [Class] decorator_class
|
|
158
|
+
def decorated_with?(decorator_class)
|
|
159
|
+
applied_decorators.include?(decorator_class)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Checks if this object is decorated.
|
|
163
|
+
#
|
|
164
|
+
# @return [true]
|
|
165
|
+
def decorated?
|
|
166
|
+
true
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Compares the source object with a possibly-decorated object.
|
|
170
|
+
#
|
|
171
|
+
# @return [Boolean]
|
|
172
|
+
def ==(other)
|
|
173
|
+
Drape::Decoratable::Equality.test(object, other)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Delegates equality to :== as expected
|
|
177
|
+
#
|
|
178
|
+
# @return [Boolean]
|
|
179
|
+
def eql?(other)
|
|
180
|
+
self == other
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Returns a unique hash for a decorated object based on
|
|
184
|
+
# the decorator class and the object being decorated.
|
|
185
|
+
#
|
|
186
|
+
# @return [Fixnum]
|
|
187
|
+
def hash
|
|
188
|
+
self.class.hash ^ object.hash
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Checks if `self.kind_of?(klass)` or `object.kind_of?(klass)`
|
|
192
|
+
#
|
|
193
|
+
# @param [Class] klass
|
|
194
|
+
def kind_of?(klass)
|
|
195
|
+
super || object.is_a?(klass)
|
|
196
|
+
end
|
|
197
|
+
alias is_a? kind_of?
|
|
198
|
+
|
|
199
|
+
# Checks if `self.instance_of?(klass)` or `object.instance_of?(klass)`
|
|
200
|
+
#
|
|
201
|
+
# @param [Class] klass
|
|
202
|
+
def instance_of?(klass)
|
|
203
|
+
super || object.instance_of?(klass)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
delegate :to_s
|
|
207
|
+
|
|
208
|
+
# In case object is nil
|
|
209
|
+
delegate :present?, :blank?
|
|
210
|
+
|
|
211
|
+
# ActiveModel compatibility
|
|
212
|
+
# @private
|
|
213
|
+
def to_model
|
|
214
|
+
self
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @return [Hash] the object's attributes, sliced to only include those
|
|
218
|
+
# implemented by the decorator.
|
|
219
|
+
def attributes
|
|
220
|
+
object.attributes.select { |attribute, _| respond_to?(attribute) }
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# ActiveModel compatibility
|
|
224
|
+
delegate :to_param, :to_partial_path
|
|
225
|
+
|
|
226
|
+
# ActiveModel compatibility
|
|
227
|
+
singleton_class.delegate :model_name, to: :object_class
|
|
228
|
+
|
|
229
|
+
# @return [Class] the class created by {decorate_collection}.
|
|
230
|
+
def self.collection_decorator_class
|
|
231
|
+
name = collection_decorator_name
|
|
232
|
+
name.constantize
|
|
233
|
+
rescue NameError => error
|
|
234
|
+
raise if name && !error.missing_name?(name)
|
|
235
|
+
Drape::CollectionDecorator
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def self.inherited(subclass)
|
|
239
|
+
subclass.alias_object_to_object_class_name
|
|
240
|
+
super
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def self.alias_object_to_object_class_name
|
|
244
|
+
alias_method object_class.name.underscore, :object if object_class?
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def self.object_class_name
|
|
248
|
+
raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/
|
|
249
|
+
name.chomp('Decorator')
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def self.inferred_object_class
|
|
253
|
+
name = object_class_name
|
|
254
|
+
name.constantize
|
|
255
|
+
rescue NameError => error
|
|
256
|
+
raise if name && !error.missing_name?(name)
|
|
257
|
+
raise Drape::UninferrableSourceError, self
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def self.collection_decorator_name
|
|
261
|
+
plural = object_class_name.pluralize
|
|
262
|
+
raise NameError if plural == object_class_name
|
|
263
|
+
"#{plural}Decorator"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
private
|
|
267
|
+
|
|
268
|
+
def handle_multiple_decoration(options)
|
|
269
|
+
if object.applied_decorators.last == self.class
|
|
270
|
+
@context = object.context unless options.key?(:context)
|
|
271
|
+
@object = object.object
|
|
272
|
+
else
|
|
273
|
+
warn "Reapplying #{self.class} decorator to target that is already decorated with it. " +
|
|
274
|
+
"Call stack:\n#{caller(1).join("\n")}"
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def decorated_associations
|
|
279
|
+
@decorated_associations ||= {}
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Drape
|
|
2
|
+
module Delegation
|
|
3
|
+
# @overload delegate(*methods, options = {})
|
|
4
|
+
# Overrides {http://api.rubyonrails.org/classes/Module.html#method-i-delegate Module.delegate}
|
|
5
|
+
# to make `:object` the default delegation target.
|
|
6
|
+
#
|
|
7
|
+
# @return [void]
|
|
8
|
+
def delegate(*methods)
|
|
9
|
+
options = methods.extract_options!
|
|
10
|
+
super(*methods, options.reverse_merge(to: :object))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module Drape
|
|
2
|
+
class Factory
|
|
3
|
+
# Creates a decorator factory.
|
|
4
|
+
#
|
|
5
|
+
# @option options [Decorator, CollectionDecorator] :with (nil)
|
|
6
|
+
# decorator class to use. If nil, it is inferred from the object
|
|
7
|
+
# passed to {#decorate}.
|
|
8
|
+
# @option options [Hash, #call] context
|
|
9
|
+
# extra data to be stored in created decorators. If a proc is given, it
|
|
10
|
+
# will be called each time {#decorate} is called and its return value
|
|
11
|
+
# will be used as the context.
|
|
12
|
+
def initialize(options = {})
|
|
13
|
+
options.assert_valid_keys(:with, :context)
|
|
14
|
+
@decorator_class = options.delete(:with)
|
|
15
|
+
@default_options = options
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Decorates an object, inferring whether to create a singular or collection
|
|
19
|
+
# decorator from the type of object passed.
|
|
20
|
+
#
|
|
21
|
+
# @param [Object] object
|
|
22
|
+
# object to decorate.
|
|
23
|
+
# @option options [Hash] context
|
|
24
|
+
# extra data to be stored in the decorator. Overrides any context passed
|
|
25
|
+
# to the constructor.
|
|
26
|
+
# @option options [Object, Array] context_args (nil)
|
|
27
|
+
# argument(s) to be passed to the context proc.
|
|
28
|
+
# @return [Decorator, CollectionDecorator] the decorated object.
|
|
29
|
+
def decorate(object, options = {})
|
|
30
|
+
return nil if object.nil?
|
|
31
|
+
Worker.new(decorator_class, object).call(options.reverse_merge(default_options))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :decorator_class, :default_options
|
|
37
|
+
|
|
38
|
+
# @private
|
|
39
|
+
class Worker
|
|
40
|
+
def initialize(decorator_class, object)
|
|
41
|
+
@decorator_class = decorator_class
|
|
42
|
+
@object = object
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def call(options)
|
|
46
|
+
update_context options
|
|
47
|
+
decorator.call(object, options)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def decorator
|
|
51
|
+
return decorator_method(decorator_class) if decorator_class
|
|
52
|
+
return object_decorator if decoratable?
|
|
53
|
+
return decorator_method(Drape::CollectionDecorator) if collection?
|
|
54
|
+
raise Drape::UninferrableDecoratorError, object.class
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
attr_reader :decorator_class, :object
|
|
60
|
+
|
|
61
|
+
def object_decorator
|
|
62
|
+
if collection?
|
|
63
|
+
->(object, options) { object.decorator_class.decorate_collection(object, options.reverse_merge(with: nil)) }
|
|
64
|
+
else
|
|
65
|
+
->(object, options) { object.decorate(options) }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def decorator_method(klass)
|
|
70
|
+
if collection? && klass.respond_to?(:decorate_collection)
|
|
71
|
+
klass.method(:decorate_collection)
|
|
72
|
+
else
|
|
73
|
+
klass.method(:decorate)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def collection?
|
|
78
|
+
object.respond_to?(:first) && !object.is_a?(Struct)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def decoratable?
|
|
82
|
+
object.respond_to?(:decorate)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def update_context(options)
|
|
86
|
+
args = options.delete(:context_args)
|
|
87
|
+
options[:context] = options[:context].call(*Array.wrap(args)) if options[:context].respond_to?(:call)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|