draper 0.18.0 → 1.1.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 (155) hide show
  1. data/.gitignore +3 -0
  2. data/.travis.yml +11 -0
  3. data/.yardopts +1 -1
  4. data/CHANGELOG.md +164 -0
  5. data/CONTRIBUTING.md +15 -0
  6. data/Gemfile +34 -0
  7. data/LICENSE +7 -0
  8. data/README.md +416 -0
  9. data/Rakefile +53 -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/equality.rb +14 -0
  14. data/lib/draper/decoratable.rb +79 -0
  15. data/lib/draper/decorated_association.rb +74 -0
  16. data/lib/draper/decorator.rb +235 -0
  17. data/lib/draper/delegation.rb +13 -0
  18. data/lib/draper/finders.rb +37 -0
  19. data/lib/draper/helper_proxy.rb +36 -0
  20. data/lib/draper/lazy_helpers.rb +10 -6
  21. data/lib/draper/railtie.rb +27 -21
  22. data/lib/draper/tasks/test.rake +22 -0
  23. data/lib/draper/test/devise_helper.rb +30 -0
  24. data/lib/draper/test/minitest_integration.rb +2 -3
  25. data/lib/draper/test/rspec_integration.rb +7 -13
  26. data/lib/draper/test_case.rb +53 -0
  27. data/lib/draper/version.rb +1 -1
  28. data/lib/draper/view_context/build_strategy.rb +48 -0
  29. data/lib/draper/view_context.rb +81 -24
  30. data/lib/draper/view_helpers.rb +37 -0
  31. data/lib/draper.rb +50 -8
  32. data/lib/generators/decorator/decorator_generator.rb +10 -2
  33. data/lib/generators/decorator/templates/decorator.rb +12 -27
  34. data/lib/generators/mini_test/decorator_generator.rb +20 -0
  35. data/lib/generators/mini_test/templates/decorator_spec.rb +4 -0
  36. data/lib/generators/mini_test/templates/decorator_test.rb +4 -0
  37. data/lib/generators/test_unit/templates/decorator_test.rb +1 -1
  38. data/spec/draper/collection_decorator_spec.rb +259 -0
  39. data/spec/draper/decoratable/equality_spec.rb +10 -0
  40. data/spec/draper/decoratable_spec.rb +167 -0
  41. data/spec/draper/decorated_association_spec.rb +145 -0
  42. data/spec/draper/decorator_spec.rb +598 -0
  43. data/spec/draper/finders_spec.rb +166 -0
  44. data/spec/draper/helper_proxy_spec.rb +42 -0
  45. data/spec/draper/view_context/build_strategy_spec.rb +116 -0
  46. data/spec/draper/view_context_spec.rb +154 -0
  47. data/spec/draper/view_helpers_spec.rb +8 -0
  48. data/spec/dummy/.rspec +2 -0
  49. data/spec/dummy/Rakefile +7 -0
  50. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  51. data/spec/dummy/app/controllers/localized_urls.rb +5 -0
  52. data/spec/dummy/app/controllers/posts_controller.rb +18 -0
  53. data/spec/dummy/app/decorators/post_decorator.rb +56 -0
  54. data/spec/dummy/app/helpers/application_helper.rb +5 -0
  55. data/spec/dummy/app/mailers/application_mailer.rb +3 -0
  56. data/spec/dummy/app/mailers/post_mailer.rb +19 -0
  57. data/spec/dummy/app/models/admin.rb +5 -0
  58. data/spec/dummy/app/models/mongoid_post.rb +5 -0
  59. data/spec/dummy/app/models/post.rb +3 -0
  60. data/spec/dummy/app/models/user.rb +5 -0
  61. data/spec/dummy/app/views/layouts/application.html.erb +11 -0
  62. data/spec/dummy/app/views/post_mailer/decorated_email.html.erb +1 -0
  63. data/spec/dummy/app/views/posts/_post.html.erb +34 -0
  64. data/spec/dummy/app/views/posts/show.html.erb +1 -0
  65. data/spec/dummy/bin/rails +4 -0
  66. data/spec/dummy/config/application.rb +70 -0
  67. data/spec/dummy/config/boot.rb +5 -0
  68. data/spec/dummy/config/database.yml +25 -0
  69. data/spec/dummy/config/environment.rb +5 -0
  70. data/spec/dummy/config/environments/development.rb +33 -0
  71. data/spec/dummy/config/environments/production.rb +57 -0
  72. data/spec/dummy/config/environments/test.rb +31 -0
  73. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  74. data/spec/dummy/config/initializers/inflections.rb +15 -0
  75. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  76. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  77. data/spec/dummy/config/initializers/session_store.rb +8 -0
  78. data/spec/dummy/config/locales/en.yml +5 -0
  79. data/spec/dummy/config/mongoid.yml +80 -0
  80. data/spec/dummy/config/routes.rb +9 -0
  81. data/spec/dummy/config.ru +4 -0
  82. data/spec/dummy/db/migrate/20121019115657_create_posts.rb +8 -0
  83. data/spec/dummy/db/schema.rb +21 -0
  84. data/spec/dummy/db/seeds.rb +2 -0
  85. data/spec/dummy/fast_spec/post_decorator_spec.rb +38 -0
  86. data/spec/dummy/lib/tasks/test.rake +16 -0
  87. data/spec/dummy/log/.gitkeep +0 -0
  88. data/spec/dummy/public/404.html +26 -0
  89. data/spec/dummy/public/422.html +26 -0
  90. data/spec/dummy/public/500.html +25 -0
  91. data/spec/dummy/public/favicon.ico +0 -0
  92. data/spec/dummy/script/rails +6 -0
  93. data/spec/dummy/spec/decorators/devise_spec.rb +64 -0
  94. data/spec/dummy/spec/decorators/helpers_spec.rb +21 -0
  95. data/spec/dummy/spec/decorators/post_decorator_spec.rb +46 -0
  96. data/spec/dummy/spec/decorators/spec_type_spec.rb +7 -0
  97. data/spec/dummy/spec/decorators/view_context_spec.rb +22 -0
  98. data/spec/dummy/spec/mailers/post_mailer_spec.rb +33 -0
  99. data/spec/dummy/spec/models/mongoid_post_spec.rb +10 -0
  100. data/spec/dummy/spec/models/post_spec.rb +14 -0
  101. data/spec/dummy/spec/spec_helper.rb +9 -0
  102. data/spec/dummy/test/decorators/minitest/devise_test.rb +64 -0
  103. data/spec/dummy/test/decorators/minitest/helpers_test.rb +21 -0
  104. data/spec/dummy/test/decorators/minitest/spec_type_test.rb +52 -0
  105. data/spec/dummy/test/decorators/minitest/view_context_test.rb +24 -0
  106. data/spec/dummy/test/decorators/test_unit/devise_test.rb +64 -0
  107. data/spec/dummy/test/decorators/test_unit/helpers_test.rb +21 -0
  108. data/spec/dummy/test/decorators/test_unit/view_context_test.rb +24 -0
  109. data/spec/dummy/test/minitest_helper.rb +4 -0
  110. data/spec/dummy/test/test_helper.rb +3 -0
  111. data/spec/generators/decorator/decorator_generator_spec.rb +48 -7
  112. data/spec/integration/integration_spec.rb +58 -0
  113. data/{performance → spec/performance}/decorators.rb +2 -4
  114. data/spec/spec_helper.rb +25 -43
  115. data/spec/support/dummy_app.rb +85 -0
  116. data/spec/support/matchers/have_text.rb +50 -0
  117. data/spec/support/shared_examples/decoratable_equality.rb +40 -0
  118. data/spec/support/shared_examples/view_helpers.rb +39 -0
  119. metadata +253 -87
  120. data/CHANGELOG.markdown +0 -57
  121. data/Readme.markdown +0 -387
  122. data/lib/draper/active_model_support.rb +0 -27
  123. data/lib/draper/base.rb +0 -312
  124. data/lib/draper/decorated_enumerable_proxy.rb +0 -90
  125. data/lib/draper/model_support.rb +0 -25
  126. data/lib/draper/rspec_integration.rb +0 -2
  127. data/lib/draper/system.rb +0 -18
  128. data/spec/draper/base_spec.rb +0 -873
  129. data/spec/draper/decorated_enumerable_proxy_spec.rb +0 -45
  130. data/spec/draper/model_support_spec.rb +0 -48
  131. data/spec/minitest-rails/spec_type_spec.rb +0 -63
  132. data/spec/support/samples/active_record.rb +0 -17
  133. data/spec/support/samples/decorator.rb +0 -5
  134. data/spec/support/samples/decorator_with_allows.rb +0 -3
  135. data/spec/support/samples/decorator_with_application_helper.rb +0 -25
  136. data/spec/support/samples/decorator_with_denies.rb +0 -3
  137. data/spec/support/samples/decorator_with_denies_all.rb +0 -3
  138. data/spec/support/samples/decorator_with_multiple_allows.rb +0 -4
  139. data/spec/support/samples/decorator_with_special_methods.rb +0 -13
  140. data/spec/support/samples/enumerable_proxy.rb +0 -3
  141. data/spec/support/samples/namespaced_product.rb +0 -51
  142. data/spec/support/samples/namespaced_product_decorator.rb +0 -7
  143. data/spec/support/samples/non_active_model_product.rb +0 -2
  144. data/spec/support/samples/product.rb +0 -80
  145. data/spec/support/samples/product_decorator.rb +0 -7
  146. data/spec/support/samples/products_decorator.rb +0 -10
  147. data/spec/support/samples/sequel_product.rb +0 -13
  148. data/spec/support/samples/some_thing.rb +0 -2
  149. data/spec/support/samples/some_thing_decorator.rb +0 -3
  150. data/spec/support/samples/specific_product_decorator.rb +0 -4
  151. data/spec/support/samples/widget.rb +0 -2
  152. data/spec/support/samples/widget_decorator.rb +0 -2
  153. /data/{performance → spec/performance}/active_record.rb +0 -0
  154. /data/{performance/bechmark.rb → spec/performance/benchmark.rb} +0 -0
  155. /data/{performance → spec/performance}/models.rb +0 -0
