grape 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of grape might be problematic. Click here for more details.

Files changed (79) hide show
  1. checksums.yaml +9 -9
  2. data/.gitignore +7 -0
  3. data/.rubocop.yml +5 -0
  4. data/.travis.yml +3 -3
  5. data/CHANGELOG.md +42 -3
  6. data/CONTRIBUTING.md +118 -0
  7. data/Gemfile +4 -4
  8. data/README.md +312 -52
  9. data/Rakefile +6 -1
  10. data/UPGRADING.md +124 -0
  11. data/lib/grape.rb +2 -0
  12. data/lib/grape/api.rb +95 -44
  13. data/lib/grape/cookies.rb +0 -2
  14. data/lib/grape/endpoint.rb +63 -39
  15. data/lib/grape/error_formatter/base.rb +0 -3
  16. data/lib/grape/error_formatter/json.rb +0 -2
  17. data/lib/grape/error_formatter/txt.rb +0 -2
  18. data/lib/grape/error_formatter/xml.rb +0 -2
  19. data/lib/grape/exceptions/base.rb +0 -2
  20. data/lib/grape/exceptions/incompatible_option_values.rb +0 -3
  21. data/lib/grape/exceptions/invalid_formatter.rb +0 -3
  22. data/lib/grape/exceptions/invalid_versioner_option.rb +0 -4
  23. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +0 -2
  24. data/lib/grape/exceptions/missing_mime_type.rb +0 -4
  25. data/lib/grape/exceptions/missing_option.rb +0 -3
  26. data/lib/grape/exceptions/missing_vendor_option.rb +0 -3
  27. data/lib/grape/exceptions/unknown_options.rb +0 -4
  28. data/lib/grape/exceptions/unknown_validator.rb +0 -2
  29. data/lib/grape/exceptions/validation_errors.rb +6 -5
  30. data/lib/grape/formatter/base.rb +0 -3
  31. data/lib/grape/formatter/json.rb +0 -2
  32. data/lib/grape/formatter/serializable_hash.rb +15 -16
  33. data/lib/grape/formatter/txt.rb +0 -2
  34. data/lib/grape/formatter/xml.rb +0 -2
  35. data/lib/grape/http/request.rb +2 -4
  36. data/lib/grape/locale/en.yml +1 -1
  37. data/lib/grape/middleware/auth/oauth2.rb +15 -6
  38. data/lib/grape/middleware/base.rb +7 -7
  39. data/lib/grape/middleware/error.rb +11 -6
  40. data/lib/grape/middleware/formatter.rb +80 -78
  41. data/lib/grape/middleware/globals.rb +13 -0
  42. data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
  43. data/lib/grape/middleware/versioner/header.rb +5 -3
  44. data/lib/grape/middleware/versioner/param.rb +2 -4
  45. data/lib/grape/middleware/versioner/path.rb +3 -4
  46. data/lib/grape/namespace.rb +0 -1
  47. data/lib/grape/parser/base.rb +0 -3
  48. data/lib/grape/parser/json.rb +0 -2
  49. data/lib/grape/parser/xml.rb +0 -2
  50. data/lib/grape/path.rb +1 -3
  51. data/lib/grape/route.rb +0 -3
  52. data/lib/grape/util/hash_stack.rb +1 -1
  53. data/lib/grape/validations.rb +72 -22
  54. data/lib/grape/validations/coerce.rb +5 -4
  55. data/lib/grape/validations/default.rb +5 -3
  56. data/lib/grape/validations/presence.rb +1 -1
  57. data/lib/grape/validations/regexp.rb +0 -2
  58. data/lib/grape/validations/values.rb +2 -1
  59. data/lib/grape/version.rb +1 -1
  60. data/spec/grape/api_spec.rb +385 -96
  61. data/spec/grape/endpoint_spec.rb +162 -15
  62. data/spec/grape/entity_spec.rb +25 -0
  63. data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
  64. data/spec/grape/middleware/auth/oauth2_spec.rb +60 -15
  65. data/spec/grape/middleware/base_spec.rb +3 -8
  66. data/spec/grape/middleware/error_spec.rb +2 -2
  67. data/spec/grape/middleware/exception_spec.rb +4 -4
  68. data/spec/grape/middleware/formatter_spec.rb +7 -4
  69. data/spec/grape/middleware/versioner/param_spec.rb +8 -7
  70. data/spec/grape/path_spec.rb +24 -14
  71. data/spec/grape/util/hash_stack_spec.rb +8 -8
  72. data/spec/grape/validations/coerce_spec.rb +75 -33
  73. data/spec/grape/validations/default_spec.rb +57 -0
  74. data/spec/grape/validations/presence_spec.rb +13 -11
  75. data/spec/grape/validations/values_spec.rb +76 -2
  76. data/spec/grape/validations_spec.rb +443 -20
  77. data/spec/spec_helper.rb +2 -2
  78. data/spec/support/content_type_helpers.rb +11 -0
  79. metadata +9 -38
