grape 1.4.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -3
  3. data/README.md +56 -8
  4. data/UPGRADING.md +43 -4
  5. data/lib/grape/api.rb +2 -2
  6. data/lib/grape/dsl/helpers.rb +1 -0
  7. data/lib/grape/dsl/inside_route.rb +26 -38
  8. data/lib/grape/dsl/routing.rb +2 -4
  9. data/lib/grape/middleware/base.rb +2 -1
  10. data/lib/grape/middleware/error.rb +10 -12
  11. data/lib/grape/middleware/stack.rb +17 -4
  12. data/lib/grape/request.rb +1 -1
  13. data/lib/grape/router.rb +1 -1
  14. data/lib/grape/router/attribute_translator.rb +2 -2
  15. data/lib/grape/util/base_inheritable.rb +2 -2
  16. data/lib/grape/util/lazy_value.rb +1 -0
  17. data/lib/grape/validations/params_scope.rb +2 -1
  18. data/lib/grape/validations/types/custom_type_coercer.rb +13 -1
  19. data/lib/grape/validations/validators/as.rb +1 -1
  20. data/lib/grape/validations/validators/base.rb +2 -4
  21. data/lib/grape/validations/validators/default.rb +3 -4
  22. data/lib/grape/validations/validators/except_values.rb +1 -1
  23. data/lib/grape/validations/validators/values.rb +1 -1
  24. data/lib/grape/version.rb +1 -1
  25. data/spec/grape/api_spec.rb +10 -0
  26. data/spec/grape/dsl/inside_route_spec.rb +7 -0
  27. data/spec/grape/endpoint/declared_spec.rb +590 -0
  28. data/spec/grape/endpoint_spec.rb +0 -534
  29. data/spec/grape/entity_spec.rb +6 -0
  30. data/spec/grape/middleware/error_spec.rb +1 -1
  31. data/spec/grape/middleware/stack_spec.rb +3 -1
  32. data/spec/grape/validations/params_scope_spec.rb +26 -0
  33. data/spec/grape/validations/validators/coerce_spec.rb +24 -0
  34. data/spec/grape/validations/validators/default_spec.rb +49 -0
  35. data/spec/grape/validations/validators/except_values_spec.rb +1 -0
  36. data/spec/spec_helper.rb +0 -10
  37. data/spec/support/chunks.rb +14 -0
  38. data/spec/support/versioned_helpers.rb +3 -5
  39. 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 = 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.block ? builder.use(m.klass, *m.args, &m.block) : builder.use(m.klass, *m.args)
106
+ m.use_in(builder)
94
107
  end
95
108
  builder
96
109
  end
@@ -8,7 +8,7 @@ module Grape
8
8
 
9
9
  alias rack_params params
10
10
 
11
- def initialize(env, options = {})
11
+ def initialize(env, **options)
12
12
  extend options[:build_params_with] || Grape.config.param_builder
13
13
  super(env)
14
14
  end
@@ -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.merge(pattern: pattern, index: @neutral_map.length))
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) # rubocop:disable Style/MethodMissing
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
 
@@ -4,6 +4,7 @@ module Grape
4
4
  module Util
5
5
  class LazyValue
6
6
  attr_reader :access_keys
7
+
7
8
  def initialize(value, access_keys = [])
8
9
  @value = value
9
10
  @access_keys = access_keys
@@ -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
- ->(value) { value.respond_to?(:all?) && value.all? { |item| item.is_a? type[0] } }
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
@@ -3,7 +3,7 @@
3
3
  module Grape
4
4
  module Validations
5
5
  class AsValidator < Base
6
- def initialize(attrs, options, required, scope, opts = {})
6
+ def initialize(attrs, options, required, scope, **opts)
7
7
  @renamed_options = options
8
8
  super
9
9
  end
@@ -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
- if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
25
- validate_param!(attr_name, resource_params)
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 ExceptValuesValidator < Base
6
- def initialize(attrs, options, required, scope, opts = {})
6
+ def initialize(attrs, options, required, scope, **opts)
7
7
  @except = options.is_a?(Hash) ? options[:value] : options
8
8
  super
9
9
  end
@@ -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]
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '1.4.0'
5
+ VERSION = '1.5.0'
6
6
  end
@@ -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