grape 1.5.0 → 1.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -2
  3. data/README.md +43 -10
  4. data/UPGRADING.md +91 -0
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +13 -17
  7. data/lib/grape/api.rb +7 -14
  8. data/lib/grape/cookies.rb +2 -0
  9. data/lib/grape/dsl/callbacks.rb +1 -1
  10. data/lib/grape/dsl/desc.rb +3 -5
  11. data/lib/grape/dsl/helpers.rb +6 -4
  12. data/lib/grape/dsl/inside_route.rb +18 -9
  13. data/lib/grape/dsl/middleware.rb +4 -4
  14. data/lib/grape/dsl/parameters.rb +11 -7
  15. data/lib/grape/dsl/request_response.rb +9 -6
  16. data/lib/grape/dsl/routing.rb +7 -6
  17. data/lib/grape/dsl/settings.rb +5 -5
  18. data/lib/grape/endpoint.rb +21 -36
  19. data/lib/grape/error_formatter/json.rb +2 -6
  20. data/lib/grape/error_formatter/xml.rb +2 -6
  21. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  22. data/lib/grape/exceptions/validation.rb +2 -3
  23. data/lib/grape/exceptions/validation_errors.rb +1 -1
  24. data/lib/grape/formatter/json.rb +1 -0
  25. data/lib/grape/formatter/serializable_hash.rb +2 -1
  26. data/lib/grape/formatter/xml.rb +1 -0
  27. data/lib/grape/locale/en.yml +1 -1
  28. data/lib/grape/middleware/auth/base.rb +3 -3
  29. data/lib/grape/middleware/base.rb +4 -2
  30. data/lib/grape/middleware/error.rb +1 -1
  31. data/lib/grape/middleware/formatter.rb +4 -4
  32. data/lib/grape/middleware/stack.rb +10 -16
  33. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -5
  34. data/lib/grape/middleware/versioner/header.rb +6 -4
  35. data/lib/grape/middleware/versioner/param.rb +1 -0
  36. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
  37. data/lib/grape/middleware/versioner/path.rb +2 -0
  38. data/lib/grape/parser/json.rb +1 -1
  39. data/lib/grape/parser/xml.rb +1 -1
  40. data/lib/grape/path.rb +1 -0
  41. data/lib/grape/request.rb +3 -0
  42. data/lib/grape/router/attribute_translator.rb +1 -1
  43. data/lib/grape/router/pattern.rb +1 -1
  44. data/lib/grape/router/route.rb +2 -2
  45. data/lib/grape/router.rb +6 -0
  46. data/lib/grape/util/inheritable_setting.rb +1 -3
  47. data/lib/grape/util/lazy_value.rb +3 -2
  48. data/lib/grape/validations/attributes_iterator.rb +8 -0
  49. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  50. data/lib/grape/validations/params_scope.rb +92 -58
  51. data/lib/grape/validations/single_attribute_iterator.rb +1 -1
  52. data/lib/grape/validations/types/custom_type_coercer.rb +3 -2
  53. data/lib/grape/validations/types/dry_type_coercer.rb +1 -1
  54. data/lib/grape/validations/types/invalid_value.rb +24 -0
  55. data/lib/grape/validations/types/json.rb +2 -1
  56. data/lib/grape/validations/types/primitive_coercer.rb +3 -3
  57. data/lib/grape/validations/types.rb +1 -4
  58. data/lib/grape/validations/validator_factory.rb +1 -1
  59. data/lib/grape/validations/validators/all_or_none.rb +1 -0
  60. data/lib/grape/validations/validators/as.rb +4 -8
  61. data/lib/grape/validations/validators/at_least_one_of.rb +1 -0
  62. data/lib/grape/validations/validators/base.rb +12 -7
  63. data/lib/grape/validations/validators/coerce.rb +8 -9
  64. data/lib/grape/validations/validators/default.rb +1 -0
  65. data/lib/grape/validations/validators/exactly_one_of.rb +1 -0
  66. data/lib/grape/validations/validators/multiple_params_base.rb +5 -2
  67. data/lib/grape/validations/validators/mutual_exclusion.rb +1 -0
  68. data/lib/grape/validations/validators/presence.rb +1 -0
  69. data/lib/grape/validations/validators/regexp.rb +1 -0
  70. data/lib/grape/validations/validators/same_as.rb +1 -0
  71. data/lib/grape/validations/validators/values.rb +3 -0
  72. data/lib/grape/version.rb +1 -1
  73. data/lib/grape.rb +3 -1
  74. data/spec/grape/api/custom_validations_spec.rb +1 -0
  75. data/spec/grape/api/routes_with_requirements_spec.rb +8 -8
  76. data/spec/grape/api_remount_spec.rb +9 -4
  77. data/spec/grape/api_spec.rb +203 -37
  78. data/spec/grape/dsl/callbacks_spec.rb +1 -1
  79. data/spec/grape/dsl/middleware_spec.rb +1 -1
  80. data/spec/grape/dsl/parameters_spec.rb +1 -0
  81. data/spec/grape/dsl/routing_spec.rb +1 -1
  82. data/spec/grape/endpoint/declared_spec.rb +259 -1
  83. data/spec/grape/endpoint_spec.rb +18 -5
  84. data/spec/grape/entity_spec.rb +10 -10
  85. data/spec/grape/middleware/auth/dsl_spec.rb +1 -1
  86. data/spec/grape/middleware/error_spec.rb +1 -2
  87. data/spec/grape/middleware/formatter_spec.rb +2 -2
  88. data/spec/grape/middleware/stack_spec.rb +4 -3
  89. data/spec/grape/request_spec.rb +1 -1
  90. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
  91. data/spec/grape/validations/params_scope_spec.rb +37 -3
  92. data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
  93. data/spec/grape/validations/types/primitive_coercer_spec.rb +2 -2
  94. data/spec/grape/validations/validators/coerce_spec.rb +129 -22
  95. data/spec/grape/validations/validators/except_values_spec.rb +2 -2
  96. data/spec/grape/validations/validators/values_spec.rb +15 -11
  97. data/spec/grape/validations_spec.rb +280 -0
  98. data/spec/shared/versioning_examples.rb +22 -22
  99. data/spec/spec_helper.rb +1 -1
  100. data/spec/support/basic_auth_encode_helpers.rb +1 -1
  101. data/spec/support/versioned_helpers.rb +1 -1
  102. metadata +8 -6