@@ -32,6 +32,38 @@ describe Grape::Validations::DefaultValidator do
32
32
  get '/message' do
33
33
  { id: params[:id], type1: params[:type1], type2: params[:type2] }
34
34
  end
35
+
36
+ params do
37
+ optional :random, default: -> { Random.rand }
38
+ optional :not_random, default: Random.rand
39
+ end
40
+ get '/numbers' do
41
+ { random_number: params[:random], non_random_number: params[:non_random_number] }
42
+ end
43
+
44
+ params do
45
+ # NOTE: The :foo parameter could be made required with json body
46
+ # params, and then an empty hash would be valid. With query parameters
47
+ # it must be optional if it isn't provided at all, as otherwise
48
+ # the validaton for the Hash itself fails because there is no such
49
+ # thing as an empty hash.
50
+ optional :foo, type: Hash do
51
+ optional :bar, default: 'foo-bar'
52
+ end
53
+ end
54
+ get '/group' do
55
+ { foo_bar: params[:foo][:bar] }
56
+ end
57
+
58
+ params do
59
+ optional :array, type: Array do
60
+ requires :name
61
+ optional :with_default, default: 'default'
62
+ end
63
+ end
64
+ get '/array' do
65
+ { array: params[:array] }
66
+ end
35
67
  end
36
68
  end
37
69
  end
@@ -63,4 +95,29 @@ describe Grape::Validations::DefaultValidator do
63
95
  last_response.status.should == 200
64
96
  last_response.body.should == { id: '1', type1: 'default-type1', type2: 'default-type2' }.to_json
65
97
  end
98
+
99
+ it 'sets lambda based defaults at the time of call' do
100
+ get("/numbers")
101
+ last_response.status.should == 200
102
+ before = JSON.parse(last_response.body)
103
+ get("/numbers")
104
+ last_response.status.should == 200
105
+ after = JSON.parse(last_response.body)
106
+
107
+ before['non_random_number'].should == after['non_random_number']
108
+ before['random_number'].should_not == after['random_number']
109
+ end
110
+
111
+ it 'set default values for optional grouped params' do
112
+ get('/group')
113
+ last_response.status.should == 200
114
+ last_response.body.should == { foo_bar: 'foo-bar' }.to_json
115
+ end
116
+
117
+ it 'sets default values for grouped arrays' do
118
+ get('/array?array[][name]=name&array[][name]=name2&array[][with_default]=bar2')
119
+ last_response.status.should == 200
120
+ last_response.body.should == { array: [{ name: "name", with_default: "default" }, { name: "name2", with_default: "bar2" }] }.to_json
121
+ end
122
+
66
123
  end
@@ -28,8 +28,9 @@ describe Grape::Validations::PresenceValidator do
28
28
  end
29
29
 
30
30
  params do
31
- group :user do
32
- requires :first_name, :last_name
31
+ requires :user, type: Hash do
32
+ requires :first_name
33
+ requires :last_name
33
34
  end
34
35
  end
35
36
  get '/nested' do
@@ -37,11 +38,12 @@ describe Grape::Validations::PresenceValidator do
37
38
  end
38
39
 
39
40
  params do
40
- group :admin do
41
+ requires :admin, type: Hash do
41
42
  requires :admin_name
42
- group :super do
43
- group :user do
44
- requires :first_name, :last_name
43
+ requires :super, type: Hash do
44
+ requires :user, type: Hash do
45
+ requires :first_name
46
+ requires :last_name
45
47
  end
46
48
  end
47
49
  end
@@ -96,7 +98,7 @@ describe Grape::Validations::PresenceValidator do
96
98
  it 'validates nested parameters' do
