grape 0.9.0 → 0.10.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 (144) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -66
  3. data/.rubocop_todo.yml +78 -17
  4. data/.travis.yml +7 -3
  5. data/Appraisals +7 -0
  6. data/CHANGELOG.md +24 -0
  7. data/CONTRIBUTING.md +7 -0
  8. data/Gemfile +1 -7
  9. data/Guardfile +1 -1
  10. data/README.md +560 -94
  11. data/RELEASING.md +1 -1
  12. data/Rakefile +10 -11
  13. data/UPGRADING.md +211 -3
  14. data/gemfiles/rails_3.gemfile +14 -0
  15. data/gemfiles/rails_4.gemfile +14 -0
  16. data/grape.gemspec +10 -9
  17. data/lib/backports/active_support/deep_dup.rb +49 -0
  18. data/lib/backports/active_support/duplicable.rb +88 -0
  19. data/lib/grape.rb +29 -2
  20. data/lib/grape/api.rb +59 -65
  21. data/lib/grape/dsl/api.rb +19 -0
  22. data/lib/grape/dsl/callbacks.rb +6 -4
  23. data/lib/grape/dsl/configuration.rb +49 -5
  24. data/lib/grape/dsl/helpers.rb +7 -8
  25. data/lib/grape/dsl/inside_route.rb +22 -10
  26. data/lib/grape/dsl/middleware.rb +5 -5
  27. data/lib/grape/dsl/parameters.rb +6 -2
  28. data/lib/grape/dsl/request_response.rb +23 -20
  29. data/lib/grape/dsl/routing.rb +52 -49
  30. data/lib/grape/dsl/settings.rb +110 -0
  31. data/lib/grape/dsl/validations.rb +14 -6
  32. data/lib/grape/endpoint.rb +104 -88
  33. data/lib/grape/exceptions/base.rb +2 -2
  34. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  35. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  36. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  37. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +1 -1
  38. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  39. data/lib/grape/exceptions/missing_option.rb +1 -1
  40. data/lib/grape/exceptions/missing_vendor_option.rb +1 -1
  41. data/lib/grape/exceptions/unknown_options.rb +1 -1
  42. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  43. data/lib/grape/exceptions/validation.rb +1 -1
  44. data/lib/grape/exceptions/validation_errors.rb +2 -2
  45. data/lib/grape/formatter/serializable_hash.rb +1 -1
  46. data/lib/grape/formatter/xml.rb +1 -1
  47. data/lib/grape/locale/en.yml +2 -0
  48. data/lib/grape/middleware/auth/dsl.rb +26 -21
  49. data/lib/grape/middleware/auth/strategies.rb +1 -1
  50. data/lib/grape/middleware/auth/strategy_info.rb +0 -2
  51. data/lib/grape/middleware/base.rb +2 -2
  52. data/lib/grape/middleware/error.rb +1 -1
  53. data/lib/grape/middleware/formatter.rb +5 -5
  54. data/lib/grape/middleware/versioner.rb +1 -1
  55. data/lib/grape/middleware/versioner/header.rb +3 -3
  56. data/lib/grape/middleware/versioner/param.rb +2 -2
  57. data/lib/grape/middleware/versioner/path.rb +1 -1
  58. data/lib/grape/namespace.rb +1 -1
  59. data/lib/grape/path.rb +9 -3
  60. data/lib/grape/util/content_types.rb +16 -8
  61. data/lib/grape/util/inheritable_setting.rb +74 -0
  62. data/lib/grape/util/inheritable_values.rb +51 -0
  63. data/lib/grape/util/stackable_values.rb +52 -0
  64. data/lib/grape/util/strict_hash_configuration.rb +106 -0
  65. data/lib/grape/validations.rb +0 -220
  66. data/lib/grape/validations/attributes_iterator.rb +21 -0
  67. data/lib/grape/validations/params_scope.rb +176 -0
  68. data/lib/grape/validations/validators/all_or_none.rb +20 -0
  69. data/lib/grape/validations/validators/allow_blank.rb +30 -0
  70. data/lib/grape/validations/validators/at_least_one_of.rb +20 -0
  71. data/lib/grape/validations/validators/base.rb +37 -0
  72. data/lib/grape/validations/{coerce.rb → validators/coerce.rb} +3 -3
  73. data/lib/grape/validations/{default.rb → validators/default.rb} +1 -1
  74. data/lib/grape/validations/validators/exactly_one_of.rb +20 -0
  75. data/lib/grape/validations/validators/multiple_params_base.rb +26 -0
  76. data/lib/grape/validations/validators/mutual_exclusion.rb +25 -0
  77. data/lib/grape/validations/{presence.rb → validators/presence.rb} +2 -2
  78. data/lib/grape/validations/validators/regexp.rb +12 -0
  79. data/lib/grape/validations/validators/values.rb +26 -0
  80. data/lib/grape/version.rb +1 -1
  81. data/spec/grape/api_spec.rb +522 -343
  82. data/spec/grape/dsl/callbacks_spec.rb +4 -4
  83. data/spec/grape/dsl/configuration_spec.rb +48 -9
  84. data/spec/grape/dsl/helpers_spec.rb +6 -13
  85. data/spec/grape/dsl/inside_route_spec.rb +43 -4
  86. data/spec/grape/dsl/middleware_spec.rb +1 -10
  87. data/spec/grape/dsl/parameters_spec.rb +8 -1
  88. data/spec/grape/dsl/request_response_spec.rb +16 -22
  89. data/spec/grape/dsl/routing_spec.rb +21 -5
  90. data/spec/grape/dsl/settings_spec.rb +219 -0
  91. data/spec/grape/dsl/validations_spec.rb +8 -11
  92. data/spec/grape/endpoint_spec.rb +115 -86
  93. data/spec/grape/entity_spec.rb +33 -33
  94. data/spec/grape/exceptions/invalid_formatter_spec.rb +3 -5
  95. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +4 -6
  96. data/spec/grape/exceptions/missing_mime_type_spec.rb +5 -6
  97. data/spec/grape/exceptions/missing_option_spec.rb +3 -5
  98. data/spec/grape/exceptions/unknown_options_spec.rb +3 -5
  99. data/spec/grape/exceptions/unknown_validator_spec.rb +3 -5
  100. data/spec/grape/exceptions/validation_errors_spec.rb +5 -5
  101. data/spec/grape/loading_spec.rb +44 -0
  102. data/spec/grape/middleware/auth/base_spec.rb +0 -4
  103. data/spec/grape/middleware/auth/dsl_spec.rb +2 -4
  104. data/spec/grape/middleware/auth/strategies_spec.rb +5 -6
  105. data/spec/grape/middleware/exception_spec.rb +8 -10
  106. data/spec/grape/middleware/formatter_spec.rb +13 -15
  107. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +10 -10
  108. data/spec/grape/middleware/versioner/header_spec.rb +25 -25
  109. data/spec/grape/middleware/versioner/param_spec.rb +15 -17
  110. data/spec/grape/middleware/versioner/path_spec.rb +1 -2
  111. data/spec/grape/middleware/versioner_spec.rb +0 -1
  112. data/spec/grape/path_spec.rb +66 -45
  113. data/spec/grape/util/inheritable_setting_spec.rb +217 -0
  114. data/spec/grape/util/inheritable_values_spec.rb +63 -0
  115. data/spec/grape/util/stackable_values_spec.rb +115 -0
  116. data/spec/grape/util/strict_hash_configuration_spec.rb +38 -0
  117. data/spec/grape/validations/attributes_iterator_spec.rb +4 -0
  118. data/spec/grape/validations/params_scope_spec.rb +57 -0
  119. data/spec/grape/validations/validators/all_or_none_spec.rb +60 -0
  120. data/spec/grape/validations/validators/allow_blank_spec.rb +170 -0
  121. data/spec/grape/validations/{at_least_one_of_spec.rb → validators/at_least_one_of_spec.rb} +7 -3
  122. data/spec/grape/validations/{coerce_spec.rb → validators/coerce_spec.rb} +8 -11
  123. data/spec/grape/validations/{default_spec.rb → validators/default_spec.rb} +7 -9
  124. data/spec/grape/validations/{exactly_one_of_spec.rb → validators/exactly_one_of_spec.rb} +15 -11
  125. data/spec/grape/validations/{mutual_exclusion_spec.rb → validators/mutual_exclusion_spec.rb} +11 -9
  126. data/spec/grape/validations/{presence_spec.rb → validators/presence_spec.rb} +30 -30
  127. data/spec/grape/validations/{regexp_spec.rb → validators/regexp_spec.rb} +2 -4
  128. data/spec/grape/validations/{values_spec.rb → validators/values_spec.rb} +95 -23
  129. data/spec/grape/validations/{zh-CN.yml → validators/zh-CN.yml} +0 -0
  130. data/spec/grape/validations_spec.rb +335 -70
  131. data/spec/shared/versioning_examples.rb +7 -8
  132. data/spec/spec_helper.rb +2 -0
  133. data/spec/support/basic_auth_encode_helpers.rb +1 -1
  134. data/spec/support/content_type_helpers.rb +1 -1
  135. data/spec/support/versioned_helpers.rb +3 -3
  136. metadata +80 -33
  137. data/lib/grape/util/deep_merge.rb +0 -23
  138. data/lib/grape/util/hash_stack.rb +0 -120
  139. data/lib/grape/validations/at_least_one_of.rb +0 -25
  140. data/lib/grape/validations/exactly_one_of.rb +0 -26
  141. data/lib/grape/validations/mutual_exclusion.rb +0 -25
  142. data/lib/grape/validations/regexp.rb +0 -12
  143. data/lib/grape/validations/values.rb +0 -23
  144. data/spec/grape/util/hash_stack_spec.rb +0 -132
