strong_presenter 0.0.1 → 0.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 (132) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -2
  3. data/.rspec +2 -0
  4. data/.travis.yml +18 -0
  5. data/.yardopts +1 -0
  6. data/CHANGELOG.md +31 -0
  7. data/Gemfile +22 -2
  8. data/Guardfile +26 -0
  9. data/README.md +210 -52
  10. data/Rakefile +77 -1
  11. data/gemfiles/3.0.gemfile +2 -0
  12. data/gemfiles/3.1.gemfile +2 -0
  13. data/gemfiles/3.2.gemfile +2 -0
  14. data/gemfiles/4.0.gemfile +2 -0
  15. data/gemfiles/4.1.gemfile +2 -0
  16. data/lib/generators/controller_override.rb +15 -0
  17. data/lib/generators/mini_test/presenter_generator.rb +20 -0
  18. data/lib/generators/mini_test/templates/presenter_spec.rb +4 -0
  19. data/lib/generators/mini_test/templates/presenter_test.rb +4 -0
  20. data/lib/generators/rails/presenter_generator.rb +36 -0
  21. data/lib/generators/rails/templates/presenter.rb +19 -0
  22. data/lib/generators/rspec/presenter_generator.rb +9 -0
  23. data/lib/generators/rspec/templates/presenter_spec.rb +4 -0
  24. data/lib/generators/test_unit/presenter_generator.rb +9 -0
  25. data/lib/generators/test_unit/templates/presenter_test.rb +4 -0
  26. data/lib/strong_presenter/associable.rb +78 -0
  27. data/lib/strong_presenter/collection_presenter.rb +90 -0
  28. data/lib/strong_presenter/controller_additions.rb +50 -0
  29. data/lib/strong_presenter/delegation.rb +18 -0
  30. data/lib/strong_presenter/factory.rb +74 -0
  31. data/lib/strong_presenter/helper_proxy.rb +29 -11
  32. data/lib/strong_presenter/inferrer.rb +54 -0
  33. data/lib/strong_presenter/permissible.rb +73 -0
  34. data/lib/strong_presenter/permissions.rb +138 -0
  35. data/lib/strong_presenter/presenter.rb +191 -0
  36. data/lib/strong_presenter/presenter_association.rb +29 -0
  37. data/lib/strong_presenter/presenter_helper_constructor.rb +60 -0
  38. data/lib/strong_presenter/railtie.rb +27 -3
  39. data/lib/strong_presenter/tasks/test.rake +22 -0
  40. data/lib/strong_presenter/test/devise_helper.rb +30 -0
  41. data/lib/strong_presenter/test/minitest_integration.rb +6 -0
  42. data/lib/strong_presenter/test/rspec_integration.rb +16 -0
  43. data/lib/strong_presenter/test_case.rb +53 -0
  44. data/lib/strong_presenter/version.rb +1 -1
  45. data/lib/strong_presenter/view_context/build_strategy.rb +48 -0
  46. data/lib/strong_presenter/view_context.rb +84 -0
  47. data/lib/strong_presenter/view_helpers.rb +39 -0
  48. data/lib/strong_presenter.rb +64 -2
  49. data/spec/dummy/.rspec +2 -0
  50. data/spec/dummy/Rakefile +7 -0
  51. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  52. data/spec/dummy/app/controllers/localized_urls.rb +5 -0
  53. data/spec/dummy/app/controllers/posts_controller.rb +25 -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/post.rb +3 -0
  59. data/spec/dummy/app/models/user.rb +5 -0
  60. data/spec/dummy/app/presenters/post_presenter.rb +69 -0
  61. data/spec/dummy/app/presenters/special_post_presenter.rb +5 -0
  62. data/spec/dummy/app/presenters/special_posts_presenter.rb +5 -0
  63. data/spec/dummy/app/views/layouts/application.html.erb +11 -0
  64. data/spec/dummy/app/views/post_mailer/presented_email.html.erb +1 -0
  65. data/spec/dummy/app/views/posts/_post.html.erb +50 -0
  66. data/spec/dummy/app/views/posts/show.html.erb +1 -0
  67. data/spec/dummy/bin/rails +4 -0
  68. data/spec/dummy/config/application.rb +70 -0
  69. data/spec/dummy/config/boot.rb +5 -0
  70. data/spec/dummy/config/database.yml +25 -0
  71. data/spec/dummy/config/environment.rb +5 -0
  72. data/spec/dummy/config/environments/development.rb +33 -0
  73. data/spec/dummy/config/environments/production.rb +57 -0
  74. data/spec/dummy/config/environments/test.rb +31 -0
  75. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  76. data/spec/dummy/config/initializers/inflections.rb +15 -0
  77. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  78. data/spec/dummy/config/initializers/secret_token.rb +8 -0
  79. data/spec/dummy/config/initializers/session_store.rb +8 -0
  80. data/spec/dummy/config/locales/en.yml +5 -0
  81. data/spec/dummy/config/routes.rb +9 -0
  82. data/spec/dummy/config.ru +4 -0
  83. data/spec/dummy/db/migrate/20121019115657_create_posts.rb +8 -0
  84. data/spec/dummy/db/schema.rb +21 -0
  85. data/spec/dummy/db/seeds.rb +2 -0
  86. data/spec/dummy/fast_spec/post_presenter_spec.rb +37 -0
  87. data/spec/dummy/lib/tasks/test.rake +16 -0
  88. data/spec/dummy/log/.gitkeep +0 -0
  89. data/spec/dummy/public/404.html +26 -0
  90. data/spec/dummy/public/422.html +26 -0
  91. data/spec/dummy/public/500.html +25 -0
  92. data/spec/dummy/public/favicon.ico +0 -0
  93. data/spec/dummy/script/rails +6 -0
  94. data/spec/dummy/spec/fast_spec_helper.rb +13 -0
  95. data/spec/dummy/spec/mailers/post_mailer_spec.rb +33 -0
  96. data/spec/dummy/spec/models/post_spec.rb +4 -0
  97. data/spec/dummy/spec/presenters/active_model_serializers_spec.rb +11 -0
  98. data/spec/dummy/spec/presenters/devise_spec.rb +64 -0
  99. data/spec/dummy/spec/presenters/helpers_spec.rb +21 -0
  100. data/spec/dummy/spec/presenters/post_presenter_spec.rb +66 -0
  101. data/spec/dummy/spec/presenters/spec_type_spec.rb +7 -0
  102. data/spec/dummy/spec/presenters/special_post_presenter_spec.rb +11 -0
  103. data/spec/dummy/spec/presenters/view_context_spec.rb +22 -0
  104. data/spec/dummy/spec/spec_helper.rb +19 -0
  105. data/spec/dummy/test/minitest_helper.rb +2 -0
  106. data/spec/dummy/test/presenters/minitest/devise_test.rb +64 -0
  107. data/spec/dummy/test/presenters/minitest/helpers_test.rb +21 -0
  108. data/spec/dummy/test/presenters/minitest/spec_type_test.rb +52 -0
  109. data/spec/dummy/test/presenters/minitest/view_context_test.rb +24 -0
  110. data/spec/dummy/test/presenters/test_unit/devise_test.rb +64 -0
  111. data/spec/dummy/test/presenters/test_unit/helpers_test.rb +21 -0
  112. data/spec/dummy/test/presenters/test_unit/view_context_test.rb +24 -0
  113. data/spec/dummy/test/test_helper.rb +13 -0
  114. data/spec/generators/presenters/presenter_generator_spec.rb +131 -0
  115. data/spec/generators/simplecov_spec.rb +5 -0
  116. data/spec/integration/integration_spec.rb +81 -0
  117. data/spec/integration/simplecov_spec.rb +4 -0
  118. data/spec/spec_helper.rb +47 -0
  119. data/spec/strong_presenter/associable_spec.rb +122 -0
  120. data/spec/strong_presenter/collection_presenter_spec.rb +34 -0
  121. data/spec/strong_presenter/delegation_spec.rb +20 -0
  122. data/spec/strong_presenter/permissible_spec.rb +24 -0
  123. data/spec/strong_presenter/permissions_spec.rb +188 -0
  124. data/spec/strong_presenter/presenter_spec.rb +43 -0
  125. data/spec/strong_presenter/simplecov_spec.rb +4 -0
  126. data/spec/support/dummy_app.rb +85 -0
  127. data/spec/support/matchers/have_text.rb +50 -0
  128. data/spec/support/models.rb +14 -0
  129. data/spec/support/schema.rb +12 -0
  130. data/strong_presenter.gemspec +15 -0
  131. metadata +392 -13
  132. data/lib/strong_presenter/base.rb +0 -217