97
99
  get '/nested'
98
100
  last_response.status.should == 400
99
- last_response.body.should == '{"error":"user[first_name] is missing"}'
101
+ last_response.body.should == '{"error":"user is missing, user[first_name] is missing, user[last_name] is missing"}'
100
102
 
101
103
  get '/nested', user: { first_name: "Billy" }
102
104
  last_response.status.should == 400
@@ -110,19 +112,19 @@ describe Grape::Validations::PresenceValidator do
110
112
  it 'validates triple nested parameters' do
111
113
  get '/nested_triple'
112
114
  last_response.status.should == 400
113
- last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
115
+ last_response.body.should include '{"error":"admin is missing'
114
116
 
115
117
  get '/nested_triple', user: { first_name: "Billy" }
116
118
  last_response.status.should == 400
117
- last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
119
+ last_response.body.should include '{"error":"admin is missing'
118
120
 
119
121
  get '/nested_triple', admin: { super: { first_name: "Billy" } }
120
122
  last_response.status.should == 400
121
- last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
123
+ last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user] is missing, admin[super][user][first_name] is missing, admin[super][user][last_name] is missing"}'
122
124
 
123
125
  get '/nested_triple', super: { user: { first_name: "Billy", last_name: "Bob" } }
124
126
  last_response.status.should == 400
125
- last_response.body.should == '{"error":"admin[admin_name] is missing, admin[super][user][first_name] is missing"}'
127
+ last_response.body.should include '{"error":"admin is missing'
126
128
 
127
129
  get '/nested_triple', admin: { super: { user: { first_name: "Billy" } } }
128
130
  last_response.status.should == 400
@@ -2,25 +2,53 @@ require 'spec_helper'
2
2
 
3
3
  describe Grape::Validations::ValuesValidator do
4
4
 
5
+ class ValuesModel
6
+ DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3']
7
+ class << self
8
+ def values
9
+ @values ||= []
10
+ [DEFAULT_VALUES + @values].flatten.uniq
11
+ end
12
+
13
+ def add_value(value)
14
+ @values ||= []
15
+ @values << value
16
+ end
17
+ end
18
+ end
19
+
5
20
  module ValidationsSpec
6
21
  module ValuesValidatorSpec
7
22
  class API < Grape::API
8
23
  default_format :json
9
24
 
10
25
  params do
11
- requires :type, values: ['valid-type1', 'valid-type2', 'valid-type3']
26
+ requires :type, values: ValuesModel.values
12
27
  end
13
28
  get '/' do
14
29
  { type: params[:type] }
15
30
  end
16
31
 
17
32
  params do
18
- optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'valid-type2'
33
+ optional :type, values: ValuesModel.values, default: 'valid-type2'
19
34
  end
20
35
  get '/default/valid' do
21
36
  { type: params[:type] }
22
37
  end
23
38
 
39
+ params do
40
+ optional :type, values: -> { ValuesModel.values }, default: 'valid-type2'
41
+ end
42
+ get '/lambda' do
43
+ { type: params[:type] }
44
+ end
45
+
46
+ params do
47
+ requires :type, type: Integer, desc: "An integer", values: [10, 11], default: 10
48
+ end
49
+ get '/values/coercion' do
50
+ { type: params[:type] }
51
+ end
24
52
  end
25
53
  end
26
54
  end
@@ -41,12 +69,46 @@ describe Grape::Validations::ValuesValidator do
41
69
  last_response.body.should eq({ error: "type does not have a valid value" }.to_json)
42
70
  end
43
71
 
72
+ it 'does not allow nil value for a parameter' do
73
+ get("/", type: nil)
74
+ last_response.status.should eq 400
75
+ last_response.body.should eq({ error: "type does not have a valid value" }.to_json)
76
+ end
77
+
44
78
  it 'allows a valid default value' do
45
79
  get("/default/valid")
46
80
  last_response.status.should eq 200
47
81
  last_response.body.should eq({ type: "valid-type2" }.to_json)
48
82
  end
49
83
 
