cancan 1.1.1 → 1.2.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.
- data/CHANGELOG.rdoc +15 -0
- data/lib/cancan.rb +1 -0
- data/lib/cancan/ability.rb +95 -135
- data/lib/cancan/active_record_additions.rb +10 -9
- data/lib/cancan/can_definition.rb +114 -0
- data/lib/cancan/controller_additions.rb +61 -51
- data/lib/cancan/controller_resource.rb +16 -9
- data/lib/cancan/exceptions.rb +11 -11
- data/lib/cancan/resource_authorization.rb +21 -19
- data/spec/cancan/ability_spec.rb +40 -28
- data/spec/cancan/active_record_additions_spec.rb +8 -8
- data/spec/cancan/can_definition_spec.rb +44 -0
- data/spec/cancan/controller_additions_spec.rb +9 -9
- data/spec/cancan/controller_resource_spec.rb +9 -9
- data/spec/cancan/exceptions_spec.rb +5 -5
- data/spec/cancan/resource_authorization_spec.rb +35 -17
- data/spec/spec_helper.rb +1 -1
- metadata +6 -4
data/CHANGELOG.rdoc
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
1.2.0 (July 16, 2010)
|
2
|
+
|
3
|
+
* Load nested parent resources on collection actions such as "index" (thanks dohzya)
|
4
|
+
|
5
|
+
* Adding :name option to load_and_authorize_resource if it does not match controller - see issue #65
|
6
|
+
|
7
|
+
* Fixing issue when using accessible_by with nil can conditions (thanks jrallison) - see issue #66
|
8
|
+
|
9
|
+
* Pluralize table name for belongs_to associations in can conditions hash (thanks logandk) - see issue #62
|
10
|
+
|
11
|
+
* Support has_many association or arrays in can conditions hash
|
12
|
+
|
13
|
+
* Adding joins clause to accessible_by when conditions are across associations
|
14
|
+
|
15
|
+
|
1
16
|
1.1.1 (April 17, 2010)
|
2
17
|
|
3
18
|
* Fixing behavior in Rails 3 by properly initializing ResourceAuthorization
|
data/lib/cancan.rb
CHANGED
data/lib/cancan/ability.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
module CanCan
|
2
|
-
|
2
|
+
|
3
3
|
# This module is designed to be included into an Ability class. This will
|
4
4
|
# provide the "can" methods for defining and checking abilities.
|
5
|
-
#
|
5
|
+
#
|
6
6
|
# class Ability
|
7
7
|
# include CanCan::Ability
|
8
8
|
#
|
@@ -14,206 +14,216 @@ module CanCan
|
|
14
14
|
# end
|
15
15
|
# end
|
16
16
|
# end
|
17
|
-
#
|
17
|
+
#
|
18
18
|
module Ability
|
19
|
-
# Use to check the user
|
20
|
-
#
|
19
|
+
# Use to check if the user has permission to perform a given action on an object.
|
20
|
+
#
|
21
21
|
# can? :destroy, @project
|
22
|
-
#
|
22
|
+
#
|
23
23
|
# You can also pass the class instead of an instance (if you don't have one handy).
|
24
|
-
#
|
24
|
+
#
|
25
25
|
# can? :create, Project
|
26
|
-
#
|
26
|
+
#
|
27
27
|
# Any additional arguments will be passed into the "can" block definition. This
|
28
28
|
# can be used to pass more information about the user's request for example.
|
29
|
-
#
|
29
|
+
#
|
30
30
|
# can? :create, Project, request.remote_ip
|
31
|
-
#
|
31
|
+
#
|
32
32
|
# can :create Project do |project, remote_ip|
|
33
33
|
# # ...
|
34
34
|
# end
|
35
|
-
#
|
36
|
-
# Not only can you use the can? method in the controller and view (see ControllerAdditions),
|
35
|
+
#
|
36
|
+
# Not only can you use the can? method in the controller and view (see ControllerAdditions),
|
37
37
|
# but you can also call it directly on an ability instance.
|
38
|
-
#
|
38
|
+
#
|
39
39
|
# ability.can? :destroy, @project
|
40
|
-
#
|
40
|
+
#
|
41
41
|
# This makes testing a user's abilities very easy.
|
42
|
-
#
|
42
|
+
#
|
43
43
|
# def test "user can only destroy projects which he owns"
|
44
44
|
# user = User.new
|
45
45
|
# ability = Ability.new(user)
|
46
46
|
# assert ability.can?(:destroy, Project.new(:user => user))
|
47
47
|
# assert ability.cannot?(:destroy, Project.new)
|
48
48
|
# end
|
49
|
-
#
|
49
|
+
#
|
50
|
+
# Also see the RSpec Matchers to aid in testing.
|
50
51
|
def can?(action, subject, *extra_args)
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
55
|
-
false
|
52
|
+
raise Error, "Nom nom nom. I eated it." if action == :has && subject == :cheezburger
|
53
|
+
can_definition = matching_can_definition(action, subject)
|
54
|
+
can_definition && can_definition.can?(action, subject, extra_args)
|
56
55
|
end
|
57
|
-
|
56
|
+
|
58
57
|
# Convenience method which works the same as "can?" but returns the opposite value.
|
59
|
-
#
|
58
|
+
#
|
60
59
|
# cannot? :destroy, @project
|
61
|
-
#
|
60
|
+
#
|
62
61
|
def cannot?(*args)
|
63
62
|
!can?(*args)
|
64
63
|
end
|
65
|
-
|
64
|
+
|
66
65
|
# Defines which abilities are allowed using two arguments. The first one is the action
|
67
66
|
# you're setting the permission for, the second one is the class of object you're setting it on.
|
68
|
-
#
|
67
|
+
#
|
69
68
|
# can :update, Article
|
70
|
-
#
|
69
|
+
#
|
71
70
|
# You can pass an array for either of these parameters to match any one.
|
72
71
|
#
|
73
72
|
# can [:update, :destroy], [Article, Comment]
|
74
73
|
#
|
75
74
|
# In this case the user has the ability to update or destroy both articles and comments.
|
76
|
-
#
|
75
|
+
#
|
77
76
|
# You can pass a hash of conditions as the third argument.
|
78
77
|
#
|
79
78
|
# can :read, Project, :active => true, :user_id => user.id
|
80
|
-
#
|
81
|
-
# Here the user can only see active projects which he owns. See
|
82
|
-
# use this in database queries.
|
83
|
-
#
|
79
|
+
#
|
80
|
+
# Here the user can only see active projects which he owns. See ActiveRecordAdditions#accessible_by
|
81
|
+
# for how to use this in database queries.
|
82
|
+
#
|
84
83
|
# If the conditions hash does not give you enough control over defining abilities, you can use a block to
|
85
84
|
# write any Ruby code you want.
|
86
85
|
#
|
87
86
|
# can :update, Project do |project|
|
88
87
|
# project && project.groups.include?(user.group)
|
89
88
|
# end
|
90
|
-
#
|
89
|
+
#
|
91
90
|
# If the block returns true then the user has that :update ability for that project, otherwise he
|
92
91
|
# will be denied access. It's possible for the passed in model to be nil if one isn't specified,
|
93
92
|
# so be sure to take that into consideration.
|
94
|
-
#
|
93
|
+
#
|
95
94
|
# The downside to using a block is that it cannot be used to generate conditions for database queries.
|
96
|
-
#
|
95
|
+
#
|
97
96
|
# You can pass :all to reference every type of object. In this case the object type will be passed
|
98
97
|
# into the block as well (just in case object is nil).
|
99
|
-
#
|
98
|
+
#
|
100
99
|
# can :read, :all do |object_class, object|
|
101
100
|
# object_class != Order
|
102
101
|
# end
|
103
|
-
#
|
102
|
+
#
|
104
103
|
# Here the user has permission to read all objects except orders.
|
105
|
-
#
|
106
|
-
# You can also pass :manage as the action which will match any action. In this case the action is
|
104
|
+
#
|
105
|
+
# You can also pass :manage as the action which will match any action. In this case the action is
|
107
106
|
# passed to the block.
|
108
|
-
#
|
107
|
+
#
|
109
108
|
# can :manage, Comment do |action, comment|
|
110
109
|
# action != :destroy
|
111
110
|
# end
|
112
|
-
#
|
111
|
+
#
|
113
112
|
# You can pass custom objects into this "can" method, this is usually done through a symbol
|
114
113
|
# and is useful if a class isn't available to define permissions on.
|
115
|
-
#
|
114
|
+
#
|
116
115
|
# can :read, :stats
|
117
116
|
# can? :read, :stats # => true
|
118
|
-
#
|
117
|
+
#
|
119
118
|
def can(action, subject, conditions = nil, &block)
|
120
|
-
|
121
|
-
@can_definitions << [true, action, subject, conditions, block]
|
119
|
+
can_definitions << CanDefinition.new(true, action, subject, conditions, block)
|
122
120
|
end
|
123
|
-
|
124
|
-
#
|
125
|
-
#
|
121
|
+
|
122
|
+
# Defines an ability which cannot be done. Accepts the same arguments as "can".
|
123
|
+
#
|
126
124
|
# can :read, :all
|
127
125
|
# cannot :read, Comment
|
128
|
-
#
|
126
|
+
#
|
129
127
|
# A block can be passed just like "can", however if the logic is complex it is recommended
|
130
128
|
# to use the "can" method.
|
131
|
-
#
|
129
|
+
#
|
132
130
|
# cannot :read, Product do |product|
|
133
131
|
# product.invisible?
|
134
132
|
# end
|
135
|
-
#
|
133
|
+
#
|
136
134
|
def cannot(action, subject, conditions = nil, &block)
|
137
|
-
|
138
|
-
@can_definitions << [false, action, subject, conditions, block]
|
135
|
+
can_definitions << CanDefinition.new(false, action, subject, conditions, block)
|
139
136
|
end
|
140
|
-
|
137
|
+
|
141
138
|
# Alias one or more actions into another one.
|
142
|
-
#
|
139
|
+
#
|
143
140
|
# alias_action :update, :destroy, :to => :modify
|
144
141
|
# can :modify, Comment
|
145
|
-
#
|
142
|
+
#
|
146
143
|
# Then :modify permission will apply to both :update and :destroy requests.
|
147
|
-
#
|
144
|
+
#
|
148
145
|
# can? :update, Comment # => true
|
149
146
|
# can? :destroy, Comment # => true
|
150
|
-
#
|
147
|
+
#
|
151
148
|
# This only works in one direction. Passing the aliased action into the "can?" call
|
152
149
|
# will not work because aliases are meant to generate more generic actions.
|
153
|
-
#
|
150
|
+
#
|
154
151
|
# alias_action :update, :destroy, :to => :modify
|
155
152
|
# can :update, Comment
|
156
153
|
# can? :modify, Comment # => false
|
157
|
-
#
|
154
|
+
#
|
158
155
|
# Unless that exact alias is used.
|
159
|
-
#
|
156
|
+
#
|
160
157
|
# can :modify, Comment
|
161
158
|
# can? :modify, Comment # => true
|
162
|
-
#
|
159
|
+
#
|
163
160
|
# The following aliases are added by default for conveniently mapping common controller actions.
|
164
|
-
#
|
161
|
+
#
|
165
162
|
# alias_action :index, :show, :to => :read
|
166
163
|
# alias_action :new, :to => :create
|
167
164
|
# alias_action :edit, :to => :update
|
168
|
-
#
|
165
|
+
#
|
169
166
|
# This way one can use params[:action] in the controller to determine the permission.
|
170
167
|
def alias_action(*args)
|
171
168
|
target = args.pop[:to]
|
172
169
|
aliased_actions[target] ||= []
|
173
170
|
aliased_actions[target] += args
|
174
171
|
end
|
175
|
-
|
172
|
+
|
176
173
|
# Returns a hash of aliased actions. The key is the target and the value is an array of actions aliasing the key.
|
177
174
|
def aliased_actions
|
178
175
|
@aliased_actions ||= default_alias_actions
|
179
176
|
end
|
180
|
-
|
177
|
+
|
181
178
|
# Removes previously aliased actions including the defaults.
|
182
179
|
def clear_aliased_actions
|
183
180
|
@aliased_actions = {}
|
184
181
|
end
|
185
|
-
|
182
|
+
|
186
183
|
# Returns a hash of conditions which match the given ability. This is useful if you need to generate a database
|
187
184
|
# query based on the current ability.
|
188
|
-
#
|
185
|
+
#
|
189
186
|
# can :read, Article, :visible => true
|
190
187
|
# conditions :read, Article # returns { :visible => true }
|
191
|
-
#
|
188
|
+
#
|
192
189
|
# Normally you will not call this method directly, but instead go through ActiveRecordAdditions#accessible_by method.
|
193
|
-
#
|
190
|
+
#
|
194
191
|
# If the ability is not defined then false is returned so be sure to take that into consideration.
|
195
192
|
# If the ability is defined using a block then this will raise an exception since a hash of conditions cannot be
|
196
193
|
# determined from that.
|
197
|
-
def conditions(action, subject)
|
198
|
-
matching_can_definition(action, subject)
|
199
|
-
|
200
|
-
|
194
|
+
def conditions(action, subject, options = {})
|
195
|
+
can_definition = matching_can_definition(action, subject)
|
196
|
+
if can_definition
|
197
|
+
raise Error, "Cannot determine ability conditions from block for #{action.inspect} #{subject.inspect}" if can_definition.block
|
198
|
+
can_definition.conditions(options) || {}
|
199
|
+
else
|
200
|
+
false
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Returns the associations used in conditions. This is usually used in the :joins option for a search.
|
205
|
+
# See ActiveRecordAdditions#accessible_by for use in Active Record.
|
206
|
+
def association_joins(action, subject)
|
207
|
+
can_definition = matching_can_definition(action, subject)
|
208
|
+
if can_definition
|
209
|
+
raise Error, "Cannot determine association joins from block for #{action.inspect} #{subject.inspect}" if can_definition.block
|
210
|
+
can_definition.association_joins
|
201
211
|
end
|
202
|
-
false
|
203
212
|
end
|
204
|
-
|
213
|
+
|
205
214
|
private
|
206
|
-
|
207
|
-
def
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
215
|
+
|
216
|
+
def can_definitions
|
217
|
+
@can_definitions ||= []
|
218
|
+
end
|
219
|
+
|
220
|
+
def matching_can_definition(action, subject)
|
221
|
+
can_definitions.reverse.detect do |can_definition|
|
222
|
+
can_definition.expand_actions(aliased_actions)
|
223
|
+
can_definition.matches? action, subject
|
214
224
|
end
|
215
225
|
end
|
216
|
-
|
226
|
+
|
217
227
|
def default_alias_actions
|
218
228
|
{
|
219
229
|
:read => [:index, :show],
|
@@ -221,55 +231,5 @@ module CanCan
|
|
221
231
|
:update => [:edit],
|
222
232
|
}
|
223
233
|
end
|
224
|
-
|
225
|
-
def expand_actions(actions)
|
226
|
-
[actions].flatten.map do |action|
|
227
|
-
if aliased_actions[action]
|
228
|
-
[action, *aliased_actions[action]]
|
229
|
-
else
|
230
|
-
action
|
231
|
-
end
|
232
|
-
end.flatten
|
233
|
-
end
|
234
|
-
|
235
|
-
def can_perform_action?(action, subject, defined_actions, defined_subjects, defined_conditions, defined_block, extra_args)
|
236
|
-
if defined_block
|
237
|
-
block_args = []
|
238
|
-
block_args << action if defined_actions.include?(:manage)
|
239
|
-
block_args << (subject.class == Class ? subject : subject.class) if defined_subjects.include?(:all)
|
240
|
-
block_args << (subject.class == Class ? nil : subject)
|
241
|
-
block_args += extra_args
|
242
|
-
defined_block.call(*block_args)
|
243
|
-
elsif defined_conditions
|
244
|
-
if subject.class == Class
|
245
|
-
true
|
246
|
-
else
|
247
|
-
matches_conditions? subject, defined_conditions
|
248
|
-
end
|
249
|
-
else
|
250
|
-
true
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
def matches_conditions?(subject, defined_conditions)
|
255
|
-
defined_conditions.all? do |name, value|
|
256
|
-
attribute = subject.send(name)
|
257
|
-
if value.kind_of?(Hash)
|
258
|
-
matches_conditions? attribute, value
|
259
|
-
elsif value.kind_of?(Array) || value.kind_of?(Range)
|
260
|
-
value.include? attribute
|
261
|
-
else
|
262
|
-
attribute == value
|
263
|
-
end
|
264
|
-
end
|
265
|
-
end
|
266
|
-
|
267
|
-
def includes_action?(actions, action)
|
268
|
-
actions.include?(:manage) || actions.include?(action)
|
269
|
-
end
|
270
|
-
|
271
|
-
def includes_subject?(subjects, subject)
|
272
|
-
subjects.include?(:all) || subjects.include?(subject) || subjects.any? { |c| c.kind_of?(Class) && subject.kind_of?(c) }
|
273
|
-
end
|
274
234
|
end
|
275
235
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module CanCan
|
2
|
-
# This module is automatically included into all Active Record.
|
2
|
+
# This module is automatically included into all Active Record models.
|
3
3
|
module ActiveRecordAdditions
|
4
4
|
module ClassMethods
|
5
5
|
# Returns a scope which fetches only the records that the passed ability
|
@@ -7,28 +7,29 @@ module CanCan
|
|
7
7
|
# is usually called from a controller and passed the +current_ability+.
|
8
8
|
#
|
9
9
|
# @articles = Article.accessible_by(current_ability)
|
10
|
-
#
|
10
|
+
#
|
11
11
|
# Here only the articles which the user is able to read will be returned.
|
12
12
|
# If the user does not have permission to read any articles then an empty
|
13
13
|
# result is returned. Since this is a scope it can be combined with any
|
14
14
|
# other scopes or pagination.
|
15
|
-
#
|
15
|
+
#
|
16
16
|
# An alternative action can optionally be passed as a second argument.
|
17
|
-
#
|
17
|
+
#
|
18
18
|
# @articles = Article.accessible_by(current_ability, :update)
|
19
|
-
#
|
19
|
+
#
|
20
20
|
# Here only the articles which the user can update are returned. This
|
21
21
|
# internally uses Ability#conditions method, see that for more information.
|
22
22
|
def accessible_by(ability, action = :read)
|
23
|
-
conditions = ability.conditions(action, self) || {:id => nil}
|
23
|
+
conditions = ability.conditions(action, self, :tableize => true) || {:id => nil}
|
24
|
+
joins = ability.association_joins(action, self)
|
24
25
|
if respond_to? :where
|
25
|
-
where(conditions)
|
26
|
+
where(conditions).joins(joins)
|
26
27
|
else
|
27
|
-
scoped(:conditions => conditions)
|
28
|
+
scoped(:conditions => conditions, :joins => joins)
|
28
29
|
end
|
29
30
|
end
|
30
31
|
end
|
31
|
-
|
32
|
+
|
32
33
|
def self.included(base)
|
33
34
|
base.extend ClassMethods
|
34
35
|
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module CanCan
|
2
|
+
# This class is used internally and should only be called through Ability.
|
3
|
+
# it holds the information about a "can" call made on Ability and provides
|
4
|
+
# helpful methods to determine permission checking and conditions hash generation.
|
5
|
+
class CanDefinition # :nodoc:
|
6
|
+
include ActiveSupport::Inflector
|
7
|
+
attr_reader :block
|
8
|
+
|
9
|
+
# The first argument when initializing is the base_behavior which is a true/false
|
10
|
+
# value. True for "can" and false for "cannot". The next two arguments are the action
|
11
|
+
# and subject respectively (such as :read, @project). The third argument is a hash
|
12
|
+
# of conditions and the last one is the block passed to the "can" call.
|
13
|
+
def initialize(base_behavior, action, subject, conditions, block)
|
14
|
+
@base_behavior = base_behavior
|
15
|
+
@actions = [action].flatten
|
16
|
+
@subjects = [subject].flatten
|
17
|
+
@conditions = conditions || {}
|
18
|
+
@block = block
|
19
|
+
end
|
20
|
+
|
21
|
+
# Accepts a hash of aliased actions and returns an array of actions which match.
|
22
|
+
# This should be called before "matches?" and other checking methods since they
|
23
|
+
# rely on the actions to be expanded.
|
24
|
+
def expand_actions(aliased_actions)
|
25
|
+
@expanded_actions = @actions.map do |action|
|
26
|
+
aliased_actions[action] ? [action, *aliased_actions[action]] : action
|
27
|
+
end.flatten
|
28
|
+
end
|
29
|
+
|
30
|
+
def matches?(action, subject)
|
31
|
+
matches_action?(action) && matches_subject?(subject)
|
32
|
+
end
|
33
|
+
|
34
|
+
def can?(action, subject, extra_args)
|
35
|
+
result = can_without_base_behavior?(action, subject, extra_args)
|
36
|
+
@base_behavior ? result : !result
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns a hash of conditions. If the ":tableize => true" option is passed
|
40
|
+
# it will pluralize the association conditions to match the table name.
|
41
|
+
def conditions(options = {})
|
42
|
+
if options[:tableize] && @conditions.kind_of?(Hash)
|
43
|
+
@conditions.inject({}) do |tableized_conditions, (name, value)|
|
44
|
+
name = tableize(name).to_sym if value.kind_of? Hash
|
45
|
+
tableized_conditions[name] = value
|
46
|
+
tableized_conditions
|
47
|
+
end
|
48
|
+
else
|
49
|
+
@conditions
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def association_joins(conditions = @conditions)
|
54
|
+
joins = []
|
55
|
+
conditions.each do |name, value|
|
56
|
+
if value.kind_of? Hash
|
57
|
+
nested = association_joins(value)
|
58
|
+
if nested
|
59
|
+
joins << {name => nested}
|
60
|
+
else
|
61
|
+
joins << name
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
joins unless joins.empty?
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def matches_action?(action)
|
71
|
+
@expanded_actions.include?(:manage) || @expanded_actions.include?(action)
|
72
|
+
end
|
73
|
+
|
74
|
+
def matches_subject?(subject)
|
75
|
+
@subjects.include?(:all) || @subjects.include?(subject) || @subjects.any? { |sub| sub.kind_of?(Class) && subject.kind_of?(sub) }
|
76
|
+
end
|
77
|
+
|
78
|
+
def can_without_base_behavior?(action, subject, extra_args)
|
79
|
+
if @block
|
80
|
+
call_block(action, subject, extra_args)
|
81
|
+
elsif @conditions && subject.class != Class
|
82
|
+
matches_conditions? subject
|
83
|
+
else
|
84
|
+
true
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def matches_conditions?(subject, conditions = @conditions)
|
89
|
+
conditions.all? do |name, value|
|
90
|
+
attribute = subject.send(name)
|
91
|
+
if value.kind_of?(Hash)
|
92
|
+
if attribute.kind_of? Array
|
93
|
+
attribute.any? { |element| matches_conditions? element, value }
|
94
|
+
else
|
95
|
+
matches_conditions? attribute, value
|
96
|
+
end
|
97
|
+
elsif value.kind_of?(Array) || value.kind_of?(Range)
|
98
|
+
value.include? attribute
|
99
|
+
else
|
100
|
+
attribute == value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def call_block(action, subject, extra_args)
|
106
|
+
block_args = []
|
107
|
+
block_args << action if @expanded_actions.include?(:manage)
|
108
|
+
block_args << (subject.class == Class ? subject : subject.class) if @subjects.include?(:all)
|
109
|
+
block_args << (subject.class == Class ? nil : subject)
|
110
|
+
block_args += extra_args
|
111
|
+
@block.call(*block_args)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|