@@ -10,7 +10,6 @@ describe Grape::Validations::RegexpValidator do
10
10
  requires :name, regexp: /^[a-z]+$/
11
11
  end
12
12
  get do
13
-
14
13
  end
15
14
  end
16
15
  end
@@ -22,7 +21,7 @@ describe Grape::Validations::RegexpValidator do
22
21
 
23
22
  context 'invalid input' do
24
23
  it 'refuses inapppopriate' do
25
- get '/', name: "invalid name"
24
+ get '/', name: 'invalid name'
26
25
  expect(last_response.status).to eq(400)
27
26
  end
28
27
 
@@ -33,8 +32,7 @@ describe Grape::Validations::RegexpValidator do
33
32
  end
34
33
 
35
34
  it 'accepts valid input' do
36
- get '/', name: "bob"
35
+ get '/', name: 'bob'
37
36
  expect(last_response.status).to eq(200)
38
37
  end
39
-
40
38
  end
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Grape::Validations::ValuesValidator do
4
-
5
4
  class ValuesModel
6
5
  DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3']
7
6
  class << self
@@ -44,15 +43,36 @@ describe Grape::Validations::ValuesValidator do
44
43
  end
45
44
 
46
45
  params do
47
- requires :type, type: Integer, desc: "An integer", values: [10, 11], default: 10
46
+ optional :type, values: ValuesModel.values, default: -> { ValuesModel.values.sample }
47
+ end
48
+ get '/default_lambda' do
49
+ { type: params[:type] }
50
+ end
51
+
52
+ params do
53
+ optional :type, values: -> { ValuesModel.values }, default: -> { ValuesModel.values.sample }
54
+ end
55
+ get '/default_and_values_lambda' do
56
+ { type: params[:type] }
57
+ end
58
+
59
+ params do
60
+ requires :type, type: Integer, desc: 'An integer', values: [10, 11], default: 10
48
61
  end