84
+ it 'allows a proc for values' do
85
+ get('/lambda', type: 'valid-type1')
86
+ last_response.status.should eq 200
87
+ last_response.body.should eq({ type: "valid-type1" }.to_json)
88
+ end
89
+
90
+ it 'does not validate updated values without proc' do
91
+ ValuesModel.add_value('valid-type4')
92
+
93
+ get('/', type: 'valid-type4')
94
+ last_response.status.should eq 400
95
+ last_response.body.should eq({ error: "type does not have a valid value" }.to_json)
96
+ end
97
+
98
+ it 'validates against values in a proc' do
99
+ ValuesModel.add_value('valid-type4')
100
+
101
+ get('/lambda', type: 'valid-type4')
102
+ last_response.status.should eq 200
103
+ last_response.body.should eq({ type: "valid-type4" }.to_json)
104
+ end
105
+
106
+ it 'does not allow an invalid value for a parameter using lambda' do
107
+ get("/lambda", type: 'invalid-type')
108
+ last_response.status.should eq 400
109
+ last_response.body.should eq({ error: "type does not have a valid value" }.to_json)
110
+ end
111
+
50
112
  it 'raises IncompatibleOptionValues on an invalid default value' do
51
113
  subject = Class.new(Grape::API)
52
114
  expect {
@@ -61,4 +123,16 @@ describe Grape::Validations::ValuesValidator do
61
123
  }.to raise_error Grape::Exceptions::IncompatibleOptionValues
62
124
  end
63
125
 
126
+ it 'allows values to be a kind of the coerced type not just an instance of it' do
127
+ get("/values/coercion", type: 10)
128
+ last_response.status.should eq 200
129
+ last_response.body.should eq({ type: 10 }.to_json)
130
+ end
131
+
132
+ it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do
133
+ subject = Class.new(Grape::API)
134
+ expect {
135
+ subject.params { requires :type, values: [10.5, 11], type: Integer }
136
+ }.to raise_error Grape::Exceptions::IncompatibleOptionValues
137
+ end
64
138
  end
@@ -78,13 +78,137 @@ describe Grape::Validations do
78
78
  end
79
79
  end
80
80
 
81
- context 'required with a block' do
81
+ context 'requires :all using Grape::Entity documentation' do
82
+ def define_requires_all
83
+ documentation = {
84
+ required_field: { type: String },
85
+ optional_field: { type: String }
86
+ }
87
+ subject.params do
88
+ requires :all, except: :optional_field, using: documentation
89
+ end
90
+ end
82
91
  before do
92
+ define_requires_all
93
+ subject.get '/required' do
94
+ 'required works'
95
+ end
96
+ end
97
+
98
+ it 'adds entity documentation to declared params' do
99
+ define_requires_all
100
+ subject.settings[:declared_params].should == [:required_field, :optional_field]
101
+ end
102
+
103
+ it 'errors when required_field is not present' do
104
+ get '/required'
105
+ last_response.status.should == 400
106
+ last_response.body.should == 'required_field is missing'
107
+ end
108
+
109
+ it 'works when required_field is present' do
110
+ get '/required', required_field: 'woof'
111
+ last_response.status.should == 200
112
+ last_response.body.should == 'required works'
113
+ end
114
+ end
115
+
116
+ context 'requires :none using Grape::Entity documentation' do
117
+ def define_requires_none
118
+ documentation = {
119
+ required_field: { type: String },
120
+ optional_field: { type: String }
121
+ }
122
+ subject.params do
123
+ requires :none, except: :required_field, using: documentation
124
+ end
125
+ end
126
+ before do
127
+ define_requires_none
128
+ subject.get '/required' do
129
+ 'required works'
130
+ end
131
+ end
132
+
133
+ it 'adds entity documentation to declared params' do
134
+ define_requires_none
135
+ subject.settings[:declared_params].should == [:required_field, :optional_field]
136
+ end
137
+
138
+ it 'errors when required_field is not present' do
139
+ get '/required'
140
+ last_response.status.should == 400
141
+ last_response.body.should == 'required_field is missing'
142
+ end
143
+
144
+ it 'works when required_field is present' do
145
+ get '/required', required_field: 'woof'
146
+ last_response.status.should == 200
147
+ last_response.body.should == 'required works'
148
+ end
149
+ end
150
+
151
+ context 'required with an Array block' do
152
+ before do
153
+ subject.params do
154
+ requires :items, type: Array do
155
+ requires :key
156
+ end
157
+ end
158
+ subject.get '/required' do
159
+ 'required works'
160
+ end
161
+ end
162
+
163
+ it 'errors when param not present' do
164
+ get '/required'
165
+ last_response.status.should == 400
166
+ last_response.body.should == 'items is missing'
167
+ end
168
+
169
+ it "errors when param is not an Array" do
170
+ get '/required', items: "hello"
171
+ last_response.status.should == 400
172
+ last_response.body.should == 'items is invalid, items[key] is missing'
173
+
174
+ get '/required', items: { key: 'foo' }
175
+ last_response.status.should == 400
176
+ last_response.body.should == 'items is invalid'
177
+ end
178
+
179
+ it "doesn't throw a missing param when param is present" do
180
+ get '/required', items: [{ key: 'hello' }, { key: 'world' }]
181
+ last_response.status.should == 200
182
+ last_response.body.should == 'required works'
183
+ end
184
+
185
+ it "doesn't allow any key in the options hash other than type" do
186
+ expect {
187
+ subject.params do
188
+ requires(:items, desc: 'Foo') do
189
+ requires :key
190
+ end
191
+ end
192
+ }.to raise_error ArgumentError
193
+ end
194
+
195
+ it 'adds to declared parameters' do
83
196
  subject.params do