data/Rakefile CHANGED
@@ -1,47 +1,69 @@
1
- require 'bundler/gem_tasks'
2
1
  require 'rake'
2
+ require 'bundler/gem_tasks'
3
3
  require 'rspec/core/rake_task'
4
4
 
5
- RCOV = RUBY_VERSION.to_f == 1.8
5
+ def run_in_dummy_app(command)
6
+ success = system("cd spec/dummy && #{command}")
7
+ raise "#{command} failed" unless success
8
+ end
9
+
10
+ task "default" => "ci"
11
+
12
+ desc "Run all tests for CI"
13
+ task "ci" => "spec"
6
14
 
7
- namespace :spec do
15
+ desc "Run all specs"
16
+ task "spec" => "spec:all"
8
17
 
9
- RSpec::Core::RakeTask.new(:coverage) do |t|
10
- t.pattern = 'spec/**/*_spec.rb'
18
+ namespace "spec" do
19
+ task "all" => ["draper", "generators", "integration"]
11
20
 
12
- if RCOV
13
- t.rcov = true
14
- t.rcov_opts = '--exclude osx\/objc,spec,gems\/'
21
+ def spec_task(name)
22
+ desc "Run #{name} specs"
23
+ RSpec::Core::RakeTask.new(name) do |t|
24
+ t.pattern = "spec/#{name}/**/*_spec.rb"
15
25
  end
