grape 1.4.0 → 1.5.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 +4 -4
- data/CHANGELOG.md +19 -3
- data/README.md +56 -8
- data/UPGRADING.md +43 -4
- data/lib/grape/api.rb +2 -2
- data/lib/grape/dsl/helpers.rb +1 -0
- data/lib/grape/dsl/inside_route.rb +26 -38
- data/lib/grape/dsl/routing.rb +2 -4
- data/lib/grape/middleware/base.rb +2 -1
- data/lib/grape/middleware/error.rb +10 -12
- data/lib/grape/middleware/stack.rb +17 -4
- data/lib/grape/request.rb +1 -1
- data/lib/grape/router.rb +1 -1
- data/lib/grape/router/attribute_translator.rb +2 -2
- data/lib/grape/util/base_inheritable.rb +2 -2
- data/lib/grape/util/lazy_value.rb +1 -0
- data/lib/grape/validations/params_scope.rb +2 -1
- data/lib/grape/validations/types/custom_type_coercer.rb +13 -1
- data/lib/grape/validations/validators/as.rb +1 -1
- data/lib/grape/validations/validators/base.rb +2 -4
- data/lib/grape/validations/validators/default.rb +3 -4
- data/lib/grape/validations/validators/except_values.rb +1 -1
- data/lib/grape/validations/validators/values.rb +1 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +10 -0
- data/spec/grape/dsl/inside_route_spec.rb +7 -0
- data/spec/grape/endpoint/declared_spec.rb +590 -0
- data/spec/grape/endpoint_spec.rb +0 -534
- data/spec/grape/entity_spec.rb +6 -0
- data/spec/grape/middleware/error_spec.rb +1 -1
- data/spec/grape/middleware/stack_spec.rb +3 -1
- data/spec/grape/validations/params_scope_spec.rb +26 -0
- data/spec/grape/validations/validators/coerce_spec.rb +24 -0
- data/spec/grape/validations/validators/default_spec.rb +49 -0
- data/spec/grape/validations/validators/except_values_spec.rb +1 -0
- data/spec/spec_helper.rb +0 -10
- data/spec/support/chunks.rb +14 -0
- data/spec/support/versioned_helpers.rb +3 -5
- metadata +9 -5
@@ -6,11 +6,12 @@ module Grape
|
|
6
6
|
# It allows to insert and insert after
|
7
7
|
class Stack
|
8
8
|
class Middleware
|
9
|
-
attr_reader :args, :block, :klass
|
9
|
+
attr_reader :args, :opts, :block, :klass
|
10
10
|
|
11
|
-
def initialize(klass, *args, &block)
|
11
|
+
def initialize(klass, *args, **opts, &block)
|
12
12
|
@klass = klass
|
13
|
-
@args
|
13
|
+
@args = args
|
14
|
+
@opts = opts
|
14
15
|
@block = block
|
15
16
|
end
|
16
17
|
|
@@ -30,6 +31,18 @@ module Grape
|
|
30
31
|
def inspect
|
31
32
|
klass.to_s
|
32
33
|
end
|
34
|
+
|
35
|
+
if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('2.7')
|
36
|
+
def use_in(builder)
|
37
|
+
block ? builder.use(klass, *args, **opts, &block) : builder.use(klass, *args, **opts)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
def use_in(builder)
|
41
|
+
args = self.args
|
42
|
+
args += [opts] unless opts.empty?
|
43
|
+
block ? builder.use(klass, *args, &block) : builder.use(klass, *args)
|
44
|
+
end
|
45
|
+
end
|
33
46
|
end
|
34
47
|
|
35
48
|
include Enumerable
|
@@ -90,7 +103,7 @@ module Grape
|
|
90
103
|
def build(builder = Rack::Builder.new)
|
91
104
|
others.shift(others.size).each(&method(:merge_with))
|
92
105
|
middlewares.each do |m|
|
93
|
-
m.
|
106
|
+
m.use_in(builder)
|
94
107
|
end
|
95
108
|
builder
|
96
109
|
end
|
data/lib/grape/request.rb
CHANGED
data/lib/grape/router.rb
CHANGED
@@ -47,7 +47,7 @@ module Grape
|
|
47
47
|
|
48
48
|
def associate_routes(pattern, **options)
|
49
49
|
@neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
|
50
|
-
@neutral_map << Grape::Router::AttributeTranslator.new(options
|
50
|
+
@neutral_map << Grape::Router::AttributeTranslator.new(**options, pattern: pattern, index: @neutral_map.length)
|
51
51
|
end
|
52
52
|
|
53
53
|
def call(env)
|
@@ -23,7 +23,7 @@ module Grape
|
|
23
23
|
|
24
24
|
ROUTER_ATTRIBUTES = %i[pattern index].freeze
|
25
25
|
|
26
|
-
def initialize(attributes
|
26
|
+
def initialize(**attributes)
|
27
27
|
@attributes = attributes
|
28
28
|
end
|
29
29
|
|
@@ -37,7 +37,7 @@ module Grape
|
|
37
37
|
attributes
|
38
38
|
end
|
39
39
|
|
40
|
-
def method_missing(method_name, *args)
|
40
|
+
def method_missing(method_name, *args)
|
41
41
|
if setter?(method_name[-1])
|
42
42
|
attributes[method_name[0..-1]] = *args
|
43
43
|
else
|
@@ -9,8 +9,8 @@ module Grape
|
|
9
9
|
|
10
10
|
# @param inherited_values [Object] An object implementing an interface
|
11
11
|
# of the Hash class.
|
12
|
-
def initialize(inherited_values =
|
13
|
-
@inherited_values = inherited_values
|
12
|
+
def initialize(inherited_values = nil)
|
13
|
+
@inherited_values = inherited_values || {}
|
14
14
|
@new_values = {}
|
15
15
|
end
|
16
16
|
|
@@ -54,11 +54,12 @@ module Grape
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def meets_dependency?(params, request_params)
|
57
|
+
return true unless @dependent_on
|
58
|
+
|
57
59
|
if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
|
58
60
|
return false
|
59
61
|
end
|
60
62
|
|
61
|
-
return true unless @dependent_on
|
62
63
|
return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array)
|
63
64
|
return false unless params.respond_to?(:with_indifferent_access)
|
64
65
|
params = params.with_indifferent_access
|
@@ -103,13 +103,25 @@ module Grape
|
|
103
103
|
# passed, or if the type also implements a parse() method.
|
104
104
|
type
|
105
105
|
elsif type.is_a?(Enumerable)
|
106
|
-
|
106
|
+
lambda do |value|
|
107
|
+
value.is_a?(Enumerable) && value.all? do |val|
|
108
|
+
recursive_type_check(type.first, val)
|
109
|
+
end
|
110
|
+
end
|
107
111
|
else
|
108
112
|
# By default, do a simple type check
|
109
113
|
->(value) { value.is_a? type }
|
110
114
|
end
|
111
115
|
end
|
112
116
|
|
117
|
+
def recursive_type_check(type, value)
|
118
|
+
if type.is_a?(Enumerable) && value.is_a?(Enumerable)
|
119
|
+
value.all? { |val| recursive_type_check(type.first, val) }
|
120
|
+
else
|
121
|
+
!type.is_a?(Enumerable) && value.is_a?(type)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
113
125
|
# Enforce symbolized keys for complex types
|
114
126
|
# by wrapping the coercion method such that
|
115
127
|
# any Hash objects in the immediate heirarchy
|
@@ -13,7 +13,7 @@ module Grape
|
|
13
13
|
# @param required [Boolean] attribute(s) are required or optional
|
14
14
|
# @param scope [ParamsScope] parent scope for this Validator
|
15
15
|
# @param opts [Hash] additional validation options
|
16
|
-
def initialize(attrs, options, required, scope, opts
|
16
|
+
def initialize(attrs, options, required, scope, **opts)
|
17
17
|
@attrs = Array(attrs)
|
18
18
|
@option = options
|
19
19
|
@required = required
|
@@ -47,9 +47,7 @@ module Grape
|
|
47
47
|
next if !@scope.required? && empty_val
|
48
48
|
next unless @scope.meets_dependency?(val, params)
|
49
49
|
begin
|
50
|
-
if @required || val.respond_to?(:key?) && val.key?(attr_name)
|
51
|
-
validate_param!(attr_name, val)
|
52
|
-
end
|
50
|
+
validate_param!(attr_name, val) if @required || val.respond_to?(:key?) && val.key?(attr_name)
|
53
51
|
rescue Grape::Exceptions::Validation => e
|
54
52
|
array_errors << e
|
55
53
|
end
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Grape
|
4
4
|
module Validations
|
5
5
|
class DefaultValidator < Base
|
6
|
-
def initialize(attrs, options, required, scope, opts
|
6
|
+
def initialize(attrs, options, required, scope, **opts)
|
7
7
|
@default = options
|
8
8
|
super
|
9
9
|
end
|
@@ -21,9 +21,8 @@ module Grape
|
|
21
21
|
def validate!(params)
|
22
22
|
attrs = SingleAttributeIterator.new(self, @scope, params)
|
23
23
|
attrs.each do |resource_params, attr_name|
|
24
|
-
|
25
|
-
|
26
|
-
end
|
24
|
+
next unless @scope.meets_dependency?(resource_params, params)
|
25
|
+
validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
|
27
26
|
end
|
28
27
|
end
|
29
28
|
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module Grape
|
4
4
|
module Validations
|
5
5
|
class ValuesValidator < Base
|
6
|
-
def initialize(attrs, options, required, scope, opts
|
6
|
+
def initialize(attrs, options, required, scope, **opts)
|
7
7
|
if options.is_a?(Hash)
|
8
8
|
@excepts = options[:except]
|
9
9
|
@values = options[:value]
|
data/lib/grape/version.rb
CHANGED
data/spec/grape/api_spec.rb
CHANGED
@@ -1149,6 +1149,11 @@ XML
|
|
1149
1149
|
expect(last_response.headers['Content-Type']).to eq('text/plain')
|
1150
1150
|
end
|
1151
1151
|
|
1152
|
+
it 'does not set Cache-Control' do
|
1153
|
+
get '/foo'
|
1154
|
+
expect(last_response.headers['Cache-Control']).to eq(nil)
|
1155
|
+
end
|
1156
|
+
|
1152
1157
|
it 'sets content type for xml' do
|
1153
1158
|
get '/foo.xml'
|
1154
1159
|
expect(last_response.headers['Content-Type']).to eq('application/xml')
|
@@ -1595,6 +1600,11 @@ XML
|
|
1595
1600
|
expect(subject.io).to receive(:write).with(message)
|
1596
1601
|
subject.logger.info 'this will be logged'
|
1597
1602
|
end
|
1603
|
+
|
1604
|
+
it 'does not unnecessarily retain duplicate setup blocks' do
|
1605
|
+
subject.logger
|
1606
|
+
expect { subject.logger }.to_not change(subject.instance_variable_get(:@setup), :size)
|
1607
|
+
end
|
1598
1608
|
end
|
1599
1609
|
|
1600
1610
|
describe '.helpers' do
|
@@ -351,6 +351,12 @@ describe Grape::Endpoint do
|
|
351
351
|
expect(subject.header['Cache-Control']).to eq 'no-cache'
|
352
352
|
end
|
353
353
|
|
354
|
+
it 'does not change Cache-Control header' do
|
355
|
+
subject.stream
|
356
|
+
|
357
|
+
expect(subject.header['Cache-Control']).to eq 'cache'
|
358
|
+
end
|
359
|
+
|
354
360
|
it 'sets Content-Length header to nil' do
|
355
361
|
subject.stream file_path
|
356
362
|
|
@@ -419,6 +425,7 @@ describe Grape::Endpoint do
|
|
419
425
|
|
420
426
|
it 'returns default' do
|
421
427
|
expect(subject.stream).to be nil
|
428
|
+
expect(subject.header['Cache-Control']).to eq nil
|
422
429
|
end
|
423
430
|
end
|
424
431
|
|
@@ -0,0 +1,590 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Grape::Endpoint do
|
6
|
+
subject { Class.new(Grape::API) }
|
7
|
+
|
8
|
+
def app
|
9
|
+
subject
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#declared' do
|
13
|
+
before do
|
14
|
+
subject.format :json
|
15
|
+
subject.params do
|
16
|
+
requires :first
|
17
|
+
optional :second
|
18
|
+
optional :third, default: 'third-default'
|
19
|
+
optional :nested, type: Hash do
|
20
|
+
optional :fourth
|
21
|
+
optional :fifth
|
22
|
+
optional :nested_two, type: Hash do
|
23
|
+
optional :sixth
|
24
|
+
optional :nested_three, type: Hash do
|
25
|
+
optional :seventh
|
26
|
+
end
|
27
|
+
end
|
28
|
+
optional :nested_arr, type: Array do
|
29
|
+
optional :eighth
|
30
|
+
end
|
31
|
+
optional :empty_arr, type: Array
|
32
|
+
optional :empty_typed_arr, type: Array[String]
|
33
|
+
optional :empty_hash, type: Hash
|
34
|
+
optional :empty_set, type: Set
|
35
|
+
optional :empty_typed_set, type: Set[String]
|
36
|
+
end
|
37
|
+
optional :arr, type: Array do
|
38
|
+
optional :nineth
|
39
|
+
end
|
40
|
+
optional :empty_arr, type: Array
|
41
|
+
optional :empty_typed_arr, type: Array[String]
|
42
|
+
optional :empty_hash, type: Hash
|
43
|
+
optional :empty_set, type: Set
|
44
|
+
optional :empty_typed_set, type: Set[String]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'when params are not built with default class' do
|
49
|
+
it 'returns an object that corresponds with the params class - hash with indifferent access' do
|
50
|
+
subject.params do
|
51
|
+
build_with Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
|
52
|
+
end
|
53
|
+
subject.get '/declared' do
|
54
|
+
d = declared(params, include_missing: true)
|
55
|
+
{ declared_class: d.class.to_s }
|
56
|
+
end
|
57
|
+
|
58
|
+
get '/declared?first=present'
|
59
|
+
expect(JSON.parse(last_response.body)['declared_class']).to eq('ActiveSupport::HashWithIndifferentAccess')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'returns an object that corresponds with the params class - hashie mash' do
|
63
|
+
subject.params do
|
64
|
+
build_with Grape::Extensions::Hashie::Mash::ParamBuilder
|
65
|
+
end
|
66
|
+
subject.get '/declared' do
|
67
|
+
d = declared(params, include_missing: true)
|
68
|
+
{ declared_class: d.class.to_s }
|
69
|
+
end
|
70
|
+
|
71
|
+
get '/declared?first=present'
|
72
|
+
expect(JSON.parse(last_response.body)['declared_class']).to eq('Hashie::Mash')
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'returns an object that corresponds with the params class - hash' do
|
76
|
+
subject.params do
|
77
|
+
build_with Grape::Extensions::Hash::ParamBuilder
|
78
|
+
end
|
79
|
+
subject.get '/declared' do
|
80
|
+
d = declared(params, include_missing: true)
|
81
|
+
{ declared_class: d.class.to_s }
|
82
|
+
end
|
83
|
+
|
84
|
+
get '/declared?first=present'
|
85
|
+
expect(JSON.parse(last_response.body)['declared_class']).to eq('Hash')
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should show nil for nested params if include_missing is true' do
|
90
|
+
subject.get '/declared' do
|
91
|
+
declared(params, include_missing: true)
|
92
|
+
end
|
93
|
+
|
94
|
+
get '/declared?first=present'
|
95
|
+
expect(last_response.status).to eq(200)
|
96
|
+
expect(JSON.parse(last_response.body)['nested']['fourth']).to be_nil
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'does not work in a before filter' do
|
100
|
+
subject.before do
|
101
|
+
declared(params)
|
102
|
+
end
|
103
|
+
subject.get('/declared') { declared(params) }
|
104
|
+
|
105
|
+
expect { get('/declared') }.to raise_error(
|
106
|
+
Grape::DSL::InsideRoute::MethodNotYetAvailable
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'has as many keys as there are declared params' do
|
111
|
+
subject.get '/declared' do
|
112
|
+
declared(params)
|
113
|
+
end
|
114
|
+
get '/declared?first=present'
|
115
|
+
expect(last_response.status).to eq(200)
|
116
|
+
expect(JSON.parse(last_response.body).keys.size).to eq(10)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'has a optional param with default value all the time' do
|
120
|
+
subject.get '/declared' do
|
121
|
+
declared(params)
|
122
|
+
end
|
123
|
+
get '/declared?first=one'
|
124
|
+
expect(last_response.status).to eq(200)
|
125
|
+
expect(JSON.parse(last_response.body)['third']).to eql('third-default')
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'builds nested params' do
|
129
|
+
subject.get '/declared' do
|
130
|
+
declared(params)
|
131
|
+
end
|
132
|
+
|
133
|
+
get '/declared?first=present&nested[fourth]=1'
|
134
|
+
expect(last_response.status).to eq(200)
|
135
|
+
expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 9
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'builds arrays correctly' do
|
139
|
+
subject.params do
|
140
|
+
requires :first
|
141
|
+
optional :second, type: Array
|
142
|
+
end
|
143
|
+
subject.post('/declared') { declared(params) }
|
144
|
+
|
145
|
+
post '/declared', first: 'present', second: ['present']
|
146
|
+
expect(last_response.status).to eq(201)
|
147
|
+
|
148
|
+
body = JSON.parse(last_response.body)
|
149
|
+
expect(body['second']).to eq(['present'])
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'builds nested params when given array' do
|
153
|
+
subject.get '/dummy' do
|
154
|
+
end
|
155
|
+
subject.params do
|
156
|
+
requires :first
|
157
|
+
optional :second
|
158
|
+
optional :third, default: 'third-default'
|
159
|
+
optional :nested, type: Array do
|
160
|
+
optional :fourth
|
161
|
+
end
|
162
|
+
end
|
163
|
+
subject.get '/declared' do
|
164
|
+
declared(params)
|
165
|
+
end
|
166
|
+
|
167
|
+
get '/declared?first=present&nested[][fourth]=1&nested[][fourth]=2'
|
168
|
+
expect(last_response.status).to eq(200)
|
169
|
+
expect(JSON.parse(last_response.body)['nested'].size).to eq 2
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'when the param is missing and include_missing=false' do
|
173
|
+
before do
|
174
|
+
subject.get('/declared') { declared(params, include_missing: false) }
|
175
|
+
end
|
176
|
+
|
177
|
+
it 'sets nested objects to be nil' do
|
178
|
+
get '/declared?first=present'
|
179
|
+
expect(last_response.status).to eq(200)
|
180
|
+
expect(JSON.parse(last_response.body)['nested']).to be_nil
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context 'when the param is missing and include_missing=true' do
|
185
|
+
before do
|
186
|
+
subject.get('/declared') { declared(params, include_missing: true) }
|
187
|
+
end
|
188
|
+
|
189
|
+
it 'sets objects with type=Hash to be a hash' do
|
190
|
+
get '/declared?first=present'
|
191
|
+
expect(last_response.status).to eq(200)
|
192
|
+
|
193
|
+
body = JSON.parse(last_response.body)
|
194
|
+
expect(body['empty_hash']).to eq({})
|
195
|
+
expect(body['nested']).to be_a(Hash)
|
196
|
+
expect(body['nested']['empty_hash']).to eq({})
|
197
|
+
expect(body['nested']['nested_two']).to be_a(Hash)
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'sets objects with type=Set to be a set' do
|
201
|
+
get '/declared?first=present'
|
202
|
+
expect(last_response.status).to eq(200)
|
203
|
+
|
204
|
+
body = JSON.parse(last_response.body)
|
205
|
+
expect(['#<Set: {}>', []]).to include(body['empty_set'])
|
206
|
+
expect(['#<Set: {}>', []]).to include(body['empty_typed_set'])
|
207
|
+
expect(['#<Set: {}>', []]).to include(body['nested']['empty_set'])
|
208
|
+
expect(['#<Set: {}>', []]).to include(body['nested']['empty_typed_set'])
|
209
|
+
end
|
210
|
+
|
211
|
+
it 'sets objects with type=Array to be an array' do
|
212
|
+
get '/declared?first=present'
|
213
|
+
expect(last_response.status).to eq(200)
|
214
|
+
|
215
|
+
body = JSON.parse(last_response.body)
|
216
|
+
expect(body['empty_arr']).to eq([])
|
217
|
+
expect(body['empty_typed_arr']).to eq([])
|
218
|
+
expect(body['arr']).to eq([])
|
219
|
+
expect(body['nested']['empty_arr']).to eq([])
|
220
|
+
expect(body['nested']['empty_typed_arr']).to eq([])
|
221
|
+
expect(body['nested']['nested_arr']).to eq([])
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'includes all declared children when type=Hash' do
|
225
|
+
get '/declared?first=present'
|
226
|
+
expect(last_response.status).to eq(200)
|
227
|
+
|
228
|
+
body = JSON.parse(last_response.body)
|
229
|
+
expect(body['nested'].keys).to eq(%w[fourth fifth nested_two nested_arr empty_arr empty_typed_arr empty_hash empty_set empty_typed_set])
|
230
|
+
expect(body['nested']['nested_two'].keys).to eq(%w[sixth nested_three])
|
231
|
+
expect(body['nested']['nested_two']['nested_three'].keys).to eq(%w[seventh])
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'filters out any additional params that are given' do
|
236
|
+
subject.get '/declared' do
|
237
|
+
declared(params)
|
238
|
+
end
|
239
|
+
get '/declared?first=one&other=two'
|
240
|
+
expect(last_response.status).to eq(200)
|
241
|
+
expect(JSON.parse(last_response.body).key?(:other)).to eq false
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'stringifies if that option is passed' do
|
245
|
+
subject.get '/declared' do
|
246
|
+
declared(params, stringify: true)
|
247
|
+
end
|
248
|
+
|
249
|
+
get '/declared?first=one&other=two'
|
250
|
+
expect(last_response.status).to eq(200)
|
251
|
+
expect(JSON.parse(last_response.body)['first']).to eq 'one'
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'does not include missing attributes if that option is passed' do
|
255
|
+
subject.get '/declared' do
|
256
|
+
error! 'expected nil', 400 if declared(params, include_missing: false).key?(:second)
|
257
|
+
''
|
258
|
+
end
|
259
|
+
|
260
|
+
get '/declared?first=one&other=two'
|
261
|
+
expect(last_response.status).to eq(200)
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'does not include renamed missing attributes if that option is passed' do
|
265
|
+
subject.params do
|
266
|
+
optional :renamed_original, as: :renamed
|
267
|
+
end
|
268
|
+
subject.get '/declared' do
|
269
|
+
error! 'expected nil', 400 if declared(params, include_missing: false).key?(:renamed)
|
270
|
+
''
|
271
|
+
end
|
272
|
+
|
273
|
+
get '/declared?first=one&other=two'
|
274
|
+
expect(last_response.status).to eq(200)
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'includes attributes with value that evaluates to false' do
|
278
|
+
subject.params do
|
279
|
+
requires :first
|
280
|
+
optional :boolean
|
281
|
+
end
|
282
|
+
|
283
|
+
subject.post '/declared' do
|
284
|
+
error!('expected false', 400) if declared(params, include_missing: false)[:boolean] != false
|
285
|
+
''
|
286
|
+
end
|
287
|
+
|
288
|
+
post '/declared', ::Grape::Json.dump(first: 'one', boolean: false), 'CONTENT_TYPE' => 'application/json'
|
289
|
+
expect(last_response.status).to eq(201)
|
290
|
+
end
|
291
|
+
|
292
|
+
it 'includes attributes with value that evaluates to nil' do
|
293
|
+
subject.params do
|
294
|
+
requires :first
|
295
|
+
optional :second
|
296
|
+
end
|
297
|
+
|
298
|
+
subject.post '/declared' do
|
299
|
+
error!('expected nil', 400) unless declared(params, include_missing: false)[:second].nil?
|
300
|
+
''
|
301
|
+
end
|
302
|
+
|
303
|
+
post '/declared', ::Grape::Json.dump(first: 'one', second: nil), 'CONTENT_TYPE' => 'application/json'
|
304
|
+
expect(last_response.status).to eq(201)
|
305
|
+
end
|
306
|
+
|
307
|
+
it 'includes missing attributes with defaults when there are nested hashes' do
|
308
|
+
subject.get '/dummy' do
|
309
|
+
end
|
310
|
+
|
311
|
+
subject.params do
|
312
|
+
requires :first
|
313
|
+
optional :second
|
314
|
+
optional :third, default: nil
|
315
|
+
optional :nested, type: Hash do
|
316
|
+
optional :fourth, default: nil
|
317
|
+
optional :fifth, default: nil
|
318
|
+
requires :nested_nested, type: Hash do
|
319
|
+
optional :sixth, default: 'sixth-default'
|
320
|
+
optional :seven, default: nil
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
subject.get '/declared' do
|
326
|
+
declared(params, include_missing: false)
|
327
|
+
end
|
328
|
+
|
329
|
+
get '/declared?first=present&nested[fourth]=&nested[nested_nested][sixth]=sixth'
|
330
|
+
json = JSON.parse(last_response.body)
|
331
|
+
expect(last_response.status).to eq(200)
|
332
|
+
expect(json['first']).to eq 'present'
|
333
|
+
expect(json['nested'].keys).to eq %w[fourth fifth nested_nested]
|
334
|
+
expect(json['nested']['fourth']).to eq ''
|
335
|
+
expect(json['nested']['nested_nested'].keys).to eq %w[sixth seven]
|
336
|
+
expect(json['nested']['nested_nested']['sixth']).to eq 'sixth'
|
337
|
+
end
|
338
|
+
|
339
|
+
it 'does not include missing attributes when there are nested hashes' do
|
340
|
+
subject.get '/dummy' do
|
341
|
+
end
|
342
|
+
|
343
|
+
subject.params do
|
344
|
+
requires :first
|
345
|
+
optional :second
|
346
|
+
optional :third
|
347
|
+
optional :nested, type: Hash do
|
348
|
+
optional :fourth
|
349
|
+
optional :fifth
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
subject.get '/declared' do
|
354
|
+
declared(params, include_missing: false)
|
355
|
+
end
|
356
|
+
|
357
|
+
get '/declared?first=present&nested[fourth]=4'
|
358
|
+
json = JSON.parse(last_response.body)
|
359
|
+
expect(last_response.status).to eq(200)
|
360
|
+
expect(json['first']).to eq 'present'
|
361
|
+
expect(json['nested'].keys).to eq %w[fourth]
|
362
|
+
expect(json['nested']['fourth']).to eq '4'
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
describe '#declared; call from child namespace' do
|
367
|
+
before do
|
368
|
+
subject.format :json
|
369
|
+
subject.namespace :parent do
|
370
|
+
params do
|
371
|
+
requires :parent_name, type: String
|
372
|
+
end
|
373
|
+
|
374
|
+
namespace ':parent_name' do
|
375
|
+
params do
|
376
|
+
requires :child_name, type: String
|
377
|
+
requires :child_age, type: Integer
|
378
|
+
end
|
379
|
+
|
380
|
+
namespace ':child_name' do
|
381
|
+
params do
|
382
|
+
requires :grandchild_name, type: String
|
383
|
+
end
|
384
|
+
|
385
|
+
get ':grandchild_name' do
|
386
|
+
{
|
387
|
+
'params' => params,
|
388
|
+
'without_parent_namespaces' => declared(params, include_parent_namespaces: false),
|
389
|
+
'with_parent_namespaces' => declared(params, include_parent_namespaces: true)
|
390
|
+
}
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
get '/parent/foo/bar/baz', child_age: 5, extra: 'hello'
|
397
|
+
end
|
398
|
+
|
399
|
+
let(:parsed_response) { JSON.parse(last_response.body, symbolize_names: true) }
|
400
|
+
|
401
|
+
it { expect(last_response.status).to eq 200 }
|
402
|
+
|
403
|
+
context 'with include_parent_namespaces: false' do
|
404
|
+
it 'returns declared parameters only from current namespace' do
|
405
|
+
expect(parsed_response[:without_parent_namespaces]).to eq(
|
406
|
+
grandchild_name: 'baz'
|
407
|
+
)
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|
411
|
+
context 'with include_parent_namespaces: true' do
|
412
|
+
it 'returns declared parameters from every parent namespace' do
|
413
|
+
expect(parsed_response[:with_parent_namespaces]).to eq(
|
414
|
+
parent_name: 'foo',
|
415
|
+
child_name: 'bar',
|
416
|
+
grandchild_name: 'baz',
|
417
|
+
child_age: 5
|
418
|
+
)
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
context 'without declaration' do
|
423
|
+
it 'returns all requested parameters' do
|
424
|
+
expect(parsed_response[:params]).to eq(
|
425
|
+
parent_name: 'foo',
|
426
|
+
child_name: 'bar',
|
427
|
+
grandchild_name: 'baz',
|
428
|
+
child_age: 5,
|
429
|
+
extra: 'hello'
|
430
|
+
)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
describe '#declared; from a nested mounted endpoint' do
|
436
|
+
before do
|
437
|
+
doubly_mounted = Class.new(Grape::API)
|
438
|
+
doubly_mounted.namespace :more do
|
439
|
+
params do
|
440
|
+
requires :y, type: Integer
|
441
|
+
end
|
442
|
+
route_param :y do
|
443
|
+
get do
|
444
|
+
{
|
445
|
+
params: params,
|
446
|
+
declared_params: declared(params)
|
447
|
+
}
|
448
|
+
end
|
449
|
+
end
|
450
|
+
end
|
451
|
+
|
452
|
+
mounted = Class.new(Grape::API)
|
453
|
+
mounted.namespace :another do
|
454
|
+
params do
|
455
|
+
requires :mount_space, type: Integer
|
456
|
+
end
|
457
|
+
route_param :mount_space do
|
458
|
+
mount doubly_mounted
|
459
|
+
end
|
460
|
+
end
|
461
|
+
|
462
|
+
subject.format :json
|
463
|
+
subject.namespace :something do
|
464
|
+
params do
|
465
|
+
requires :id, type: Integer
|
466
|
+
end
|
467
|
+
resource ':id' do
|
468
|
+
mount mounted
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
it 'can access parent attributes' do
|
474
|
+
get '/something/123/another/456/more/789'
|
475
|
+
expect(last_response.status).to eq 200
|
476
|
+
json = JSON.parse(last_response.body, symbolize_names: true)
|
477
|
+
|
478
|
+
# test all three levels of params
|
479
|
+
expect(json[:declared_params][:y]).to eq 789
|
480
|
+
expect(json[:declared_params][:mount_space]).to eq 456
|
481
|
+
expect(json[:declared_params][:id]).to eq 123
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
describe '#declared; mixed nesting' do
|
486
|
+
before do
|
487
|
+
subject.format :json
|
488
|
+
subject.resource :users do
|
489
|
+
route_param :id, type: Integer, desc: 'ID desc' do
|
490
|
+
# Adding this causes route_setting(:declared_params) to be nil for the
|
491
|
+
# get block in namespace 'foo' below
|
492
|
+
get do
|
493
|
+
end
|
494
|
+
|
495
|
+
namespace 'foo' do
|
496
|
+
get do
|
497
|
+
{
|
498
|
+
params: params,
|
499
|
+
declared_params: declared(params),
|
500
|
+
declared_params_no_parent: declared(params, include_parent_namespaces: false)
|
501
|
+
}
|
502
|
+
end
|
503
|
+
end
|
504
|
+
end
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
it 'can access parent route_param' do
|
509
|
+
get '/users/123/foo', bar: 'bar'
|
510
|
+
expect(last_response.status).to eq 200
|
511
|
+
json = JSON.parse(last_response.body, symbolize_names: true)
|
512
|
+
|
513
|
+
expect(json[:declared_params][:id]).to eq 123
|
514
|
+
expect(json[:declared_params_no_parent][:id]).to eq nil
|
515
|
+
end
|
516
|
+
end
|
517
|
+
|
518
|
+
describe '#declared; with multiple route_param' do
|
519
|
+
before do
|
520
|
+
mounted = Class.new(Grape::API)
|
521
|
+
mounted.namespace :albums do
|
522
|
+
get do
|
523
|
+
declared(params)
|
524
|
+
end
|
525
|
+
end
|
526
|
+
|
527
|
+
subject.format :json
|
528
|
+
subject.namespace :artists do
|
529
|
+
route_param :id, type: Integer do
|
530
|
+
get do
|
531
|
+
declared(params)
|
532
|
+
end
|
533
|
+
|
534
|
+
params do
|
535
|
+
requires :filter, type: String
|
536
|
+
end
|
537
|
+
get :some_route do
|
538
|
+
declared(params)
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
route_param :artist_id, type: Integer do
|
543
|
+
namespace :compositions do
|
544
|
+
get do
|
545
|
+
declared(params)
|
546
|
+
end
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
route_param :compositor_id, type: Integer do
|
551
|
+
mount mounted
|
552
|
+
end
|
553
|
+
end
|
554
|
+
end
|
555
|
+
|
556
|
+
it 'return only :id without :artist_id' do
|
557
|
+
get '/artists/1'
|
558
|
+
json = JSON.parse(last_response.body, symbolize_names: true)
|
559
|
+
|
560
|
+
expect(json.key?(:id)).to be_truthy
|
561
|
+
expect(json.key?(:artist_id)).not_to be_truthy
|
562
|
+
end
|
563
|
+
|
564
|
+
it 'return only :artist_id without :id' do
|
565
|
+
get '/artists/1/compositions'
|
566
|
+
json = JSON.parse(last_response.body, symbolize_names: true)
|
567
|
+
|
568
|
+
expect(json.key?(:artist_id)).to be_truthy
|
569
|
+
expect(json.key?(:id)).not_to be_truthy
|
570
|
+
end
|
571
|
+
|
572
|
+
it 'return :filter and :id parameters in declared for second enpoint inside route_param' do
|
573
|
+
get '/artists/1/some_route', filter: 'some_filter'
|
574
|
+
json = JSON.parse(last_response.body, symbolize_names: true)
|
575
|
+
|
576
|
+
expect(json.key?(:filter)).to be_truthy
|
577
|
+
expect(json.key?(:id)).to be_truthy
|
578
|
+
expect(json.key?(:artist_id)).not_to be_truthy
|
579
|
+
end
|
580
|
+
|
581
|
+
it 'return :compositor_id for mounter in route_param' do
|
582
|
+
get '/artists/1/albums'
|
583
|
+
json = JSON.parse(last_response.body, symbolize_names: true)
|
584
|
+
|
585
|
+
expect(json.key?(:compositor_id)).to be_truthy
|
586
|
+
expect(json.key?(:id)).not_to be_truthy
|
587
|
+
expect(json.key?(:artist_id)).not_to be_truthy
|
588
|
+
end
|
589
|
+
end
|
590
|
+
end
|