49
62
  get '/values/coercion' do
50
63
  { type: params[:type] }
51
64
  end
52
65
 
66
+ params do
67
+ requires :type, type: Array[Integer], desc: 'An integer', values: [10, 11], default: 10
68
+ end
69
+ get '/values/array_coercion' do
70
+ { type: params[:type] }
71
+ end
72
+
53
73
  params do
54
74
  optional :optional do
55
- requires :type, values: ["a", "b"]
75
+ requires :type, values: %w(a b)
56
76
  end
57
77
  end
58
78
  get '/optional_with_required_values'
@@ -65,22 +85,22 @@ describe Grape::Validations::ValuesValidator do
65
85
  end
66
86
 
67
87
  it 'allows a valid value for a parameter' do
68
- get("/", type: 'valid-type1')
88
+ get('/', type: 'valid-type1')
69
89
  expect(last_response.status).to eq 200
70
- expect(last_response.body).to eq({ type: "valid-type1" }.to_json)
90
+ expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
71
91
  end
72
92
 
73
93
  it 'does not allow an invalid value for a parameter' do
74
- get("/", type: 'invalid-type')
94
+ get('/', type: 'invalid-type')
75
95
  expect(last_response.status).to eq 400
76
- expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
96
+ expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
77
97
  end
78
98
 
79
99
  context 'nil value for a parameter' do
80
100
  it 'does not allow for root params scope' do
81
- get("/", type: nil)
101
+ get('/', type: nil)
82
102
  expect(last_response.status).to eq 400
83
- expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
103
+ expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
84
104
  end
85
105
 
86
106
  it 'allows for a required param in child scope' do
@@ -90,15 +110,15 @@ describe Grape::Validations::ValuesValidator do
90
110
  end
91
111
 
92
112
  it 'allows a valid default value' do
93
- get("/default/valid")
113
+ get('/default/valid')
94
114
  expect(last_response.status).to eq 200
95
- expect(last_response.body).to eq({ type: "valid-type2" }.to_json)
115
+ expect(last_response.body).to eq({ type: 'valid-type2' }.to_json)
96
116
  end
97
117
 
98
118
  it 'allows a proc for values' do
99
119
  get('/lambda', type: 'valid-type1')
100
120
  expect(last_response.status).to eq 200
101
- expect(last_response.body).to eq({ type: "valid-type1" }.to_json)
121
+ expect(last_response.body).to eq({ type: 'valid-type1' }.to_json)
102
122
  end
103
123
 
104
124
  it 'does not validate updated values without proc' do
@@ -106,7 +126,7 @@ describe Grape::Validations::ValuesValidator do
106
126
 
107
127
  get('/', type: 'valid-type4')
108
128
  expect(last_response.status).to eq 400
109
- expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
129
+ expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
110
130
  end
111
131
 
112
132
  it 'validates against values in a proc' do
@@ -114,39 +134,91 @@ describe Grape::Validations::ValuesValidator do
114
134
 
115
135
  get('/lambda', type: 'valid-type4')
116
136
  expect(last_response.status).to eq 200
117
- expect(last_response.body).to eq({ type: "valid-type4" }.to_json)
137
+ expect(last_response.body).to eq({ type: 'valid-type4' }.to_json)
118
138
  end
119
139
 
120
140
  it 'does not allow an invalid value for a parameter using lambda' do
121
- get("/lambda", type: 'invalid-type')
141
+ get('/lambda', type: 'invalid-type')
122
142
  expect(last_response.status).to eq 400
123
- expect(last_response.body).to eq({ error: "type does not have a valid value" }.to_json)
143
+ expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
144
+ end
145
+
146
+ it 'validates default value from proc' do
147
+ get('/default_lambda')
148
+ expect(last_response.status).to eq 200
149
+ end
150
+
151
+ it 'validates default value from proc against values in a proc' do
152
+ get('/default_and_values_lambda')
153
+ expect(last_response.status).to eq 200
154
+ end
155
+
156
+ it 'raises IncompatibleOptionValues on an invalid default value from proc' do
157
+ subject = Class.new(Grape::API)
158
+ expect do
159
+ subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: ValuesModel.values.sample + '_invalid' }
160
+ end.to raise_error Grape::Exceptions::IncompatibleOptionValues
124
161
  end
125
162
 
126
163
  it 'raises IncompatibleOptionValues on an invalid default value' do
127
164
  subject = Class.new(Grape::API)
128
- expect {
165
+ expect do
129
166
  subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'invalid-type' }
130
- }.to raise_error Grape::Exceptions::IncompatibleOptionValues
167
+ end.to raise_error Grape::Exceptions::IncompatibleOptionValues
131
168
  end
132
169
 
133
170
  it 'raises IncompatibleOptionValues when type is incompatible with values array' do
134
171
  subject = Class.new(Grape::API)
135
- expect {
172
+ expect do
136
173
  subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol }
137
- }.to raise_error Grape::Exceptions::IncompatibleOptionValues
174
+ end.to raise_error Grape::Exceptions::IncompatibleOptionValues
138
175
  end
139
176
 
140
177
  it 'allows values to be a kind of the coerced type not just an instance of it' do
141
- get("/values/coercion", type: 10)
178
+ get('/values/coercion', type: 10)
142
179
  expect(last_response.status).to eq 200
143
180
  expect(last_response.body).to eq({ type: 10 }.to_json)
144
181
  end
145
182
 
