grape 1.2.4 → 1.2.5

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -4
  3. data/README.md +144 -4
  4. data/grape.gemspec +3 -1
  5. data/lib/grape.rb +94 -66
  6. data/lib/grape/api.rb +46 -4
  7. data/lib/grape/api/instance.rb +23 -12
  8. data/lib/grape/dsl/desc.rb +11 -2
  9. data/lib/grape/dsl/validations.rb +4 -3
  10. data/lib/grape/eager_load.rb +18 -0
  11. data/lib/grape/endpoint.rb +3 -3
  12. data/lib/grape/error_formatter.rb +1 -1
  13. data/lib/grape/exceptions/validation_errors.rb +4 -2
  14. data/lib/grape/formatter.rb +1 -1
  15. data/lib/grape/middleware/auth/base.rb +2 -4
  16. data/lib/grape/middleware/base.rb +2 -0
  17. data/lib/grape/middleware/helpers.rb +10 -0
  18. data/lib/grape/parser.rb +1 -1
  19. data/lib/grape/util/base_inheritable.rb +34 -0
  20. data/lib/grape/util/inheritable_values.rb +5 -25
  21. data/lib/grape/util/lazy_block.rb +25 -0
  22. data/lib/grape/util/lazy_value.rb +5 -0
  23. data/lib/grape/util/reverse_stackable_values.rb +7 -36
  24. data/lib/grape/util/stackable_values.rb +19 -22
  25. data/lib/grape/validations/attributes_iterator.rb +5 -3
  26. data/lib/grape/validations/multiple_attributes_iterator.rb +11 -0
  27. data/lib/grape/validations/params_scope.rb +12 -12
  28. data/lib/grape/validations/single_attribute_iterator.rb +13 -0
  29. data/lib/grape/validations/validator_factory.rb +6 -11
  30. data/lib/grape/validations/validators/all_or_none.rb +6 -13
  31. data/lib/grape/validations/validators/at_least_one_of.rb +5 -13
  32. data/lib/grape/validations/validators/base.rb +11 -10
  33. data/lib/grape/validations/validators/coerce.rb +4 -0
  34. data/lib/grape/validations/validators/default.rb +1 -1
  35. data/lib/grape/validations/validators/exactly_one_of.rb +6 -23
  36. data/lib/grape/validations/validators/multiple_params_base.rb +14 -10
  37. data/lib/grape/validations/validators/mutual_exclusion.rb +6 -18
  38. data/lib/grape/version.rb +1 -1
  39. data/spec/grape/api/defines_boolean_in_params_spec.rb +37 -0
  40. data/spec/grape/api_remount_spec.rb +158 -0
  41. data/spec/grape/api_spec.rb +72 -0
  42. data/spec/grape/endpoint_spec.rb +1 -1
  43. data/spec/grape/exceptions/base_spec.rb +4 -0
  44. data/spec/grape/exceptions/validation_errors_spec.rb +6 -4
  45. data/spec/grape/integration/rack_spec.rb +22 -6
  46. data/spec/grape/middleware/base_spec.rb +8 -0
  47. data/spec/grape/middleware/formatter_spec.rb +11 -1
  48. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +29 -0
  49. data/spec/grape/validations/params_scope_spec.rb +13 -0
  50. data/spec/grape/validations/single_attribute_iterator_spec.rb +33 -0
  51. data/spec/grape/validations/validators/all_or_none_spec.rb +138 -30
  52. data/spec/grape/validations/validators/at_least_one_of_spec.rb +173 -29
  53. data/spec/grape/validations/validators/coerce_spec.rb +6 -2
  54. data/spec/grape/validations/validators/exactly_one_of_spec.rb +202 -38
  55. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +184 -27
  56. data/spec/grape/validations_spec.rb +32 -20
  57. metadata +103 -115
  58. data/Appraisals +0 -28
  59. data/Dangerfile +0 -2
  60. data/Gemfile +0 -33
  61. data/Gemfile.lock +0 -231
  62. data/Guardfile +0 -10
  63. data/RELEASING.md +0 -111
  64. data/Rakefile +0 -25
  65. data/benchmark/simple.rb +0 -27
  66. data/benchmark/simple_with_type_coercer.rb +0 -22
  67. data/gemfiles/multi_json.gemfile +0 -35
  68. data/gemfiles/multi_xml.gemfile +0 -35
  69. data/gemfiles/rack_1.5.2.gemfile.lock +0 -232
  70. data/gemfiles/rack_edge.gemfile +0 -35
  71. data/gemfiles/rails_3.gemfile +0 -36
  72. data/gemfiles/rails_3.gemfile.lock +0 -288
  73. data/gemfiles/rails_4.gemfile +0 -35
  74. data/gemfiles/rails_4.gemfile.lock +0 -280
  75. data/gemfiles/rails_5.gemfile +0 -35
  76. data/gemfiles/rails_5.gemfile.lock +0 -312
  77. data/gemfiles/rails_edge.gemfile +0 -35
  78. data/pkg/grape-1.2.0.gem +0 -0
  79. data/pkg/grape-1.2.1.gem +0 -0
  80. data/pkg/grape-1.2.3.gem +0 -0