84
197
  requires :items do
85
198
  requires :key
86
199
  end
87
200
  end
201
+ subject.settings[:declared_params].should == [items: [:key]]
202
+ end
203
+ end
204
+
205
+ context 'required with a Hash block' do
206
+ before do
207
+ subject.params do
208
+ requires :items, type: Hash do
209
+ requires :key
210
+ end
211
+ end
88
212
  subject.get '/required' do
89
213
  'required works'
90
214
  end
@@ -93,16 +217,26 @@ describe Grape::Validations do
93
217
  it 'errors when param not present' do
94
218
  get '/required'
95
219
  last_response.status.should == 400
96
- last_response.body.should == 'items[key] is missing'
220
+ last_response.body.should == 'items is missing, items[key] is missing'
221
+ end
222
+
223
+ it "errors when param is not a Hash" do
224
+ get '/required', items: "hello"
225
+ last_response.status.should == 400
226
+ last_response.body.should == 'items is invalid, items[key] is missing'
227
+
228
+ get '/required', items: [{ key: 'foo' }]
229
+ last_response.status.should == 400
230
+ last_response.body.should == 'items is invalid'
97
231
  end
98
232
 
99
233
  it "doesn't throw a missing param when param is present" do
100
- get '/required', items: [key: 'hello', key: 'world']
234
+ get '/required', items: { key: 'hello' }
101
235
  last_response.status.should == 200
102
236
  last_response.body.should == 'required works'
103
237
  end
104
238
 