183
+ it 'allows values to be a kind of the coerced type in an array' do
184
+ get('/values/array_coercion', type: [10])
185
+ expect(last_response.status).to eq 200
186
+ expect(last_response.body).to eq({ type: [10] }.to_json)
187
+ end
188
+
146
189
  it 'raises IncompatibleOptionValues when values contains a value that is not a kind of the type' do
147
190
  subject = Class.new(Grape::API)
148
- expect {
191
+ expect do
149
192
  subject.params { requires :type, values: [10.5, 11], type: Integer }
150
- }.to raise_error Grape::Exceptions::IncompatibleOptionValues
193
+ end.to raise_error Grape::Exceptions::IncompatibleOptionValues
194
+ end
195
+
196
+ context 'with a lambda values' do
197
+ subject do
198
+ Class.new(Grape::API) do
199
+ params do
200
+ optional :type, type: String, values: -> { [SecureRandom.uuid] }, default: -> { SecureRandom.uuid }
201
+ end
202
+ get '/random_values'
203
+ end
204
+ end
205
+
206
+ def app
207
+ subject
208
+ end
209
+
210
+ before do
211
+ expect(SecureRandom).to receive(:uuid).and_return('foo').once
212
+ end
213
+
214
+ it 'only evaluates values dynamically with each request' do
215
+ get '/random_values', type: 'foo'
216
+ expect(last_response.status).to eq 200
217
+ end
218
+
219
+ it 'chooses default' do
220
+ get '/random_values'
221
+ expect(last_response.status).to eq 200
222
+ end
151
223
  end
152
224
  end
@@ -1,7 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Grape::Validations do
4
-
5
4
  subject { Class.new(Grape::API) }
6
5
 
7
6
  def app
@@ -44,18 +43,17 @@ describe Grape::Validations do
44
43
  subject.params do
45
44
  optional :some_param
46
45
  end
47
- expect(subject.settings[:declared_params]).to eq([:some_param])
46
+ expect(subject.route_setting(:declared_params)).to eq([:some_param])
48
47
  end
49
48
  end
50
49
 
51
50
  context 'required' do
52
51
  before do
53
52
  subject.params do
54
- requires :key
55
- end
56
- subject.get '/required' do
57
- 'required works'
53
+ requires :key, type: String
58
54
  end
55
+ subject.get('/required') { 'required works' }
56
+ subject.put('/required') { { key: params[:key] }.to_json }
59
57
  end
60
58
 
61
59
  it 'errors when param not present' do
@@ -74,7 +72,13 @@ describe Grape::Validations do
74
72
  subject.params do
75
73
  requires :some_param
76
74
  end
77
- expect(subject.settings[:declared_params]).to eq([:some_param])
75
+ expect(subject.route_setting(:declared_params)).to eq([:some_param])
76
+ end
77
+
78
+ it 'works when required field is present but nil' do
79
+ put '/required', { key: nil }.to_json, 'CONTENT_TYPE' => 'application/json'
80
+ expect(last_response.status).to eq(200)
81
+ expect(JSON.parse(last_response.body)).to eq('key' => nil)
78
82
  end
79
83
  end
80
84
 
@@ -97,7 +101,7 @@ describe Grape::Validations do
97
101
 
98
102
  it 'adds entity documentation to declared params' do
99
103
  define_requires_all
100
- expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
104
+ expect(subject.route_setting(:declared_params)).to eq([:required_field, :optional_field])
101
105
  end
102
106
 
103
107
  it 'errors when required_field is not present' do
@@ -132,7 +136,7 @@ describe Grape::Validations do
132
136
 
133
137
  it 'adds entity documentation to declared params' do
134
138
  define_requires_none
135
- expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
139
+ expect(subject.route_setting(:declared_params)).to eq([:required_field, :optional_field])
136
140
  end
137
141
 
138
142
  it 'errors when required_field is not present' do
@@ -162,7 +166,7 @@ describe Grape::Validations do
162
166
 
163
167
  it 'adds only the entity documentation to declared params, nothing more' do
164
168
  define_requires_all
165
- expect(subject.settings[:declared_params]).to eq([:required_field, :optional_field])
169
+ expect(subject.route_setting(:declared_params)).to eq([:required_field, :optional_field])
166
170
  end
167
171
  end
168
172
 
@@ -190,9 +194,8 @@ describe Grape::Validations do
190
194
  requires :key
191
195
  end
192
196
  end
193
- subject.get '/required' do
194
- 'required works'
195
- end
197
+ subject.get('/required') { 'required works' }
198
+ subject.put('/required') { { items: params[:items] }.to_json }
196
199
  end
197
200
 
198
201
  it 'errors when param not present' do
@@ -201,8 +204,8 @@ describe Grape::Validations do
201
204
  expect(last_response.body).to eq('items is missing')
202
205
  end
203
206
 
204
- it "errors when param is not an Array" do
205
- get '/required', items: "hello"
207
+ it 'errors when param is not an Array' do
208
+ get '/required', items: 'hello'
206
209
  expect(last_response.status).to eq(400)
207
210
  expect(last_response.body).to eq('items is invalid, items[key] is missing')
208
211
 
@@ -217,14 +220,10 @@ describe Grape::Validations do
217
220
  expect(last_response.body).to eq('required works')
218
221
  end
219
222
 
220
- it "doesn't allow any key in the options hash other than type" do
221
- expect {
222
- subject.params do
223
- requires(:items, desc: 'Foo') do
224
- requires :key
225
- end
226
- end
227
- }.to raise_error ArgumentError
223
+ it "doesn't throw a missing param when param is present but empty" do
224
+ put '/required', { items: [] }.to_json, 'CONTENT_TYPE' => 'application/json'
225
+ expect(last_response.status).to eq(200)
226
+ expect(JSON.parse(last_response.body)).to eq('items' => [])
228
227
  end
229
228
 