@@ -20,10 +20,13 @@ describe Grape::Validations::CoerceValidator do
20
20
 
21
21
  context 'i18n' do
22
22
  after :each do
23
+ I18n.available_locales = %i[en]
23
24
  I18n.locale = :en
25
+ I18n.default_locale = :en
24
26
  end
25
27
 
26
28
  it 'i18n error on malformed input' do
29
+ I18n.available_locales = %i[en zh-CN]
27
30
  I18n.load_path << File.expand_path('../zh-CN.yml', __FILE__)
28
31
  I18n.reload!
29
32
  I18n.locale = 'zh-CN'.to_sym
@@ -40,6 +43,7 @@ describe Grape::Validations::CoerceValidator do
40
43
  end
41
44
 
42
45
  it 'gives an english fallback error when default locale message is blank' do
46
+ I18n.available_locales = %i[en pt-BR]
43
47
  I18n.locale = 'pt-BR'.to_sym
44
48
  subject.params do
45
49
  requires :age, type: Integer
@@ -576,7 +580,7 @@ describe Grape::Validations::CoerceValidator do
576
580
  expect(last_response.status).to eq(200)
577
581
  expect(last_response.body).to eq('arrays work')
578
582
 
579
- get '/', splines: [{ x: 2, ints: [] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
583
+ get '/', splines: [{ x: 2, ints: [5] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
580
584
  expect(last_response.status).to eq(200)
581
585
  expect(last_response.body).to eq('arrays work')
582
586
 
@@ -592,7 +596,7 @@ describe Grape::Validations::CoerceValidator do
592
596
  expect(last_response.status).to eq(400)
593
597
  expect(last_response.body).to eq('splines[x] does not have a valid value')
594
598
 
595
- get '/', splines: [{ x: 1, ints: [] }, { x: 4, ints: [] }]
599
+ get '/', splines: [{ x: 1, ints: [5] }, { x: 4, ints: [6] }]
596
600
  expect(last_response.status).to eq(400)
597
601
  expect(last_response.body).to eq('splines[x] does not have a valid value')
598
602
  end
@@ -2,73 +2,237 @@ require 'spec_helper'
2
2
 
3
3
  describe Grape::Validations::ExactlyOneOfValidator do
4
4
  describe '#validate!' do
5
- let(:scope) do
6
- Struct.new(:opts) do
7
- def params(arg)
8
- arg
9
- end
5
+ subject(:validate) { post path, params }
6
+
7
+ module ValidationsSpec
8
+ module ExactlyOneOfValidatorSpec
9
+ class API < Grape::API
10
+ rescue_from Grape::Exceptions::ValidationErrors do |e|
11
+ error!(e.errors.transform_keys! { |key| key.join(',') }, 400)
12
+ end
13
+
14
+ params do
15
+ optional :beer
16
+ optional :wine
17
+ optional :grapefruit
18
+ exactly_one_of :beer, :wine, :grapefruit
19
+ end
20
+ post do
21
+ end
22
+
23
+ params do
24
+ optional :beer
25
+ optional :wine
26
+ optional :grapefruit
27
+ optional :other
28
+ exactly_one_of :beer, :wine, :grapefruit
29
+ end
30
+ post 'mixed-params' do
31
+ end
32
+
33
+ params do
34
+ optional :beer
35
+ optional :wine
36
+ optional :grapefruit
37
+ exactly_one_of :beer, :wine, :grapefruit, message: 'you should choose one'
38
+ end
39
+ post '/custom-message' do
40
+ end
10
41
 
11
- def required?; end
42
+ params do
43
+ requires :item, type: Hash do
44
+ optional :beer
45
+ optional :wine
46
+ optional :grapefruit
47
+ exactly_one_of :beer, :wine, :grapefruit
48
+ end
49
+ end
50
+ post '/nested-hash' do
51
+ end
52
+
53
+ params do
54
+ optional :item, type: Hash do
55
+ optional :beer
56
+ optional :wine
57
+ optional :grapefruit
58
+ exactly_one_of :beer, :wine, :grapefruit
59
+ end
60
+ end
61
+ post '/nested-optional-hash' do
62
+ end
63
+
64
+ params do
65
+ requires :items, type: Array do
66
+ optional :beer
67
+ optional :wine
68
+ optional :grapefruit
69
+ exactly_one_of :beer, :wine, :grapefruit
70
+ end
71
+ end
72
+ post '/nested-array' do
73
+ end
74
+
75
+ params do
76
+ requires :items, type: Array do
77
+ requires :nested_items, type: Array do
78
+ optional :beer, :wine, :grapefruit, type: Boolean
79
+ exactly_one_of :beer, :wine, :grapefruit
80
+ end
81
+ end
82
+ end
83
+ post '/deeply-nested-array' do
84
+ end
85
+ end
12
86
  end
13
87
  end
14
- let(:exactly_one_of_params) { %i[beer wine grapefruit] }
15
- let(:validator) { described_class.new(exactly_one_of_params, {}, false, scope.new) }
16
88
 
17
- context 'when all restricted params are present' do
89
+ def app
90
+ ValidationsSpec::ExactlyOneOfValidatorSpec::API
91
+ end
92
+
93
+ context 'when all params are present' do
94
+ let(:path) { '/' }
18
95
  let(:params) { { beer: true, wine: true, grapefruit: true } }
19
96
 
20
- it 'raises a validation exception' do
21
- expect do
22
- validator.validate! params
23
- end.to raise_error(Grape::Exceptions::Validation)
97
+ it 'returns a validation error' do
98
+ validate
99
+ expect(last_response.status).to eq 400
100
+ expect(JSON.parse(last_response.body)).to eq(
101
+ 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided']
102
+ )
24
103
  end
25
104
 
26
105
  context 'mixed with other params' do
27
- let(:mixed_params) { params.merge!(other: true, andanother: true) }
106
+ let(:path) { '/mixed-params' }
107
+ let(:params) { { beer: true, wine: true, grapefruit: true, other: true } }
28
108
 
29
- it 'still raises a validation exception' do
30
- expect do
31
- validator.validate! mixed_params
32
- end.to raise_error(Grape::Exceptions::Validation)
109
+ it 'returns a validation error' do
110
+ validate
111
+ expect(last_response.status).to eq 400
112
+ expect(JSON.parse(last_response.body)).to eq(
113
+ 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided']
114
+ )
33
115
  end
34
116
  end
35
117
  end
36
118
 
37
- context 'when a subset of restricted params are present' do
119
+ context 'when a subset of params are present' do
120
+ let(:path) { '/' }
38
121
  let(:params) { { beer: true, grapefruit: true } }
39
122
 
40
- it 'raises a validation exception' do
41
- expect do
42
- validator.validate! params
43
- end.to raise_error(Grape::Exceptions::Validation)
123
+ it 'returns a validation error' do
124
+ validate
125
+ expect(last_response.status).to eq 400
126
+ expect(JSON.parse(last_response.body)).to eq(
127
+ 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided']
128
+ )
44
129
  end
45
130
  end
46
131
 
47
- context 'when params keys come as strings' do
48
- let(:params) { { 'beer' => true, 'grapefruit' => true } }
132
+ context 'when custom message is specified' do
133
+ let(:path) { '/custom-message' }
134
+ let(:params) { { beer: true, wine: true } }
49
135
 
50
- it 'raises a validation exception' do
51
- expect do
52
- validator.validate! params
53
- end.to raise_error(Grape::Exceptions::Validation)
136
+ it 'returns a validation error' do
137
+ validate
138
+ expect(last_response.status).to eq 400
139
+ expect(JSON.parse(last_response.body)).to eq(
140
+ 'beer,wine,grapefruit' => ['you should choose one']
141
+ )
54
142
  end
55
143
  end
56
144
 
57
- context 'when none of the restricted params is selected' do
145
+ context 'when exacly one param is present' do
146
+ let(:path) { '/' }
147
+ let(:params) { { beer: true, somethingelse: true } }
148
+
149
+ it 'does not return a validation error' do
150
+ validate
151
+ expect(last_response.status).to eq 201
152
+ end
153
+ end
154
+
155
+ context 'when none of the params are present' do
156
+ let(:path) { '/' }
58
157
  let(:params) { { somethingelse: true } }
59
158
 
60
- it 'raises a validation exception' do
61
- expect do
62
- validator.validate! params
63
- end.to raise_error(Grape::Exceptions::Validation)
159
+ it 'returns a validation error' do
160
+ validate
161
+ expect(last_response.status).to eq 400
162
+ expect(JSON.parse(last_response.body)).to eq(
163
+ 'beer,wine,grapefruit' => ['are missing, exactly one parameter must be provided']
164
+ )
64
165
  end
65
166
  end
66
167
 
67
- context 'when exactly one of the restricted params is selected' do
68
- let(:params) { { beer: true, somethingelse: true } }
168
+ context 'when params are nested inside required hash' do
169
+ let(:path) { '/nested-hash' }
170
+ let(:params) { { item: { beer: true, wine: true } } }
171
+
172
+ it 'returns a validation error with full names of the params' do
173
+ validate
174
+ expect(last_response.status).to eq 400
175
+ expect(JSON.parse(last_response.body)).to eq(
176
+ 'item[beer],item[wine],item[grapefruit]' => ['are missing, exactly one parameter must be provided']
177
+ )
178
+ end
179
+ end
180
+
181
+ context 'when params are nested inside optional hash' do
182
+ let(:path) { '/nested-optional-hash' }
183
+
184
+ context 'when params are passed' do
185
+ let(:params) { { item: { beer: true, wine: true } } }
186
+
187
+ it 'returns a validation error with full names of the params' do
188
+ validate
189
+ expect(last_response.status).to eq 400
190
+ expect(JSON.parse(last_response.body)).to eq(
191
+ 'item[beer],item[wine],item[grapefruit]' => ['are missing, exactly one parameter must be provided']
192
+ )
193
+ end
194
+ end
195
+
196
+ context 'when params are empty' do
197
+ let(:params) { { other: true } }
198
+
199
+ it 'does not return a validation error' do
200
+ validate
201
+ expect(last_response.status).to eq 201
202
+ end
203
+ end
204
+ end
205
+
206
+ context 'when params are nested inside array' do
207
+ let(:path) { '/nested-array' }
208
+ let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } }
209
+
210
+ it 'returns a validation error with full names of the params' do
211
+ validate
212
+ expect(last_response.status).to eq 400
213
+ expect(JSON.parse(last_response.body)).to eq(
214
+ 'items[0][beer],items[0][wine],items[0][grapefruit]' => [
215
+ 'are missing, exactly one parameter must be provided'
216
+ ],
217
+ 'items[1][beer],items[1][wine],items[1][grapefruit]' => [
218
+ 'are missing, exactly one parameter must be provided'
219
+ ]
220
+ )
221
+ end
222
+ end
223
+
224
+ context 'when params are deeply nested' do
225
+ let(:path) { '/deeply-nested-array' }
226
+ let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } }
69
227
 
