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.
- 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
|