230
229
  it 'adds to declared parameters' do
@@ -233,7 +232,7 @@ describe Grape::Validations do
233
232
  requires :key
234
233
  end
235
234
  end
236
- expect(subject.settings[:declared_params]).to eq([items: [:key]])
235
+ expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
237
236
  end
238
237
  end
239
238
 
@@ -255,8 +254,8 @@ describe Grape::Validations do
255
254
  expect(last_response.body).to eq('items is missing, items[key] is missing')
256
255
  end
257
256
 
258
- it "errors when param is not a Hash" do
259
- get '/required', items: "hello"
257
+ it 'errors when param is not a Hash' do
258
+ get '/required', items: 'hello'
260
259
  expect(last_response.status).to eq(400)
261
260
  expect(last_response.body).to eq('items is invalid, items[key] is missing')
262
261
 
@@ -271,23 +270,13 @@ describe Grape::Validations do
271
270
  expect(last_response.body).to eq('required works')
272
271
  end
273
272
 
274
- it "doesn't allow any key in the options hash other than type" do
275
- expect {
276
- subject.params do
277
- requires(:items, desc: 'Foo') do
278
- requires :key
279
- end
280
- end
281
- }.to raise_error ArgumentError
282
- end
283
-
284
273
  it 'adds to declared parameters' do
285
274
  subject.params do
286
275
  requires :items do
287
276
  requires :key
288
277
  end
289
278
  end
290
- expect(subject.settings[:declared_params]).to eq([items: [:key]])
279
+ expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
291
280
  end
292
281
  end
293
282
 
@@ -321,7 +310,85 @@ describe Grape::Validations do
321
310
  requires :key
322
311
  end
323
312
  end
324
- expect(subject.settings[:declared_params]).to eq([items: [:key]])
313
+ expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
314
+ end
315
+ end
316
+
317
+ context 'group params with nested params which has a type' do
318
+ let(:invalid_items){ { items: '' } }
319
+
320
+ before do
321
+ subject.params do
322
+ optional :items do
323
+ optional :key1, type: String
324
+ optional :key2, type: String
325
+ end
326
+ end
327
+ subject.post '/group_with_nested' do
328
+ 'group with nested works'
329
+ end
330
+ end
331
+
332
+ it 'errors when group param is invalid'do
333
+ post '/group_with_nested', items: invalid_items
334
+ expect(last_response.status).to eq(400)
335
+ end
336
+ end
337
+
338
+ context 'custom validator for a Hash' do
339
+ module DateRangeValidations
340
+ class DateRangeValidator < Grape::Validations::Base
341
+ def validate_param!(attr_name, params)
342
+ unless params[attr_name][:from] <= params[attr_name][:to]
343
+ fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'"
344
+ end
345
+ end
346
+ end
347
+ end
348
+
349
+ before do
350
+ subject.params do
351
+ optional :date_range, date_range: true, type: Hash do
352
+ requires :from, type: Integer
353
+ requires :to, type: Integer
354
+ end
355
+ end
356
+ subject.get('/optional') do
357
+ 'optional works'
358
+ end
359
+ subject.params do
360
+ requires :date_range, date_range: true, type: Hash do
361
+ requires :from, type: Integer
362
+ requires :to, type: Integer
363
+ end
364
+ end
365
+ subject.get('/required') do
366
+ 'required works'
367
+ end
368
+ end
369
+
370
+ context 'which is optional' do
371
+ it "doesn't throw an error if the validation passes" do
372
+ get '/optional', date_range: { from: 1, to: 2 }
373
+ expect(last_response.status).to eq(200)
374
+ end
375
+
376
+ it 'errors if the validation fails' do
377
+ get '/optional', date_range: { from: 2, to: 1 }
378
+ expect(last_response.status).to eq(400)
379
+ end
380
+ end
381
+
382
+ context 'which is required' do
383
+ it "doesn't throw an error if the validation passes" do
384
+ get '/required', date_range: { from: 1, to: 2 }
385
+ expect(last_response.status).to eq(200)
386
+ end
387
+
388
+ it 'errors if the validation fails' do
389
+ get '/required', date_range: { from: 2, to: 1 }
390
+ expect(last_response.status).to eq(400)
391
+ end
325
392
  end
326
393
  end
327
394
 
@@ -371,9 +438,9 @@ describe Grape::Validations do
371
438
  expect(last_response.body).to eq('children[parents] is missing')
372
439
  end
373
440
 
374
- it "errors when param is not an Array" do
441
+ it 'errors when param is not an Array' do
375
442
  # NOTE: would be nicer if these just returned 'children is invalid'
376
- get '/within_array', children: "hello"
443
+ get '/within_array', children: 'hello'
377
444
  expect(last_response.status).to eq(400)
378
445
  expect(last_response.body).to eq('children is invalid, children[name] is missing, children[parents] is missing, children[parents] is invalid, children[parents][name] is missing')
379
446
 
@@ -428,7 +495,7 @@ describe Grape::Validations do
428
495
  end
429
496
 
430
497
  it 'requires defaults to Array type' do
431
- get '/req', planets: "Jupiter, Saturn"
498
+ get '/req', planets: 'Jupiter, Saturn'
432
499
  expect(last_response.status).to eq(400)
433
500
  expect(last_response.body).to eq('planets is invalid, planets[name] is missing')
434
501
 
@@ -444,26 +511,26 @@ describe Grape::Validations do
444
511
  end
445
512
 
446
513
  it 'optional defaults to Array type' do
447
- get '/opt', name: "Jupiter", moons: "Europa, Ganymede"
514
+ get '/opt', name: 'Jupiter', moons: 'Europa, Ganymede'
448
515
  expect(last_response.status).to eq(400)
449
516
  expect(last_response.body).to eq('moons is invalid, moons[name] is missing')