@@ -0,0 +1,138 @@
1
+ module StrongPresenter
2
+ # @private
3
+ #
4
+ # Storage format:
5
+ # The permissions object is shared by collection presenters with its constituent presenters,
6
+ # and it is also shared with all of its associations. Each attribute path is stored as an array
7
+ # of symbols in a Set. There is one top level presenter - the one which initialized the
8
+ # Permissions object.
9
+ #
10
+ # When a presenter checks for permissions, the attribute path relative to the top
11
+ # presenter is prepended to each attribute path, and its existence checked in the Set.
12
+ #
13
+ # Arguments can also be part of permissions control. They are simply additional elements in the attribute path array,
14
+ # and need not be symbols. If they are symbols, there is no way for Permissions to know whether they
15
+ # are part of the attribute path, or additional arguments. Only the presenter knows that.
16
+ class Permissions
17
+
18
+ # Checks whether everything is permitted.
19
+ #
20
+ # @return [Boolean]
21
+ def complete?
22
+ permitted_paths.include? [] and ((@permitted_paths = Set[[]] if permitted_paths.count > 1) or true)
23
+ end
24
+
25
+ # Permits everything
26
+ #
27
+ # @return self
28
+ def permit_all!
29
+ permitted_paths.clear
30
+ permitted_paths << []
31
+ self
32
+ end
33
+
34
+ # @overload permitted? prefix_path = nil, attribute_path
35
+ #
36
+ # Checks if the attribute path is permitted. This is the case if
37
+ # any array prefix has been permitted.
38
+ #
39
+ # @param [Symbol, Array<Symbol>] prefix_path
40
+ # @param [Symbol, Array<Symbol,Object>] attribute_path
41
+ # @return [Boolean]
42
+ def permitted? prefix_path, attribute_path = nil
43
+ raw_permitted? Array(prefix_path), Array(attribute_path)
44
+ end
45
+
46
+ # Selects the attribute paths which are permitted.
47
+ #
48
+ # @param [Array] prefix_path
49
+ # namespace in which each of the given attribute paths are in
50
+ # @param [[Symbol, Array<Symbol,Object>]*] *attribute_paths
51
+ # each attribute path is a symbol or array of symbols
52
+ # @return [Array<Symbol, Array<Symbol,Object>>] array of attribute paths permitted
53
+ def select_permitted prefix_path, *attribute_paths
54
+ raw_select_permitted Array(prefix_path), nested_array(attribute_paths)
55
+ end
56
+
57
+ # Rejects the attribute paths which are permitted. Opposite of select_permitted.
58
+ # Returns the attribute paths which are not permitted.
59
+ #
60
+ # @param [Array] prefix_path
61
+ # namespace in which each of the given attribute paths are in
62
+ # @param [[Symbol, Array<Symbol,Object>]*] *attribute_paths
63
+ # each attribute path is a symbol or array of symbols
64
+ # @return [Array<Symbol, Array<Symbol,Object>>] array of attribute paths remaining
65
+ def reject_permitted prefix_path, *attribute_paths
66
+ raw_reject_permitted Array(prefix_path), nested_array(attribute_paths)
67
+ end
68
+
69
+ # Permits some attribute paths
70
+ #
71
+ # @param [Array<Symbol>] prefix_path
72
+ # path to prepend to each attribute path
73
+ # @param [[Symbol, Array<Symbol,Object>]*] *attribute_paths
74
+ def permit prefix_path, *attribute_paths
75
+ prefix_path = Array(prefix_path)
76
+ # don't permit if already permitted
77
+ raw_reject_permitted(prefix_path, nested_array(attribute_paths)).each do |attribute_path|
78
+ permitted_paths << prefix_path + attribute_path
79
+ end
80
+ self
81
+ end
82
+
83
+ # Merges the permissions from another Permissions object
84
+ #
85
+ # @param [Permissions] permissions
86
+ # @param [Array<Symbol>] prefix
87
+ # prefix to prepend to paths in permissions
88
+ # @return self
89
+ def merge permissions, prefix = []
90
+ permitted_paths.merge permissions.permitted_paths.map{|path| prefix+path} if permissions.is_a? self.class
91
+ self
92
+ end
93
+
94
+ protected
95
+
96
+ def permitted_paths
97
+ @permitted_paths ||= Set.new
98
+ end
99
+
100
+ private
101
+ # We trust path parameters are arrays
102
+ def raw_permitted? prefix_path, attribute_path = nil # const - does not alter arguments
103
+ return true if complete?
104
+ permitted_partial?([], prefix_path + Array(attribute_path))
105
+ end
106
+
107
+ def raw_reject_permitted prefix_path, attribute_paths # const - does not alter arguments
108
+ return [] if raw_permitted? prefix_path
109
+ attribute_paths.reject do |attribute_path|
110
+ permitted_partial? prefix_path.dup, attribute_path
111
+ end
112
+ end
113
+
114
+ def raw_select_permitted prefix_path, attribute_paths # const - does not alter arguments
115
+ return attribute_paths if raw_permitted? prefix_path
116
+ attribute_paths.select do |attribute_path|
117
+ permitted_partial? prefix_path.dup, attribute_path
118
+ end
119
+ end
120
+
121
+ # For internal use, checks if permitted explicitly by a subpath of [prefix_path, attribute_partial+]
122
+ # where attribute_partial is a prefix of at least one symbol from attribute_part
123
+ # Caution: Will mutate prefix_path
124
+ def permitted_partial? prefix_path, attribute_path
125
+ !!Array(attribute_path).detect do |attr|
126
+ break unless attr.is_a? Symbol
127
+ prefix_path << attr
128
+ permitted_paths.include? prefix_path
129
+ end
130
+ end
131
+
132
+ # Ensures that every array element is an array
133
+ def nested_array array
134
+ array.map{|e|Array(e)}
135
+ end
136
+
137
+ end
138
+ end
@@ -0,0 +1,191 @@
1
+ module StrongPresenter
2
+ class Presenter
3
+ include StrongPresenter::ViewHelpers
4
+ include StrongPresenter::Permissible
5
+ include StrongPresenter::Associable
6
+ include StrongPresenter::Delegation
7
+
8
+ include ActiveModel::Serialization
9
+ include ActiveModel::Serializers::JSON
10
+ include ActiveModel::Serializers::Xml
11
+
12
+ # Constructs the presenter, taking 1 argument for the object being wrapped. For example:
13
+ #
14
+ # user_presenter = UserPresenter.new @user
15
+ #
16
+ # A block can also be passed to use the presenter. For example:
17
+ #
18
+ # <% UserPresenter.new @user do |user_presenter| %>
19
+ # Username: <%= user_presenter.username %>
20
+ # <% end %>
21
+ #
22
+
23
+ def initialize(object)
24
+ @object = object
25
+ yield self if block_given?
26
+ end
27
+
28
+ # Performs mass presentation - if it is allowed, subject to `permit`. To permit all without checking, call `permit!` first.
29
+ #
30
+ # Presents and returns the result of each field in the argument list. If a block is given, then each result
31
+ # is passed to the block. Each field is presented by calling the method on the presenter.
32
+ #
33
+ # user_presenter.presents :username, :email # returns [user_presenter.username, user_presenter.email]
34
+ #
35
+ # Or with two arguments, the name of the field is passed first:
36
+ #
37
+ # <ul>
38
+ # <% user_presenter.presents :username, :email, :address do |field, value| %>
39
+ # <li><%= field.capitalize %>: <% value %></li>
40
+ # <% end %>
41
+ # </ul>
42
+ #
43
+ # If only the presented value is desired, use `each`:
44
+ #
45
+ # <% user_presenter.presents(:username, :email).each do |value| %>
46
+ # <td><%= value %></td>
47
+ # <% end %>
48
+ #
49
+ # A field can have arguments in an array:
50
+ #
51
+ # user_presenter.presents :username, [:notifications, :unread] # returns [user_presenter.username, user_presenter.notifications(:unread)]
52
+ #
53
+ # Notice that this interface allows you to concisely put authorization logic in the controller, with a dumb view layer:
54
+ #
55
+ # # app/controllers/users_controller.rb
56
+ # class UsersController < ApplicationController
57
+ # def visible_params
58
+ # @visible_params ||= begin
59
+ # field = [:username]
60
+ # field << :email if can? :read_email, @user
61
+ # field << :edit_link if can? :edit, @user
62
+ # end
63
+ # end
64
+ # def show
65
+ # @users_presenter = UserPresenter.wrap_each(User.all).permit!
66
+ # end
67
+ # end
68
+ #
69
+ # # app/views/users/show.html.erb
70
+ # <table>
71
+ # <tr>
72
+ # <% visible_params.each do |field| %>
73
+ # <th><%= field %></th>
74
+ # <% end %>
75
+ # </tr>
76
+ # <% @users_presenter.each do |user_presenter| %>
77
+ # <tr>
78
+ # <% user_presenter.presents(*visible_params).each do |value| %>
79
+ # <td><%= value %></td>
80
+ # <% end %>
81
+ # </tr>
82
+ # <% end %>
83
+ # </table>
84
+ #
85
+ def presents *attributes
86
+ select_permitted(*attributes).map do |args|
87
+ obj = self # drill into associations
88
+ while (args.size > 1) && self.class.send(:presenter_associations).include?(args[0]) do
89
+ obj = obj.public_send args.slice!(0)
90
+ end
91
+ value = obj.public_send *args # call final method with args
92
+ yield args[0], value if block_given?
93
+ value
94
+ end
95
+ end
96
+
97
+ # Same as presents, but for a single attribute. The differences are:
98
+ # - the return value is not in an Array
99
+ # - passes the value only (without attribute key as the 1st argument) to a block
100
+ def present field
101
+ presents field do |key, value|
102
+ yield value if block_given?
103
+ end.first
104
+ end
105
+
106
+ delegate :to_s
107
+
108
+ # In case object is nil
109
+ delegate :present?, :blank?
110
+
111
+ # ActiveModel compatibility
112
+ # @private
113
+ def to_model
114
+ self
115
+ end
116
+
117
+ # @return [Hash] the object's attributes, sliced to only include those
118
+ # implemented by the presenter.
119
+ def attributes
120
+ object.attributes.select {|attribute, _| respond_to?(attribute) }
121
+ end
122
+
123
+ # ActiveModel compatibility
124
+ delegate :to_param, :to_partial_path
125
+
126
+ # ActiveModel compatibility
127
+ singleton_class.delegate :model_name, to: :object_class
128
+
129
+ protected
130
+ def object
131
+ @object
132
+ end
133
+
134
+ class << self
135
+ def inferred_presenter(object)
136
+ Inferrer.new(object.class.name).inferred_class { |name| "#{name}Presenter" } or raise StrongPresenter::UninferrablePresenterError.new(self)
137
+ end
138
+
139
+ protected
140
+ def alias_object_to_object_class_name
141
+ if object_class?
142
+ alias_method object_class.name.underscore, :object
143
+ private object_class.name.underscore
144
+ end
145
+ end
146
+
147
+ def set_presenter_collection
148
+ collection_presenter = get_collection_presenter
149
+ const_set "Collection", collection_presenter unless const_defined?("Collection")
150
+ end
151
+
152
+ private
153
+ def inherited(subclass)
154
+ subclass.alias_object_to_object_class_name
155
+ subclass.set_presenter_collection
156
+ super
157
+ end
158
+
159
+ def get_collection_presenter
160
+ collection_presenter = Inferrer.new(name).chomp("Presenter").inferred_class {|name| "#{name.pluralize}Presenter"}
161
+ return collection_presenter unless collection_presenter.nil? || collection_presenter == self
162
+ Class.new(StrongPresenter::CollectionPresenter).presents_with(self)
163
+ end
164
+
165
+ # Returns the source class corresponding to the presenter class, as set by
166
+ # {presents}, or as inferred from the presenter class name (e.g.
167
+ # `ProductPresenter` maps to `Product`).
168
+ #
169
+ # @return [Class] the source class that corresponds to this presenter.
170
+ def object_class
171
+ @object_class ||= Inferrer.new(name).chomp("Presenter").inferred_class or raise UninferrableSourceError.new(self)
172
+ end
173
+
174
+ # Checks whether this presenter class has a corresponding {object_class}.
175
+ def object_class?
176
+ !!(@object_class ||= Inferrer.new(name).chomp("Presenter").inferred_class)
177
+ rescue NameError
178
+ false
179
+ end
180
+
181
+ # Sets the model presented by the class
182
+ #
183
+ def presents name
184
+ @object_class = name.to_s.camelize.constantize
185
+ alias_object_to_object_class_name
186
+ end
187
+
188
+ end
189
+ end
190
+ end
191
+
@@ -0,0 +1,29 @@
1
+ module StrongPresenter
2
+ # @private
3
+ class PresenterAssociation
4
+
5
+ def initialize(association, options, &block)
6
+ options.assert_valid_keys(:with, :scope)
7
+
8
+ @association = association
9
+
10
+ @scope = options.delete(:scope)
11
+ @block = block
12
+
13
+ @factory = StrongPresenter::Factory.new(options)
14
+ end
15
+
16
+ def wrap(parent)
17
+ associated = parent.send(:object).send(association)
18
+ associated = associated.send(scope) if scope
19
+
20
+ factory.wrap(associated) do |presenter|
21
+ parent.instance_exec presenter, &@block if @block
22
+ end
23
+ end
24
+
25
+ private
26
+ attr_reader :factory, :association, :scope
27
+
28
+ end
29
+ end
@@ -0,0 +1,60 @@
1
+ module StrongPresenter
2
+ # @private
3
+ # Defines helper methods in controllers to access instance variables as presenters
4
+ class PresenterHelperConstructor
5
+ # settings: controller to define in, presenter factory, block to execute, options for valid actions
6
+ def initialize(controller_class, block, options)
7
+ @controller_class = controller_class
8
+ @factory = StrongPresenter::Factory.new(options.slice!(:only, :except))
9
+ @block = block
10
+ @action_matcher = setup_action_matcher(options)
11
+ end
12
+
13
+ # Returns proc to check if action matches
14
+ def setup_action_matcher(options)
15
+ options.each { |k,v| options[k] = Array(v).map(&:to_sym) unless v.nil? }
16
+ ->(action) { (options[:only].nil? || options[:only].include?(action)) && (options[:except].nil? || !options[:except].include?(action)) }
17
+ end
18
+
19
+ # call to construct helper
20
+ def call(variable)
21
+ @object = "@#{variable}"
22
+ @presenter = "@#{variable}_presenter"
23
+ construct(variable)
24
+ end
25
+
26
+ private
27
+ attr_accessor :controller_class, :factory, :presenter, :block
28
+
29
+ # actually construct the helper
30
+ def construct(variable)
31
+ shadowed_method = get_shadow_method(variable)
32
+ action_matcher = @action_matcher
33
+ memoized_presenter = method(:memoized_presenter).to_proc
34
+
35
+ controller_class.send :define_method, variable do |*args|
36
+ return shadowed_method.call self, *args unless action_matcher.call(action_name.to_sym) # scoped by controller action?
37
+ raise ArgumentError.new("wrong number of arguments (#{args.size} for 0)") unless args.empty?
38
+ memoized_presenter.call(self)
39
+ end
40
+ end
41
+
42
+ # method which will be shadowed by the defined helper
43
+ def get_shadow_method(method_name) # alias_method_chain without name pollution
44
+ shadowed_method = controller_class.send :instance_method, method_name if controller_class.send :method_defined?, method_name
45
+ return lambda { |obj, *args| raise NoMethodError } if shadowed_method.nil?
46
+ return lambda { |obj, *args| shadowed_method.bind(obj).call(*args) }
47
+ end
48
+
49
+ # get presenter, memoized
50
+ def memoized_presenter(controller)
51
+ return controller.send(:instance_variable_get, presenter) if controller.send(:instance_variable_defined?, presenter)
52
+ controller.send(:instance_variable_set, presenter, wrapped_object(controller))
53
+ end
54
+
55
+ # wrap model with presenter and return
56
+ def wrapped_object(controller)
57
+ factory.wrap(controller.send :instance_variable_get, @object) { |presenter| self.instance_exec presenter, &block }
58
+ end
59
+ end
60
+ end
@@ -1,9 +1,33 @@
1
1
  module StrongPresenter