70
- it 'does not raise a validation exception' do
71
- expect(validator.validate!(params)).to eql params
228
+ it 'returns a validation error with full names of the params' do
229
+ validate
230
+ expect(last_response.status).to eq 400
231
+ expect(JSON.parse(last_response.body)).to eq(
232
+ 'items[0][nested_items][0][beer],items[0][nested_items][0][wine],items[0][nested_items][0][grapefruit]' => [
233
+ 'are missing, exactly one parameter must be provided'
234
+ ]
235
+ )
72
236
  end
73
237
  end
74
238
  end
@@ -2,61 +2,218 @@ require 'spec_helper'
2
2
 
3
3
  describe Grape::Validations::MutualExclusionValidator do
4
4
  describe '#validate!' do
5
- let(:scope) do
6
- Struct.new(:opts) do
7
- def params(arg)
8
- arg
5
+ subject(:validate) { post path, params }
6
+
7
+ module ValidationsSpec
8
+ module MutualExclusionValidatorSpec
9
+ class API < Grape::API
10
+ rescue_from Grape::Exceptions::ValidationErrors do |e|
11
+ error!(e.errors.transform_keys! { |key| key.join(',') }, 400)
12
+ end
13
+
14
+ params do
15
+ optional :beer
16
+ optional :wine
17
+ optional :grapefruit
18
+ mutually_exclusive :beer, :wine, :grapefruit
19
+ end
20
+ post do
21
+ end
22
+
23
+ params do
24
+ optional :beer
25
+ optional :wine
26
+ optional :grapefruit
27
+ optional :other
28
+ mutually_exclusive :beer, :wine, :grapefruit
29
+ end
30
+ post 'mixed-params' do
31
+ end
32
+
33
+ params do
34
+ optional :beer
35
+ optional :wine
36
+ optional :grapefruit
37
+ mutually_exclusive :beer, :wine, :grapefruit, message: 'you should not mix beer and wine'
38
+ end
39
+ post '/custom-message' do
40
+ end
41
+
42
+ params do
43
+ requires :item, type: Hash do
44
+ optional :beer
45
+ optional :wine
46
+ optional :grapefruit
47
+ mutually_exclusive :beer, :wine, :grapefruit
48
+ end
49
+ end
50
+ post '/nested-hash' do
51
+ end
52
+
53
+ params do
54
+ optional :item, type: Hash do
55
+ optional :beer
56
+ optional :wine
57
+ optional :grapefruit
58
+ mutually_exclusive :beer, :wine, :grapefruit
59
+ end
60
+ end
61
+ post '/nested-optional-hash' do
62
+ end
63
+
64
+ params do
65
+ requires :items, type: Array do
66
+ optional :beer
67
+ optional :wine
68
+ optional :grapefruit
69
+ mutually_exclusive :beer, :wine, :grapefruit
70
+ end
71
+ end
72
+ post '/nested-array' do
73
+ end
74
+
75
+ params do
76
+ requires :items, type: Array do
77
+ requires :nested_items, type: Array do
78
+ optional :beer, :wine, :grapefruit, type: Boolean
79
+ mutually_exclusive :beer, :wine, :grapefruit
80
+ end
81
+ end
82
+ end
83
+ post '/deeply-nested-array' do
84
+ end
9
85
  end