16
26
  end
17
27
 
18
- RSpec::Core::RakeTask.new(:normal) do |t|
19
- t.pattern ='spec/**/*_spec.rb'
20
- t.rcov = false
21
- end
28
+ spec_task "draper"
29
+ spec_task "generators"
22
30
 
23
- namespace :coverage do
24
- desc "Cleanup coverage data"
25
- task :cleanup do
26
- rm_rf 'coverage.data'
27
- rm_rf 'coverage'
28
- end
31
+ desc "Run integration specs"
32
+ task "integration" => ["db:setup", "integration:all"]
29
33
 
30
- desc "Browse the code coverage report."
31
- task :report => ["spec:coverage:cleanup", "spec:coverage"] do
32
- if RCOV
33
- require "launchy"
34
- Launchy.open("coverage/index.html")
35
- else
36
- require 'cover_me'
37
- CoverMe.complete!
34
+ namespace "integration" do
35
+ task "all" => ["development", "production", "test"]
36
+
37
+ ["development", "production"].each do |environment|
38
+ task environment do
39
+ Rake::Task["spec:integration:run"].execute environment
38
40
  end
39
41
  end
40
- end
41
42
 
42
- end
43
+ task "run" do |t, environment|
44
+ puts "Running integration specs in #{environment}"
45
+
46
+ ENV["RAILS_ENV"] = environment
47
+ success = system("rspec spec/integration")
43
48
 
