grape 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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