10
86
  end
11
87
  end
12
- let(:mutually_exclusive_params) { %i[beer wine grapefruit] }
13
- let(:validator) { described_class.new(mutually_exclusive_params, {}, false, scope.new) }
88
+
89
+ def app
90
+ ValidationsSpec::MutualExclusionValidatorSpec::API
91
+ end
14
92
 
15
93
  context 'when all mutually exclusive params are present' do
94
+ let(:path) { '/' }
16
95
  let(:params) { { beer: true, wine: true, grapefruit: true } }
17
96
 
18
- it 'raises a validation exception' do
19
- expect do
20
- validator.validate! params
21
- end.to raise_error(Grape::Exceptions::Validation)
97
+ it 'returns a validation error' do
98
+ validate
99
+ expect(last_response.status).to eq 400
100
+ expect(JSON.parse(last_response.body)).to eq(
101
+ 'beer,wine,grapefruit' => ['are mutually exclusive']
102
+ )
22
103
  end
23
104
 
24
105
  context 'mixed with other params' do
25
- let(:mixed_params) { params.merge!(other: true, andanother: true) }
106
+ let(:path) { '/mixed-params' }
107
+ let(:params) { { beer: true, wine: true, grapefruit: true, other: true } }
26
108
 
27
- it 'still raises a validation exception' do
28
- expect do
29
- validator.validate! mixed_params
30
- end.to raise_error(Grape::Exceptions::Validation)
109
+ it 'returns a validation error' do
110
+ validate
111
+ expect(last_response.status).to eq 400
112
+ expect(JSON.parse(last_response.body)).to eq(
113
+ 'beer,wine,grapefruit' => ['are mutually exclusive']
114
+ )
31
115
  end