105
- it "doesn't allow more than one parameter" do
239
+ it "doesn't allow any key in the options hash other than type" do
106
240
  expect {
107
241
  subject.params do
108
242
  requires(:items, desc: 'Foo') do
@@ -137,7 +271,7 @@ describe Grape::Validations do
137
271
  it 'errors when param not present' do
138
272
  get '/required'
139
273
  last_response.status.should == 400
140
- last_response.body.should == 'items[key] is missing'
274
+ last_response.body.should == 'items is missing'
141
275
  end
142
276
 
143
277
  it "doesn't throw a missing param when param is present" do
@@ -156,10 +290,206 @@ describe Grape::Validations do
156
290
  end
157
291
  end
158
292
 
159
- context 'optional with a block' do
293
+ context 'validation within arrays' do
160
294
  before do
161
295
  subject.params do
162
- optional :items do
296
+ group :children do
297
+ requires :name
298
+ group :parents do
299
+ requires :name
300
+ end
301
+ end
302
+ end
303
+ subject.get '/within_array' do
304
+ 'within array works'
305
+ end
306
+ end
307
+
308
+ it 'can handle new scopes within child elements' do
309
+ get '/within_array', children: [
310
+ { name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] },
311
+ { name: 'Joe', parents: [{ name: 'Josie' }] }
312
+ ]
313
+ last_response.status.should == 200
314
+ last_response.body.should == 'within array works'
315
+ end
316
+
317
+ it 'errors when a parameter is not present' do
318
+ get '/within_array', children: [
319
+ { name: 'Jim', parents: [{}] },
320
+ { name: 'Job', parents: [{ name: 'Joy' }] }
321
+ ]
322
+ # NOTE: with body parameters in json or XML or similar this
323
+ # should actually fail with: children[parents][name] is missing.
324
+ last_response.status.should == 400
325
+ last_response.body.should == 'children[parents] is missing'
326
+ end
327
+
328
+ it 'safely handles empty arrays and blank parameters' do
329
+ # NOTE: with body parameters in json or XML or similar this
330
+ # should actually return 200, since an empty array is valid.
331
+ get '/within_array', children: []
332
+ last_response.status.should == 400
333
+ last_response.body.should == 'children is missing'
334
+ get '/within_array', children: [name: 'Jay']
335
+ last_response.status.should == 400
336
+ last_response.body.should == 'children[parents] is missing'
337
+ end
338
+
339
+ it "errors when param is not an Array" do
340
+ # NOTE: would be nicer if these just returned 'children is invalid'
341
+ get '/within_array', children: "hello"
342
+ last_response.status.should == 400
343
+ last_response.body.should == 'children is invalid, children[name] is missing, children[parents] is missing, children[parents] is invalid, children[parents][name] is missing'
344
+
345
+ get '/within_array', children: { name: 'foo' }
346
+ last_response.status.should == 400
347
+ last_response.body.should == 'children is invalid, children[parents] is missing'
348
+
349
+ get '/within_array', children: [name: 'Jay', parents: { name: 'Fred' }]
350
+ last_response.status.should == 400
351
+ last_response.body.should == 'children[parents] is invalid'
352
+ end
353
+ end
354
+
355
+ context 'with block param' do
356
+ before do
357
+ subject.params do
358
+ requires :planets do
359
+ requires :name
360
+ end
361
+ end
362
+ subject.get '/req' do
363
+ 'within array works'
364
+ end
365
+ subject.put '/req' do
366
+ ''
367
+ end
368
+
369
+ subject.params do
370
+ group :stars do
371
+ requires :name
372
+ end
373
+ end
374
+ subject.get '/grp' do
375
+ 'within array works'
376
+ end
377
+ subject.put '/grp' do
378
+ ''
379
+ end
380
+
381
+ subject.params do
382
+ requires :name
383
+ optional :moons do
384
+ requires :name
385
+ end
386
+ end
387
+ subject.get '/opt' do
388
+ 'within array works'
389
+ end
390
+ subject.put '/opt' do
391
+ ''
392
+ end
393
+ end
394
+
395
+ it 'requires defaults to Array type' do
396
+ get '/req', planets: "Jupiter, Saturn"
397
+ last_response.status.should == 400
398
+ last_response.body.should == 'planets is invalid, planets[name] is missing'
399
+
400
+ get '/req', planets: { name: 'Jupiter' }
401
+ last_response.status.should == 400
402
+ last_response.body.should == 'planets is invalid'
403
+
404
+ get '/req', planets: [{ name: 'Venus' }, { name: 'Mars' }]
405
+ last_response.status.should == 200
406
+
407
+ put_with_json '/req', planets: []
408
+ last_response.status.should == 200
409
+ end
410
+
411
+ it 'optional defaults to Array type' do
412
+ get '/opt', name: "Jupiter", moons: "Europa, Ganymede"
413
+ last_response.status.should == 400
414
+ last_response.body.should == 'moons is invalid, moons[name] is missing'
415
+
416
+ get '/opt', name: "Jupiter", moons: { name: 'Ganymede' }
417
+ last_response.status.should == 400
418
+ last_response.body.should == 'moons is invalid'
419
+
420
+ get '/opt', name: "Jupiter", moons: [{ name: 'Io' }, { name: 'Callisto' }]
421
+ last_response.status.should == 200
422
+
423
+ put_with_json '/opt', name: "Venus"
424
+ last_response.status.should == 200
425
+
426
+ put_with_json '/opt', name: "Mercury", moons: []
427
+ last_response.status.should == 200
428
+ end
429
+
430
+ it 'group defaults to Array type' do
431
+ get '/grp', stars: "Sun"
432
+ last_response.status.should == 400
433
+ last_response.body.should == 'stars is invalid, stars[name] is missing'
434
+
435
+ get '/grp', stars: { name: 'Sun' }
436
+ last_response.status.should == 400
437
+ last_response.body.should == 'stars is invalid'
438
+
439
+ get '/grp', stars: [{ name: 'Sun' }]
440
+ last_response.status.should == 200
441
+
442
+ put_with_json '/grp', stars: []
443
+ last_response.status.should == 200
444
+ end
445
+ end
446
+
447
+ context 'validation within arrays with JSON' do
448
+ before do
449
+ subject.params do
450
+ group :children do
451
+ requires :name
452
+ group :parents do
453
+ requires :name
454
+ end
455
+ end
456
+ end
457
+ subject.put '/within_array' do
458
+ 'within array works'
459
+ end
460
+ end
461
+
462
+ it 'can handle new scopes within child elements' do
463
+ put_with_json '/within_array', children: [
464
+ { name: 'John', parents: [{ name: 'Jane' }, { name: 'Bob' }] },
465
+ { name: 'Joe', parents: [{ name: 'Josie' }] }
466
+ ]
467
+ last_response.status.should == 200
468
+ last_response.body.should == 'within array works'
469
+ end
470
+
471
+ it 'errors when a parameter is not present' do
472
+ put_with_json '/within_array', children: [
473
+ { name: 'Jim', parents: [{}] },
474
+ { name: 'Job', parents: [{ name: 'Joy' }] }
475
+ ]
476
+ last_response.status.should == 400
477
+ last_response.body.should == 'children[parents][name] is missing'
478
+ end
479
+
480
+ it 'safely handles empty arrays and blank parameters' do
481
+ put_with_json '/within_array', children: []
482
+ last_response.status.should == 200
483
+ put_with_json '/within_array', children: [name: 'Jay']
484
+ last_response.status.should == 400
485
+ last_response.body.should == 'children[parents] is missing'
486
+ end
487
+ end
488
+
489
+ context 'optional with an Array block' do
490
+ before do
491
+ subject.params do
492
+ optional :items, type: Array do
163
493
  requires :key
