draper 0.18.0 → 1.0.0
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.
- data/.gitignore +3 -0
- data/.travis.yml +11 -0
- data/.yardopts +1 -1
- data/CHANGELOG.md +161 -0
- data/CONTRIBUTING.md +11 -0
- data/Gemfile +20 -0
- data/LICENSE +7 -0
- data/README.md +324 -0
- data/Rakefile +52 -31
- data/draper.gemspec +11 -9
- data/lib/draper/automatic_delegation.rb +50 -0
- data/lib/draper/collection_decorator.rb +107 -0
- data/lib/draper/decoratable.rb +83 -0
- data/lib/draper/decorated_association.rb +74 -0
- data/lib/draper/decorator.rb +238 -0
- data/lib/draper/delegation.rb +13 -0
- data/lib/draper/finders.rb +37 -0
- data/lib/draper/helper_proxy.rb +17 -0
- data/lib/draper/lazy_helpers.rb +10 -6
- data/lib/draper/railtie.rb +27 -21
- data/lib/draper/tasks/test.rake +22 -0
- data/lib/draper/test/devise_helper.rb +34 -0
- data/lib/draper/test/minitest_integration.rb +2 -3
- data/lib/draper/test/rspec_integration.rb +3 -13
- data/lib/draper/test_case.rb +33 -0
- data/lib/draper/version.rb +1 -1
- data/lib/draper/view_context.rb +12 -17
- data/lib/draper/view_helpers.rb +37 -0
- data/lib/draper.rb +47 -8
- data/lib/generators/decorator/decorator_generator.rb +10 -2
- data/lib/generators/decorator/templates/decorator.rb +12 -27
- 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/test_unit/templates/decorator_test.rb +1 -1
- data/spec/draper/collection_decorator_spec.rb +286 -0
- data/spec/draper/decoratable_spec.rb +192 -0
- data/spec/draper/decorated_association_spec.rb +142 -0
- data/spec/draper/decorator_spec.rb +624 -0
- data/spec/draper/finders_spec.rb +132 -0
- data/spec/draper/helper_proxy_spec.rb +12 -0
- data/spec/draper/view_helpers_spec.rb +41 -0
- data/spec/dummy/.rspec +2 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +4 -0
- data/spec/dummy/app/controllers/localized_urls.rb +5 -0
- data/spec/dummy/app/controllers/posts_controller.rb +11 -0
- data/spec/dummy/app/decorators/post_decorator.rb +32 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/application_mailer.rb +3 -0
- data/spec/dummy/app/mailers/post_mailer.rb +9 -0
- data/spec/dummy/app/models/post.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +11 -0
- data/spec/dummy/app/views/post_mailer/decorated_email.html.erb +1 -0
- data/spec/dummy/app/views/posts/_post.html.erb +19 -0
- data/spec/dummy/app/views/posts/show.html.erb +1 -0
- data/spec/dummy/config/application.rb +64 -0
- data/spec/dummy/config/boot.rb +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 +34 -0
- data/spec/dummy/config/environments/production.rb +55 -0
- data/spec/dummy/config/environments/test.rb +32 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +7 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20121019115657_create_posts.rb +8 -0
- data/spec/dummy/db/schema.rb +21 -0
- data/spec/dummy/db/seeds.rb +2 -0
- data/spec/dummy/lib/tasks/test.rake +10 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/mini_test/mini_test_integration_test.rb +46 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/spec/decorators/post_decorator_spec.rb +26 -0
- data/spec/dummy/spec/decorators/rspec_integration_spec.rb +19 -0
- data/spec/dummy/spec/mailers/post_mailer_spec.rb +29 -0
- data/spec/dummy/spec/models/post_spec.rb +14 -0
- data/spec/dummy/spec/spec_helper.rb +8 -0
- data/spec/generators/decorator/decorator_generator_spec.rb +47 -7
- data/spec/integration/integration_spec.rb +33 -0
- data/{performance → spec/performance}/decorators.rb +2 -4
- data/spec/spec_helper.rb +20 -45
- data/spec/support/action_controller.rb +12 -0
- data/spec/support/active_model.rb +7 -0
- data/spec/support/active_record.rb +9 -0
- data/spec/support/{samples → decorators}/decorator_with_application_helper.rb +1 -1
- data/spec/support/decorators/namespaced_product_decorator.rb +5 -0
- data/spec/support/decorators/non_active_model_product_decorator.rb +2 -0
- data/spec/support/decorators/product_decorator.rb +23 -0
- data/spec/support/{samples → decorators}/products_decorator.rb +1 -1
- data/spec/support/decorators/some_thing_decorator.rb +2 -0
- data/spec/support/{samples → decorators}/specific_product_decorator.rb +0 -2
- data/spec/support/dummy_app.rb +85 -0
- data/spec/support/matchers/have_text.rb +50 -0
- data/spec/support/{samples → models}/namespaced_product.rb +1 -3
- data/spec/support/{samples → models}/non_active_model_product.rb +1 -0
- data/spec/support/{samples → models}/product.rb +17 -2
- data/spec/support/models/some_thing.rb +5 -0
- data/spec/support/models/uninferrable_decorator_model.rb +3 -0
- metadata +241 -87
- data/CHANGELOG.markdown +0 -57
- data/Readme.markdown +0 -387
- data/lib/draper/active_model_support.rb +0 -27
- data/lib/draper/base.rb +0 -312
- data/lib/draper/decorated_enumerable_proxy.rb +0 -90
- data/lib/draper/model_support.rb +0 -25
- data/lib/draper/rspec_integration.rb +0 -2
- data/lib/draper/system.rb +0 -18
- data/spec/draper/base_spec.rb +0 -873
- data/spec/draper/decorated_enumerable_proxy_spec.rb +0 -45
- data/spec/draper/model_support_spec.rb +0 -48
- data/spec/minitest-rails/spec_type_spec.rb +0 -63
- data/spec/support/samples/active_record.rb +0 -17
- data/spec/support/samples/decorator.rb +0 -5
- data/spec/support/samples/decorator_with_allows.rb +0 -3
- data/spec/support/samples/decorator_with_denies.rb +0 -3
- data/spec/support/samples/decorator_with_denies_all.rb +0 -3
- data/spec/support/samples/decorator_with_multiple_allows.rb +0 -4
- data/spec/support/samples/decorator_with_special_methods.rb +0 -13
- data/spec/support/samples/enumerable_proxy.rb +0 -3
- data/spec/support/samples/namespaced_product_decorator.rb +0 -7
- data/spec/support/samples/product_decorator.rb +0 -7
- data/spec/support/samples/sequel_product.rb +0 -13
- data/spec/support/samples/some_thing.rb +0 -2
- data/spec/support/samples/some_thing_decorator.rb +0 -3
- /data/{performance → spec/performance}/active_record.rb +0 -0
- /data/{performance/bechmark.rb → spec/performance/benchmark.rb} +0 -0
- /data/{performance → spec/performance}/models.rb +0 -0
- /data/spec/support/{samples → decorators}/widget_decorator.rb +0 -0
- /data/spec/support/{samples → models}/widget.rb +0 -0
data/draper.gemspec
CHANGED
|
@@ -7,22 +7,24 @@ Gem::Specification.new do |s|
|
|
|
7
7
|
s.version = Draper::VERSION
|
|
8
8
|
s.authors = ["Jeff Casimir", "Steve Klabnik"]
|
|
9
9
|
s.email = ["jeff@casimircreative.com", "steve@steveklabnik.com"]
|
|
10
|
-
s.homepage = "http://github.com/
|
|
11
|
-
s.summary = "
|
|
12
|
-
s.description = "Draper
|
|
10
|
+
s.homepage = "http://github.com/drapergem/draper"
|
|
11
|
+
s.summary = "View Models for Rails"
|
|
12
|
+
s.description = "Draper adds a nicely-separated object-oriented layer of presentation logic to your Rails apps."
|
|
13
13
|
s.rubyforge_project = "draper"
|
|
14
14
|
s.files = `git ls-files`.split("\n")
|
|
15
15
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
|
16
16
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
|
17
17
|
s.require_paths = ["lib"]
|
|
18
18
|
|
|
19
|
-
s.add_dependency 'activesupport', '
|
|
20
|
-
s.add_dependency 'actionpack', '
|
|
19
|
+
s.add_dependency 'activesupport', '>= 3.0'
|
|
20
|
+
s.add_dependency 'actionpack', '>= 3.0'
|
|
21
|
+
s.add_dependency 'request_store', '~> 1.0.3'
|
|
21
22
|
|
|
22
23
|
s.add_development_dependency 'ammeter'
|
|
23
|
-
s.add_development_dependency 'rake', '
|
|
24
|
-
s.add_development_dependency 'rspec', '~> 2.
|
|
25
|
-
s.add_development_dependency '
|
|
24
|
+
s.add_development_dependency 'rake', '>= 0.9.2'
|
|
25
|
+
s.add_development_dependency 'rspec', '~> 2.12'
|
|
26
|
+
s.add_development_dependency 'rspec-mocks', '>= 2.12.1'
|
|
27
|
+
s.add_development_dependency 'rspec-rails', '~> 2.12'
|
|
26
28
|
s.add_development_dependency 'minitest-rails', '~> 0.2'
|
|
27
|
-
s.add_development_dependency '
|
|
29
|
+
s.add_development_dependency 'capybara'
|
|
28
30
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
module Draper
|
|
2
|
+
module AutomaticDelegation
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
# Delegates missing instance methods to the source object.
|
|
6
|
+
def method_missing(method, *args, &block)
|
|
7
|
+
return super unless delegatable?(method)
|
|
8
|
+
|
|
9
|
+
self.class.delegate method
|
|
10
|
+
send(method, *args, &block)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Checks if the decorator responds to an instance method, or is able to
|
|
14
|
+
# proxy it to the source object.
|
|
15
|
+
def respond_to?(method, include_private = false)
|
|
16
|
+
super || delegatable?(method)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @private
|
|
20
|
+
def delegatable?(method)
|
|
21
|
+
source.respond_to?(method)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module ClassMethods
|
|
25
|
+
# Proxies missing class methods to the source class.
|
|
26
|
+
def method_missing(method, *args, &block)
|
|
27
|
+
return super unless delegatable?(method)
|
|
28
|
+
|
|
29
|
+
source_class.send(method, *args, &block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Checks if the decorator responds to a class method, or is able to proxy
|
|
33
|
+
# it to the source class.
|
|
34
|
+
def respond_to?(method, include_private = false)
|
|
35
|
+
super || delegatable?(method)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @private
|
|
39
|
+
def delegatable?(method)
|
|
40
|
+
source_class? && source_class.respond_to?(method)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
included do
|
|
45
|
+
private :delegatable?
|
|
46
|
+
private_class_method :delegatable?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module Draper
|
|
2
|
+
class CollectionDecorator
|
|
3
|
+
include Enumerable
|
|
4
|
+
include Draper::ViewHelpers
|
|
5
|
+
extend Draper::Delegation
|
|
6
|
+
|
|
7
|
+
# @return [Hash] extra data to be used in user-defined methods, and passed
|
|
8
|
+
# to each item's decorator.
|
|
9
|
+
attr_accessor :context
|
|
10
|
+
|
|
11
|
+
array_methods = Array.instance_methods - Object.instance_methods
|
|
12
|
+
delegate :==, :as_json, *array_methods, to: :decorated_collection
|
|
13
|
+
|
|
14
|
+
# @param [Enumerable] source
|
|
15
|
+
# collection to decorate.
|
|
16
|
+
# @option options [Class, nil] :with (nil)
|
|
17
|
+
# the decorator class used to decorate each item. When `nil`, it is
|
|
18
|
+
# inferred from the collection decorator class if possible (e.g.
|
|
19
|
+
# `ProductsDecorator` maps to `ProductDecorator`), otherwise each item's
|
|
20
|
+
# {Decoratable#decorate decorate} method will be used.
|
|
21
|
+
# @option options [Hash] :context ({})
|
|
22
|
+
# extra data to be stored in the collection decorator and used in
|
|
23
|
+
# user-defined methods, and passed to each item's decorator.
|
|
24
|
+
def initialize(source, options = {})
|
|
25
|
+
options.assert_valid_keys(:with, :context)
|
|
26
|
+
@source = source
|
|
27
|
+
@decorator_class = options[:with]
|
|
28
|
+
@context = options.fetch(:context, {})
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
alias_method :decorate, :new
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [Array] the decorated items.
|
|
36
|
+
def decorated_collection
|
|
37
|
+
@decorated_collection ||= source.map{|item| decorate_item(item)}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Delegated to the decorated collection when using the block form
|
|
41
|
+
# (`Enumerable#find`) or to the decorator class if not
|
|
42
|
+
# (`ActiveRecord::FinderMethods#find`)
|
|
43
|
+
def find(*args, &block)
|
|
44
|
+
if block_given?
|
|
45
|
+
decorated_collection.find(*args, &block)
|
|
46
|
+
else
|
|
47
|
+
decorator_class.find(*args)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_s
|
|
52
|
+
klass = begin
|
|
53
|
+
decorator_class
|
|
54
|
+
rescue Draper::UninferrableDecoratorError
|
|
55
|
+
"inferred decorators"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
"#<#{self.class.name} of #{klass} for #{source.inspect}>"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def context=(value)
|
|
62
|
+
@context = value
|
|
63
|
+
each {|item| item.context = value } if @decorated_collection
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [Class] the decorator class used to decorate each item, as set by
|
|
67
|
+
# {#initialize} or as inferred from the collection decorator class (e.g.
|
|
68
|
+
# `ProductsDecorator` maps to `ProductDecorator`).
|
|
69
|
+
def decorator_class
|
|
70
|
+
@decorator_class ||= self.class.inferred_decorator_class
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
protected
|
|
74
|
+
|
|
75
|
+
# @return the collection being decorated.
|
|
76
|
+
attr_reader :source
|
|
77
|
+
|
|
78
|
+
# Decorates the given item.
|
|
79
|
+
def decorate_item(item)
|
|
80
|
+
item_decorator.call(item, context: context)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def self.inferred_decorator_class
|
|
86
|
+
decorator_name = "#{name.chomp("Decorator").singularize}Decorator"
|
|
87
|
+
decorator_uninferrable if decorator_name == name
|
|
88
|
+
|
|
89
|
+
decorator_name.constantize
|
|
90
|
+
|
|
91
|
+
rescue NameError
|
|
92
|
+
decorator_uninferrable
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.decorator_uninferrable
|
|
96
|
+
raise Draper::UninferrableDecoratorError.new(self)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def item_decorator
|
|
100
|
+
@item_decorator ||= begin
|
|
101
|
+
decorator_class.method(:decorate)
|
|
102
|
+
rescue Draper::UninferrableDecoratorError
|
|
103
|
+
->(item, options) { item.decorate(options) }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
module Draper
|
|
2
|
+
# Provides shortcuts to decorate objects directly, so you can do
|
|
3
|
+
# `@product.decorate` instead of `ProductDecorator.new(@product)`.
|
|
4
|
+
#
|
|
5
|
+
# This module is included by default into `ActiveRecord::Base` and
|
|
6
|
+
# `Mongoid::Document`, but you're using another ORM, or want to decorate
|
|
7
|
+
# plain old Ruby objects, you can include it manually.
|
|
8
|
+
module Decoratable
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
# Decorates the object using the inferred {#decorator_class}.
|
|
12
|
+
# @param [Hash] options
|
|
13
|
+
# see {Decorator#initialize}
|
|
14
|
+
def decorate(options = {})
|
|
15
|
+
decorator_class.decorate(self, options)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# (see ClassMethods#decorator_class)
|
|
19
|
+
def decorator_class
|
|
20
|
+
self.class.decorator_class
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# The list of decorators that have been applied to the object.
|
|
24
|
+
#
|
|
25
|
+
# @return [Array<Class>] `[]`
|
|
26
|
+
def applied_decorators
|
|
27
|
+
[]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# (see Decorator#decorated_with?)
|
|
31
|
+
# @return [false]
|
|
32
|
+
def decorated_with?(decorator_class)
|
|
33
|
+
false
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Checks if this object is decorated.
|
|
37
|
+
#
|
|
38
|
+
# @return [false]
|
|
39
|
+
def decorated?
|
|
40
|
+
false
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Compares with possibly-decorated objects.
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean]
|
|
46
|
+
def ==(other)
|
|
47
|
+
super || (other.respond_to?(:source) && self == other.source)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
module ClassMethods
|
|
51
|
+
|
|
52
|
+
# Decorates a collection of objects. Used at the end of a scope chain.
|
|
53
|
+
#
|
|
54
|
+
# @example
|
|
55
|
+
# Product.popular.decorate
|
|
56
|
+
# @param [Hash] options
|
|
57
|
+
# see {Decorator.decorate_collection}.
|
|
58
|
+
def decorate(options = {})
|
|
59
|
+
decorator_class.decorate_collection(self.scoped, options)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Infers the decorator class to be used by {Decoratable#decorate} (e.g.
|
|
63
|
+
# `Product` maps to `ProductDecorator`).
|
|
64
|
+
#
|
|
65
|
+
# @return [Class] the inferred decorator class.
|
|
66
|
+
def decorator_class
|
|
67
|
+
prefix = respond_to?(:model_name) ? model_name : name
|
|
68
|
+
"#{prefix}Decorator".constantize
|
|
69
|
+
rescue NameError
|
|
70
|
+
raise Draper::UninferrableDecoratorError.new(self)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Compares with possibly-decorated objects.
|
|
74
|
+
#
|
|
75
|
+
# @return [Boolean]
|
|
76
|
+
def ===(other)
|
|
77
|
+
super || (other.respond_to?(:source) && super(other.source))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
module Draper
|
|
2
|
+
# @private
|
|
3
|
+
class DecoratedAssociation
|
|
4
|
+
|
|
5
|
+
def initialize(owner, association, options)
|
|
6
|
+
options.assert_valid_keys(:with, :scope, :context)
|
|
7
|
+
|
|
8
|
+
@owner = owner
|
|
9
|
+
@association = association
|
|
10
|
+
|
|
11
|
+
@decorator_class = options[:with]
|
|
12
|
+
@scope = options[:scope]
|
|
13
|
+
@context = options.fetch(:context, owner.context)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call
|
|
17
|
+
return undecorated if undecorated.nil?
|
|
18
|
+
decorated
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def context
|
|
22
|
+
return @context.call(owner.context) if @context.respond_to?(:call)
|
|
23
|
+
@context
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
attr_reader :owner, :association, :scope
|
|
29
|
+
|
|
30
|
+
def source
|
|
31
|
+
owner.source
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def undecorated
|
|
35
|
+
@undecorated ||= begin
|
|
36
|
+
associated = source.send(association)
|
|
37
|
+
associated = associated.send(scope) if scope
|
|
38
|
+
associated
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def decorated
|
|
43
|
+
@decorated ||= decorator.call(undecorated, context: context)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def collection?
|
|
47
|
+
undecorated.respond_to?(:first)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def decorator
|
|
51
|
+
return collection_decorator if collection?
|
|
52
|
+
decorator_class.method(:decorate)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def collection_decorator
|
|
56
|
+
klass = decorator_class || Draper::CollectionDecorator
|
|
57
|
+
|
|
58
|
+
if klass.respond_to?(:decorate_collection)
|
|
59
|
+
klass.method(:decorate_collection)
|
|
60
|
+
else
|
|
61
|
+
klass.method(:decorate)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def decorator_class
|
|
66
|
+
@decorator_class || inferred_decorator_class
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def inferred_decorator_class
|
|
70
|
+
undecorated.decorator_class if undecorated.respond_to?(:decorator_class)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
require 'active_support/core_ext/array/extract_options'
|
|
2
|
+
|
|
3
|
+
module Draper
|
|
4
|
+
class Decorator
|
|
5
|
+
include Draper::ViewHelpers
|
|
6
|
+
extend Draper::Delegation
|
|
7
|
+
include ActiveModel::Serialization if defined?(ActiveModel::Serialization)
|
|
8
|
+
|
|
9
|
+
# @return the object being decorated.
|
|
10
|
+
attr_reader :source
|
|
11
|
+
alias_method :model, :source
|
|
12
|
+
alias_method :to_source, :source
|
|
13
|
+
|
|
14
|
+
# @return [Hash] extra data to be used in user-defined methods.
|
|
15
|
+
attr_accessor :context
|
|
16
|
+
|
|
17
|
+
# Wraps an object in a new instance of the decorator.
|
|
18
|
+
#
|
|
19
|
+
# Decorators may be applied to other decorators. However, applying a
|
|
20
|
+
# decorator to an instance of itself will create a decorator with the same
|
|
21
|
+
# source as the original, rather than redecorating the other instance.
|
|
22
|
+
#
|
|
23
|
+
# @param [Object] source
|
|
24
|
+
# object to decorate.
|
|
25
|
+
# @option options [Hash] :context ({})
|
|
26
|
+
# extra data to be stored in the decorator and used in user-defined
|
|
27
|
+
# methods.
|
|
28
|
+
def initialize(source, options = {})
|
|
29
|
+
options.assert_valid_keys(:context)
|
|
30
|
+
source.to_a if source.respond_to?(:to_a) # forces evaluation of a lazy query from AR
|
|
31
|
+
@source = source
|
|
32
|
+
@context = options.fetch(:context, {})
|
|
33
|
+
handle_multiple_decoration(options) if source.instance_of?(self.class)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
alias_method :decorate, :new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Automatically delegates instance methods to the source object. Class
|
|
41
|
+
# methods will be delegated to the {source_class}, if it is set.
|
|
42
|
+
#
|
|
43
|
+
# @return [void]
|
|
44
|
+
def self.delegate_all
|
|
45
|
+
include Draper::AutomaticDelegation
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Sets the source class corresponding to the decorator class.
|
|
49
|
+
#
|
|
50
|
+
# @note This is only necessary if you wish to proxy class methods to the
|
|
51
|
+
# source (including when using {decorates_finders}), and the source class
|
|
52
|
+
# cannot be inferred from the decorator class (e.g. `ProductDecorator`
|
|
53
|
+
# maps to `Product`).
|
|
54
|
+
# @param [String, Symbol, Class] source_class
|
|
55
|
+
# source class (or class name) that corresponds to this decorator.
|
|
56
|
+
# @return [void]
|
|
57
|
+
def self.decorates(source_class)
|
|
58
|
+
@source_class = source_class.to_s.camelize.constantize
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the source class corresponding to the decorator class, as set by
|
|
62
|
+
# {decorates}, or as inferred from the decorator class name (e.g.
|
|
63
|
+
# `ProductDecorator` maps to `Product`).
|
|
64
|
+
#
|
|
65
|
+
# @return [Class] the source class that corresponds to this decorator.
|
|
66
|
+
def self.source_class
|
|
67
|
+
@source_class ||= inferred_source_class
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Checks whether this decorator class has a corresponding {source_class}.
|
|
71
|
+
def self.source_class?
|
|
72
|
+
source_class
|
|
73
|
+
rescue Draper::UninferrableSourceError
|
|
74
|
+
false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Automatically decorates ActiveRecord finder methods, so that you can use
|
|
78
|
+
# `ProductDecorator.find(id)` instead of
|
|
79
|
+
# `ProductDecorator.decorate(Product.find(id))`.
|
|
80
|
+
#
|
|
81
|
+
# Finder methods are applied to the {source_class}.
|
|
82
|
+
#
|
|
83
|
+
# @return [void]
|
|
84
|
+
def self.decorates_finders
|
|
85
|
+
extend Draper::Finders
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Automatically decorate an association.
|
|
89
|
+
#
|
|
90
|
+
# @param [Symbol] association
|
|
91
|
+
# name of the association to decorate (e.g. `:products`).
|
|
92
|
+
# @option options [Class] :with
|
|
93
|
+
# the decorator to apply to the association.
|
|
94
|
+
# @option options [Symbol] :scope
|
|
95
|
+
# a scope to apply when fetching the association.
|
|
96
|
+
# @option options [Hash, #call] :context
|
|
97
|
+
# extra data to be stored in the associated decorator. If omitted, the
|
|
98
|
+
# associated decorator's context will be the same as the parent
|
|
99
|
+
# decorator's. If a Proc is given, it will be called with the parent's
|
|
100
|
+
# context and should return a new context hash for the association.
|
|
101
|
+
# @return [void]
|
|
102
|
+
def self.decorates_association(association, options = {})
|
|
103
|
+
options.assert_valid_keys(:with, :scope, :context)
|
|
104
|
+
define_method(association) do
|
|
105
|
+
decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
|
|
106
|
+
decorated_associations[association].call
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @overload decorates_associations(*associations, options = {})
|
|
111
|
+
# Automatically decorate multiple associations.
|
|
112
|
+
# @param [Symbols*] associations
|
|
113
|
+
# names of the associations to decorate.
|
|
114
|
+
# @param [Hash] options
|
|
115
|
+
# see {decorates_association}.
|
|
116
|
+
# @return [void]
|
|
117
|
+
def self.decorates_associations(*associations)
|
|
118
|
+
options = associations.extract_options!
|
|
119
|
+
associations.each do |association|
|
|
120
|
+
decorates_association(association, options)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Decorates a collection of objects. The class of the collection decorator
|
|
125
|
+
# is inferred from the decorator class if possible (e.g. `ProductDecorator`
|
|
126
|
+
# maps to `ProductsDecorator`), but otherwise defaults to
|
|
127
|
+
# {Draper::CollectionDecorator}.
|
|
128
|
+
#
|
|
129
|
+
# @param [Object] source
|
|
130
|
+
# collection to decorate.
|
|
131
|
+
# @option options [Class, nil] :with (self)
|
|
132
|
+
# the decorator class used to decorate each item. When `nil`, it is
|
|
133
|
+
# inferred from each item.
|
|
134
|
+
# @option options [Hash] :context
|
|
135
|
+
# extra data to be stored in the collection decorator.
|
|
136
|
+
def self.decorate_collection(source, options = {})
|
|
137
|
+
options.assert_valid_keys(:with, :context)
|
|
138
|
+
collection_decorator_class.new(source, options.reverse_merge(with: self))
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @return [Array<Class>] the list of decorators that have been applied to
|
|
142
|
+
# the object.
|
|
143
|
+
def applied_decorators
|
|
144
|
+
chain = source.respond_to?(:applied_decorators) ? source.applied_decorators : []
|
|
145
|
+
chain << self.class
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Checks if a given decorator has been applied to the object.
|
|
149
|
+
#
|
|
150
|
+
# @param [Class] decorator_class
|
|
151
|
+
def decorated_with?(decorator_class)
|
|
152
|
+
applied_decorators.include?(decorator_class)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Checks if this object is decorated.
|
|
156
|
+
#
|
|
157
|
+
# @return [true]
|
|
158
|
+
def decorated?
|
|
159
|
+
true
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Delegated to the source object.
|
|
163
|
+
#
|
|
164
|
+
# @return [Boolean]
|
|
165
|
+
def ==(other)
|
|
166
|
+
source == (other.respond_to?(:source) ? other.source : other)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Checks if `self.kind_of?(klass)` or `source.kind_of?(klass)`
|
|
170
|
+
#
|
|
171
|
+
# @param [Class] klass
|
|
172
|
+
def kind_of?(klass)
|
|
173
|
+
super || source.kind_of?(klass)
|
|
174
|
+
end
|
|
175
|
+
alias_method :is_a?, :kind_of?
|
|
176
|
+
|
|
177
|
+
# Checks if `self.instance_of?(klass)` or `source.instance_of?(klass)`
|
|
178
|
+
#
|
|
179
|
+
# @param [Class] klass
|
|
180
|
+
def instance_of?(klass)
|
|
181
|
+
super || source.instance_of?(klass)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# In case source is nil
|
|
185
|
+
delegate :present?
|
|
186
|
+
|
|
187
|
+
# ActiveModel compatibility
|
|
188
|
+
# @private
|
|
189
|
+
def to_model
|
|
190
|
+
self
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# ActiveModel compatibility
|
|
194
|
+
delegate :to_param, :to_partial_path
|
|
195
|
+
|
|
196
|
+
# ActiveModel compatibility
|
|
197
|
+
singleton_class.delegate :model_name, to: :source_class
|
|
198
|
+
|
|
199
|
+
# @return [Class] the class created by {decorate_collection}.
|
|
200
|
+
def self.collection_decorator_class
|
|
201
|
+
collection_decorator_name.constantize
|
|
202
|
+
rescue NameError
|
|
203
|
+
Draper::CollectionDecorator
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def self.source_name
|
|
209
|
+
raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/
|
|
210
|
+
name.chomp("Decorator")
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def self.inferred_source_class
|
|
214
|
+
source_name.constantize
|
|
215
|
+
rescue NameError
|
|
216
|
+
raise Draper::UninferrableSourceError.new(self)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.collection_decorator_name
|
|
220
|
+
plural = source_name.pluralize
|
|
221
|
+
raise NameError if plural == source_name
|
|
222
|
+
"#{plural}Decorator"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def handle_multiple_decoration(options)
|
|
226
|
+
if source.applied_decorators.last == self.class
|
|
227
|
+
@context = source.context unless options.has_key?(:context)
|
|
228
|
+
@source = source.source
|
|
229
|
+
else
|
|
230
|
+
warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}"
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def decorated_associations
|
|
235
|
+
@decorated_associations ||= {}
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Draper
|
|
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 `:source` the default delegation target.
|
|
6
|
+
#
|
|
7
|
+
# @return [void]
|
|
8
|
+
def delegate(*methods)
|
|
9
|
+
options = methods.extract_options!
|
|
10
|
+
super *methods, options.reverse_merge(to: :source)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Draper
|
|
2
|
+
# Provides automatically-decorated finder methods for your decorators. You
|
|
3
|
+
# do not have to extend this module directly; it is extended by
|
|
4
|
+
# {Decorator.decorates_finders}.
|
|
5
|
+
module Finders
|
|
6
|
+
|
|
7
|
+
def find(id, options = {})
|
|
8
|
+
decorate(source_class.find(id), options)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def all(options = {})
|
|
12
|
+
decorate_collection(source_class.all, options)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def first(options = {})
|
|
16
|
+
decorate(source_class.first, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def last(options = {})
|
|
20
|
+
decorate(source_class.last, options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Decorates dynamic finder methods (`find_all_by_` and friends).
|
|
24
|
+
def method_missing(method, *args, &block)
|
|
25
|
+
return super unless method =~ /^find_(all_|last_|or_(initialize_|create_))?by_/
|
|
26
|
+
|
|
27
|
+
result = source_class.send(method, *args, &block)
|
|
28
|
+
options = args.extract_options!
|
|
29
|
+
|
|
30
|
+
if method =~ /^find_all/
|
|
31
|
+
decorate_collection(result, options)
|
|
32
|
+
else
|
|
33
|
+
decorate(result, options)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module Draper
|
|
2
|
+
# Provides access to helper methods - both Rails built-in helpers, and those
|
|
3
|
+
# defined in your application.
|
|
4
|
+
class HelperProxy
|
|
5
|
+
|
|
6
|
+
# Sends helper methods to the view context.
|
|
7
|
+
def method_missing(method, *args, &block)
|
|
8
|
+
view_context.send(method, *args, &block)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def view_context
|
|
14
|
+
Draper::ViewContext.current
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/draper/lazy_helpers.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
module Draper
|
|
2
|
+
# Include this module in your decorators to get direct access to the helpers
|
|
3
|
+
# so that you can stop typing `h.` everywhere, at the cost of mixing in a
|
|
4
|
+
# bazillion methods.
|
|
2
5
|
module LazyHelpers
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
|
|
7
|
+
# Sends missing methods to the {HelperProxy}.
|
|
8
|
+
def method_missing(method, *args, &block)
|
|
9
|
+
helpers.send(method, *args, &block)
|
|
10
|
+
rescue NoMethodError
|
|
11
|
+
super
|
|
9
12
|
end
|
|
13
|
+
|
|
10
14
|
end
|
|
11
15
|
end
|