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.
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