Skip to content

Commit 7e43215

Browse files
authored
Merge pull request #2103 from tlconnor/deepDeclared
Ensure complete declared params structure is present
2 parents 0f3d30e + 678cd13 commit 7e43215

File tree

7 files changed

+694
-582
lines changed

7 files changed

+694
-582
lines changed

CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
### 1.4.1 (Next)
1+
### 1.5.0 (Next)
22

33
#### Features
44

@@ -7,6 +7,7 @@
77
#### Fixes
88

99
* Your contribution here.
10+
* [#2103](https://github.com/ruby-grape/grape/pull/2103): Ensure complete declared params structure is present - [@tlconnor](https://github.com/tlconnor).
1011
* [#2099](https://github.com/ruby-grape/grape/pull/2099): Added truffleruby to Travis-CI - [@gogainda](https://github.com/gogainda).
1112
* [#2089](https://github.com/ruby-grape/grape/pull/2089): Specify order of mounting Grape with Rack::Cascade in README - [@jonmchan](https://github.com/jonmchan).
1213
* [#2083](https://github.com/ruby-grape/grape/pull/2083): Set `Cache-Control` header only for streamed responses - [@stanhu](https://github.com/stanhu).

README.md

+49-6
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ content negotiation, versioning and much more.
156156

157157
## Stable Release
158158

159-
You're reading the documentation for the next release of Grape, which should be **1.4.1**.
159+
You're reading the documentation for the next release of Grape, which should be **1.5.0**.
160160
Please read [UPGRADING](UPGRADING.md) when upgrading from a previous version.
161161
The current stable release is [1.4.0](https://github.com/ruby-grape/grape/blob/v1.4.0/README.md).
162162

@@ -353,7 +353,7 @@ use Rack::Session::Cookie
353353
run Rack::Cascade.new [Web, API]
354354
```
355355

356-
Note that order of loading apps using `Rack::Cascade` matters. The grape application must be last if you want to raise custom 404 errors from grape (such as `error!('Not Found',404)`). If the grape application is not last and returns 404 or 405 response, [cascade utilizes that as a signal to try the next app](https://www.rubydoc.info/gems/rack/Rack/Cascade). This may lead to undesirable behavior showing the [wrong 404 page from the wrong app](https://github.com/ruby-grape/grape/issues/1515).
356+
Note that order of loading apps using `Rack::Cascade` matters. The grape application must be last if you want to raise custom 404 errors from grape (such as `error!('Not Found',404)`). If the grape application is not last and returns 404 or 405 response, [cascade utilizes that as a signal to try the next app](https://www.rubydoc.info/gems/rack/Rack/Cascade). This may lead to undesirable behavior showing the [wrong 404 page from the wrong app](https://github.com/ruby-grape/grape/issues/1515).
357357

358358

359359
### Rails
@@ -787,7 +787,12 @@ Available parameter builders are `Grape::Extensions::Hash::ParamBuilder`, `Grape
787787

788788
### Declared
789789

790-
Grape allows you to access only the parameters that have been declared by your `params` block. It filters out the params that have been passed, but are not allowed. Consider the following API endpoint:
790+
Grape allows you to access only the parameters that have been declared by your `params` block. It will:
791+
792+
* Filter out the params that have been passed, but are not allowed.
793+
* Include any optional params that are declared but not passed.
794+
795+
Consider the following API endpoint:
791796

792797
````ruby
793798
format :json
@@ -820,9 +825,9 @@ Once we add parameters requirements, grape will start returning only the declare
820825
format :json
821826

822827
params do
823-
requires :user, type: Hash do
824-
requires :first_name, type: String
825-
requires :last_name, type: String
828+
optional :user, type: Hash do
829+
optional :first_name, type: String
830+
optional :last_name, type: String
826831
end
827832
end
828833

@@ -850,6 +855,44 @@ curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d
850855
}
851856
````
852857

858+
Missing params that are declared as type `Hash` or `Array` will be included.
859+
860+
````ruby
861+
format :json
862+
863+
params do
864+
optional :user, type: Hash do
865+
optional :first_name, type: String
866+
optional :last_name, type: String
867+
end
868+
optional :widgets, type: Array
869+
end
870+
871+
post 'users/signup' do
872+
{ 'declared_params' => declared(params) }
873+
end
874+
````
875+
876+
**Request**
877+
878+
````bash
879+
curl -X POST -H "Content-Type: application/json" localhost:9292/users/signup -d '{}'
880+
````
881+
882+
**Response**
883+
884+
````json
885+
{
886+
"declared_params": {
887+
"user": {
888+
"first_name": null,
889+
"last_name": null
890+
},
891+
"widgets": []
892+
}
893+
}
894+
````
895+
853896
The returned hash is an `ActiveSupport::HashWithIndifferentAccess`.
854897

855898
The `#declared` method is not available to `before` filters, as those are evaluated prior to parameter coercion.

UPGRADING.md

+43-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,45 @@
11
Upgrading Grape
22
===============
33

4+
### Upgrading to >= 1.5.0
5+
6+
Prior to 1.3.3, the `declared` helper would always return the complete params structure if `include_missing=true` was set. In 1.3.3 a regression was introduced such that a missing Hash with or without nested parameters would always resolve to `{}`.
7+
8+
In 1.5.0 this behavior is reverted, so the whole params structure will always be available via `declared`, regardless of whether any params are passed.
9+
10+
The following rules now apply to the `declared` helper when params are missing and `include_missing=true`:
11+
12+
* Hash params with children will resolve to a Hash with keys for each declared child.
13+
* Hash params with no children will resolve to `{}`.
14+
* Set params will resolve to `Set.new`.
15+
* Array params will resolve to `[]`.
16+
* All other params will resolve to `nil`.
17+
18+
#### Example
19+
20+
```ruby
21+
class Api < Grape::API
22+
params do
23+
optional :outer, type: Hash do
24+
optional :inner, type: Hash do
25+
optional :value, type: String
26+
end
27+
end
28+
end
29+
get 'example' do
30+
declared(params, include_missing: true)
31+
end
32+
end
33+
```
34+
35+
```
36+
get '/example'
37+
# 1.3.3 = {}
38+
# 1.5.0 = {outer: {inner: {value:null}}}
39+
```
40+
41+
For more information see [#2103](https://github.com/ruby-grape/grape/pull/2103).
42+
443
### Upgrading to >= 1.4.0
544

645
#### Reworking stream and file and un-deprecating stream like-objects
@@ -28,17 +67,17 @@ class API < Grape::API
2867
end
2968
```
3069

31-
Or use `stream` to stream other kinds of content. In the following example a streamer class
70+
Or use `stream` to stream other kinds of content. In the following example a streamer class
3271
streams paginated data from a database.
3372

3473
```ruby
35-
class MyObject
74+
class MyObject
3675
attr_accessor :result
3776

3877
def initialize(query)
3978
@result = query
4079
end
41-
80+
4281
def each
4382
yield '['
4483
# Do paginated DB fetches and return each page formatted
@@ -47,7 +86,7 @@ class MyObject
4786
yield process_records(records, first)
4887
first = false
4988
end
50-
yield ']'
89+
yield ']'
5190
end
5291

5392
def process_records(records, first)

lib/grape/dsl/inside_route.rb

+23-36
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def declared_hash(passed_params, options, declared_params, params_nested_path)
5858
passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
5959
memo_key = optioned_param_key(declared_parent_param, options)
6060

61-
memo[memo_key] = handle_passed_param(passed_children_params, params_nested_path_dup) do
61+
memo[memo_key] = handle_passed_param(params_nested_path_dup, passed_children_params.any?) do
6262
declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
6363
end
6464
end
@@ -70,57 +70,44 @@ def declared_hash(passed_params, options, declared_params, params_nested_path)
7070

7171
next unless options[:include_missing] || passed_params.key?(declared_param) || (param_renaming && passed_params.key?(param_renaming))
7272

73-
if param_renaming
74-
memo[optioned_param_key(param_renaming, options)] = passed_params[param_renaming]
75-
else
76-
memo[optioned_param_key(declared_param, options)] = passed_params[declared_param]
73+
memo_key = optioned_param_key(param_renaming || declared_param, options)
74+
passed_param = passed_params[param_renaming || declared_param]
75+
76+
params_nested_path_dup = params_nested_path.dup
77+
params_nested_path_dup << declared_param.to_s
78+
79+
memo[memo_key] = handle_passed_param(params_nested_path_dup) do
80+
passed_param
7781
end
7882
end
7983
end
8084
end
8185

82-
def handle_passed_param(passed_children_params, params_nested_path, &_block)
83-
if should_be_empty_hash?(passed_children_params, params_nested_path)
86+
def handle_passed_param(params_nested_path, has_passed_children = false, &_block)
87+
return yield if has_passed_children
88+
89+
key = params_nested_path[0]
90+
key += '[' + params_nested_path[1..-1].join('][') + ']' if params_nested_path.size > 1
91+
92+
route_options_params = options[:route_options][:params] || {}
93+
type = route_options_params.dig(key, :type)
94+
has_children = route_options_params.keys.any? { |k| k != key && k.start_with?(key) }
95+
96+
if type == 'Hash' && !has_children
8497
{}
85-
elsif should_be_empty_array?(passed_children_params, params_nested_path)
98+
elsif type == 'Array' || type&.start_with?('[')
8699
[]
100+
elsif type == 'Set' || type&.start_with?('#<Set')
101+
Set.new
87102
else
88103
yield
89104
end
90105
end
91106

92-
def should_be_empty_array?(passed_children_params, params_nested_path)
93-
passed_children_params.empty? && declared_param_is_array?(params_nested_path)
94-
end
95-
96-
def declared_param_is_array?(params_nested_path)
97-
key = route_options_params_key(params_nested_path)
98-
route_options_params[key] && route_options_params[key][:type] == 'Array'
99-
end
100-
101-
def should_be_empty_hash?(passed_children_params, params_nested_path)
102-
passed_children_params.empty? && declared_param_is_hash?(params_nested_path)
103-
end
104-
105-
def declared_param_is_hash?(params_nested_path)
106-
key = route_options_params_key(params_nested_path)
107-
route_options_params[key] && route_options_params[key][:type] == 'Hash'
108-
end
109-
110-
def route_options_params
111-
options[:route_options][:params] || {}
112-
end
113-
114107
def optioned_param_key(declared_param, options)
115108
options[:stringify] ? declared_param.to_s : declared_param.to_sym
116109
end
117110

118-
def route_options_params_key(params_nested_path)
119-
key = params_nested_path[0]
120-
key += '[' + params_nested_path[1..-1].join('][') + ']' if params_nested_path.size > 1
121-
key
122-
end
123-
124111
def optioned_declared_params(**options)
125112
declared_params = if options[:include_parent_namespaces]
126113
# Declared params including parent namespaces

lib/grape/version.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33
module Grape
44
# The current version of Grape.
5-
VERSION = '1.4.1'
5+
VERSION = '1.5.0'
66
end

0 commit comments

Comments
 (0)