@@ -31,9 +31,9 @@ describe Grape::Validations::CoerceValidator do
31
31
 
32
32
  it 'i18n error on malformed input' do
33
33
  I18n.available_locales = %i[en zh-CN]
34
- I18n.load_path << File.expand_path('../zh-CN.yml', __FILE__)
34
+ I18n.load_path << File.expand_path('zh-CN.yml', __dir__)
35
35
  I18n.reload!
36
- I18n.locale = 'zh-CN'.to_sym
36
+ I18n.locale = :'zh-CN'
37
37
  subject.params do
38
38
  requires :age, type: Integer
39
39
  end
@@ -48,7 +48,7 @@ describe Grape::Validations::CoerceValidator do
48
48
 
49
49
  it 'gives an english fallback error when default locale message is blank' do
50
50
  I18n.available_locales = %i[en pt-BR]
51
- I18n.locale = 'pt-BR'.to_sym
51
+ I18n.locale = :'pt-BR'
52
52
  subject.params do
53
53
  requires :age, type: Integer
54
54
  end
@@ -84,9 +84,10 @@ describe Grape::Validations::CoerceValidator do
84
84
  before do
85
85
  subject.params do
86
86
  requires :a, types: { value: [Boolean, String], message: 'type cast is invalid' }, coerce_with: (lambda do |val|
87
- if val == 'yup'
87
+ case val
88
+ when 'yup'
88
89
  true
89
- elsif val == 'false'
90
+ when 'false'
90
91
  0
91
92
  else
92
93
  val
@@ -227,23 +228,51 @@ describe Grape::Validations::CoerceValidator do
227
228
  expect(last_response.body).to eq('NilClass')
228
229
  end
229
230
 
230
- it 'is a custom type' do
231
- subject.params do
232
- requires :uri, coerce: SecureURIOnly
233
- end
234
- subject.get '/secure_uri' do
235
- params[:uri].class
231
+ context 'a custom type' do
232
+ it 'coerces the given value' do
233
+ subject.params do
234
+ requires :uri, coerce: SecureURIOnly
235
+ end
236
+ subject.get '/secure_uri' do
237
+ params[:uri].class
238
+ end
239
+
240
+ get 'secure_uri', uri: 'https://www.example.com'
241
+
242
+ expect(last_response.status).to eq(200)
243
+ expect(last_response.body).to eq('URI::HTTPS')
244
+
245
+ get 'secure_uri', uri: 'http://www.example.com'
246
+
247
+ expect(last_response.status).to eq(400)
248
+ expect(last_response.body).to eq('uri is invalid')
236
249
  end
237
250
 
238
- get 'secure_uri', uri: 'https://www.example.com'
251
+ context 'returning the InvalidValue instance when invalid' do
252
+ let(:custom_type) do
253
+ Class.new do
254
+ def self.parse(_val)
255
+ Grape::Types::InvalidValue.new('must be unique')
256
+ end
257
+ end
258
+ end
259
+
260
+ it 'uses a custom message added to the invalid value' do
261
+ type = custom_type
239
262
 
240
- expect(last_response.status).to eq(200)
241
- expect(last_response.body).to eq('URI::HTTPS')
263
+ subject.params do
264
+ requires :name, type: type
265
+ end
266
+ subject.get '/whatever' do
267
+ params[:name].class
268
+ end
242
269
 
243
- get 'secure_uri', uri: 'http://www.example.com'
270
+ get 'whatever', name: 'Bob'
244
271
 
245
- expect(last_response.status).to eq(400)
246
- expect(last_response.body).to eq('uri is invalid')
272
+ expect(last_response.status).to eq(400)
273
+ expect(last_response.body).to eq('name must be unique')
274
+ end
275
+ end
247
276
  end
248
277
 
249
278
  context 'Array' do
@@ -622,7 +651,7 @@ describe Grape::Validations::CoerceValidator do
622
651
 
623
652
  it 'parses parameters with Array[Array[String]] type and coerce_with' do
624
653
  subject.params do
625
- requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(/,/).map(&:strip)] : val }
654
+ requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(',').map(&:strip)] : val }
626
655
  end
