pundit 1.1.0 → 2.0.0.beta1
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 +5 -5
- data/.rubocop.yml +18 -10
- data/.travis.yml +19 -9
- data/CHANGELOG.md +13 -0
- data/Gemfile +13 -1
- data/README.md +233 -60
- data/Rakefile +0 -1
- data/lib/generators/pundit/install/install_generator.rb +1 -1
- data/lib/generators/pundit/install/templates/application_policy.rb +2 -6
- data/lib/generators/pundit/policy/policy_generator.rb +1 -1
- data/lib/generators/pundit/policy/templates/policy.rb +1 -1
- data/lib/generators/rspec/policy_generator.rb +1 -1
- data/lib/generators/rspec/templates/policy_spec.rb +0 -1
- data/lib/generators/test_unit/policy_generator.rb +1 -1
- data/lib/generators/test_unit/templates/policy_test.rb +0 -1
- data/lib/pundit.rb +84 -59
- data/lib/pundit/policy_finder.rb +25 -31
- data/lib/pundit/rspec.rb +11 -7
- data/lib/pundit/version.rb +3 -1
- data/pundit.gemspec +2 -11
- data/spec/policy_finder_spec.rb +122 -0
- data/spec/pundit_spec.rb +136 -32
- data/spec/spec_helper.rb +73 -11
- metadata +8 -119
data/Rakefile
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
module Pundit
|
2
2
|
module Generators
|
3
3
|
class InstallGenerator < ::Rails::Generators::Base
|
4
|
-
source_root File.expand_path(
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
5
|
|
6
6
|
def copy_application_policy
|
7
7
|
template 'application_policy.rb', 'app/policies/application_policy.rb'
|
@@ -11,7 +11,7 @@ class ApplicationPolicy
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def show?
|
14
|
-
|
14
|
+
false
|
15
15
|
end
|
16
16
|
|
17
17
|
def create?
|
@@ -34,10 +34,6 @@ class ApplicationPolicy
|
|
34
34
|
false
|
35
35
|
end
|
36
36
|
|
37
|
-
def scope
|
38
|
-
Pundit.policy_scope!(user, record.class)
|
39
|
-
end
|
40
|
-
|
41
37
|
class Scope
|
42
38
|
attr_reader :user, :scope
|
43
39
|
|
@@ -47,7 +43,7 @@ class ApplicationPolicy
|
|
47
43
|
end
|
48
44
|
|
49
45
|
def resolve
|
50
|
-
scope
|
46
|
+
scope.all
|
51
47
|
end
|
52
48
|
end
|
53
49
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Pundit
|
2
2
|
module Generators
|
3
3
|
class PolicyGenerator < ::Rails::Generators::NamedBase
|
4
|
-
source_root File.expand_path(
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
5
|
|
6
6
|
def create_policy
|
7
7
|
template 'policy.rb', File.join('app/policies', class_path, "#{file_name}_policy.rb")
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Rspec
|
2
2
|
module Generators
|
3
3
|
class PolicyGenerator < ::Rails::Generators::NamedBase
|
4
|
-
source_root File.expand_path(
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
5
|
|
6
6
|
def create_policy_spec
|
7
7
|
template 'policy_spec.rb', File.join('spec/policies', class_path, "#{file_name}_policy_spec.rb")
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module TestUnit
|
2
2
|
module Generators
|
3
3
|
class PolicyGenerator < ::Rails::Generators::NamedBase
|
4
|
-
source_root File.expand_path(
|
4
|
+
source_root File.expand_path('templates', __dir__)
|
5
5
|
|
6
6
|
def create_policy_test
|
7
7
|
template 'policy_test.rb', File.join('test/policies', class_path, "#{file_name}_policy_test.rb")
|
data/lib/pundit.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require "pundit/version"
|
2
4
|
require "pundit/policy_finder"
|
3
5
|
require "active_support/concern"
|
@@ -8,7 +10,7 @@ require "active_support/dependencies/autoload"
|
|
8
10
|
|
9
11
|
# @api public
|
10
12
|
module Pundit
|
11
|
-
SUFFIX = "Policy"
|
13
|
+
SUFFIX = "Policy".freeze
|
12
14
|
|
13
15
|
# @api private
|
14
16
|
module Generators; end
|
@@ -16,7 +18,7 @@ module Pundit
|
|
16
18
|
# @api private
|
17
19
|
class Error < StandardError; end
|
18
20
|
|
19
|
-
# Error that will be
|
21
|
+
# Error that will be raised when authorization has failed
|
20
22
|
class NotAuthorizedError < Error
|
21
23
|
attr_reader :query, :record, :policy
|
22
24
|
|
@@ -35,6 +37,9 @@ module Pundit
|
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
40
|
+
# Error that will be raised if a policy or policy scope constructor is not called correctly.
|
41
|
+
class InvalidConstructorError < Error; end
|
42
|
+
|
38
43
|
# Error that will be raised if a controller action has not called the
|
39
44
|
# `authorize` or `skip_authorization` methods.
|
40
45
|
class AuthorizationNotPerformedError < Error; end
|
@@ -55,61 +60,80 @@ module Pundit
|
|
55
60
|
#
|
56
61
|
# @param user [Object] the user that initiated the action
|
57
62
|
# @param record [Object] the object we're checking permissions of
|
58
|
-
# @param
|
63
|
+
# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`)
|
64
|
+
# @param policy_class [Class] the policy class we want to force use of
|
59
65
|
# @raise [NotAuthorizedError] if the given query method returned false
|
60
|
-
# @return [
|
61
|
-
def authorize(user, record, query)
|
62
|
-
policy = policy!(user, record)
|
66
|
+
# @return [Object] Always returns the passed object record
|
67
|
+
def authorize(user, record, query, policy_class: nil)
|
68
|
+
policy = policy_class ? policy_class.new(user, record) : policy!(user, record)
|
63
69
|
|
64
|
-
unless policy.public_send(query)
|
65
|
-
raise NotAuthorizedError, query: query, record: record, policy: policy
|
66
|
-
end
|
70
|
+
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
|
67
71
|
|
68
|
-
|
72
|
+
record
|
69
73
|
end
|
70
74
|
|
71
75
|
# Retrieves the policy scope for the given record.
|
72
76
|
#
|
73
|
-
# @see https://github.com/
|
77
|
+
# @see https://github.com/varvet/pundit#scopes
|
74
78
|
# @param user [Object] the user that initiated the action
|
75
|
-
# @param
|
79
|
+
# @param scope [Object] the object we're retrieving the policy scope for
|
80
|
+
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
76
81
|
# @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
|
77
82
|
def policy_scope(user, scope)
|
78
83
|
policy_scope = PolicyFinder.new(scope).scope
|
79
|
-
policy_scope.new(user, scope).resolve if policy_scope
|
84
|
+
policy_scope.new(user, pundit_model(scope)).resolve if policy_scope
|
85
|
+
rescue ArgumentError
|
86
|
+
raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called"
|
80
87
|
end
|
81
88
|
|
82
89
|
# Retrieves the policy scope for the given record.
|
83
90
|
#
|
84
|
-
# @see https://github.com/
|
91
|
+
# @see https://github.com/varvet/pundit#scopes
|
85
92
|
# @param user [Object] the user that initiated the action
|
86
|
-
# @param
|
93
|
+
# @param scope [Object] the object we're retrieving the policy scope for
|
87
94
|
# @raise [NotDefinedError] if the policy scope cannot be found
|
95
|
+
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
88
96
|
# @return [Scope{#resolve}] instance of scope class which can resolve to a scope
|
89
97
|
def policy_scope!(user, scope)
|
90
|
-
PolicyFinder.new(scope).scope
|
98
|
+
policy_scope = PolicyFinder.new(scope).scope!
|
99
|
+
policy_scope.new(user, pundit_model(scope)).resolve
|
100
|
+
rescue ArgumentError
|
101
|
+
raise InvalidConstructorError, "Invalid #<#{policy_scope}> constructor is called"
|
91
102
|
end
|
92
103
|
|
93
104
|
# Retrieves the policy for the given record.
|
94
105
|
#
|
95
|
-
# @see https://github.com/
|
106
|
+
# @see https://github.com/varvet/pundit#policies
|
96
107
|
# @param user [Object] the user that initiated the action
|
97
108
|
# @param record [Object] the object we're retrieving the policy for
|
109
|
+
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
98
110
|
# @return [Object, nil] instance of policy class with query methods
|
99
111
|
def policy(user, record)
|
100
112
|
policy = PolicyFinder.new(record).policy
|
101
|
-
policy.new(user, record) if policy
|
113
|
+
policy.new(user, pundit_model(record)) if policy
|
114
|
+
rescue ArgumentError
|
115
|
+
raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
|
102
116
|
end
|
103
117
|
|
104
118
|
# Retrieves the policy for the given record.
|
105
119
|
#
|
106
|
-
# @see https://github.com/
|
120
|
+
# @see https://github.com/varvet/pundit#policies
|
107
121
|
# @param user [Object] the user that initiated the action
|
108
122
|
# @param record [Object] the object we're retrieving the policy for
|
109
123
|
# @raise [NotDefinedError] if the policy cannot be found
|
124
|
+
# @raise [InvalidConstructorError] if the policy constructor called incorrectly
|
110
125
|
# @return [Object] instance of policy class with query methods
|
111
126
|
def policy!(user, record)
|
112
|
-
PolicyFinder.new(record).policy
|
127
|
+
policy = PolicyFinder.new(record).policy!
|
128
|
+
policy.new(user, pundit_model(record))
|
129
|
+
rescue ArgumentError
|
130
|
+
raise InvalidConstructorError, "Invalid #<#{policy}> constructor is called"
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def pundit_model(record)
|
136
|
+
record.is_a?(Array) ? record.last : record
|
113
137
|
end
|
114
138
|
end
|
115
139
|
|
@@ -127,23 +151,10 @@ module Pundit
|
|
127
151
|
helper_method :pundit_policy_scope
|
128
152
|
helper_method :pundit_user
|
129
153
|
end
|
130
|
-
if respond_to?(:hide_action)
|
131
|
-
hide_action :policy
|
132
|
-
hide_action :policy_scope
|
133
|
-
hide_action :policies
|
134
|
-
hide_action :policy_scopes
|
135
|
-
hide_action :authorize
|
136
|
-
hide_action :verify_authorized
|
137
|
-
hide_action :verify_policy_scoped
|
138
|
-
hide_action :permitted_attributes
|
139
|
-
hide_action :pundit_user
|
140
|
-
hide_action :skip_authorization
|
141
|
-
hide_action :skip_policy_scope
|
142
|
-
hide_action :pundit_policy_authorized?
|
143
|
-
hide_action :pundit_policy_scoped?
|
144
|
-
end
|
145
154
|
end
|
146
155
|
|
156
|
+
protected
|
157
|
+
|
147
158
|
# @return [Boolean] whether authorization has been performed, i.e. whether
|
148
159
|
# one {#authorize} or {#skip_authorization} has been called
|
149
160
|
def pundit_policy_authorized?
|
@@ -160,7 +171,7 @@ module Pundit
|
|
160
171
|
# `after_action` filter to prevent programmer error in forgetting to call
|
161
172
|
# {#authorize} or {#skip_authorization}.
|
162
173
|
#
|
163
|
-
# @see https://github.com/
|
174
|
+
# @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
|
164
175
|
# @raise [AuthorizationNotPerformedError] if authorization has not been performed
|
165
176
|
# @return [void]
|
166
177
|
def verify_authorized
|
@@ -171,7 +182,7 @@ module Pundit
|
|
171
182
|
# `after_action` filter to prevent programmer error in forgetting to call
|
172
183
|
# {#policy_scope} or {#skip_policy_scope} in index actions.
|
173
184
|
#
|
174
|
-
# @see https://github.com/
|
185
|
+
# @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
|
175
186
|
# @raise [AuthorizationNotPerformedError] if policy scoping has not been performed
|
176
187
|
# @return [void]
|
177
188
|
def verify_policy_scoped
|
@@ -183,26 +194,26 @@ module Pundit
|
|
183
194
|
# authorized to perform the given action.
|
184
195
|
#
|
185
196
|
# @param record [Object] the object we're checking permissions of
|
186
|
-
# @param
|
197
|
+
# @param query [Symbol, String] the predicate method to check on the policy (e.g. `:show?`).
|
198
|
+
# If omitted then this defaults to the Rails controller action name.
|
199
|
+
# @param policy_class [Class] the policy class we want to force use of
|
187
200
|
# @raise [NotAuthorizedError] if the given query method returned false
|
188
|
-
# @return [
|
189
|
-
def authorize(record, query = nil)
|
190
|
-
query ||=
|
201
|
+
# @return [Object] Always returns the passed object record
|
202
|
+
def authorize(record, query = nil, policy_class: nil)
|
203
|
+
query ||= "#{action_name}?"
|
191
204
|
|
192
205
|
@_pundit_policy_authorized = true
|
193
206
|
|
194
|
-
policy = policy(record)
|
207
|
+
policy = policy_class ? policy_class.new(pundit_user, record) : policy(record)
|
195
208
|
|
196
|
-
unless policy.public_send(query)
|
197
|
-
raise NotAuthorizedError, query: query, record: record, policy: policy
|
198
|
-
end
|
209
|
+
raise NotAuthorizedError, query: query, record: record, policy: policy unless policy.public_send(query)
|
199
210
|
|
200
|
-
|
211
|
+
record
|
201
212
|
end
|
202
213
|
|
203
214
|
# Allow this action not to perform authorization.
|
204
215
|
#
|
205
|
-
# @see https://github.com/
|
216
|
+
# @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
|
206
217
|
# @return [void]
|
207
218
|
def skip_authorization
|
208
219
|
@_pundit_policy_authorized = true
|
@@ -210,7 +221,7 @@ module Pundit
|
|
210
221
|
|
211
222
|
# Allow this action not to perform policy scoping.
|
212
223
|
#
|
213
|
-
# @see https://github.com/
|
224
|
+
# @see https://github.com/varvet/pundit#ensuring-policies-and-scopes-are-used
|
214
225
|
# @return [void]
|
215
226
|
def skip_policy_scope
|
216
227
|
@_pundit_policy_scoped = true
|
@@ -218,17 +229,18 @@ module Pundit
|
|
218
229
|
|
219
230
|
# Retrieves the policy scope for the given record.
|
220
231
|
#
|
221
|
-
# @see https://github.com/
|
222
|
-
# @param
|
232
|
+
# @see https://github.com/varvet/pundit#scopes
|
233
|
+
# @param scope [Object] the object we're retrieving the policy scope for
|
234
|
+
# @param policy_scope_class [Class] the policy scope class we want to force use of
|
223
235
|
# @return [Scope{#resolve}, nil] instance of scope class which can resolve to a scope
|
224
|
-
def policy_scope(scope)
|
236
|
+
def policy_scope(scope, policy_scope_class: nil)
|
225
237
|
@_pundit_policy_scoped = true
|
226
|
-
pundit_policy_scope(scope)
|
238
|
+
policy_scope_class ? policy_scope_class.new(pundit_user, scope).resolve : pundit_policy_scope(scope)
|
227
239
|
end
|
228
240
|
|
229
241
|
# Retrieves the policy for the given record.
|
230
242
|
#
|
231
|
-
# @see https://github.com/
|
243
|
+
# @see https://github.com/varvet/pundit#policies
|
232
244
|
# @param record [Object] the object we're retrieving the policy for
|
233
245
|
# @return [Object, nil] instance of policy class with query methods
|
234
246
|
def policy(record)
|
@@ -237,42 +249,55 @@ module Pundit
|
|
237
249
|
|
238
250
|
# Retrieves a set of permitted attributes from the policy by instantiating
|
239
251
|
# the policy class for the given record and calling `permitted_attributes` on
|
240
|
-
# it, or `permitted_attributes_for_{action}` if
|
252
|
+
# it, or `permitted_attributes_for_{action}` if `action` is defined. It then infers
|
241
253
|
# what key the record should have in the params hash and retrieves the
|
242
254
|
# permitted attributes from the params hash under that key.
|
243
255
|
#
|
244
|
-
# @see https://github.com/
|
256
|
+
# @see https://github.com/varvet/pundit#strong-parameters
|
245
257
|
# @param record [Object] the object we're retrieving permitted attributes for
|
258
|
+
# @param action [Symbol, String] the name of the action being performed on the record (e.g. `:update`).
|
259
|
+
# If omitted then this defaults to the Rails controller action name.
|
246
260
|
# @return [Hash{String => Object}] the permitted attributes
|
247
|
-
def permitted_attributes(record, action =
|
248
|
-
param_key = PolicyFinder.new(record).param_key
|
261
|
+
def permitted_attributes(record, action = action_name)
|
249
262
|
policy = policy(record)
|
250
263
|
method_name = if policy.respond_to?("permitted_attributes_for_#{action}")
|
251
264
|
"permitted_attributes_for_#{action}"
|
252
265
|
else
|
253
266
|
"permitted_attributes"
|
254
267
|
end
|
255
|
-
|
268
|
+
pundit_params_for(record).permit(*policy.public_send(method_name))
|
269
|
+
end
|
270
|
+
|
271
|
+
# Retrieves the params for the given record.
|
272
|
+
#
|
273
|
+
# @param record [Object] the object we're retrieving params for
|
274
|
+
# @return [ActionController::Parameters] the params
|
275
|
+
def pundit_params_for(record)
|
276
|
+
params.require(PolicyFinder.new(record).param_key)
|
256
277
|
end
|
257
278
|
|
258
279
|
# Cache of policies. You should not rely on this method.
|
259
280
|
#
|
260
281
|
# @api private
|
282
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
261
283
|
def policies
|
262
284
|
@_pundit_policies ||= {}
|
263
285
|
end
|
286
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
264
287
|
|
265
288
|
# Cache of policy scope. You should not rely on this method.
|
266
289
|
#
|
267
290
|
# @api private
|
291
|
+
# rubocop:disable Naming/MemoizedInstanceVariableName
|
268
292
|
def policy_scopes
|
269
293
|
@_pundit_policy_scopes ||= {}
|
270
294
|
end
|
295
|
+
# rubocop:enable Naming/MemoizedInstanceVariableName
|
271
296
|
|
272
297
|
# Hook method which allows customizing which user is passed to policies and
|
273
298
|
# scopes initialized by {#authorize}, {#policy} and {#policy_scope}.
|
274
299
|
#
|
275
|
-
# @see https://github.com/
|
300
|
+
# @see https://github.com/varvet/pundit#customize-pundit-user
|
276
301
|
# @return [Object] the user object to be used with pundit
|
277
302
|
def pundit_user
|
278
303
|
current_user
|
data/lib/pundit/policy_finder.rb
CHANGED
@@ -17,75 +17,69 @@ module Pundit
|
|
17
17
|
end
|
18
18
|
|
19
19
|
# @return [nil, Scope{#resolve}] scope class which can resolve to a scope
|
20
|
-
# @see https://github.com/
|
20
|
+
# @see https://github.com/varvet/pundit#scopes
|
21
21
|
# @example
|
22
22
|
# scope = finder.scope #=> UserPolicy::Scope
|
23
23
|
# scope.resolve #=> <#ActiveRecord::Relation ...>
|
24
24
|
#
|
25
25
|
def scope
|
26
|
-
policy::Scope
|
27
|
-
rescue NameError
|
28
|
-
nil
|
26
|
+
"#{policy}::Scope".safe_constantize
|
29
27
|
end
|
30
28
|
|
31
29
|
# @return [nil, Class] policy class with query methods
|
32
|
-
# @see https://github.com/
|
30
|
+
# @see https://github.com/varvet/pundit#policies
|
33
31
|
# @example
|
34
32
|
# policy = finder.policy #=> UserPolicy
|
35
33
|
# policy.show? #=> true
|
36
34
|
# policy.update? #=> false
|
37
35
|
#
|
38
36
|
def policy
|
39
|
-
klass = find
|
40
|
-
klass
|
41
|
-
klass
|
42
|
-
rescue NameError
|
43
|
-
nil
|
37
|
+
klass = find(object)
|
38
|
+
klass.is_a?(String) ? klass.safe_constantize : klass
|
44
39
|
end
|
45
40
|
|
46
41
|
# @return [Scope{#resolve}] scope class which can resolve to a scope
|
47
42
|
# @raise [NotDefinedError] if scope could not be determined
|
48
43
|
#
|
49
44
|
def scope!
|
50
|
-
raise NotDefinedError, "unable to find
|
51
|
-
scope or raise NotDefinedError, "unable to find scope `#{find}::Scope` for `#{object.inspect}`"
|
45
|
+
scope or raise NotDefinedError, "unable to find scope `#{find(object)}::Scope` for `#{object.inspect}`"
|
52
46
|
end
|
53
47
|
|
54
48
|
# @return [Class] policy class with query methods
|
55
49
|
# @raise [NotDefinedError] if policy could not be determined
|
56
50
|
#
|
57
51
|
def policy!
|
58
|
-
raise NotDefinedError, "unable to find policy
|
59
|
-
policy or raise NotDefinedError, "unable to find policy `#{find}` for `#{object.inspect}`"
|
52
|
+
policy or raise NotDefinedError, "unable to find policy `#{find(object)}` for `#{object.inspect}`"
|
60
53
|
end
|
61
54
|
|
62
55
|
# @return [String] the name of the key this object would have in a params hash
|
63
56
|
#
|
64
57
|
def param_key
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
58
|
+
model = object.is_a?(Array) ? object.last : object
|
59
|
+
|
60
|
+
if model.respond_to?(:model_name)
|
61
|
+
model.model_name.param_key.to_s
|
62
|
+
elsif model.is_a?(Class)
|
63
|
+
model.to_s.demodulize.underscore
|
69
64
|
else
|
70
|
-
|
65
|
+
model.class.to_s.demodulize.underscore
|
71
66
|
end
|
72
67
|
end
|
73
68
|
|
74
69
|
private
|
75
70
|
|
76
|
-
def find
|
77
|
-
if
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
71
|
+
def find(subject)
|
72
|
+
if subject.is_a?(Array)
|
73
|
+
modules = subject.dup
|
74
|
+
last = modules.pop
|
75
|
+
context = modules.map { |x| find_class_name(x) }.join("::")
|
76
|
+
[context, find(last)].join("::")
|
77
|
+
elsif subject.respond_to?(:policy_class)
|
78
|
+
subject.policy_class
|
79
|
+
elsif subject.class.respond_to?(:policy_class)
|
80
|
+
subject.class.policy_class
|
83
81
|
else
|
84
|
-
klass =
|
85
|
-
object.map { |x| find_class_name(x) }.join("::")
|
86
|
-
else
|
87
|
-
find_class_name(object)
|
88
|
-
end
|
82
|
+
klass = find_class_name(subject)
|
89
83
|
"#{klass}#{SUFFIX}"
|
90
84
|
end
|
91
85
|
end
|