450
517
 
451
- get '/opt', name: "Jupiter", moons: { name: 'Ganymede' }
518
+ get '/opt', name: 'Jupiter', moons: { name: 'Ganymede' }
452
519
  expect(last_response.status).to eq(400)
453
520
  expect(last_response.body).to eq('moons is invalid')
454
521
 
455
- get '/opt', name: "Jupiter", moons: [{ name: 'Io' }, { name: 'Callisto' }]
522
+ get '/opt', name: 'Jupiter', moons: [{ name: 'Io' }, { name: 'Callisto' }]
456
523
  expect(last_response.status).to eq(200)
457
524
 
458
- put_with_json '/opt', name: "Venus"
525
+ put_with_json '/opt', name: 'Venus'
459
526
  expect(last_response.status).to eq(200)
460
527
 
461
- put_with_json '/opt', name: "Mercury", moons: []
528
+ put_with_json '/opt', name: 'Mercury', moons: []
462
529
  expect(last_response.status).to eq(200)
463
530
  end
464
531
 
465
532
  it 'group defaults to Array type' do
466
- get '/grp', stars: "Sun"
533
+ get '/grp', stars: 'Sun'
467
534
  expect(last_response.status).to eq(400)
468
535
  expect(last_response.body).to eq('stars is invalid, stars[name] is missing')
469
536
 
@@ -545,14 +612,14 @@ describe Grape::Validations do
545
612
  expect(last_response.body).to eq('optional group works')
546
613
  end
547
614
 
548
- it "errors when group is present, but required param is not" do
615
+ it 'errors when group is present, but required param is not' do
549
616
  get '/optional_group', items: [{ not_key: 'foo' }]
550
617
  expect(last_response.status).to eq(400)
551
618
  expect(last_response.body).to eq('items[key] is missing')
552
619
  end
553
620
 
554
621
  it "errors when param is present but isn't an Array" do
555
- get '/optional_group', items: "hello"
622
+ get '/optional_group', items: 'hello'
556
623
  expect(last_response.status).to eq(400)
557
624
  expect(last_response.body).to eq('items is invalid, items[key] is missing')
558
625
 
@@ -567,7 +634,7 @@ describe Grape::Validations do
567
634
  requires :key
568
635
  end
569
636
  end
570
- expect(subject.settings[:declared_params]).to eq([items: [:key]])
637
+ expect(subject.route_setting(:declared_params)).to eq([items: [:key]])
571
638
  end
572
639
  end
573
640
 
@@ -631,7 +698,7 @@ describe Grape::Validations do
631
698
  requires(:required_subitems) { requires :value }
632
699
  end
633
700
  end
634
- expect(subject.settings[:declared_params]).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
701
+ expect(subject.route_setting(:declared_params)).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
635
702
  end
636
703
  end
637
704
 
@@ -656,10 +723,10 @@ describe Grape::Validations do
656
723
 
657
724
  context 'custom validation' do
658
725
  module CustomValidations
659
- class Customvalidator < Grape::Validations::Validator
726
+ class Customvalidator < Grape::Validations::Base
660
727
  def validate_param!(attr_name, params)
661
728
  unless params[attr_name] == 'im custom'
662
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: "is not custom!"
729
+ fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: 'is not custom!'
663
730
  end
664
731
  end
665
732
  end
@@ -776,13 +843,13 @@ describe Grape::Validations do
776
843
  expect(last_response.body).to eq('custom is not custom!')
777
844
  end
778
845
 
779
- specify 'the nested namesapce inherits the custom validator' do
846
+ specify 'the nested namespace inherits the custom validator' do
780
847
  get '/nested/nested/two', custom: 'im wrong, validate me'
781
848
  expect(last_response.status).to eq(400)
782
849
  expect(last_response.body).to eq('custom is not custom!')
783
850
  end
784
851
 
785
- specify 'peer namesapces does not have the validator' do
852
+ specify 'peer namespaces does not have the validator' do
786
853
  get '/peer/one', custom: 'im not validated'
787
854
  expect(last_response.status).to eq(200)
788
855
  expect(last_response.body).to eq('no validation required')
@@ -805,10 +872,10 @@ describe Grape::Validations do
805
872
 
806
873
  context 'when using options on param' do
807
874
  module CustomValidations
808
- class CustomvalidatorWithOptions < Grape::Validations::SingleOptionValidator
875
+ class CustomvalidatorWithOptions < Grape::Validations::Base
809
876
  def validate_param!(attr_name, params)
810
877
  unless params[attr_name] == @option[:text]
811
- raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: @option[:error_message]
878
+ fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: @option[:error_message]
812
879
  end
813
880
  end
814
881
  end
@@ -816,7 +883,7 @@ describe Grape::Validations do
816
883
 
817
884
  before do
818
885
  subject.params do
819
- optional :custom, customvalidator_with_options: { text: 'im custom with options', error_message: "is not custom with options!" }
886
+ optional :custom, customvalidator_with_options: { text: 'im custom with options', error_message: 'is not custom with options!' }
820
887
  end
821
888
  subject.get '/optional_custom' do
822
889
  'optional with custom works!'
@@ -877,16 +944,15 @@ describe Grape::Validations do
877
944
  subject.params do
878
945
  use :pagination
879
946
  end
880
- expect(subject.settings[:declared_params]).to eq [:page, :per_page]
947
+ expect(subject.route_setting(:declared_params)).to eq [:page, :per_page]
881
948
  end
882
949
 
883
950
  it 'by #use with multiple params' do
884
951
  subject.params do
885
952
  use :pagination, :period
886
953
  end
887
- expect(subject.settings[:declared_params]).to eq [:page, :per_page, :start_date, :end_date]
954
+ expect(subject.route_setting(:declared_params)).to eq [:page, :per_page, :start_date, :end_date]
888
955
  end