627
656
  subject.post '/coerce_nested_strings' do
628
657
  params[:values]
@@ -678,6 +707,44 @@ describe Grape::Validations::CoerceValidator do
678
707
  expect(JSON.parse(last_response.body)).to eq([1, 1, 1, 1])
679
708
  end
680
709
 
710
+ context 'Array type and coerce_with should' do
711
+ before do
712
+ subject.params do
713
+ optional :arr, type: Array, coerce_with: (lambda do |val|
714
+ if val.nil?
715
+ []
716
+ else
717
+ val
718
+ end
719
+ end)
720
+ end
721
+ subject.get '/' do
722
+ params[:arr].class.to_s
723
+ end
724
+ end
725
+
726
+ it 'coerce nil value to array' do
727
+ get '/', arr: nil
728
+
729
+ expect(last_response.status).to eq(200)
730
+ expect(last_response.body).to eq('Array')
731
+ end
732
+
733
+ it 'not coerce missing field' do
734
+ get '/'
735
+
736
+ expect(last_response.status).to eq(200)
737
+ expect(last_response.body).to eq('NilClass')
738
+ end
739
+
740
+ it 'coerce array as array' do
741
+ get '/', arr: []
742
+
743
+ expect(last_response.status).to eq(200)
744
+ expect(last_response.body).to eq('Array')
745
+ end
746
+ end
747
+
681
748
  it 'uses parse where available' do
