Skip to content

Commit 5b4a2dd

Browse files
authored
Corrected the endpoint parameter name generation. (#2189)
* Corrected the endpoint parameter name generation. Signed-off-by: Hermann Mayer <[email protected]> * Reworked the parameter renaming to be a declared-only feature. Signed-off-by: Hermann Mayer <[email protected]> * Corrected already existing, but now broken specs. Signed-off-by: Hermann Mayer <[email protected]> * PR fixes and docs. Signed-off-by: Hermann Mayer <[email protected]> * Version bump and rephrasing. Signed-off-by: Hermann Mayer <[email protected]>
1 parent 0ff93fb commit 5b4a2dd

File tree

10 files changed

+369
-54
lines changed

10 files changed

+369
-54
lines changed

.rubocop_todo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ Metrics/BlockLength:
167167
# Offense count: 11
168168
# Configuration parameters: CountComments, CountAsOne.
169169
Metrics/ClassLength:
170-
Max: 304
170+
Max: 310
171171

172172
# Offense count: 30
173173
# Configuration parameters: IgnoredMethods.

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
### 1.5.4 (Next)
1+
### 1.6.0 (Next)
22

33
#### Features
44

@@ -9,7 +9,9 @@
99
* [#2176](https://github.com/ruby-grape/grape/pull/2176): Fix: OPTIONS fails if matching all routes - [@myxoh](https://github.com/myxoh).
1010
* [#2177](https://github.com/ruby-grape/grape/pull/2177): Fix: `default` validator fails if preceded by `as` validator - [@Catsuko](https://github.com/Catsuko).
1111
* [#2180](https://github.com/ruby-grape/grape/pull/2180): Call `super` in `API.inherited` - [@yogeshjain999](https://github.com/yogeshjain999).
12+
* [#2189](https://github.com/ruby-grape/grape/pull/2189): Fix: rename parameters when using `:as` (behaviour and grape-swagger documentation) - [@Jack12816](https://github.com/Jack12816).
1213
* Your contribution here.
14+
1315
### 1.5.3 (2021/03/07)
1416

1517
#### Fixes

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,7 @@ Grape allows you to access only the parameters that have been declared by your `
801801

802802
* Filter out the params that have been passed, but are not allowed.
803803
* Include any optional params that are declared but not passed.
804+
* Perform any parameter renaming on the resulting hash.
804805

805806
Consider the following API endpoint:
806807

@@ -995,8 +996,10 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d
995996
````json
996997
{
997998
"declared_params": {
998-
"first_name": "first name",
999-
"last_name": null
999+
"user": {
1000+
"first_name": "first name",
1001+
"last_name": null
1002+
}
10001003
}
10011004
}
10021005
````

UPGRADING.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
11
Upgrading Grape
22
===============
33

4+
### Upgrading to >= 1.6.0
5+
6+
#### Parameter renaming with :as
7+
8+
Prior to 1.6.0 the [parameter renaming](https://github.com/ruby-grape/grape#renaming) with `:as` was directly touching the request payload ([`#params`](https://github.com/ruby-grape/grape#parameters)) while duplicating the old and the new key to be both available in the hash. This allowed clients to bypass any validation in case they knew the internal name of the parameter. Unfortunately, in combination with [grape-swagger](https://github.com/ruby-grape/grape-swagger) the internal name (name set with `:as`) of the parameters were documented.
9+
10+
This behavior was fixed. Parameter renaming is now done when using the [`#declared(params)`](https://github.com/ruby-grape/grape#declared) parameters helper. This stops confusing validation/coercion behavior.
11+
12+
Here comes an illustration of the old and new behaviour as code:
13+
14+
```ruby
15+
# (1) Rename a to b, while client sends +a+
16+
optional :a, type: Integer, as: :b
17+
params = { a: 1 }
18+
declared(params, include_missing: false)
19+
# expected => { b: 1 }
20+
# actual => { b: 1 }
21+
22+
# (2) Rename a to b, while client sends +b+
23+
optional :a, type: Integer, as: :b, values: [1, 2, 3]
24+
params = { b: '5' }
25+
declared(params, include_missing: false)
26+
# expected => { } (>= 1.6.0)
27+
# actual => { b: '5' } (uncasted, unvalidated, <= 1.5.3)
28+
```
29+
30+
Another implication of this change is the dependent parameter resolution. Prior to 1.6.0 the following code produced an `Grape::Exceptions::UnknownParameter` because `:a` was replace by `:b`:
31+
32+
```ruby
33+
params do
34+
optional :a, as: :b
35+
given :a do # (<= 1.5.3 you had to reference +:b+ here to make it work)
36+
requires :c
37+
end
38+
end
39+
```
40+
41+
This code now works without any errors, as the renaming is just an internal behaviour of the `#declared(params)` parameter helper.
42+
43+
See [#2189](https://github.com/ruby-grape/grape/pull/2189) for more information.
444

545
### Upgrading to >= 1.5.3
646

7-
### Nil value and coercion
47+
#### Nil value and coercion
848

949
Prior to 1.2.5 version passing a `nil` value for a parameter with a custom coercer would invoke the coercer, and not passing a parameter would not invoke it.
1050
This behavior was not tested or documented. Version 1.3.0 quietly changed this behavior, in such that `nil` values skipped the coercion. Version 1.5.3 fixes and documents this as follows:

lib/grape/dsl/inside_route.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,20 @@ def declared_array(passed_params, options, declared_params, params_nested_path)
4848
end
4949

5050
def declared_hash(passed_params, options, declared_params, params_nested_path)
51+
renamed_params = route_setting(:renamed_params) || {}
52+
5153
declared_params.each_with_object(passed_params.class.new) do |declared_param, memo|
5254
if declared_param.is_a?(Hash)
5355
declared_param.each_pair do |declared_parent_param, declared_children_params|
5456
params_nested_path_dup = params_nested_path.dup
5557
params_nested_path_dup << declared_parent_param.to_s
5658
next unless options[:include_missing] || passed_params.key?(declared_parent_param)
5759

60+
rename_path = params_nested_path + [declared_parent_param.to_s]
61+
renamed_param_name = renamed_params[rename_path]
62+
63+
memo_key = optioned_param_key(renamed_param_name || declared_parent_param, options)
5864
passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
59-
memo_key = optioned_param_key(declared_parent_param, options)
6065

6166
memo[memo_key] = handle_passed_param(params_nested_path_dup, passed_children_params.any?) do
6267
declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
@@ -65,13 +70,13 @@ def declared_hash(passed_params, options, declared_params, params_nested_path)
6570
else
6671
# If it is not a Hash then it does not have children.
6772
# Find its value or set it to nil.
68-
has_renaming = route_setting(:renamed_params) && route_setting(:renamed_params).find { |current| current[declared_param] }
69-
param_renaming = has_renaming[declared_param] if has_renaming
73+
next unless options[:include_missing] || passed_params.key?(declared_param)
7074

71-
next unless options[:include_missing] || passed_params.key?(declared_param) || (param_renaming && passed_params.key?(param_renaming))
75+
rename_path = params_nested_path + [declared_param.to_s]
76+
renamed_param_name = renamed_params[rename_path]
7277

73-
memo_key = optioned_param_key(param_renaming || declared_param, options)
74-
passed_param = passed_params[param_renaming || declared_param]
78+
memo_key = optioned_param_key(renamed_param_name || declared_param, options)
79+
passed_param = passed_params[declared_param]
7580

7681
params_nested_path_dup = params_nested_path.dup
7782
params_nested_path_dup << declared_param.to_s

lib/grape/endpoint.rb

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,6 @@ def run
262262
else
263263
run_filters before_validations, :before_validation
264264
run_validators validations, request
265-
remove_renamed_params
266265
run_filters after_validations, :after_validation
267266
response_object = execute
268267
end
@@ -328,14 +327,7 @@ def build_helpers
328327
Module.new { helpers.each { |mod_to_include| include mod_to_include } }
329328
end
330329

331-
def remove_renamed_params
332-
return unless route_setting(:renamed_params)
333-
route_setting(:renamed_params).flat_map(&:keys).each do |renamed_param|
334-
@params.delete(renamed_param)
335-
end
336-
end
337-
338-
private :build_stack, :build_helpers, :remove_renamed_params
330+
private :build_stack, :build_helpers
339331

340332
def execute
341333
@block ? @block.call(self) : nil

lib/grape/validations/params_scope.rb

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ class ParamsScope
1313
# @param opts [Hash] options for this scope
1414
# @option opts :element [Symbol] the element that contains this scope; for
1515
# this to be relevant, @parent must be set
16+
# @option opts :element_renamed [Symbol, nil] whenever this scope should
17+
# be renamed and to what, given +nil+ no renaming is done
1618
# @option opts :parent [ParamsScope] the scope containing this scope
1719
# @option opts :api [API] the API endpoint to modify
1820
# @option opts :optional [Boolean] whether or not this scope needs to have
@@ -23,13 +25,14 @@ class ParamsScope
2325
# validate if this param is present in the parent scope
2426
# @yield the instance context, open for parameter definitions
2527
def initialize(opts, &block)
26-
@element = opts[:element]
27-
@parent = opts[:parent]
28-
@api = opts[:api]
29-
@optional = opts[:optional] || false
30-
@type = opts[:type]
31-
@group = opts[:group] || {}
32-
@dependent_on = opts[:dependent_on]
28+
@element = opts[:element]
29+
@element_renamed = opts[:element_renamed]
30+
@parent = opts[:parent]
31+
@api = opts[:api]
32+
@optional = opts[:optional] || false
33+
@type = opts[:type]
34+
@group = opts[:group] || {}
35+
@dependent_on = opts[:dependent_on]
3336
@declared_params = []
3437
@index = nil
3538

@@ -129,18 +132,35 @@ def push_declared_params(attrs, **opts)
129132
if lateral?
130133
@parent.push_declared_params(attrs, **opts)
131134
else
132-
if opts && opts[:as]
133-
@api.route_setting(:renamed_params, @api.route_setting(:renamed_params) || [])
134-
@api.route_setting(:renamed_params) << { attrs.first => opts[:as] }
135-
attrs = [opts[:as]]
136-
end
135+
push_renamed_param(full_path + [attrs.first], opts[:as]) \
136+
if opts && opts[:as]
137137

138138
@declared_params.concat attrs
139139
end
140140
end
141141

142+
# Get the full path of the parameter scope in the hierarchy.
143+
#
144+
# @return [Array<Symbol>] the nesting/path of the current parameter scope
145+
def full_path
146+
nested? ? @parent.full_path + [@element] : []
147+
end
148+
142149
private
143150

151+
# Add a new parameter which should be renamed when using the +#declared+
152+
# method.
153+
#
154+
# @param path [Array<String, Symbol>] the full path of the parameter
155+
# (including the parameter name as last array element)
156+
# @param new_name [String, Symbol] the new name of the parameter (the
157+
# renamed name, with the +as: ...+ semantic)
158+
def push_renamed_param(path, new_name)
159+
base = @api.route_setting(:renamed_params) || {}
160+
base[Array(path).map(&:to_s)] = new_name.to_s
161+
@api.route_setting(:renamed_params, base)
162+
end
163+
144164
def require_required_and_optional_fields(context, opts)
145165
if context == :all
146166
optional_fields = Array(opts[:except])
@@ -191,11 +211,12 @@ def new_scope(attrs, optional = false, &block)
191211
end
192212

193213
self.class.new(
194-
api: @api,
195-
element: attrs[1][:as] || attrs.first,
196-
parent: self,
197-
optional: optional,
198-
type: type || Array,
214+
api: @api,
215+
element: attrs.first,
216+
element_renamed: attrs[1][:as],
217+
parent: self,
218+
optional: optional,
219+
type: type || Array,
199220
&block
200221
)
201222
end
@@ -235,6 +256,8 @@ def new_group_scope(attrs, &block)
235256

236257
# Pushes declared params to parent or settings
237258
def configure_declared_params
259+
push_renamed_param(full_path, @element_renamed) if @element_renamed
260+
238261
if nested?
239262
@parent.push_declared_params [element => @declared_params]
240263
else
@@ -303,10 +326,6 @@ def validates(attrs, validations)
303326
next if order_specific_validations.include?(type)
304327
validate(type, options, attrs, doc_attrs, opts)
305328
end
306-
307-
# Apply as validator last so other validations are applied to
308-
# renamed param
309-
validate(:as, validations[:as], attrs, doc_attrs, opts) if validations.key?(:as)
310329
end
311330

312331
# Validate and comprehend the +:type+, +:types+, and +:coerce_with+

lib/grape/validations/validators/as.rb

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,10 @@
33
module Grape
44
module Validations
55
class AsValidator < Base
6-
def initialize(attrs, options, required, scope, **opts)
7-
@renamed_options = options
8-
super
9-
end
10-
11-
def validate_param!(attr_name, params)
12-
params[@renamed_options] = params[attr_name]
13-
end
6+
# We use a validator for renaming parameters. This is just a marker for
7+
# the parameter scope to handle the renaming. No actual validation
8+
# happens here.
9+
def validate_param!(*); end
1410
end
1511
end
1612
end

0 commit comments

Comments
 (0)