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.
Files changed (140) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +11 -0
  3. data/.yardopts +1 -1
  4. data/CHANGELOG.md +161 -0
  5. data/CONTRIBUTING.md +11 -0
  6. data/Gemfile +20 -0
  7. data/LICENSE +7 -0
  8. data/README.md +324 -0
  9. data/Rakefile +52 -31
  10. data/draper.gemspec +11 -9
  11. data/lib/draper/automatic_delegation.rb +50 -0
  12. data/lib/draper/collection_decorator.rb +107 -0
  13. data/lib/draper/decoratable.rb +83 -0
  14. data/lib/draper/decorated_association.rb +74 -0
  15. data/lib/draper/decorator.rb +238 -0
  16. data/lib/draper/delegation.rb +13 -0
  17. data/lib/draper/finders.rb +37 -0
  18. data/lib/draper/helper_proxy.rb +17 -0
  19. data/lib/draper/lazy_helpers.rb +10 -6
  20. data/lib/draper/railtie.rb +27 -21
  21. data/lib/draper/tasks/test.rake +22 -0
  22. data/lib/draper/test/devise_helper.rb +34 -0
  23. data/lib/draper/test/minitest_integration.rb +2 -3
  24. data/lib/draper/test/rspec_integration.rb +3 -13
  25. data/lib/draper/test_case.rb +33 -0
  26. data/lib/draper/version.rb +1 -1
  27. data/lib/draper/view_context.rb +12 -17
  28. data/lib/draper/view_helpers.rb +37 -0
  29. data/lib/draper.rb +47 -8
  30. data/lib/generators/decorator/decorator_generator.rb +10 -2
  31. data/lib/generators/decorator/templates/decorator.rb +12 -27
  32. data/lib/generators/mini_test/decorator_generator.rb +20 -0
  33. data/lib/generators/mini_test/templates/decorator_spec.rb +4 -0
  34. data/lib/generators/mini_test/templates/decorator_test.rb +4 -0
  35. data/lib/generators/test_unit/templates/decorator_test.rb +1 -1
  36. data/spec/draper/collection_decorator_spec.rb +286 -0
  37. data/spec/draper/decoratable_spec.rb +192 -0
  38. data/spec/draper/decorated_association_spec.rb +142 -0
  39. data/spec/draper/decorator_spec.rb +624 -0
  40. data/spec/draper/finders_spec.rb +132 -0
  41. data/spec/draper/helper_proxy_spec.rb +12 -0
  42. data/spec/draper/view_helpers_spec.rb +41 -0
  43. data/spec/dummy/.rspec +2 -0
  44. data/spec/dummy/README.rdoc +261 -0
  45. data/spec/dummy/Rakefile +7 -0
  46. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  47. data/spec/dummy/app/controllers/localized_urls.rb +5 -0
  48. data/spec/dummy/app/controllers/posts_controller.rb +11 -0
  49. data/spec/dummy/app/decorators/post_decorator.rb +32 -0
  50. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  51. data/spec/dummy/app/mailers/application_mailer.rb +3 -0
  52. data/spec/dummy/app/mailers/post_mailer.rb +9 -0
  53. data/spec/dummy/app/models/post.rb +3 -0
  54. data/spec/dummy/app/views/layouts/application.html.erb +11 -0
  55. data/spec/dummy/app/views/post_mailer/decorated_email.html.erb +1 -0
  56. data/spec/dummy/app/views/posts/_post.html.erb +19 -0
  57. data/spec/dummy/app/views/posts/show.html.erb +1 -0
  58. data/spec/dummy/config/application.rb +64 -0
  59. data/spec/dummy/config/boot.rb +10 -0
  60. data/spec/dummy/config/database.yml +25 -0
  61. data/spec/dummy/config/environment.rb +5 -0
  62. data/spec/dummy/config/environments/development.rb +34 -0
  63. data/spec/dummy/config/environments/production.rb +55 -0
  64. data/spec/dummy/config/environments/test.rb +32 -0
  65. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  66. data/spec/dummy/config/initializers/inflections.rb +15 -0
  67. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  68. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  69. data/spec/dummy/config/initializers/session_store.rb +8 -0
  70. data/spec/dummy/config/locales/en.yml +5 -0
  71. data/spec/dummy/config/routes.rb +7 -0
  72. data/spec/dummy/config.ru +4 -0
  73. data/spec/dummy/db/migrate/20121019115657_create_posts.rb +8 -0
  74. data/spec/dummy/db/schema.rb +21 -0
  75. data/spec/dummy/db/seeds.rb +2 -0
  76. data/spec/dummy/lib/tasks/test.rake +10 -0
  77. data/spec/dummy/log/.gitkeep +0 -0
  78. data/spec/dummy/mini_test/mini_test_integration_test.rb +46 -0
  79. data/spec/dummy/public/404.html +26 -0
  80. data/spec/dummy/public/422.html +26 -0
  81. data/spec/dummy/public/500.html +25 -0
  82. data/spec/dummy/public/favicon.ico +0 -0
  83. data/spec/dummy/script/rails +6 -0
  84. data/spec/dummy/spec/decorators/post_decorator_spec.rb +26 -0
  85. data/spec/dummy/spec/decorators/rspec_integration_spec.rb +19 -0
  86. data/spec/dummy/spec/mailers/post_mailer_spec.rb +29 -0
  87. data/spec/dummy/spec/models/post_spec.rb +14 -0
  88. data/spec/dummy/spec/spec_helper.rb +8 -0
  89. data/spec/generators/decorator/decorator_generator_spec.rb +47 -7
  90. data/spec/integration/integration_spec.rb +33 -0
  91. data/{performance → spec/performance}/decorators.rb +2 -4
  92. data/spec/spec_helper.rb +20 -45
  93. data/spec/support/action_controller.rb +12 -0
  94. data/spec/support/active_model.rb +7 -0
  95. data/spec/support/active_record.rb +9 -0
  96. data/spec/support/{samples → decorators}/decorator_with_application_helper.rb +1 -1
  97. data/spec/support/decorators/namespaced_product_decorator.rb +5 -0
  98. data/spec/support/decorators/non_active_model_product_decorator.rb +2 -0
  99. data/spec/support/decorators/product_decorator.rb +23 -0
  100. data/spec/support/{samples → decorators}/products_decorator.rb +1 -1
  101. data/spec/support/decorators/some_thing_decorator.rb +2 -0
  102. data/spec/support/{samples → decorators}/specific_product_decorator.rb +0 -2
  103. data/spec/support/dummy_app.rb +85 -0
  104. data/spec/support/matchers/have_text.rb +50 -0
  105. data/spec/support/{samples → models}/namespaced_product.rb +1 -3
  106. data/spec/support/{samples → models}/non_active_model_product.rb +1 -0
  107. data/spec/support/{samples → models}/product.rb +17 -2
  108. data/spec/support/models/some_thing.rb +5 -0
  109. data/spec/support/models/uninferrable_decorator_model.rb +3 -0
  110. metadata +241 -87
  111. data/CHANGELOG.markdown +0 -57
  112. data/Readme.markdown +0 -387
  113. data/lib/draper/active_model_support.rb +0 -27
  114. data/lib/draper/base.rb +0 -312
  115. data/lib/draper/decorated_enumerable_proxy.rb +0 -90
  116. data/lib/draper/model_support.rb +0 -25
  117. data/lib/draper/rspec_integration.rb +0 -2
  118. data/lib/draper/system.rb +0 -18
  119. data/spec/draper/base_spec.rb +0 -873
  120. data/spec/draper/decorated_enumerable_proxy_spec.rb +0 -45
  121. data/spec/draper/model_support_spec.rb +0 -48
  122. data/spec/minitest-rails/spec_type_spec.rb +0 -63
  123. data/spec/support/samples/active_record.rb +0 -17
  124. data/spec/support/samples/decorator.rb +0 -5
  125. data/spec/support/samples/decorator_with_allows.rb +0 -3
  126. data/spec/support/samples/decorator_with_denies.rb +0 -3
  127. data/spec/support/samples/decorator_with_denies_all.rb +0 -3
  128. data/spec/support/samples/decorator_with_multiple_allows.rb +0 -4
  129. data/spec/support/samples/decorator_with_special_methods.rb +0 -13
  130. data/spec/support/samples/enumerable_proxy.rb +0 -3
  131. data/spec/support/samples/namespaced_product_decorator.rb +0 -7
  132. data/spec/support/samples/product_decorator.rb +0 -7
  133. data/spec/support/samples/sequel_product.rb +0 -13
  134. data/spec/support/samples/some_thing.rb +0 -2
  135. data/spec/support/samples/some_thing_decorator.rb +0 -3
  136. /data/{performance → spec/performance}/active_record.rb +0 -0
  137. /data/{performance/bechmark.rb → spec/performance/benchmark.rb} +0 -0
  138. /data/{performance → spec/performance}/models.rb +0 -0
  139. /data/spec/support/{samples → decorators}/widget_decorator.rb +0 -0
  140. /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/jcasimir/draper"
11
- s.summary = "Decorator pattern implementation for Rails."
12
- s.description = "Draper implements a decorator or presenter pattern for Rails applications."
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', '~> 3.2'
20
- s.add_dependency 'actionpack', '~> 3.2'
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', '~> 0.9.2'
24
- s.add_development_dependency 'rspec', '~> 2.10'
25
- s.add_development_dependency 'yard'
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 'minitest', '~> 3.0' if RUBY_PLATFORM == "java"
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
@@ -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
- def method_missing(method_name, *args, &block)
4
- begin
5
- helpers.send method_name, *args, &block
6
- rescue NoMethodError
7
- super
8
- end
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