164
494
  end
165
495
  end
@@ -175,17 +505,27 @@ describe Grape::Validations do
175
505
  end
176
506
 
177
507
  it "doesn't throw a missing param when both group and param are given" do
178
- get '/optional_group', items: { key: 'foo' }
508
+ get '/optional_group', items: [{ key: 'foo' }]
179
509
  last_response.status.should == 200
180
510
  last_response.body.should == 'optional group works'
181
511
  end
182
512
 
183
513
  it "errors when group is present, but required param is not" do
184
- get '/optional_group', items: { not_key: 'foo' }
514
+ get '/optional_group', items: [{ not_key: 'foo' }]
185
515
  last_response.status.should == 400
186
516
  last_response.body.should == 'items[key] is missing'
187
517
  end
188
518
 
519
+ it "errors when param is present but isn't an Array" do
520
+ get '/optional_group', items: "hello"
521
+ last_response.status.should == 400
522
+ last_response.body.should == 'items is invalid, items[key] is missing'
523
+
524
+ get '/optional_group', items: { key: 'foo' }
525
+ last_response.status.should == 400
526
+ last_response.body.should == 'items is invalid'
527
+ end
528
+
189
529
  it 'adds to declared parameters' do
190
530
  subject.params do
191
531
  optional :items do
@@ -196,13 +536,13 @@ describe Grape::Validations do
196
536
  end
197
537
  end
198
538
 
199
- context 'nested optional blocks' do
539
+ context 'nested optional Array blocks' do
200
540
  before do
201
541
  subject.params do
202
- optional :items do
542
+ optional :items, type: Array do
203
543
  requires :key
204
- optional(:optional_subitems) { requires :value }
205
- requires(:required_subitems) { requires :value }
544
+ optional(:optional_subitems, type: Array) { requires :value }
545
+ requires(:required_subitems, type: Array) { requires :value }
206
546
  end
207
547
  end
208
548
  subject.get('/nested_optional_group') { 'nested optional group works' }
@@ -215,25 +555,39 @@ describe Grape::Validations do
215
555
  end
216
556
 
217
557
  it 'does internal validations if the outer group is present' do
218
- get '/nested_optional_group', items: { key: 'foo' }
558
+ get '/nested_optional_group', items: [{ key: 'foo' }]
219
559
  last_response.status.should == 400
220
- last_response.body.should == 'items[required_subitems][value] is missing'
560
+ last_response.body.should == 'items[required_subitems] is missing'
221
561
 