682
749
  subject.params do
683
750
  requires :ints, type: Array, coerce_with: JSON do
@@ -726,13 +793,52 @@ describe Grape::Validations::CoerceValidator do
726
793
  expect(last_response.body).to eq('3')
727
794
  end
728
795
 
796
+ context 'Integer type and coerce_with should' do
797
+ before do
798
+ subject.params do
799
+ optional :int, type: Integer, coerce_with: (lambda do |val|
800
+ if val.nil?
801
+ 0
802
+ else
803
+ val.to_i
804
+ end
805
+ end)
806
+ end
807
+ subject.get '/' do
808
+ params[:int].class.to_s
809
+ end
810
+ end
811
+
812
+ it 'coerce nil value to integer' do
813
+ get '/', int: nil
814
+
815
+ expect(last_response.status).to eq(200)
816
+ expect(last_response.body).to eq('Integer')
817
+ end
818
+
819
+ it 'not coerce missing field' do
820
+ get '/'
821
+
822
+ expect(last_response.status).to eq(200)
823
+ expect(last_response.body).to eq('NilClass')
824
+ end
825
+
826
+ it 'coerce integer as integer' do
827
+ get '/', int: 1
828
+
829
+ expect(last_response.status).to eq(200)
830
+ expect(last_response.body).to eq('Integer')
831
+ end
832
+ end
833
+
729
834
  context 'Integer type and coerce_with potentially returning nil' do
730
835
  before do
731
836
  subject.params do
732
837
  requires :int, type: Integer, coerce_with: (lambda do |val|
733
- if val == '0'
838
+ case val
839
+ when '0'
734
840
  nil
735
- elsif val.match?(/^-?\d+$/)
841
+ when /^-?\d+$/
736
842
  val.to_i
737
843
  else
738
844
  val
@@ -1097,9 +1203,10 @@ describe Grape::Validations::CoerceValidator do
1097
1203
  before do
1098
1204
  subject.params do
1099
1205
  requires :a, types: [Boolean, String], coerce_with: (lambda do |val|
1100
- if val == 'yup'
1206
+ case val
1207
+ when 'yup'
1101
1208
  true
1102
- elsif val == 'false'
1209
+ when 'false'
1103
1210
  0
1104
1211
  else
1105
1212
  val
@@ -5,7 +5,7 @@ require 'spec_helper'
5
5
  describe Grape::Validations::ExceptValuesValidator do
6
6
  module ValidationsSpec
7
7
  class ExceptValuesModel
8
- DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze
8
+ DEFAULT_EXCEPTS = %w[invalid-type1 invalid-type2 invalid-type3].freeze
9
9
  class << self
10
10
  attr_accessor :excepts
11
11
 
@@ -170,7 +170,7 @@ describe Grape::Validations::ExceptValuesValidator do
170
170
  it 'raises IncompatibleOptionValues when type is incompatible with values array' do
171
171
  subject = Class.new(Grape::API)
172
172
  expect do
173
- subject.params { optional :type, except_values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol }
173
+ subject.params { optional :type, except_values: %w[valid-type1 valid-type2 valid-type3], type: Symbol }
174
174
  end.to raise_error Grape::Exceptions::IncompatibleOptionValues
175
175
  end
176
176
 
@@ -5,8 +5,8 @@ require 'spec_helper'
5
5
  describe Grape::Validations::ValuesValidator do
6
6
  module ValidationsSpec
7
7
  class ValuesModel
8
- DEFAULT_VALUES = ['valid-type1', 'valid-type2', 'valid-type3'].freeze
9
- DEFAULT_EXCEPTS = ['invalid-type1', 'invalid-type2', 'invalid-type3'].freeze
8
+ DEFAULT_VALUES = %w[valid-type1 valid-type2 valid-type3].freeze
9
+ DEFAULT_EXCEPTS = %w[invalid-type1 invalid-type2 invalid-type3].freeze
10
10
  class << self
11
11
  def values
12
12
  @values ||= []
@@ -27,6 +27,10 @@ describe Grape::Validations::ValuesValidator do
27
27
  @excepts ||= []
28
28
  @excepts << except
29
29
  end
30
+
31
+ def include?(value)
32
+ values.include?(value)
33
+ end
30
34
  end
31
35
  end
32
36
 
@@ -106,7 +110,7 @@ describe Grape::Validations::ValuesValidator do
106
110
  end
107
111
 
108
112
  params do
109
- requires :type, values: ->(v) { ValuesModel.values.include? v }
113
+ requires :type, values: ->(v) { ValuesModel.include? v }
110
114
  end
111
115
  get '/lambda_val' do
112
116
  { type: params[:type] }
@@ -214,14 +218,14 @@ describe Grape::Validations::ValuesValidator do
214
218
  put '/optional_with_array_of_string_values'
215
219
 
216
220
  params do
217
- requires :type, values: { proc: ->(v) { ValuesModel.values.include? v } }
221
+ requires :type, values: { proc: ->(v) { ValuesModel.include? v } }
218
222
  end
219
223
  get '/proc' do
220
224
  { type: params[:type] }
221
225
  end
222
226
 
223
227
  params do
224
- requires :type, values: { proc: ->(v) { ValuesModel.values.include? v }, message: 'failed check' }
228
+ requires :type, values: { proc: ->(v) { ValuesModel.include? v }, message: 'failed check' }
225
229
  end
226
230
  get '/proc/message'
227
231
 
@@ -420,21 +424,21 @@ describe Grape::Validations::ValuesValidator do
420
424
  it 'raises IncompatibleOptionValues on an invalid default value from proc' do
421
425
  subject = Class.new(Grape::API)
422
426
  expect do
423
- subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: ValidationsSpec::ValuesModel.values.sample + '_invalid' }
427
+ subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: "#{ValidationsSpec::ValuesModel.values.sample}_invalid" }
424
428
  end.to raise_error Grape::Exceptions::IncompatibleOptionValues