32
116
  end
33
117
  end
34
118
 
35
119
  context 'when a subset of mutually exclusive params are present' do
120
+ let(:path) { '/' }
36
121
  let(:params) { { beer: true, grapefruit: true } }
37
122
 
38
- it 'raises a validation exception' do
39
- expect do
40
- validator.validate! params
41
- end.to raise_error(Grape::Exceptions::Validation)
123
+ it 'returns a validation error' do
124
+ validate
125
+ expect(last_response.status).to eq 400
126
+ expect(JSON.parse(last_response.body)).to eq(
127
+ 'beer,grapefruit' => ['are mutually exclusive']
128
+ )
42
129
  end
43
130
  end
44
131
 
45
- context 'when params keys come as strings' do
46
- let(:params) { { 'beer' => true, 'grapefruit' => true } }
132
+ context 'when custom message is specified' do
133
+ let(:path) { '/custom-message' }
134
+ let(:params) { { beer: true, wine: true } }
47
135
 
48
- it 'raises a validation exception' do
49
- expect do
50
- validator.validate! params
51
- end.to raise_error(Grape::Exceptions::Validation)
136
+ it 'returns a validation error' do
137
+ validate
138
+ expect(last_response.status).to eq 400
139
+ expect(JSON.parse(last_response.body)).to eq(
140
+ 'beer,wine' => ['you should not mix beer and wine']
141
+ )
52
142
  end