44
- desc "RSpec tests"
45
- task "spec" => "spec:normal"
49
+ raise "Integration specs failed in #{environment}" unless success
50
+ end
46
51
 
47
- task "default" => "spec"
52
+ task "test" do
53
+ puts "Running rake in dummy app"
54
+ ENV["RAILS_ENV"] = "test"
55
+ run_in_dummy_app "rake"
56
+ end
57
+ end
58
+ end
59
+
60
+ namespace "db" do
61
+ desc "Set up databases for integration testing"
62
+ task "setup" do
63
+ puts "Setting up databases"
64
+ run_in_dummy_app "rm -f db/*.sqlite3"
65
+ run_in_dummy_app "RAILS_ENV=development rake db:schema:load db:seed"
66
+ run_in_dummy_app "RAILS_ENV=production rake db:schema:load db:seed"
67
+ run_in_dummy_app "RAILS_ENV=test rake db:schema:load"
68
+ end
69
+ end
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 an 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,14 @@
1
+ module Draper
2
+ module Decoratable
3
+ module Equality
4
+ # Compares self with a possibly-decorated object.
5
+ #
6
+ # @return [Boolean]
7
+ def ==(other)
8
+ super ||
9
+ other.respond_to?(:decorated?) && other.decorated? &&
10
+ other.respond_to?(:source) && self == other.source
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,79 @@
1
+ require 'draper/decoratable/equality'
2
+
3
+ module Draper
4
+ # Provides shortcuts to decorate objects directly, so you can do
5
+ # `@product.decorate` instead of `ProductDecorator.new(@product)`.
6
+ #
7
+ # This module is included by default into `ActiveRecord::Base` and
8
+ # `Mongoid::Document`, but you're using another ORM, or want to decorate
9
+ # plain old Ruby objects, you can include it manually.
10
+ module Decoratable
11
+ extend ActiveSupport::Concern
12
+ include Draper::Decoratable::Equality
13
+
14
+ # Decorates the object using the inferred {#decorator_class}.
15
+ # @param [Hash] options
16
+ # see {Decorator#initialize}
17
+ def decorate(options = {})
18
+ decorator_class.decorate(self, options)
19
+ end
20
+
21
+ # (see ClassMethods#decorator_class)
22
+ def decorator_class
23
+ self.class.decorator_class
24
+ end
25
+
26
+ # The list of decorators that have been applied to the object.
27
+ #
28
+ # @return [Array<Class>] `[]`
29
+ def applied_decorators
30
+ []
31
+ end
32
+
33
+ # (see Decorator#decorated_with?)
34
+ # @return [false]
35
+ def decorated_with?(decorator_class)
36
+ false
37
+ end
38
+
39
+ # Checks if this object is decorated.
40
+ #
41
+ # @return [false]
42
+ def decorated?
43
+ false
44
+ end
45
+
46
+ module ClassMethods
47
+
48
+ # Decorates a collection of objects. Used at the end of a scope chain.
49
+ #
50
+ # @example
51
+ # Product.popular.decorate
52
+ # @param [Hash] options
53
+ # see {Decorator.decorate_collection}.
54
+ def decorate(options = {})
55
+ decorator_class.decorate_collection(self.scoped, options)
56
+ end
57
+
58
+ # Infers the decorator class to be used by {Decoratable#decorate} (e.g.
59
+ # `Product` maps to `ProductDecorator`).
60
+ #
61
+ # @return [Class] the inferred decorator class.
62
+ def decorator_class
63
+ prefix = respond_to?(:model_name) ? model_name : name
64
+ "#{prefix}Decorator".constantize
65
+ rescue NameError
66
+ raise Draper::UninferrableDecoratorError.new(self)
67
+ end
68
+
69
+ # Compares with possibly-decorated objects.
70
+ #
71
+ # @return [Boolean]
72
+ def ===(other)
73
+ super || (other.respond_to?(:source) && super(other.source))
74
+ end
75
+
76
+ end
77
+
78
+ end
79
+ 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,235 @@
1
+ module Draper
2
+ class Decorator
3
+ include Draper::ViewHelpers
4
+ extend Draper::Delegation
5
+ include ActiveModel::Serialization
6
+
7
+ # @return the object being decorated.
8
+ attr_reader :source
9
+ alias_method :model, :source
10
+ alias_method :to_source, :source
11
+
12
+ # @return [Hash] extra data to be used in user-defined methods.
13
+ attr_accessor :context
14
+
15
+ # Wraps an object in a new instance of the decorator.
16
+ #
17
+ # Decorators may be applied to other decorators. However, applying a
18
+ # decorator to an instance of itself will create a decorator with the same
19
+ # source as the original, rather than redecorating the other instance.
20
+ #
21
+ # @param [Object] source
22
+ # object to decorate.
23
+ # @option options [Hash] :context ({})
24
+ # extra data to be stored in the decorator and used in user-defined
25
+ # methods.
26
+ def initialize(source, options = {})
27
+ options.assert_valid_keys(:context)
28
+ @source = source
29
+ @context = options.fetch(:context, {})
30
+ handle_multiple_decoration(options) if source.instance_of?(self.class)
31
+ end
32
+
33
+ class << self
34
+ alias_method :decorate, :new
35
+ end
36
+
37
+ # Automatically delegates instance methods to the source object. Class
38
+ # methods will be delegated to the {source_class}, if it is set.
39
+ #
40
+ # @return [void]
41
+ def self.delegate_all
42
+ include Draper::AutomaticDelegation
43
+ end
44
+
45
+ # Sets the source class corresponding to the decorator class.
46
+ #
47
+ # @note This is only necessary if you wish to proxy class methods to the
48
+ # source (including when using {decorates_finders}), and the source class
49
+ # cannot be inferred from the decorator class (e.g. `ProductDecorator`
50
+ # maps to `Product`).
51
+ # @param [String, Symbol, Class] source_class
52
+ # source class (or class name) that corresponds to this decorator.
53
+ # @return [void]
54
+ def self.decorates(source_class)
55
+ @source_class = source_class.to_s.camelize.constantize
56
+ end
57
+
58
+ # Returns the source class corresponding to the decorator class, as set by
59
+ # {decorates}, or as inferred from the decorator class name (e.g.
60
+ # `ProductDecorator` maps to `Product`).
61
+ #
62
+ # @return [Class] the source class that corresponds to this decorator.
63
+ def self.source_class
64
+ @source_class ||= inferred_source_class
65
+ end
66
+
67
+ # Checks whether this decorator class has a corresponding {source_class}.
68
+ def self.source_class?
69
+ source_class
70
+ rescue Draper::UninferrableSourceError
71
+ false
72
+ end
73
+
74
+ # Automatically decorates ActiveRecord finder methods, so that you can use
75
+ # `ProductDecorator.find(id)` instead of
76
+ # `ProductDecorator.decorate(Product.find(id))`.
77
+ #
78
+ # Finder methods are applied to the {source_class}.
79
+ #
80
+ # @return [void]
81
+ def self.decorates_finders
82
+ extend Draper::Finders
83
+ end
84
+
85
+ # Automatically decorate an association.
86
+ #
87
+ # @param [Symbol] association
88
+ # name of the association to decorate (e.g. `:products`).
89
+ # @option options [Class] :with
90
+ # the decorator to apply to the association.
91
+ # @option options [Symbol] :scope
92
+ # a scope to apply when fetching the association.
93
+ # @option options [Hash, #call] :context
94
+ # extra data to be stored in the associated decorator. If omitted, the
95
+ # associated decorator's context will be the same as the parent
96
+ # decorator's. If a Proc is given, it will be called with the parent's
97
+ # context and should return a new context hash for the association.
98
+ # @return [void]
99
+ def self.decorates_association(association, options = {})
100
+ options.assert_valid_keys(:with, :scope, :context)
101
+ define_method(association) do
102
+ decorated_associations[association] ||= Draper::DecoratedAssociation.new(self, association, options)
103
+ decorated_associations[association].call
104
+ end
105
+ end
106
+
107
+ # @overload decorates_associations(*associations, options = {})
108
+ # Automatically decorate multiple associations.
109
+ # @param [Symbols*] associations
110
+ # names of the associations to decorate.
111
+ # @param [Hash] options
112
+ # see {decorates_association}.
113
+ # @return [void]
114
+ def self.decorates_associations(*associations)
115
+ options = associations.extract_options!
116
+ associations.each do |association|
117
+ decorates_association(association, options)
118
+ end
119
+ end
120
+
121
+ # Decorates a collection of objects. The class of the collection decorator
122
+ # is inferred from the decorator class if possible (e.g. `ProductDecorator`
123
+ # maps to `ProductsDecorator`), but otherwise defaults to
124
+ # {Draper::CollectionDecorator}.
125
+ #
126
+ # @param [Object] source
127
+ # collection to decorate.
128
+ # @option options [Class, nil] :with (self)
129
+ # the decorator class used to decorate each item. When `nil`, it is
130
+ # inferred from each item.
131
+ # @option options [Hash] :context
132
+ # extra data to be stored in the collection decorator.
133
+ def self.decorate_collection(source, options = {})
134
+ options.assert_valid_keys(:with, :context)
135
+ collection_decorator_class.new(source, options.reverse_merge(with: self))
136
+ end
137
+
138
+ # @return [Array<Class>] the list of decorators that have been applied to
139
+ # the object.
140
+ def applied_decorators
141
+ chain = source.respond_to?(:applied_decorators) ? source.applied_decorators : []
142
+ chain << self.class
143
+ end
144
+
145
+ # Checks if a given decorator has been applied to the object.
146
+ #
147
+ # @param [Class] decorator_class
148
+ def decorated_with?(decorator_class)
149
+ applied_decorators.include?(decorator_class)
150
+ end
151
+
152
+ # Checks if this object is decorated.
153
+ #
154
+ # @return [true]
155
+ def decorated?
156
+ true
157
+ end
158
+
159
+ # Compares the source with a possibly-decorated object.
160
+ #
161
+ # @return [Boolean]
162
+ def ==(other)
163
+ source.extend(Draper::Decoratable::Equality) == other
164
+ end
165
+
166
+ # Checks if `self.kind_of?(klass)` or `source.kind_of?(klass)`
167
+ #
168
+ # @param [Class] klass
169
+ def kind_of?(klass)
170
+ super || source.kind_of?(klass)
171
+ end
172
+ alias_method :is_a?, :kind_of?
173
+
174
+ # Checks if `self.instance_of?(klass)` or `source.instance_of?(klass)`
175
+ #
176
+ # @param [Class] klass
177
+ def instance_of?(klass)
178
+ super || source.instance_of?(klass)
179
+ end
180
+
181
+ # In case source is nil
182
+ delegate :present?
183
+
184
+ # ActiveModel compatibility
185
+ # @private
186
+ def to_model
187
+ self
188
+ end
189
+
190
+ # ActiveModel compatibility
191
+ delegate :attributes, :to_param, :to_partial_path
192
+
193
+ # ActiveModel compatibility
194
+ singleton_class.delegate :model_name, to: :source_class
195
+
196
+ # @return [Class] the class created by {decorate_collection}.
197
+ def self.collection_decorator_class
198
+ collection_decorator_name.constantize
199
+ rescue NameError
200
+ Draper::CollectionDecorator
201
+ end
202
+
203
+ private
204
+
205
+ def self.source_name
206
+ raise NameError if name.nil? || name.demodulize !~ /.+Decorator$/
207
+ name.chomp("Decorator")
208
+ end
209
+
210
+ def self.inferred_source_class
211
+ source_name.constantize
212
+ rescue NameError
213
+ raise Draper::UninferrableSourceError.new(self)
214
+ end
215
+
216
+ def self.collection_decorator_name
217
+ plural = source_name.pluralize
218
+ raise NameError if plural == source_name
219
+ "#{plural}Decorator"
220
+ end
221
+
222
+ def handle_multiple_decoration(options)
223
+ if source.applied_decorators.last == self.class
224
+ @context = source.context unless options.has_key?(:context)
225
+ @source = source.source
226
+ else
227
+ warn "Reapplying #{self.class} decorator to target that is already decorated with it. Call stack:\n#{caller(1).join("\n")}"
228
+ end
229
+ end
230
+
231
+ def decorated_associations
232
+ @decorated_associations ||= {}
233
+ end
234
+ end
235
+ end