425
429
  end
426
430
 
427
431
  it 'raises IncompatibleOptionValues on an invalid default value' do
428
432
  subject = Class.new(Grape::API)
429
433
  expect do
430
- subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], default: 'invalid-type' }
434
+ subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], default: 'invalid-type' }
431
435
  end.to raise_error Grape::Exceptions::IncompatibleOptionValues
432
436
  end
433
437
 
434
438
  it 'raises IncompatibleOptionValues when type is incompatible with values array' do
435
439
  subject = Class.new(Grape::API)
436
440
  expect do
437
- subject.params { optional :type, values: ['valid-type1', 'valid-type2', 'valid-type3'], type: Symbol }
441
+ subject.params { optional :type, values: %w[valid-type1 valid-type2 valid-type3], type: Symbol }
438
442
  end.to raise_error Grape::Exceptions::IncompatibleOptionValues
439
443
  end
440
444
 
@@ -648,9 +652,9 @@ describe Grape::Validations::ValuesValidator do
648
652
  end
649
653
 
650
654
  it 'accepts multiple valid values' do
651
- get '/proc', type: ['valid-type1', 'valid-type3']
655
+ get '/proc', type: %w[valid-type1 valid-type3]
652
656
  expect(last_response.status).to eq 200
653
- expect(last_response.body).to eq({ type: ['valid-type1', 'valid-type3'] }.to_json)
657
+ expect(last_response.body).to eq({ type: %w[valid-type1 valid-type3] }.to_json)
654
658
  end
655
659
 
656
660
  it 'rejects a single invalid value' do
@@ -660,7 +664,7 @@ describe Grape::Validations::ValuesValidator do
660
664
  end
661
665
 
662
666
  it 'rejects an invalid value among valid ones' do
663
- get '/proc', type: ['valid-type1', 'invalid-type1', 'valid-type3']
667
+ get '/proc', type: %w[valid-type1 invalid-type1 valid-type3]
664
668
  expect(last_response.status).to eq 400