2
2
  class Railtie < Rails::Railtie
3
- initializer "setup_helper_proxy" do |app|
4
- ActiveSupport.on_load :action_controller do
5
- require 'strong_presenter/helper_proxy' # a hack currently
3
+
4
+ config.after_initialize do |app|
5
+ app.config.paths.add 'app/presenters', eager_load: true
6
+
7
+ if Rails.env.test?
8
+ require 'strong_presenter/test_case'
9
+ require 'strong_presenter/test/rspec_integration' if defined?(RSpec) and RSpec.respond_to?(:configure)
10
+ require 'strong_presenter/test/minitest_integration' if defined?(MiniTest::Rails)
6
11
  end
7
12
  end
13
+
14
+ [:action_controller, :action_mailer, :active_model_serializers].each do |klass|
15
+ initializer "strong_presenter.setup_#{klass}" do |app|
16
+ ActiveSupport.on_load klass do
17
+ StrongPresenter.send "setup_#{klass}", self
18
+ end
19
+ end
20
+ end
21
+
22
+ console do
23
+ require 'action_controller/test_case'
24
+ ApplicationController.new.view_context
25
+ StrongPresenter::ViewContext.build
26
+ end
27
+
28
+ rake_tasks do
29
+ Dir[File.join(File.dirname(__FILE__),'tasks/*.rake')].each { |f| load f }
30
+ end
31
+
8
32
  end