222
- get '/nested_optional_group', items: { key: 'foo', required_subitems: { value: 'bar' } }
562
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
223
563
  last_response.status.should == 200
224
564
  last_response.body.should == 'nested optional group works'
225
565
  end
226
566
 
227
567
  it 'handles deep nesting' do
228
- get '/nested_optional_group', items: { key: 'foo', required_subitems: { value: 'bar' }, optional_subitems: { not_value: 'baz' } }
568
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }]
229
569
  last_response.status.should == 400
230
570
  last_response.body.should == 'items[optional_subitems][value] is missing'
231
571
 
232
- get '/nested_optional_group', items: { key: 'foo', required_subitems: { value: 'bar' }, optional_subitems: { value: 'baz' } }
572
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ value: 'baz' }] }]
233
573
  last_response.status.should == 200
234
574
  last_response.body.should == 'nested optional group works'
235
575
  end
236
576
 
577
+ it 'handles validation within arrays' do
578
+ get '/nested_optional_group', items: [{ key: 'foo' }]
579
+ last_response.status.should == 400
580
+ last_response.body.should == 'items[required_subitems] is missing'
581
+
582
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
583
+ last_response.status.should == 200
584
+ last_response.body.should == 'nested optional group works'
585
+
586
+ get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }], optional_subitems: [{ not_value: 'baz' }] }]
587
+ last_response.status.should == 400
588
+ last_response.body.should == 'items[optional_subitems][value] is missing'
589
+ end
590
+
237
591
  it 'adds to declared parameters' do
238
592
  subject.params do
239
593
  optional :items do
@@ -287,11 +641,11 @@ describe Grape::Validations do
287
641
  end
288
642
 
289
643
  it 'validates when param is present' do
290
- get '/optional_custom', { custom: 'im custom' }
644
+ get '/optional_custom', custom: 'im custom'
291
645
  last_response.status.should == 200
292
646
  last_response.body.should == 'optional with custom works!'
293
647
 
294
- get '/optional_custom', { custom: 'im wrong' }
648
+ get '/optional_custom', custom: 'im wrong'
295
649
  last_response.status.should == 400
296
650
  last_response.body.should == 'custom is not custom!'
297
651
  end
@@ -414,5 +768,74 @@ describe Grape::Validations do
414
768
  end
415
769
  end
416
770
  end # end custom validation
771
+
772
+ context 'named' do
773
+ context 'can be defined' do
774
+ it 'in helpers' do
775
+ subject.helpers do
776
+ params :pagination do
777
+ end
778
+ end
779
+ end
780
+
781
+ it 'in helper module which kind of Grape::API::Helpers' do
782
+ module SharedParams
783
+ extend Grape::API::Helpers
784
+ params :pagination do
785
+ end
786
+ end
787
+ subject.helpers SharedParams
788
+ end
789
+ end
790
+
791
+ context 'can be included in usual params' do
792
+ before do
793
+ module SharedParams
794
+ extend Grape::API::Helpers
795
+ params :period do
796
+ optional :start_date
797
+ optional :end_date
798
+ end
799
+ end
800
+ subject.helpers SharedParams
801
+
802
+ subject.helpers do
803
+ params :pagination do
804
+ optional :page, type: Integer
805
+ optional :per_page, type: Integer
806
+ end
807
+ end
808
+ end
809
+
810
+ it 'by #use' do
811
+ subject.params do
812
+ use :pagination
813
+ end
814
+ subject.settings[:declared_params].should eq [:page, :per_page]
815
+ end
816
+
817
+ it 'by #use with multiple params' do
818
+ subject.params do
819
+ use :pagination, :period
820
+ end
821
+ subject.settings[:declared_params].should eq [:page, :per_page, :start_date, :end_date]
822
+ end
823
+
824
+ end
825
+ end
826
+
827
+ context 'documentation' do
828
+ it 'can be included with a hash' do
829
+ documentation = { example: 'Joe' }
830
+
831
+ subject.params do
832
+ requires 'first_name', documentation: documentation
833
+ end
834
+ subject.get '/' do
835
+ end
836
+
837
+ subject.routes.first.route_params['first_name'][:documentation].should eq(documentation)
838
+ end
839
+ end
417
840
  end
418
841
  end