665
669
  expect(last_response.body).to eq({ error: 'type does not have a valid value' }.to_json)
666
670
  end
@@ -494,6 +494,7 @@ describe Grape::Validations do
494
494
  class DateRangeValidator < Grape::Validations::Base
495
495
  def validate_param!(attr_name, params)
496
496
  return if params[attr_name][:from] <= params[attr_name][:to]
497
+
497
498
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: "'from' must be lower or equal to 'to'")
498
499
  end
499
500
  end
@@ -883,6 +884,283 @@ describe Grape::Validations do
883
884
  end
884
885
  expect(declared_params).to eq([items: [:key, { optional_subitems: [:value] }, { required_subitems: [:value] }]])
885
886
  end
887
+
888
+ context <<~DESC do
889
+ Issue occurs whenever:
890
+ * param structure with at least three levels
891
+ * 1st level item is a required Array that has >1 entry with an optional item present and >1 entry with an optional item missing#{' '}
892
+ * 2nd level is an optional Array or Hash#{' '}
893
+ * 3rd level is a required item (can be any type)
894
+ * additional levels do not effect the issue from occuring
895
+ DESC
896
+
897
+ it 'example based off actual real world use case' do
898
+ subject.params do
899
+ requires :orders, type: Array do
900
+ requires :id, type: Integer
901
+ optional :drugs, type: Array do
902
+ requires :batches, type: Array do
903
+ requires :batch_no, type: String
904
+ end
905
+ end
906
+ end
907
+ end
908
+
909
+ subject.get '/validate_required_arrays_under_optional_arrays' do
910
+ 'validate_required_arrays_under_optional_arrays works!'
911
+ end
912
+
913
+ data = {
914
+ orders: [
915
+ { id: 77, drugs: [{ batches: [{ batch_no: 'A1234567' }] }] },
916
+ { id: 70 }
917
+ ]
918
+ }
919
+
920
+ get '/validate_required_arrays_under_optional_arrays', data
921
+ expect(last_response.body).to eq('validate_required_arrays_under_optional_arrays works!')
922
+ expect(last_response.status).to eq(200)
923
+ end
924
+
925
+ it 'simplest example using Array -> Array -> Hash -> String' do
926
+ subject.params do
927
+ requires :orders, type: Array do
928
+ requires :id, type: Integer
929
+ optional :drugs, type: Array do
930
+ requires :batch_no, type: String
931
+ end
932
+ end
933
+ end
934
+
935
+ subject.get '/validate_required_arrays_under_optional_arrays' do
936
+ 'validate_required_arrays_under_optional_arrays works!'
937
+ end
938
+
939
+ data = {
940
+ orders: [
941
+ { id: 77, drugs: [{ batch_no: 'A1234567' }] },
942
+ { id: 70 }
943
+ ]
944
+ }
945
+
946
+ get '/validate_required_arrays_under_optional_arrays', data
947
+ expect(last_response.body).to eq('validate_required_arrays_under_optional_arrays works!')
948
+ expect(last_response.status).to eq(200)
949
+ end
950
+
951
+ it 'simplest example using Array -> Hash -> String' do
952
+ subject.params do
953
+ requires :orders, type: Array do
954
+ requires :id, type: Integer
955
+ optional :drugs, type: Hash do
956
+ requires :batch_no, type: String
957
+ end
958
+ end
959
+ end
960
+
961
+ subject.get '/validate_required_arrays_under_optional_arrays' do
962
+ 'validate_required_arrays_under_optional_arrays works!'
963
+ end
964
+
965
+ data = {
966
+ orders: [
967
+ { id: 77, drugs: { batch_no: 'A1234567' } },
968
+ { id: 70 }
969
+ ]
970
+ }
971
+
972
+ get '/validate_required_arrays_under_optional_arrays', data
973
+ expect(last_response.body).to eq('validate_required_arrays_under_optional_arrays works!')
974
+ expect(last_response.status).to eq(200)
975
+ end
976
+
977
+ it 'correctly indexes invalida data' do
978
+ subject.params do
979
+ requires :orders, type: Array do
980
+ requires :id, type: Integer
981
+ optional :drugs, type: Array do
982
+ requires :batch_no, type: String
983
+ requires :quantity, type: Integer
984
+ end
985
+ end
986
+ end
987
+
988
+ subject.get '/correctly_indexes' do
989
+ 'correctly_indexes works!'
990
+ end
991
+
992
+ data = {
993
+ orders: [
994
+ { id: 70 },
995
+ { id: 77, drugs: [{ batch_no: 'A1234567', quantity: 12 }, { batch_no: 'B222222' }] }
996
+ ]
997
+ }
998
+
999
+ get '/correctly_indexes', data
1000
+ expect(last_response.body).to eq('orders[1][drugs][1][quantity] is missing')
1001
+ expect(last_response.status).to eq(400)
1002
+ end
1003
+
1004
+ context 'multiple levels of optional and requires settings' do
1005
+ before do
1006
+ subject.params do
1007
+ requires :top, type: Array do
1008
+ requires :top_id, type: Integer, allow_blank: false
1009
+ optional :middle_1, type: Array do
1010
+ requires :middle_1_id, type: Integer, allow_blank: false
1011
+ optional :middle_2, type: Array do
1012
+ requires :middle_2_id, type: String, allow_blank: false
1013
+ optional :bottom, type: Array do
1014
+ requires :bottom_id, type: Integer, allow_blank: false
1015
+ end
1016
+ end
1017
+ end
1018
+ end
1019
+ end
1020
+
1021
+ subject.get '/multi_level' do
1022
+ 'multi_level works!'
1023
+ end
1024
+ end
1025
+
1026
+ it 'with valid data' do
1027
+ data = {
1028
+ top: [
1029
+ { top_id: 1, middle_1: [
1030
+ { middle_1_id: 11 }, { middle_1_id: 12, middle_2: [
1031
+ { middle_2_id: 121 }, { middle_2_id: 122, bottom: [{ bottom_id: 1221 }] }
1032
+ ] }
1033
+ ] },
1034
+ { top_id: 2, middle_1: [
1035
+ { middle_1_id: 21 }, { middle_1_id: 22, middle_2: [
1036
+ { middle_2_id: 221 }
1037
+ ] }
1038
+ ] },
1039
+ { top_id: 3, middle_1: [
1040
+ { middle_1_id: 31 }, { middle_1_id: 32 }
1041
+ ] },
1042
+ { top_id: 4 }
1043
+ ]
1044
+ }
1045
+
1046
+ get '/multi_level', data
1047
+ expect(last_response.body).to eq('multi_level works!')
1048
+ expect(last_response.status).to eq(200)
1049
+ end
1050
+
1051
+ it 'with invalid data' do
1052
+ data = {
1053
+ top: [
1054
+ { top_id: 1, middle_1: [
1055
+ { middle_1_id: 11 }, { middle_1_id: 12, middle_2: [
1056
+ { middle_2_id: 121 }, { middle_2_id: 122, bottom: [{ bottom_id: nil }] }
1057
+ ] }
1058
+ ] },
1059
+ { top_id: 2, middle_1: [
1060
+ { middle_1_id: 21 }, { middle_1_id: 22, middle_2: [{ middle_2_id: nil }] }
1061
+ ] },
1062
+ { top_id: 3, middle_1: [
1063
+ { middle_1_id: nil }, { middle_1_id: 32 }
1064
+ ] },
1065
+ { top_id: nil, missing_top_id: 4 }
1066
+ ]
1067
+ }
1068
+ # debugger
1069
+ get '/multi_level', data
1070
+ expect(last_response.body.split(', ')).to match_array([
1071
+ 'top[3][top_id] is empty',
1072
+ 'top[2][middle_1][0][middle_1_id] is empty',
1073
+ 'top[1][middle_1][1][middle_2][0][middle_2_id] is empty',
1074
+ 'top[0][middle_1][1][middle_2][1][bottom][0][bottom_id] is empty'
1075
+ ])
1076
+ expect(last_response.status).to eq(400)
1077
+ end
1078
+ end
1079
+ end
1080
+
1081
+ it 'exactly_one_of' do
1082
+ subject.params do
1083
+ requires :orders, type: Array do
1084
+ requires :id, type: Integer
1085
+ optional :drugs, type: Hash do
1086
+ optional :batch_no, type: String
1087
+ optional :batch_id, type: String
1088
+ exactly_one_of :batch_no, :batch_id
1089
+ end
1090
+ end
1091
+ end
1092
+
1093
+ subject.get '/exactly_one_of' do
1094
+ 'exactly_one_of works!'
1095
+ end
1096
+
1097
+ data = {
1098
+ orders: [
1099
+ { id: 77, drugs: { batch_no: 'A1234567' } },
1100
+ { id: 70 }
1101
+ ]
1102
+ }
1103
+
1104
+ get '/exactly_one_of', data
1105
+ expect(last_response.body).to eq('exactly_one_of works!')
1106
+ expect(last_response.status).to eq(200)
1107
+ end
1108
+
1109
+ it 'at_least_one_of' do
1110
+ subject.params do
1111
+ requires :orders, type: Array do
1112
+ requires :id, type: Integer
1113
+ optional :drugs, type: Hash do
1114
+ optional :batch_no, type: String
1115
+ optional :batch_id, type: String
1116
+ at_least_one_of :batch_no, :batch_id
1117
+ end
1118
+ end
1119
+ end
1120
+
1121
+ subject.get '/at_least_one_of' do
1122
+ 'at_least_one_of works!'
1123
+ end
1124
+
1125
+ data = {
1126
+ orders: [
1127
+ { id: 77, drugs: { batch_no: 'A1234567' } },
1128
+ { id: 70 }
1129
+ ]
1130
+ }
1131
+
1132
+ get '/at_least_one_of', data
1133
+ expect(last_response.body).to eq('at_least_one_of works!')
1134
+ expect(last_response.status).to eq(200)
1135
+ end
1136
+
1137
+ it 'all_or_none_of' do
1138
+ subject.params do
1139
+ requires :orders, type: Array do
1140
+ requires :id, type: Integer
1141
+ optional :drugs, type: Hash do
1142
+ optional :batch_no, type: String
1143
+ optional :batch_id, type: String
1144
+ all_or_none_of :batch_no, :batch_id
1145
+ end
1146
+ end
1147
+ end
1148
+
1149
+ subject.get '/all_or_none_of' do
1150
+ 'all_or_none_of works!'
1151
+ end
1152
+
1153
+ data = {
1154
+ orders: [
1155
+ { id: 77, drugs: { batch_no: 'A1234567', batch_id: '12' } },
1156
+ { id: 70 }
1157
+ ]
1158
+ }
1159
+
1160
+ get '/all_or_none_of', data
1161
+ expect(last_response.body).to eq('all_or_none_of works!')
1162
+ expect(last_response.status).to eq(200)
1163
+ end
886
1164
  end
887
1165
 
888
1166
  context 'multiple validation errors' do
@@ -909,6 +1187,7 @@ describe Grape::Validations do
909
1187
  class Customvalidator < Grape::Validations::Base
910
1188
  def validate_param!(attr_name, params)
911
1189
  return if params[attr_name] == 'im custom'
1190
+
912
1191
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: 'is not custom!')
913
1192
  end
914
1193
  end
@@ -1057,6 +1336,7 @@ describe Grape::Validations do
1057
1336
  class CustomvalidatorWithOptions < Grape::Validations::Base
1058
1337
  def validate_param!(attr_name, params)
1059
1338
  return if params[attr_name] == @option[:text]
1339
+
1060
1340
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message)
1061
1341
  end
1062
1342
  end