53
143
  end
54
144
 
55
145
  context 'when no mutually exclusive params are present' do
146
+ let(:path) { '/' }
56
147
  let(:params) { { beer: true, somethingelse: true } }
57
148
 
58
- it 'params' do
59
- expect(validator.validate!(params)).to eql params
149
+ it 'does not return a validation error' do
150
+ validate
151
+ expect(last_response.status).to eq 201
152
+ end
153
+ end
154
+
155
+ context 'when mutually exclusive params are nested inside required hash' do
156
+ let(:path) { '/nested-hash' }
157
+ let(:params) { { item: { beer: true, wine: true } } }
158
+
159
+ it 'returns a validation error with full names of the params' do
160
+ validate
161
+ expect(last_response.status).to eq 400
162
+ expect(JSON.parse(last_response.body)).to eq(
163
+ 'item[beer],item[wine]' => ['are mutually exclusive']
164
+ )
165
+ end
166
+ end
167
+
168
+ context 'when mutually exclusive params are nested inside optional hash' do
169
+ let(:path) { '/nested-optional-hash' }
170
+
171
+ context 'when params are passed' do
172
+ let(:params) { { item: { beer: true, wine: true } } }
173
+
174
+ it 'returns a validation error with full names of the params' do
175
+ validate
176
+ expect(last_response.status).to eq 400
177
+ expect(JSON.parse(last_response.body)).to eq(
178
+ 'item[beer],item[wine]' => ['are mutually exclusive']
179
+ )
180
+ end
181
+ end
182
+
183
+ context 'when params are empty' do
184
+ let(:params) { {} }
185
+
186
+ it 'does not return a validation error' do
187
+ validate
188
+ expect(last_response.status).to eq 201
189
+ end
190
+ end
191
+ end
192
+
193
+ context 'when mutually exclusive params are nested inside array' do
194
+ let(:path) { '/nested-array' }
195
+ let(:params) { { items: [{ beer: true, wine: true }, { wine: true, grapefruit: true }] } }
196
+
197
+ it 'returns a validation error with full names of the params' do
198
+ validate
199
+ expect(last_response.status).to eq 400
200
+ expect(JSON.parse(last_response.body)).to eq(
201
+ 'items[0][beer],items[0][wine]' => ['are mutually exclusive'],
202
+ 'items[1][wine],items[1][grapefruit]' => ['are mutually exclusive']
203
+ )
204
+ end
205
+ end
206
+
207
+ context 'when mutually exclusive params are deeply nested' do
208
+ let(:path) { '/deeply-nested-array' }
209
+ let(:params) { { items: [{ nested_items: [{ beer: true, wine: true }] }] } }
210
+
211
+ it 'returns a validation error with full names of the params' do
212
+ validate
213
+ expect(last_response.status).to eq 400
214
+ expect(JSON.parse(last_response.body)).to eq(
215
+ 'items[0][nested_items][0][beer],items[0][nested_items][0][wine]' => ['are mutually exclusive']
216
+ )
60
217
  end
61
218
  end
62
219
  end