strong_presenter 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -2
- data/.rspec +2 -0
- data/.travis.yml +18 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +31 -0
- data/Gemfile +22 -2
- data/Guardfile +26 -0
- data/README.md +210 -52
- data/Rakefile +77 -1
- data/gemfiles/3.0.gemfile +2 -0
- data/gemfiles/3.1.gemfile +2 -0
- data/gemfiles/3.2.gemfile +2 -0
- data/gemfiles/4.0.gemfile +2 -0
- data/gemfiles/4.1.gemfile +2 -0
- data/lib/generators/controller_override.rb +15 -0
- data/lib/generators/mini_test/presenter_generator.rb +20 -0
- data/lib/generators/mini_test/templates/presenter_spec.rb +4 -0
- data/lib/generators/mini_test/templates/presenter_test.rb +4 -0
- data/lib/generators/rails/presenter_generator.rb +36 -0
- data/lib/generators/rails/templates/presenter.rb +19 -0
- data/lib/generators/rspec/presenter_generator.rb +9 -0
- data/lib/generators/rspec/templates/presenter_spec.rb +4 -0
- data/lib/generators/test_unit/presenter_generator.rb +9 -0
- data/lib/generators/test_unit/templates/presenter_test.rb +4 -0
- data/lib/strong_presenter/associable.rb +78 -0
- data/lib/strong_presenter/collection_presenter.rb +90 -0
- data/lib/strong_presenter/controller_additions.rb +50 -0
- data/lib/strong_presenter/delegation.rb +18 -0
- data/lib/strong_presenter/factory.rb +74 -0
- data/lib/strong_presenter/helper_proxy.rb +29 -11
- data/lib/strong_presenter/inferrer.rb +54 -0
- data/lib/strong_presenter/permissible.rb +73 -0
- data/lib/strong_presenter/permissions.rb +138 -0
- data/lib/strong_presenter/presenter.rb +191 -0
- data/lib/strong_presenter/presenter_association.rb +29 -0
- data/lib/strong_presenter/presenter_helper_constructor.rb +60 -0
- data/lib/strong_presenter/railtie.rb +27 -3
- data/lib/strong_presenter/tasks/test.rake +22 -0
- data/lib/strong_presenter/test/devise_helper.rb +30 -0
- data/lib/strong_presenter/test/minitest_integration.rb +6 -0
- data/lib/strong_presenter/test/rspec_integration.rb +16 -0
- data/lib/strong_presenter/test_case.rb +53 -0
- data/lib/strong_presenter/version.rb +1 -1
- data/lib/strong_presenter/view_context/build_strategy.rb +48 -0
- data/lib/strong_presenter/view_context.rb +84 -0
- data/lib/strong_presenter/view_helpers.rb +39 -0
- data/lib/strong_presenter.rb +64 -2
- data/spec/dummy/.rspec +2 -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 +25 -0
- data/spec/dummy/app/helpers/application_helper.rb +5 -0
- data/spec/dummy/app/mailers/application_mailer.rb +3 -0
- data/spec/dummy/app/mailers/post_mailer.rb +19 -0
- data/spec/dummy/app/models/admin.rb +5 -0
- data/spec/dummy/app/models/post.rb +3 -0
- data/spec/dummy/app/models/user.rb +5 -0
- data/spec/dummy/app/presenters/post_presenter.rb +69 -0
- data/spec/dummy/app/presenters/special_post_presenter.rb +5 -0
- data/spec/dummy/app/presenters/special_posts_presenter.rb +5 -0
- data/spec/dummy/app/views/layouts/application.html.erb +11 -0
- data/spec/dummy/app/views/post_mailer/presented_email.html.erb +1 -0
- data/spec/dummy/app/views/posts/_post.html.erb +50 -0
- data/spec/dummy/app/views/posts/show.html.erb +1 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/config/application.rb +70 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +33 -0
- data/spec/dummy/config/environments/production.rb +57 -0
- data/spec/dummy/config/environments/test.rb +31 -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 +8 -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 +9 -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/fast_spec/post_presenter_spec.rb +37 -0
- data/spec/dummy/lib/tasks/test.rake +16 -0
- data/spec/dummy/log/.gitkeep +0 -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/fast_spec_helper.rb +13 -0
- data/spec/dummy/spec/mailers/post_mailer_spec.rb +33 -0
- data/spec/dummy/spec/models/post_spec.rb +4 -0
- data/spec/dummy/spec/presenters/active_model_serializers_spec.rb +11 -0
- data/spec/dummy/spec/presenters/devise_spec.rb +64 -0
- data/spec/dummy/spec/presenters/helpers_spec.rb +21 -0
- data/spec/dummy/spec/presenters/post_presenter_spec.rb +66 -0
- data/spec/dummy/spec/presenters/spec_type_spec.rb +7 -0
- data/spec/dummy/spec/presenters/special_post_presenter_spec.rb +11 -0
- data/spec/dummy/spec/presenters/view_context_spec.rb +22 -0
- data/spec/dummy/spec/spec_helper.rb +19 -0
- data/spec/dummy/test/minitest_helper.rb +2 -0
- data/spec/dummy/test/presenters/minitest/devise_test.rb +64 -0
- data/spec/dummy/test/presenters/minitest/helpers_test.rb +21 -0
- data/spec/dummy/test/presenters/minitest/spec_type_test.rb +52 -0
- data/spec/dummy/test/presenters/minitest/view_context_test.rb +24 -0
- data/spec/dummy/test/presenters/test_unit/devise_test.rb +64 -0
- data/spec/dummy/test/presenters/test_unit/helpers_test.rb +21 -0
- data/spec/dummy/test/presenters/test_unit/view_context_test.rb +24 -0
- data/spec/dummy/test/test_helper.rb +13 -0
- data/spec/generators/presenters/presenter_generator_spec.rb +131 -0
- data/spec/generators/simplecov_spec.rb +5 -0
- data/spec/integration/integration_spec.rb +81 -0
- data/spec/integration/simplecov_spec.rb +4 -0
- data/spec/spec_helper.rb +47 -0
- data/spec/strong_presenter/associable_spec.rb +122 -0
- data/spec/strong_presenter/collection_presenter_spec.rb +34 -0
- data/spec/strong_presenter/delegation_spec.rb +20 -0
- data/spec/strong_presenter/permissible_spec.rb +24 -0
- data/spec/strong_presenter/permissions_spec.rb +188 -0
- data/spec/strong_presenter/presenter_spec.rb +43 -0
- data/spec/strong_presenter/simplecov_spec.rb +4 -0
- data/spec/support/dummy_app.rb +85 -0
- data/spec/support/matchers/have_text.rb +50 -0
- data/spec/support/models.rb +14 -0
- data/spec/support/schema.rb +12 -0
- data/strong_presenter.gemspec +15 -0
- metadata +392 -13
- 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
|
-
|
4
|
-
|
5
|
-
|
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,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
|
@@ -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
|