889
-
890
956
  end
891
957
 
892
958
  context 'with block' do
@@ -960,7 +1026,7 @@ describe Grape::Validations do
960
1026
 
961
1027
  get '/mutually_exclusive', beer: 'string', wine: 'anotherstring'
962
1028
  expect(last_response.status).to eq(400)
963
- expect(last_response.body).to eq "beer, wine are mutually exclusive"
1029
+ expect(last_response.body).to eq 'beer, wine are mutually exclusive'
964
1030
  end
965
1031
  end
966
1032
 
@@ -970,17 +1036,62 @@ describe Grape::Validations do
970
1036
  optional :beer
971
1037
  optional :wine
972
1038
  mutually_exclusive :beer, :wine
973
- optional :scotch
974
- optional :aquavit
975
- mutually_exclusive :scotch, :aquavit
1039
+ optional :nested, type: Hash do
1040
+ optional :scotch
1041
+ optional :aquavit
1042
+ mutually_exclusive :scotch, :aquavit
1043
+ end
1044
+ optional :nested2, type: Array do
1045
+ optional :scotch2
1046
+ optional :aquavit2
1047
+ mutually_exclusive :scotch2, :aquavit2
1048
+ end
976
1049
  end
977
1050
  subject.get '/mutually_exclusive' do
978
1051
  'mutually_exclusive works!'
979
1052
  end
980
1053
 
981
- get '/mutually_exclusive', beer: 'true', wine: 'true', scotch: 'true', aquavit: 'true'
1054
+ get '/mutually_exclusive', beer: 'true', wine: 'true', nested: { scotch: 'true', aquavit: 'true' }, nested2: [{ scotch2: 'true' }, { scotch2: 'true', aquavit2: 'true' }]
1055
+ expect(last_response.status).to eq(400)
1056
+ expect(last_response.body).to eq 'beer, wine are mutually exclusive, scotch, aquavit are mutually exclusive, scotch2, aquavit2 are mutually exclusive'
1057
+ end
1058
+ end
1059
+
1060
+ context 'in a group' do
1061
+ it 'works when only one from the set is present' do
1062
+ subject.params do
1063
+ group :drink, type: Hash do
1064
+ optional :wine
1065
+ optional :beer
1066
+ optional :juice
1067
+
1068
+ mutually_exclusive :beer, :wine, :juice
1069
+ end
1070
+ end
1071
+ subject.get '/mutually_exclusive_group' do
1072
+ 'mutually_exclusive_group works!'
1073
+ end
1074
+
1075
+ get '/mutually_exclusive_group', drink: { beer: 'true' }
1076
+ expect(last_response.status).to eq(200)
1077
+ end
1078
+
1079
+ it 'errors when more than one from the set is present' do
1080
+ subject.params do
1081
+ group :drink, type: Hash do
1082
+ optional :wine
1083
+ optional :beer
1084
+ optional :juice
1085
+
1086
+ mutually_exclusive :beer, :wine, :juice
1087
+ end
1088
+ end
1089
+ subject.get '/mutually_exclusive_group' do
1090
+ 'mutually_exclusive_group works!'
1091
+ end
1092
+
1093
+ get '/mutually_exclusive_group', drink: { beer: 'true', juice: 'true', wine: 'true' }
982
1094
  expect(last_response.status).to eq(400)
983
- expect(last_response.body).to eq "beer, wine are mutually exclusive, scotch, aquavit are mutually exclusive"
984
1095
  end
985
1096
  end
986
1097
  end
@@ -1002,7 +1113,7 @@ describe Grape::Validations do
1002
1113
  it 'errors when none are present' do
1003
1114
  get '/exactly_one_of'
1004
1115
  expect(last_response.status).to eq(400)
1005
- expect(last_response.body).to eq "beer, wine, juice are missing, exactly one parameter must be provided"
1116
+ expect(last_response.body).to eq 'beer, wine, juice are missing, exactly one parameter must be provided'
1006
1117
  end
1007
1118
 
1008
1119
  it 'succeeds when one is present' do
@@ -1014,7 +1125,47 @@ describe Grape::Validations do
1014
1125
  it 'errors when two or more are present' do
1015
1126
  get '/exactly_one_of', beer: 'string', wine: 'anotherstring'
1016
1127
  expect(last_response.status).to eq(400)
1017
- expect(last_response.body).to eq "beer, wine are mutually exclusive"
1128
+ expect(last_response.body).to eq 'beer, wine are mutually exclusive'
1129
+ end
1130
+ end
1131
+
1132
+ context 'nested params' do
1133
+ before :each do
1134
+ subject.params do
1135
+ requires :nested, type: Hash do
1136
+ optional :beer_nested
1137
+ optional :wine_nested
1138
+ optional :juice_nested
1139
+ exactly_one_of :beer_nested, :wine_nested, :juice_nested
1140
+ end
1141
+ optional :nested2, type: Array do
1142
+ optional :beer_nested2
1143
+ optional :wine_nested2
1144
+ optional :juice_nested2
1145
+ exactly_one_of :beer_nested2, :wine_nested2, :juice_nested2
1146
+ end
1147
+ end
1148
+ subject.get '/exactly_one_of_nested' do
1149
+ 'exactly_one_of works!'
1150
+ end
1151
+ end
1152
+
1153
+ it 'errors when none are present' do
1154
+ get '/exactly_one_of_nested'
1155
+ expect(last_response.status).to eq(400)
1156
+ expect(last_response.body).to eq 'nested is missing, beer_nested, wine_nested, juice_nested are missing, exactly one parameter must be provided'
1157
+ end
1158
+
1159
+ it 'succeeds when one is present' do
1160
+ get '/exactly_one_of_nested', nested: { beer_nested: 'string' }
1161
+ expect(last_response.status).to eq(200)
1162
+ expect(last_response.body).to eq 'exactly_one_of works!'
1163
+ end
1164
+
1165
+ it 'errors when two or more are present' do
1166
+ get '/exactly_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'anotherstring' }]
1167
+ expect(last_response.status).to eq(400)
1168
+ expect(last_response.body).to eq 'beer_nested2, wine_nested2 are mutually exclusive'
1018
1169
  end