9
33
  end
@@ -0,0 +1,22 @@
1
+ require 'rake/testtask'
2
+
3
+ test_task = if Rails.version.to_f < 3.2
4
+ require 'rails/test_unit/railtie'
5
+ Rake::TestTask
6
+ else
7
+ require 'rails/test_unit/sub_test_task'
8
+ Rails::SubTestTask
9
+ end
10
+
11
+ namespace :test do
12
+ test_task.new(:presenters => "test:prepare") do |t|
13
+ t.libs << "test"
14
+ t.pattern = "test/presenters/**/*_test.rb"
15
+ end
16
+ end
17
+
18
+ if Rake::Task.task_defined?('test:run')
19
+ Rake::Task['test:run'].enhance do
20
+ Rake::Task['test:presenters'].invoke
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module StrongPresenter
2
+ module DeviseHelper
3
+ def sign_in(resource_or_scope, resource = nil)
4
+ scope = begin
5
+ Devise::Mapping.find_scope!(resource_or_scope)
6
+ rescue RuntimeError => e
7
+ # Draper 1.0 didn't require the mapping to exist
8
+ ActiveSupport::Deprecation.warn("#{e.message}.\nUse `sign_in :user, mock_user` instead.", caller)
9
+ :user
10
+ end
11
+
12
+ _stub_current_scope scope, resource || resource_or_scope
13
+ end
14
+
15
+ def sign_out(resource_or_scope)
16
+ scope = Devise::Mapping.find_scope!(resource_or_scope)
17
+ _stub_current_scope scope, nil
18
+ end
19
+
20
+ private
21
+
22
+ def _stub_current_scope(scope, resource)
23
+ StrongPresenter::ViewContext.current.controller.singleton_class.class_eval do
24
+ define_method "current_#{scope}" do
25
+ resource
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ class StrongPresenter::TestCase
2
+ register_spec_type(self) do |desc|
3
+ desc < StrongPresenter::Presenter || desc < StrongPresenter::CollectionPresenter if desc.is_a?(Class)
4
+ end
5
+ register_spec_type(/Presenter( ?Test)?\z/i, self)
6
+ end
@@ -0,0 +1,16 @@
1
+ module StrongPresenter
2
+ module PresenterExampleGroup
3
+ include StrongPresenter::TestCase::Behavior
4
+ extend ActiveSupport::Concern
5
+
6
+ included { metadata[:type] = :presenter }
7
+ end
8
+
9
+ RSpec.configure do |config|
10
+ config.include PresenterExampleGroup, example_group: {file_path: %r{spec/presenters}}, type: :presenter
11
+
12
+ [:presenter, :controller, :mailer].each do |type|
13
+ config.before(:each, type: type) { StrongPresenter::ViewContext.clear! }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,53 @@
1
+ module StrongPresenter
2
+ begin
3
+ require 'minitest/rails'
4
+ rescue LoadError
5
+ end
6
+
7
+ active_support_test_case = begin
8
+ require 'minitest/rails/active_support' # minitest-rails < 0.5
9
+ ::MiniTest::Rails::ActiveSupport::TestCase
10
+ rescue LoadError
11
+ require 'active_support/test_case'
12
+ ::ActiveSupport::TestCase
13
+ end
14
+
15
+ class TestCase < active_support_test_case
16
+ module ViewContextTeardown
17
+ def teardown
18
+ super
19
+ StrongPresenter::ViewContext.clear!
20
+ end
21
+ end
22
+
23
+ module Behavior
24
+ if defined?(::Devise)
25
+ require 'strong_presenter/test/devise_helper'
26
+ include StrongPresenter::DeviseHelper
27
+ end
28
+
29
+ if defined?(::Capybara) && (defined?(::RSpec) || defined?(::MiniTest::Matchers))
30
+ require 'capybara/rspec/matchers'
31
+ include ::Capybara::RSpecMatchers
32
+ end
33
+
34
+ include StrongPresenter::ViewHelpers::ClassMethods
35
+ alias_method :helper, :helpers
36
+ end
37
+
38
+ include Behavior
39
+ include ViewContextTeardown
40
+ end
41
+ end
42
+
43
+ if defined?(ActionController::TestCase)
44
+ class ActionController::TestCase
45
+ include StrongPresenter::TestCase::ViewContextTeardown
46
+ end
47
+ end
48
+
49
+ if defined?(ActionMailer::TestCase)
50
+ class ActionMailer::TestCase
51
+ include StrongPresenter::TestCase::ViewContextTeardown
52
+ end
53
+ end
@@ -1,3 +1,3 @@
1
1
  module StrongPresenter
2
- VERSION = "0.0.1"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -0,0 +1,48 @@
1
+ module StrongPresenter
2
+ module ViewContext
3
+ # @private
4
+ module BuildStrategy
5
+
6
+ def self.new(name, &block)
7
+ const_get(name.to_s.camelize).new(&block)
8
+ end
9
+
10
+ class Fast
11
+ def initialize(&block)
12
+ @view_context_class = Class.new(ActionView::Base, &block)
13
+ end
14
+
15
+ def call
16
+ view_context_class.new
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :view_context_class
22
+ end
23
+
24
+ class Full
25
+ def initialize(&block)
26
+ @block = block
27
+ end
28
+
29
+ def call
30
+ controller.view_context.tap do |context|
31
+ context.singleton_class.class_eval(&block) if block
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ attr_reader :block
38
+
39
+ def controller
40
+ (StrongPresenter::ViewContext.controller || ApplicationController.new).tap do |controller|
41
+ controller.request ||= ActionController::TestRequest.new if defined?(ActionController::TestRequest)
42
+ end
43
+ end
44
+ end
45
+
46
+ end
47
+ end
48
+ end