1019
1170
  end
1020
1171
  end
@@ -1036,7 +1187,7 @@ describe Grape::Validations do
1036
1187
  it 'errors when none are present' do
1037
1188
  get '/at_least_one_of'
1038
1189
  expect(last_response.status).to eq(400)
1039
- expect(last_response.body).to eq "beer, wine, juice are missing, at least one parameter must be provided"
1190
+ expect(last_response.body).to eq 'beer, wine, juice are missing, at least one parameter must be provided'
1040
1191
  end
1041
1192
 
1042
1193
  it 'does not error when one is present' do
@@ -1051,6 +1202,120 @@ describe Grape::Validations do
1051
1202
  expect(last_response.body).to eq 'at_least_one_of works!'
1052
1203
  end
1053
1204
  end
1205
+
1206
+ context 'nested params' do
1207
+ before :each do
1208
+ subject.params do
1209
+ requires :nested, type: Hash do
1210
+ optional :beer_nested
1211
+ optional :wine_nested
1212
+ optional :juice_nested
1213
+ at_least_one_of :beer_nested, :wine_nested, :juice_nested
1214
+ end
1215
+ optional :nested2, type: Array do
1216
+ optional :beer_nested2
1217
+ optional :wine_nested2
1218
+ optional :juice_nested2
1219
+ at_least_one_of :beer_nested2, :wine_nested2, :juice_nested2
1220
+ end
1221
+ end
1222
+ subject.get '/at_least_one_of_nested' do
1223
+ 'at_least_one_of works!'
1224
+ end
1225
+ end
1226
+
1227
+ it 'errors when none are present' do
1228
+ get '/at_least_one_of_nested'
1229
+ expect(last_response.status).to eq(400)
1230
+ expect(last_response.body).to eq 'nested is missing, beer_nested, wine_nested, juice_nested are missing, at least one parameter must be provided'
1231
+ end
1232
+
1233
+ it 'does not error when one is present' do
1234
+ get '/at_least_one_of_nested', nested: { beer_nested: 'string' }, nested2: [{ beer_nested2: 'string' }]
1235
+ expect(last_response.status).to eq(200)
1236
+ expect(last_response.body).to eq 'at_least_one_of works!'
1237
+ end
1238
+
1239
+ it 'does not error when two are present' do
1240
+ get '/at_least_one_of_nested', nested: { beer_nested: 'string', wine_nested: 'string' }, nested2: [{ beer_nested2: 'string', wine_nested2: 'string' }]
1241
+ expect(last_response.status).to eq(200)
1242
+ expect(last_response.body).to eq 'at_least_one_of works!'
1243
+ end
1244
+ end
1245
+ end
1246
+
1247
+ context 'in a group' do
1248
+ it 'works when only one from the set is present' do
1249
+ subject.params do
1250
+ group :drink, type: Hash do
1251
+ optional :wine
1252
+ optional :beer
1253
+ optional :juice
1254
+
1255
+ exactly_one_of :beer, :wine, :juice
1256
+ end
1257
+ end
1258
+ subject.get '/exactly_one_of_group' do
1259
+ 'exactly_one_of_group works!'
1260
+ end
1261
+
1262
+ get '/exactly_one_of_group', drink: { beer: 'true' }
1263
+ expect(last_response.status).to eq(200)
1264
+ end
1265
+
1266
+ it 'errors when no parameter from the set is present' do
1267
+ subject.params do
1268
+ group :drink, type: Hash do
1269
+ optional :wine
1270
+ optional :beer
1271
+ optional :juice
1272
+
1273
+ exactly_one_of :beer, :wine, :juice
1274
+ end
1275
+ end
1276
+ subject.get '/exactly_one_of_group' do
1277
+ 'exactly_one_of_group works!'
1278
+ end
1279
+
1280
+ get '/exactly_one_of_group', drink: {}
1281
+ expect(last_response.status).to eq(400)
1282
+ end
1283
+
1284
+ it 'errors when more than one from the set is present' do
1285
+ subject.params do
1286
+ group :drink, type: Hash do
1287
+ optional :wine
1288
+ optional :beer
1289
+ optional :juice
1290
+
1291
+ exactly_one_of :beer, :wine, :juice
1292
+ end
1293
+ end
1294
+ subject.get '/exactly_one_of_group' do
1295
+ 'exactly_one_of_group works!'
1296
+ end
1297
+
1298
+ get '/exactly_one_of_group', drink: { beer: 'true', juice: 'true', wine: 'true' }
1299
+ expect(last_response.status).to eq(400)
1300
+ end
1301
+
1302
+ it 'does not falsely think the param is there if it is provided outside the block' do
1303
+ subject.params do
1304
+ group :drink, type: Hash do
1305
+ optional :wine
1306
+ optional :beer
1307
+ optional :juice
1308
+
1309
+ exactly_one_of :beer, :wine, :juice
1310
+ end
1311
+ end
1312
+ subject.get '/exactly_one_of_group' do
1313
+ 'exactly_one_of_group works!'
1314
+ end
1315
+
1316
+ get '/exactly_one_of_group', drink: { foo: 'bar' }, beer: 'true'
1317
+ expect(last_response.status).to eq(400)
1318
+ end
1054
1319
  end
1055
1320
  end
1056
1321
  end