grape 1.1.0 → 1.5.3

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 (286) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +278 -44
  3. data/LICENSE +1 -1
  4. data/README.md +514 -69
  5. data/UPGRADING.md +424 -17
  6. data/grape.gemspec +13 -2
  7. data/lib/grape.rb +104 -71
  8. data/lib/grape/api.rb +138 -175
  9. data/lib/grape/api/helpers.rb +2 -0
  10. data/lib/grape/api/instance.rb +283 -0
  11. data/lib/grape/config.rb +34 -0
  12. data/lib/grape/content_types.rb +34 -0
  13. data/lib/grape/cookies.rb +2 -0
  14. data/lib/grape/dsl/api.rb +2 -0
  15. data/lib/grape/dsl/callbacks.rb +22 -0
  16. data/lib/grape/dsl/configuration.rb +2 -0
  17. data/lib/grape/dsl/desc.rb +41 -7
  18. data/lib/grape/dsl/headers.rb +2 -0
  19. data/lib/grape/dsl/helpers.rb +5 -2
  20. data/lib/grape/dsl/inside_route.rb +92 -49
  21. data/lib/grape/dsl/logger.rb +2 -0
  22. data/lib/grape/dsl/middleware.rb +9 -0
  23. data/lib/grape/dsl/parameters.rb +25 -14
  24. data/lib/grape/dsl/request_response.rb +4 -2
  25. data/lib/grape/dsl/routing.rb +17 -10
  26. data/lib/grape/dsl/settings.rb +7 -1
  27. data/lib/grape/dsl/validations.rb +24 -4
  28. data/lib/grape/eager_load.rb +20 -0
  29. data/lib/grape/endpoint.rb +59 -35
  30. data/lib/grape/error_formatter.rb +4 -2
  31. data/lib/grape/error_formatter/base.rb +2 -0
  32. data/lib/grape/error_formatter/json.rb +2 -0
  33. data/lib/grape/error_formatter/txt.rb +2 -0
  34. data/lib/grape/error_formatter/xml.rb +2 -0
  35. data/lib/grape/exceptions/base.rb +20 -14
  36. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  37. data/lib/grape/exceptions/incompatible_option_values.rb +2 -0
  38. data/lib/grape/exceptions/invalid_accept_header.rb +2 -0
  39. data/lib/grape/exceptions/invalid_formatter.rb +2 -0
  40. data/lib/grape/exceptions/invalid_message_body.rb +2 -0
  41. data/lib/grape/exceptions/invalid_response.rb +11 -0
  42. data/lib/grape/exceptions/invalid_version_header.rb +2 -0
  43. data/lib/grape/exceptions/invalid_versioner_option.rb +2 -0
  44. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +2 -0
  45. data/lib/grape/exceptions/method_not_allowed.rb +2 -0
  46. data/lib/grape/exceptions/missing_group_type.rb +2 -0
  47. data/lib/grape/exceptions/missing_mime_type.rb +2 -0
  48. data/lib/grape/exceptions/missing_option.rb +2 -0
  49. data/lib/grape/exceptions/missing_vendor_option.rb +2 -0
  50. data/lib/grape/exceptions/unknown_options.rb +2 -0
  51. data/lib/grape/exceptions/unknown_parameter.rb +2 -0
  52. data/lib/grape/exceptions/unknown_validator.rb +2 -0
  53. data/lib/grape/exceptions/unsupported_group_type.rb +2 -0
  54. data/lib/grape/exceptions/validation.rb +4 -2
  55. data/lib/grape/exceptions/validation_array_errors.rb +2 -0
  56. data/lib/grape/exceptions/validation_errors.rb +16 -13
  57. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +4 -3
  58. data/lib/grape/extensions/deep_mergeable_hash.rb +2 -0
  59. data/lib/grape/extensions/deep_symbolize_hash.rb +2 -0
  60. data/lib/grape/extensions/hash.rb +2 -0
  61. data/lib/grape/extensions/hashie/mash.rb +2 -0
  62. data/lib/grape/formatter.rb +5 -3
  63. data/lib/grape/formatter/json.rb +2 -0
  64. data/lib/grape/formatter/serializable_hash.rb +2 -0
  65. data/lib/grape/formatter/txt.rb +2 -0
  66. data/lib/grape/formatter/xml.rb +2 -0
  67. data/lib/grape/http/headers.rb +50 -18
  68. data/lib/grape/locale/en.yml +3 -1
  69. data/lib/grape/middleware/auth/base.rb +7 -7
  70. data/lib/grape/middleware/auth/dsl.rb +2 -0
  71. data/lib/grape/middleware/auth/strategies.rb +2 -0
  72. data/lib/grape/middleware/auth/strategy_info.rb +2 -0
  73. data/lib/grape/middleware/base.rb +10 -7
  74. data/lib/grape/middleware/error.rb +21 -16
  75. data/lib/grape/middleware/filter.rb +2 -0
  76. data/lib/grape/middleware/formatter.rb +8 -6
  77. data/lib/grape/middleware/globals.rb +2 -0
  78. data/lib/grape/middleware/helpers.rb +12 -0
  79. data/lib/grape/middleware/stack.rb +13 -3
  80. data/lib/grape/middleware/versioner.rb +2 -0
  81. data/lib/grape/middleware/versioner/accept_version_header.rb +2 -0
  82. data/lib/grape/middleware/versioner/header.rb +10 -8
  83. data/lib/grape/middleware/versioner/param.rb +3 -1
  84. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +4 -1
  85. data/lib/grape/middleware/versioner/path.rb +3 -1
  86. data/lib/grape/namespace.rb +14 -2
  87. data/lib/grape/parser.rb +4 -2
  88. data/lib/grape/parser/json.rb +3 -1
  89. data/lib/grape/parser/xml.rb +3 -1
  90. data/lib/grape/path.rb +15 -3
  91. data/lib/grape/presenters/presenter.rb +2 -0
  92. data/lib/grape/request.rb +19 -10
  93. data/lib/grape/router.rb +30 -29
  94. data/lib/grape/router/attribute_translator.rb +41 -8
  95. data/lib/grape/router/pattern.rb +20 -16
  96. data/lib/grape/router/route.rb +14 -28
  97. data/lib/grape/{serve_file → serve_stream}/file_body.rb +3 -1
  98. data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +3 -1
  99. data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +10 -8
  100. data/lib/grape/util/base_inheritable.rb +43 -0
  101. data/lib/grape/util/cache.rb +20 -0
  102. data/lib/grape/util/endpoint_configuration.rb +8 -0
  103. data/lib/grape/util/env.rb +19 -17
  104. data/lib/grape/util/inheritable_setting.rb +2 -0
  105. data/lib/grape/util/inheritable_values.rb +7 -25
  106. data/lib/grape/util/json.rb +2 -0
  107. data/lib/grape/util/lazy_block.rb +27 -0
  108. data/lib/grape/util/lazy_object.rb +43 -0
  109. data/lib/grape/util/lazy_value.rb +98 -0
  110. data/lib/grape/util/registrable.rb +2 -0
  111. data/lib/grape/util/reverse_stackable_values.rb +10 -35
  112. data/lib/grape/util/stackable_values.rb +21 -34
  113. data/lib/grape/util/strict_hash_configuration.rb +2 -0
  114. data/lib/grape/util/xml.rb +2 -0
  115. data/lib/grape/validations.rb +2 -0
  116. data/lib/grape/validations/attributes_iterator.rb +16 -6
  117. data/lib/grape/validations/multiple_attributes_iterator.rb +13 -0
  118. data/lib/grape/validations/params_scope.rb +51 -30
  119. data/lib/grape/validations/single_attribute_iterator.rb +24 -0
  120. data/lib/grape/validations/types.rb +13 -38
  121. data/lib/grape/validations/types/array_coercer.rb +65 -0
  122. data/lib/grape/validations/types/build_coercer.rb +47 -49
  123. data/lib/grape/validations/types/custom_type_coercer.rb +29 -51
  124. data/lib/grape/validations/types/custom_type_collection_coercer.rb +10 -25
  125. data/lib/grape/validations/types/dry_type_coercer.rb +76 -0
  126. data/lib/grape/validations/types/file.rb +22 -18
  127. data/lib/grape/validations/types/invalid_value.rb +24 -0
  128. data/lib/grape/validations/types/json.rb +46 -39
  129. data/lib/grape/validations/types/multiple_type_coercer.rb +14 -33
  130. data/lib/grape/validations/types/primitive_coercer.rb +67 -0
  131. data/lib/grape/validations/types/set_coercer.rb +40 -0
  132. data/lib/grape/validations/types/variant_collection_coercer.rb +5 -13
  133. data/lib/grape/validations/validator_factory.rb +8 -11
  134. data/lib/grape/validations/validators/all_or_none.rb +8 -13
  135. data/lib/grape/validations/validators/allow_blank.rb +3 -1
  136. data/lib/grape/validations/validators/as.rb +5 -4
  137. data/lib/grape/validations/validators/at_least_one_of.rb +7 -13
  138. data/lib/grape/validations/validators/base.rb +20 -16
  139. data/lib/grape/validations/validators/coerce.rb +46 -29
  140. data/lib/grape/validations/validators/default.rb +6 -6
  141. data/lib/grape/validations/validators/exactly_one_of.rb +10 -23
  142. data/lib/grape/validations/validators/except_values.rb +4 -2
  143. data/lib/grape/validations/validators/multiple_params_base.rb +17 -10
  144. data/lib/grape/validations/validators/mutual_exclusion.rb +8 -18
  145. data/lib/grape/validations/validators/presence.rb +3 -1
  146. data/lib/grape/validations/validators/regexp.rb +4 -2
  147. data/lib/grape/validations/validators/same_as.rb +26 -0
  148. data/lib/grape/validations/validators/values.rb +18 -6
  149. data/lib/grape/version.rb +3 -1
  150. data/spec/grape/api/custom_validations_spec.rb +5 -3
  151. data/spec/grape/api/deeply_included_options_spec.rb +2 -0
  152. data/spec/grape/api/defines_boolean_in_params_spec.rb +39 -0
  153. data/spec/grape/api/inherited_helpers_spec.rb +2 -0
  154. data/spec/grape/api/instance_spec.rb +104 -0
  155. data/spec/grape/api/invalid_format_spec.rb +2 -0
  156. data/spec/grape/api/namespace_parameters_in_route_spec.rb +2 -0
  157. data/spec/grape/api/nested_helpers_spec.rb +2 -0
  158. data/spec/grape/api/optional_parameters_in_route_spec.rb +2 -0
  159. data/spec/grape/api/parameters_modification_spec.rb +3 -1
  160. data/spec/grape/api/patch_method_helpers_spec.rb +2 -0
  161. data/spec/grape/api/recognize_path_spec.rb +2 -0
  162. data/spec/grape/api/required_parameters_in_route_spec.rb +2 -0
  163. data/spec/grape/api/required_parameters_with_invalid_method_spec.rb +2 -0
  164. data/spec/grape/api/routes_with_requirements_spec.rb +61 -0
  165. data/spec/grape/api/shared_helpers_exactly_one_of_spec.rb +2 -0
  166. data/spec/grape/api/shared_helpers_spec.rb +2 -0
  167. data/spec/grape/api_remount_spec.rb +473 -0
  168. data/spec/grape/api_spec.rb +565 -12
  169. data/spec/grape/config_spec.rb +19 -0
  170. data/spec/grape/dsl/callbacks_spec.rb +2 -0
  171. data/spec/grape/dsl/configuration_spec.rb +2 -0
  172. data/spec/grape/dsl/desc_spec.rb +42 -16
  173. data/spec/grape/dsl/headers_spec.rb +2 -0
  174. data/spec/grape/dsl/helpers_spec.rb +4 -2
  175. data/spec/grape/dsl/inside_route_spec.rb +184 -33
  176. data/spec/grape/dsl/logger_spec.rb +2 -0
  177. data/spec/grape/dsl/middleware_spec.rb +10 -0
  178. data/spec/grape/dsl/parameters_spec.rb +2 -0
  179. data/spec/grape/dsl/request_response_spec.rb +2 -0
  180. data/spec/grape/dsl/routing_spec.rb +12 -0
  181. data/spec/grape/dsl/settings_spec.rb +2 -0
  182. data/spec/grape/dsl/validations_spec.rb +2 -0
  183. data/spec/grape/endpoint/declared_spec.rb +601 -0
  184. data/spec/grape/endpoint_spec.rb +53 -523
  185. data/spec/grape/entity_spec.rb +9 -1
  186. data/spec/grape/exceptions/base_spec.rb +67 -0
  187. data/spec/grape/exceptions/body_parse_errors_spec.rb +2 -0
  188. data/spec/grape/exceptions/invalid_accept_header_spec.rb +2 -0
  189. data/spec/grape/exceptions/invalid_formatter_spec.rb +2 -0
  190. data/spec/grape/exceptions/invalid_response_spec.rb +13 -0
  191. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +2 -0
  192. data/spec/grape/exceptions/missing_mime_type_spec.rb +2 -0
  193. data/spec/grape/exceptions/missing_option_spec.rb +2 -0
  194. data/spec/grape/exceptions/unknown_options_spec.rb +2 -0
  195. data/spec/grape/exceptions/unknown_validator_spec.rb +2 -0
  196. data/spec/grape/exceptions/validation_errors_spec.rb +8 -4
  197. data/spec/grape/exceptions/validation_spec.rb +3 -1
  198. data/spec/grape/extensions/param_builders/hash_spec.rb +2 -0
  199. data/spec/grape/extensions/param_builders/hash_with_indifferent_access_spec.rb +2 -0
  200. data/spec/grape/extensions/param_builders/hashie/mash_spec.rb +2 -0
  201. data/spec/grape/integration/global_namespace_function_spec.rb +2 -0
  202. data/spec/grape/integration/rack_sendfile_spec.rb +14 -8
  203. data/spec/grape/integration/rack_spec.rb +25 -7
  204. data/spec/grape/loading_spec.rb +2 -0
  205. data/spec/grape/middleware/auth/base_spec.rb +2 -0
  206. data/spec/grape/middleware/auth/dsl_spec.rb +5 -3
  207. data/spec/grape/middleware/auth/strategies_spec.rb +3 -1
  208. data/spec/grape/middleware/base_spec.rb +10 -0
  209. data/spec/grape/middleware/error_spec.rb +3 -1
  210. data/spec/grape/middleware/exception_spec.rb +4 -2
  211. data/spec/grape/middleware/formatter_spec.rb +33 -16
  212. data/spec/grape/middleware/globals_spec.rb +2 -0
  213. data/spec/grape/middleware/stack_spec.rb +12 -0
  214. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +3 -1
  215. data/spec/grape/middleware/versioner/header_spec.rb +9 -1
  216. data/spec/grape/middleware/versioner/param_spec.rb +3 -1
  217. data/spec/grape/middleware/versioner/path_spec.rb +3 -1
  218. data/spec/grape/middleware/versioner_spec.rb +2 -0
  219. data/spec/grape/named_api_spec.rb +21 -0
  220. data/spec/grape/parser_spec.rb +7 -5
  221. data/spec/grape/path_spec.rb +6 -4
  222. data/spec/grape/presenters/presenter_spec.rb +2 -0
  223. data/spec/grape/request_spec.rb +26 -0
  224. data/spec/grape/util/inheritable_setting_spec.rb +2 -0
  225. data/spec/grape/util/inheritable_values_spec.rb +2 -0
  226. data/spec/grape/util/reverse_stackable_values_spec.rb +2 -0
  227. data/spec/grape/util/stackable_values_spec.rb +3 -1
  228. data/spec/grape/util/strict_hash_configuration_spec.rb +2 -0
  229. data/spec/grape/validations/attributes_iterator_spec.rb +2 -0
  230. data/spec/grape/validations/instance_behaivour_spec.rb +5 -3
  231. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +41 -0
  232. data/spec/grape/validations/params_scope_spec.rb +213 -9
  233. data/spec/grape/validations/single_attribute_iterator_spec.rb +58 -0
  234. data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
  235. data/spec/grape/validations/types/primitive_coercer_spec.rb +135 -0
  236. data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
  237. data/spec/grape/validations/types_spec.rb +9 -36
  238. data/spec/grape/validations/validators/all_or_none_spec.rb +140 -30
  239. data/spec/grape/validations/validators/allow_blank_spec.rb +2 -0
  240. data/spec/grape/validations/validators/at_least_one_of_spec.rb +175 -29
  241. data/spec/grape/validations/validators/coerce_spec.rb +476 -135
  242. data/spec/grape/validations/validators/default_spec.rb +172 -0
  243. data/spec/grape/validations/validators/exactly_one_of_spec.rb +204 -38
  244. data/spec/grape/validations/validators/except_values_spec.rb +4 -1
  245. data/spec/grape/validations/validators/mutual_exclusion_spec.rb +186 -27
  246. data/spec/grape/validations/validators/presence_spec.rb +30 -0
  247. data/spec/grape/validations/validators/regexp_spec.rb +2 -0
  248. data/spec/grape/validations/validators/same_as_spec.rb +65 -0
  249. data/spec/grape/validations/validators/values_spec.rb +30 -5
  250. data/spec/grape/validations_spec.rb +388 -50
  251. data/spec/integration/eager_load/eager_load_spec.rb +15 -0
  252. data/spec/integration/multi_json/json_spec.rb +2 -0
  253. data/spec/integration/multi_xml/xml_spec.rb +2 -0
  254. data/spec/shared/versioning_examples.rb +22 -20
  255. data/spec/spec_helper.rb +12 -1
  256. data/spec/support/basic_auth_encode_helpers.rb +2 -0
  257. data/spec/support/chunks.rb +14 -0
  258. data/spec/support/content_type_helpers.rb +2 -0
  259. data/spec/support/eager_load.rb +19 -0
  260. data/spec/support/endpoint_faker.rb +2 -0
  261. data/spec/support/file_streamer.rb +2 -0
  262. data/spec/support/integer_helpers.rb +2 -0
  263. data/spec/support/versioned_helpers.rb +8 -8
  264. metadata +86 -48
  265. data/Appraisals +0 -32
  266. data/Dangerfile +0 -2
  267. data/Gemfile +0 -33
  268. data/Gemfile.lock +0 -231
  269. data/Guardfile +0 -10
  270. data/RELEASING.md +0 -111
  271. data/Rakefile +0 -25
  272. data/benchmark/simple.rb +0 -27
  273. data/benchmark/simple_with_type_coercer.rb +0 -22
  274. data/gemfiles/multi_json.gemfile +0 -35
  275. data/gemfiles/multi_xml.gemfile +0 -35
  276. data/gemfiles/rack_1.5.2.gemfile +0 -35
  277. data/gemfiles/rack_edge.gemfile +0 -35
  278. data/gemfiles/rails_3.gemfile +0 -36
  279. data/gemfiles/rails_4.gemfile +0 -35
  280. data/gemfiles/rails_5.gemfile +0 -35
  281. data/gemfiles/rails_edge.gemfile +0 -35
  282. data/lib/grape/extensions/deep_hash_with_indifferent_access.rb +0 -18
  283. data/lib/grape/util/content_types.rb +0 -26
  284. data/lib/grape/validations/types/virtus_collection_patch.rb +0 -16
  285. data/pkg/grape-0.17.0.gem +0 -0
  286. data/pkg/grape-0.19.0.gem +0 -0
@@ -1,66 +1,212 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe Grape::Validations::AtLeastOneOfValidator do
4
6
  describe '#validate!' do
5
- let(:scope) do
6
- Struct.new(:opts) do
7
- def params(arg)
8
- arg
9
- end
7
+ subject(:validate) { post path, params }
8
+
9
+ module ValidationsSpec
10
+ module AtLeastOneOfValidatorSpec
11
+ class API < Grape::API
12
+ rescue_from Grape::Exceptions::ValidationErrors do |e|
13
+ error!(e.errors.transform_keys! { |key| key.join(',') }, 400)
14
+ end
15
+
16
+ params do
17
+ optional :beer, :wine, :grapefruit
18
+ at_least_one_of :beer, :wine, :grapefruit
19
+ end
20
+ post do
21
+ end
22
+
23
+ params do
24
+ optional :beer, :wine, :grapefruit, :other
25
+ at_least_one_of :beer, :wine, :grapefruit
26
+ end
27
+ post 'mixed-params' do
28
+ end
29
+
30
+ params do
31
+ optional :beer, :wine, :grapefruit
32
+ at_least_one_of :beer, :wine, :grapefruit, message: 'you should choose something'
33
+ end
34
+ post '/custom-message' do
35
+ end
10
36
 
11
- def required?; end
37
+ params do
38
+ requires :item, type: Hash do
39
+ optional :beer, :wine, :grapefruit
40
+ at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
41
+ end
42
+ end
43
+ post '/nested-hash' do
44
+ end
45
+
46
+ params do
47
+ requires :items, type: Array do
48
+ optional :beer, :wine, :grapefruit
49
+ at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
50
+ end
51
+ end
52
+ post '/nested-array' do
53
+ end
54
+
55
+ params do
56
+ requires :items, type: Array do
57
+ requires :nested_items, type: Array do
58
+ optional :beer, :wine, :grapefruit
59
+ at_least_one_of :beer, :wine, :grapefruit, message: 'fail'
60
+ end
61
+ end
62
+ end
63
+ post '/deeply-nested-array' do
64
+ end
65
+ end
12
66
  end
13
67
  end
14
- let(:at_least_one_of_params) { %i[beer wine grapefruit] }
15
- let(:validator) { described_class.new(at_least_one_of_params, {}, false, scope.new) }
68
+
69
+ def app
70
+ ValidationsSpec::AtLeastOneOfValidatorSpec::API
71
+ end
16
72
 
17
73
  context 'when all restricted params are present' do
74
+ let(:path) { '/' }
18
75
  let(:params) { { beer: true, wine: true, grapefruit: true } }
19
76
 
20
- it 'does not raise a validation exception' do
21
- expect(validator.validate!(params)).to eql params
77
+ it 'does not return a validation error' do
78
+ validate
79
+ expect(last_response.status).to eq 201
22
80
  end
23
81
 
24
82
  context 'mixed with other params' do
25
- let(:mixed_params) { params.merge!(other: true, andanother: true) }
83
+ let(:path) { '/mixed-params' }
84
+ let(:params) { { beer: true, wine: true, grapefruit: true, other: true } }
26
85
 
27
- it 'does not raise a validation exception' do
28
- expect(validator.validate!(mixed_params)).to eql mixed_params
86
+ it 'does not return a validation error' do
87
+ validate
88
+ expect(last_response.status).to eq 201
29
89
  end
30
90
  end
31
91
  end
32
92
 
33
93
  context 'when a subset of restricted params are present' do
94
+ let(:path) { '/' }
34
95
  let(:params) { { beer: true, grapefruit: true } }
35
96
 
36
- it 'does not raise a validation exception' do
37
- expect(validator.validate!(params)).to eql params
97
+ it 'does not return a validation error' do
98
+ validate
99
+ expect(last_response.status).to eq 201
38
100
  end
39
101
  end
40
102
 
41
- context 'when params keys come as strings' do
42
- let(:params) { { 'beer' => true, 'grapefruit' => true } }
103
+ context 'when none of the restricted params is selected' do
104
+ let(:path) { '/' }
105
+ let(:params) { { other: true } }
43
106
 
44
- it 'does not raise a validation exception' do
45
- expect(validator.validate!(params)).to eql params
107
+ it 'returns a validation error' do
108
+ validate
109
+ expect(last_response.status).to eq 400
110
+ expect(JSON.parse(last_response.body)).to eq(
111
+ 'beer,wine,grapefruit' => ['are missing, at least one parameter must be provided']
112
+ )
46
113
  end
47
- end
48
114
 
49
- context 'when none of the restricted params is selected' do
50
- let(:params) { { somethingelse: true } }
115
+ context 'when custom message is specified' do
116
+ let(:path) { '/custom-message' }
51
117
 
52
- it 'raises a validation exception' do
53
- expect do
54
- validator.validate! params
55
- end.to raise_error(Grape::Exceptions::Validation)
118
+ it 'returns a validation error' do
119
+ validate
120
+ expect(last_response.status).to eq 400
121
+ expect(JSON.parse(last_response.body)).to eq(
122
+ 'beer,wine,grapefruit' => ['you should choose something']
123
+ )
124
+ end
56
125
  end
57
126
  end
58
127
 
59
128
  context 'when exactly one of the restricted params is selected' do
60
- let(:params) { { beer: true, somethingelse: true } }
129
+ let(:path) { '/' }
130
+ let(:params) { { beer: true } }
131
+
132
+ it 'does not return a validation error' do
133
+ validate
134
+ expect(last_response.status).to eq 201
135
+ end
136
+ end
137
+
138
+ context 'when restricted params are nested inside hash' do
139
+ let(:path) { '/nested-hash' }
140
+
141
+ context 'when at least one of them is present' do
142
+ let(:params) { { item: { beer: true, wine: true } } }
143
+
144
+ it 'does not return a validation error' do
145
+ validate
146
+ expect(last_response.status).to eq 201
147
+ end
148
+ end
61
149
 
62
- it 'does not raise a validation exception' do
63
- expect(validator.validate!(params)).to eql params
150
+ context 'when none of them are present' do
151
+ let(:params) { { item: { other: true } } }
152
+
153
+ it 'returns a validation error with full names of the params' do
154
+ validate
155
+ expect(last_response.status).to eq 400
156
+ expect(JSON.parse(last_response.body)).to eq(
157
+ 'item[beer],item[wine],item[grapefruit]' => ['fail']
158
+ )
159
+ end
160
+ end
161
+ end
162
+
163
+ context 'when restricted params are nested inside array' do
164
+ let(:path) { '/nested-array' }
165
+
166
+ context 'when at least one of them is present' do
167
+ let(:params) { { items: [{ beer: true, wine: true }, { grapefruit: true }] } }
168
+
169
+ it 'does not return a validation error' do
170
+ validate
171
+ expect(last_response.status).to eq 201
172
+ end
173
+ end
174
+
175
+ context 'when none of them are present' do
176
+ let(:params) { { items: [{ beer: true, other: true }, { other: true }] } }
177
+
178
+ it 'returns a validation error with full names of the params' do
179
+ validate
180
+ expect(last_response.status).to eq 400
181
+ expect(JSON.parse(last_response.body)).to eq(
182
+ 'items[1][beer],items[1][wine],items[1][grapefruit]' => ['fail']
183
+ )
184
+ end
185
+ end
186
+ end
187
+
188
+ context 'when restricted params are deeply nested' do
189
+ let(:path) { '/deeply-nested-array' }
190
+
191
+ context 'when at least one of them is present' do
192
+ let(:params) { { items: [{ nested_items: [{ wine: true }] }] } }
193
+
194
+ it 'does not return a validation error' do
195
+ validate
196
+ expect(last_response.status).to eq 201
197
+ end
198
+ end
199
+
200
+ context 'when none of them are present' do
201
+ let(:params) { { items: [{ nested_items: [{ other: true }] }] } }
202
+
203
+ it 'returns a validation error with full names of the params' do
204
+ validate
205
+ expect(last_response.status).to eq 400
206
+ expect(JSON.parse(last_response.body)).to eq(
207
+ 'items[0][nested_items][0][beer],items[0][nested_items][0][wine],items[0][nested_items][0][grapefruit]' => ['fail']
208
+ )
209
+ end
64
210
  end
65
211
  end
66
212
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  describe Grape::Validations::CoerceValidator do
@@ -10,20 +12,25 @@ describe Grape::Validations::CoerceValidator do
10
12
  end
11
13
 
12
14
  describe 'coerce' do
13
- module CoerceValidatorSpec
14
- class User
15
- include Virtus.model
16
- attribute :id, Integer
17
- attribute :name, String
15
+ class SecureURIOnly
16
+ def self.parse(value)
17
+ URI.parse(value)
18
+ end
19
+
20
+ def self.parsed?(value)
21
+ value.is_a? URI::HTTPS
18
22
  end
19
23
  end
20
24
 
21
25
  context 'i18n' do
22
26
  after :each do
27
+ I18n.available_locales = %i[en]
23
28
  I18n.locale = :en
29
+ I18n.default_locale = :en
24
30
  end
25
31
 
26
32
  it 'i18n error on malformed input' do
33
+ I18n.available_locales = %i[en zh-CN]
27
34
  I18n.load_path << File.expand_path('../zh-CN.yml', __FILE__)
28
35
  I18n.reload!
29
36
  I18n.locale = 'zh-CN'.to_sym
@@ -40,6 +47,7 @@ describe Grape::Validations::CoerceValidator do
40
47
  end
41
48
 
42
49
  it 'gives an english fallback error when default locale message is blank' do
50
+ I18n.available_locales = %i[en pt-BR]
43
51
  I18n.locale = 'pt-BR'.to_sym
44
52
  subject.params do
45
53
  requires :age, type: Integer
@@ -92,6 +100,7 @@ describe Grape::Validations::CoerceValidator do
92
100
 
93
101
  it 'respects :coerce_with' do
94
102
  get '/', a: 'yup'
103
+
95
104
  expect(last_response.status).to eq(200)
96
105
  expect(last_response.body).to eq('TrueClass')
97
106
  end
@@ -144,26 +153,50 @@ describe Grape::Validations::CoerceValidator do
144
153
  expect(last_response.body).to eq('array int works')
145
154
  end
146
155
 
147
- context 'complex objects' do
148
- it 'error on malformed input for complex objects' do
149
- subject.params do
150
- requires :user, type: CoerceValidatorSpec::User
156
+ context 'coerces' do
157
+ context 'json' do
158
+ let(:headers) { { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } }
159
+
160
+ it 'BigDecimal' do
161
+ subject.params do
162
+ requires :bigdecimal, type: BigDecimal
163
+ end
164
+ subject.post '/bigdecimal' do
165
+ "#{params[:bigdecimal].class} #{params[:bigdecimal].to_f}"
166
+ end
167
+
168
+ post '/bigdecimal', { bigdecimal: 45.1 }.to_json, headers
169
+ expect(last_response.status).to eq(201)
170
+ expect(last_response.body).to eq('BigDecimal 45.1')
151
171
  end
152
- subject.get '/user' do
153
- 'complex works'
172
+
173
+ it 'Boolean' do
174
+ subject.params do
175
+ requires :boolean, type: Boolean
176
+ end
177
+ subject.post '/boolean' do
178
+ params[:boolean]
179
+ end
180
+
181
+ post '/boolean', { boolean: 'true' }.to_json, headers
182
+ expect(last_response.status).to eq(201)
183
+ expect(last_response.body).to eq('true')
154
184
  end
185
+ end
155
186
 
156
- get '/user', user: '32'
157
- expect(last_response.status).to eq(400)
158
- expect(last_response.body).to eq('user is invalid')
187
+ it 'BigDecimal' do
188
+ subject.params do
189
+ requires :bigdecimal, coerce: BigDecimal
190
+ end
191
+ subject.get '/bigdecimal' do
192
+ params[:bigdecimal].class
193
+ end
159
194
 
160
- get '/user', user: { id: 32, name: 'Bob' }
195
+ get '/bigdecimal', bigdecimal: '45'
161
196
  expect(last_response.status).to eq(200)
162
- expect(last_response.body).to eq('complex works')
197
+ expect(last_response.body).to eq('BigDecimal')
163
198
  end
164
- end
165
199
 
166
- context 'coerces' do
167
200
  it 'Integer' do
168
201
  subject.params do
169
202
  requires :int, coerce: Integer
@@ -177,6 +210,70 @@ describe Grape::Validations::CoerceValidator do
177
210
  expect(last_response.body).to eq(integer_class_name)
178
211
  end
179
212
 
213
+ it 'String' do
214
+ subject.params do
215
+ requires :string, coerce: String
216
+ end
217
+ subject.get '/string' do
218
+ params[:string].class
219
+ end
220
+
221
+ get '/string', string: 45
222
+ expect(last_response.status).to eq(200)
223
+ expect(last_response.body).to eq('String')
224
+
225
+ get '/string', string: nil
226
+ expect(last_response.status).to eq(200)
227
+ expect(last_response.body).to eq('NilClass')
228
+ end
229
+
230
+ context 'a custom type' do
231
+ it 'coerces the given value' do
232
+ subject.params do
233
+ requires :uri, coerce: SecureURIOnly
234
+ end
235
+ subject.get '/secure_uri' do
236
+ params[:uri].class
237
+ end
238
+
239
+ get 'secure_uri', uri: 'https://www.example.com'
240
+
241
+ expect(last_response.status).to eq(200)
242
+ expect(last_response.body).to eq('URI::HTTPS')
243
+
244
+ get 'secure_uri', uri: 'http://www.example.com'
245
+
246
+ expect(last_response.status).to eq(400)
247
+ expect(last_response.body).to eq('uri is invalid')
248
+ end
249
+
250
+ context 'returning the InvalidValue instance when invalid' do
251
+ let(:custom_type) do
252
+ Class.new do
253
+ def self.parse(_val)
254
+ Grape::Types::InvalidValue.new('must be unique')
255
+ end
256
+ end
257
+ end
258
+
259
+ it 'uses a custom message added to the invalid value' do
260
+ type = custom_type
261
+
262
+ subject.params do
263
+ requires :name, type: type
264
+ end
265
+ subject.get '/whatever' do
266
+ params[:name].class
267
+ end
268
+
269
+ get 'whatever', name: 'Bob'
270
+
271
+ expect(last_response.status).to eq(400)
272
+ expect(last_response.body).to eq('name must be unique')
273
+ end
274
+ end
275
+ end
276
+
180
277
  context 'Array' do
181
278
  it 'Array of Integers' do
182
279
  subject.params do
@@ -193,7 +290,7 @@ describe Grape::Validations::CoerceValidator do
193
290
 
194
291
  it 'Array of Bools' do
195
292
  subject.params do
196
- requires :arry, coerce: Array[Virtus::Attribute::Boolean]
293
+ requires :arry, coerce: Array[Grape::API::Boolean]
197
294
  end
198
295
  subject.get '/array' do
199
296
  params[:arry][0].class
@@ -204,27 +301,6 @@ describe Grape::Validations::CoerceValidator do
204
301
  expect(last_response.body).to eq('TrueClass')
205
302
  end
206
303
 
207
- it 'Array of Complex' do
208
- subject.params do
209
- requires :arry, coerce: Array[CoerceValidatorSpec::User]
210
- end
211
- subject.get '/array' do
212
- params[:arry].size
213
- end
214
-
215
- get 'array', arry: [31]
216
- expect(last_response.status).to eq(400)
217
- expect(last_response.body).to eq('arry is invalid')
218
-
219
- get 'array', arry: { id: 31, name: 'Alice' }
220
- expect(last_response.status).to eq(400)
221
- expect(last_response.body).to eq('arry is invalid')
222
-
223
- get 'array', arry: [{ id: 31, name: 'Alice' }]
224
- expect(last_response.status).to eq(200)
225
- expect(last_response.body).to eq('1')
226
- end
227
-
228
304
  it 'Array of type implementing parse' do
229
305
  subject.params do
230
306
  requires :uri, type: Array[URI]
@@ -249,17 +325,7 @@ describe Grape::Validations::CoerceValidator do
249
325
  expect(last_response.body).to eq('Set,URI::HTTP,1')
250
326
  end
251
327
 
252
- it 'Array of class implementing parse and parsed?' do
253
- class SecureURIOnly
254
- def self.parse(value)
255
- URI.parse(value)
256
- end
257
-
258
- def self.parsed?(value)
259
- value.is_a? URI::HTTPS
260
- end
261
- end
262
-
328
+ it 'Array of a custom type' do
263
329
  subject.params do
264
330
  requires :uri, type: Array[SecureURIOnly]
265
331
  end
@@ -291,7 +357,7 @@ describe Grape::Validations::CoerceValidator do
291
357
 
292
358
  it 'Set of Bools' do
293
359
  subject.params do
294
- requires :set, coerce: Set[Virtus::Attribute::Boolean]
360
+ requires :set, coerce: Set[Grape::API::Boolean]
295
361
  end
296
362
  subject.get '/set' do
297
363
  params[:set].first.class
@@ -303,100 +369,73 @@ describe Grape::Validations::CoerceValidator do
303
369
  end
304
370
  end
305
371
 
306
- it 'Bool' do
307
- subject.params do
308
- requires :bool, coerce: Virtus::Attribute::Boolean
309
- end
310
- subject.get '/bool' do
311
- params[:bool].class
312
- end
313
-
314
- get '/bool', bool: 1
315
- expect(last_response.status).to eq(200)
316
- expect(last_response.body).to eq('TrueClass')
317
-
318
- get '/bool', bool: 0
319
- expect(last_response.status).to eq(200)
320
- expect(last_response.body).to eq('FalseClass')
321
-
322
- get '/bool', bool: 'false'
323
- expect(last_response.status).to eq(200)
324
- expect(last_response.body).to eq('FalseClass')
325
-
326
- get '/bool', bool: 'true'
327
- expect(last_response.status).to eq(200)
328
- expect(last_response.body).to eq('TrueClass')
329
- end
330
-
331
372
  it 'Boolean' do
332
373
  subject.params do
333
- optional :boolean, type: Boolean, default: true
374
+ requires :boolean, type: Boolean
334
375
  end
335
376
  subject.get '/boolean' do
336
377
  params[:boolean].class
337
378
  end
338
379
 
339
- get '/boolean'
340
- expect(last_response.status).to eq(200)
341
- expect(last_response.body).to eq('TrueClass')
342
-
343
- get '/boolean', boolean: true
344
- expect(last_response.status).to eq(200)
345
- expect(last_response.body).to eq('TrueClass')
346
-
347
- get '/boolean', boolean: false
348
- expect(last_response.status).to eq(200)
349
- expect(last_response.body).to eq('FalseClass')
350
-
351
- get '/boolean', boolean: 'true'
380
+ get '/boolean', boolean: 1
352
381
  expect(last_response.status).to eq(200)
353
382
  expect(last_response.body).to eq('TrueClass')
383
+ end
354
384
 
355
- get '/boolean', boolean: 'false'
356
- expect(last_response.status).to eq(200)
357
- expect(last_response.body).to eq('FalseClass')
385
+ context 'File' do
386
+ let(:file) { Rack::Test::UploadedFile.new(__FILE__) }
387
+ let(:filename) { File.basename(__FILE__).to_s }
358
388
 
359
- get '/boolean', boolean: 123
360
- expect(last_response.status).to eq(400)
361
- expect(last_response.body).to eq('boolean is invalid')
389
+ it 'Rack::Multipart::UploadedFile' do
390
+ subject.params do
391
+ requires :file, type: Rack::Multipart::UploadedFile
392
+ end
393
+ subject.post '/upload' do
394
+ params[:file][:filename]
395
+ end
362
396
 
363
- get '/boolean', boolean: '123'
364
- expect(last_response.status).to eq(400)
365
- expect(last_response.body).to eq('boolean is invalid')
366
- end
397
+ post '/upload', file: file
398
+ expect(last_response.status).to eq(201)
399
+ expect(last_response.body).to eq(filename)
367
400
 
368
- it 'Rack::Multipart::UploadedFile' do
369
- subject.params do
370
- requires :file, type: Rack::Multipart::UploadedFile
371
- end
372
- subject.post '/upload' do
373
- params[:file][:filename]
401
+ post '/upload', file: 'not a file'
402
+ expect(last_response.status).to eq(400)
403
+ expect(last_response.body).to eq('file is invalid')
374
404
  end
375
405
 
376
- post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
377
- expect(last_response.status).to eq(201)
378
- expect(last_response.body).to eq(File.basename(__FILE__).to_s)
406
+ it 'File' do
407
+ subject.params do
408
+ requires :file, coerce: File
409
+ end
410
+ subject.post '/upload' do
411
+ params[:file][:filename]
412
+ end
379
413
 
380
- post '/upload', file: 'not a file'
381
- expect(last_response.status).to eq(400)
382
- expect(last_response.body).to eq('file is invalid')
383
- end
414
+ post '/upload', file: file
415
+ expect(last_response.status).to eq(201)
416
+ expect(last_response.body).to eq(filename)
384
417
 
385
- it 'File' do
386
- subject.params do
387
- requires :file, coerce: File
388
- end
389
- subject.post '/upload' do
390
- params[:file][:filename]
418
+ post '/upload', file: 'not a file'
419
+ expect(last_response.status).to eq(400)
420
+ expect(last_response.body).to eq('file is invalid')
421
+
422
+ post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
423
+ expect(last_response.status).to eq(400)
424
+ expect(last_response.body).to eq('file is invalid')
391
425
  end
392
426
 
393
- post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
394
- expect(last_response.status).to eq(201)
395
- expect(last_response.body).to eq(File.basename(__FILE__).to_s)
427
+ it 'collection' do
428
+ subject.params do
429
+ requires :files, type: Array[File]
430
+ end
431
+ subject.post '/upload' do
432
+ params[:files].first[:filename]
433
+ end
396
434
 
397
- post '/upload', file: 'not a file'
398
- expect(last_response.status).to eq(400)
399
- expect(last_response.body).to eq('file is invalid')
435
+ post '/upload', files: [file]
436
+ expect(last_response.status).to eq(201)
437
+ expect(last_response.body).to eq(filename)
438
+ end
400
439
  end
401
440
 
402
441
  it 'Nests integers' do
@@ -413,6 +452,165 @@ describe Grape::Validations::CoerceValidator do
413
452
  expect(last_response.status).to eq(200)
414
453
  expect(last_response.body).to eq(integer_class_name)
415
454
  end
455
+
456
+ context 'nil values' do
457
+ context 'primitive types' do
458
+ Grape::Validations::Types::PRIMITIVES.each do |type|
459
+ it 'respects the nil value' do
460
+ subject.params do
461
+ requires :param, type: type
462
+ end
463
+ subject.get '/nil_value' do
464
+ params[:param].class
465
+ end
466
+
467
+ get '/nil_value', param: nil
468
+ expect(last_response.status).to eq(200)
469
+ expect(last_response.body).to eq('NilClass')
470
+ end
471
+ end
472
+ end
473
+
474
+ context 'structures types' do
475
+ Grape::Validations::Types::STRUCTURES.each do |type|
476
+ it 'respects the nil value' do
477
+ subject.params do
478
+ requires :param, type: type
479
+ end
480
+ subject.get '/nil_value' do
481
+ params[:param].class
482
+ end
483
+
484
+ get '/nil_value', param: nil
485
+ expect(last_response.status).to eq(200)
486
+ expect(last_response.body).to eq('NilClass')
487
+ end
488
+ end
489
+ end
490
+
491
+ context 'special types' do
492
+ Grape::Validations::Types::SPECIAL.each_key do |type|
493
+ it 'respects the nil value' do
494
+ subject.params do
495
+ requires :param, type: type
496
+ end
497
+ subject.get '/nil_value' do
498
+ params[:param].class
499
+ end
500
+
501
+ get '/nil_value', param: nil
502
+ expect(last_response.status).to eq(200)
503
+ expect(last_response.body).to eq('NilClass')
504
+ end
505
+ end
506
+
507
+ context 'variant-member-type collections' do
508
+ [
509
+ Array[Integer, String],
510
+ [Integer, String, Array[Integer, String]]
511
+ ].each do |type|
512
+ it 'respects the nil value' do
513
+ subject.params do
514
+ requires :param, type: type
515
+ end
516
+ subject.get '/nil_value' do
517
+ params[:param].class
518
+ end
519
+
520
+ get '/nil_value', param: nil
521
+ expect(last_response.status).to eq(200)
522
+ expect(last_response.body).to eq('NilClass')
523
+ end
524
+ end
525
+ end
526
+ end
527
+ end
528
+
529
+ context 'empty string' do
530
+ context 'primitive types' do
531
+ (Grape::Validations::Types::PRIMITIVES - [String]).each do |type|
532
+ it "is coerced to nil for type #{type}" do
533
+ subject.params do
534
+ requires :param, type: type
535
+ end
536
+ subject.get '/empty_string' do
537
+ params[:param].class
538
+ end
539
+
540
+ get '/empty_string', param: ''
541
+ expect(last_response.status).to eq(200)
542
+ expect(last_response.body).to eq('NilClass')
543
+ end
544
+ end
545
+
546
+ it 'is not coerced to nil for type String' do
547
+ subject.params do
548
+ requires :param, type: String
549
+ end
550
+ subject.get '/empty_string' do
551
+ params[:param].class
552
+ end
553
+
554
+ get '/empty_string', param: ''
555
+ expect(last_response.status).to eq(200)
556
+ expect(last_response.body).to eq('String')
557
+ end
558
+ end
559
+
560
+ context 'structures types' do
561
+ (Grape::Validations::Types::STRUCTURES - [Hash]).each do |type|
562
+ it "is coerced to nil for type #{type}" do
563
+ subject.params do
564
+ requires :param, type: type
565
+ end
566
+ subject.get '/empty_string' do
567
+ params[:param].class
568
+ end
569
+
570
+ get '/empty_string', param: ''
571
+ expect(last_response.status).to eq(200)
572
+ expect(last_response.body).to eq('NilClass')
573
+ end
574
+ end
575
+ end
576
+
577
+ context 'special types' do
578
+ (Grape::Validations::Types::SPECIAL.keys - [File, Rack::Multipart::UploadedFile]).each do |type|
579
+ it "is coerced to nil for type #{type}" do
580
+ subject.params do
581
+ requires :param, type: type
582
+ end
583
+ subject.get '/empty_string' do
584
+ params[:param].class
585
+ end
586
+
587
+ get '/empty_string', param: ''
588
+ expect(last_response.status).to eq(200)
589
+ expect(last_response.body).to eq('NilClass')
590
+ end
591
+ end
592
+
593
+ context 'variant-member-type collections' do
594
+ [
595
+ Array[Integer, String],
596
+ [Integer, String, Array[Integer, String]]
597
+ ].each do |type|
598
+ it "is coerced to nil for type #{type}" do
599
+ subject.params do
600
+ requires :param, type: type
601
+ end
602
+ subject.get '/empty_string' do
603
+ params[:param].class
604
+ end
605
+
606
+ get '/empty_string', param: ''
607
+ expect(last_response.status).to eq(200)
608
+ expect(last_response.body).to eq('NilClass')
609
+ end
610
+ end
611
+ end
612
+ end
613
+ end
416
614
  end
417
615
 
418
616
  context 'using coerce_with' do
@@ -435,19 +633,43 @@ describe Grape::Validations::CoerceValidator do
435
633
 
436
634
  it 'parses parameters with Array[String] type' do
437
635
  subject.params do
438
- requires :values, type: Array[String], coerce_with: ->(val) { val.split(/\s+/).map(&:to_i) }
636
+ requires :values, type: Array[String], coerce_with: ->(val) { val.split(/\s+/) }
439
637
  end
440
- subject.get '/ints' do
638
+ subject.get '/strings' do
441
639
  params[:values]
442
640
  end
443
641
 
444
- get '/ints', values: '1 2 3 4'
642
+ get '/strings', values: '1 2 3 4'
445
643
  expect(last_response.status).to eq(200)
446
644
  expect(JSON.parse(last_response.body)).to eq(%w[1 2 3 4])
447
645
 
448
- get '/ints', values: 'a b c d'
646
+ get '/strings', values: 'a b c d'
449
647
  expect(last_response.status).to eq(200)
450
- expect(JSON.parse(last_response.body)).to eq(%w[0 0 0 0])
648
+ expect(JSON.parse(last_response.body)).to eq(%w[a b c d])
649
+ end
650
+
651
+ it 'parses parameters with Array[Array[String]] type and coerce_with' do
652
+ subject.params do
653
+ requires :values, type: Array[Array[String]], coerce_with: ->(val) { val.is_a?(String) ? [val.split(/,/).map(&:strip)] : val }
654
+ end
655
+ subject.post '/coerce_nested_strings' do
656
+ params[:values]
657
+ end
658
+
659
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: 'a,b,c,d'), 'CONTENT_TYPE' => 'application/json'
660
+ expect(last_response.status).to eq(201)
661
+ expect(JSON.parse(last_response.body)).to eq([%w[a b c d]])
662
+
663
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [%w[a c], %w[b]]), 'CONTENT_TYPE' => 'application/json'
664
+ expect(last_response.status).to eq(201)
665
+ expect(JSON.parse(last_response.body)).to eq([%w[a c], %w[b]])
666
+
667
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [[]]), 'CONTENT_TYPE' => 'application/json'
668
+ expect(last_response.status).to eq(201)
669
+ expect(JSON.parse(last_response.body)).to eq([[]])
670
+
671
+ post '/coerce_nested_strings', ::Grape::Json.dump(values: [['a', { bar: 0 }], ['b']]), 'CONTENT_TYPE' => 'application/json'
672
+ expect(last_response.status).to eq(400)
451
673
  end
452
674
 
453
675
  it 'parses parameters with Array[Integer] type' do
@@ -484,6 +706,44 @@ describe Grape::Validations::CoerceValidator do
484
706
  expect(JSON.parse(last_response.body)).to eq([1, 1, 1, 1])
485
707
  end
486
708
 
709
+ context 'Array type and coerce_with should' do
710
+ before do
711
+ subject.params do
712
+ optional :arr, type: Array, coerce_with: (lambda do |val|
713
+ if val.nil?
714
+ []
715
+ else
716
+ val
717
+ end
718
+ end)
719
+ end
720
+ subject.get '/' do
721
+ params[:arr].class.to_s
722
+ end
723
+ end
724
+
725
+ it 'coerce nil value to array' do
726
+ get '/', arr: nil
727
+
728
+ expect(last_response.status).to eq(200)
729
+ expect(last_response.body).to eq('Array')
730
+ end
731
+
732
+ it 'not coerce missing field' do
733
+ get '/'
734
+
735
+ expect(last_response.status).to eq(200)
736
+ expect(last_response.body).to eq('NilClass')
737
+ end
738
+
739
+ it 'coerce array as array' do
740
+ get '/', arr: []
741
+
742
+ expect(last_response.status).to eq(200)
743
+ expect(last_response.body).to eq('Array')
744
+ end
745
+ end
746
+
487
747
  it 'uses parse where available' do
488
748
  subject.params do
489
749
  requires :ints, type: Array, coerce_with: JSON do
@@ -532,6 +792,84 @@ describe Grape::Validations::CoerceValidator do
532
792
  expect(last_response.body).to eq('3')
533
793
  end
534
794
 
795
+ context 'Integer type and coerce_with should' do
796
+ before do
797
+ subject.params do
798
+ optional :int, type: Integer, coerce_with: (lambda do |val|
799
+ if val.nil?
800
+ 0
801
+ else
802
+ val.to_i
803
+ end
804
+ end)
805
+ end
806
+ subject.get '/' do
807
+ params[:int].class.to_s
808
+ end
809
+ end
810
+
811
+ it 'coerce nil value to integer' do
812
+ get '/', int: nil
813
+
814
+ expect(last_response.status).to eq(200)
815
+ expect(last_response.body).to eq('Integer')
816
+ end
817
+
818
+ it 'not coerce missing field' do
819
+ get '/'
820
+
821
+ expect(last_response.status).to eq(200)
822
+ expect(last_response.body).to eq('NilClass')
823
+ end
824
+
825
+ it 'coerce integer as integer' do
826
+ get '/', int: 1
827
+
828
+ expect(last_response.status).to eq(200)
829
+ expect(last_response.body).to eq('Integer')
830
+ end
831
+ end
832
+
833
+ context 'Integer type and coerce_with potentially returning nil' do
834
+ before do
835
+ subject.params do
836
+ requires :int, type: Integer, coerce_with: (lambda do |val|
837
+ if val == '0'
838
+ nil
839
+ elsif val.match?(/^-?\d+$/)
840
+ val.to_i
841
+ else
842
+ val
843
+ end
844
+ end)
845
+ end
846
+ subject.get '/' do
847
+ params[:int].class.to_s
848
+ end
849
+ end
850
+
851
+ it 'accepts value that coerces to nil' do
852
+ get '/', int: '0'
853
+
854
+ expect(last_response.status).to eq(200)
855
+ expect(last_response.body).to eq('NilClass')
856
+ end
857
+
858
+ it 'coerces to Integer' do
859
+ get '/', int: '1'
860
+
861
+ expect(last_response.status).to eq(200)
862
+ expect(last_response.body).to eq('Integer')
863
+ end
864
+
865
+ it 'returns invalid value if coercion returns a wrong type' do
866
+ get '/', int: 'lol'
867
+
868
+ expect(last_response.status).to eq(400)
869
+ expect(last_response.body).to eq('int is invalid')
870
+ end
871
+ end
872
+
535
873
  it 'must be supplied with :type or :coerce' do
536
874
  expect do
537
875
  subject.params do
@@ -572,7 +910,7 @@ describe Grape::Validations::CoerceValidator do
572
910
  expect(last_response.status).to eq(200)
573
911
  expect(last_response.body).to eq('arrays work')
574
912
 
575
- get '/', splines: [{ x: 2, ints: [] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
913
+ get '/', splines: [{ x: 2, ints: [5] }, { x: 3, ints: [4], obj: { y: 'quack' } }]
576
914
  expect(last_response.status).to eq(200)
577
915
  expect(last_response.body).to eq('arrays work')
578
916
 
@@ -588,7 +926,7 @@ describe Grape::Validations::CoerceValidator do
588
926
  expect(last_response.status).to eq(400)
589
927
  expect(last_response.body).to eq('splines[x] does not have a valid value')
590
928
 
591
- get '/', splines: [{ x: 1, ints: [] }, { x: 4, ints: [] }]
929
+ get '/', splines: [{ x: 1, ints: [5] }, { x: 4, ints: [6] }]
592
930
  expect(last_response.status).to eq(400)
593
931
  expect(last_response.body).to eq('splines[x] does not have a valid value')
594
932
  end
@@ -906,14 +1244,17 @@ describe Grape::Validations::CoerceValidator do
906
1244
  end
907
1245
 
908
1246
  context 'converter' do
909
- it 'does not build Virtus::Attribute multiple times' do
1247
+ it 'does not build a coercer multiple times' do
910
1248
  subject.params do
911
1249
  requires :something, type: Array[String]
912
1250
  end
913
1251
  subject.get do
914
1252
  end
915
1253
 
916
- expect(Virtus::Attribute).to receive(:build).at_most(2).times.and_call_original
1254
+ expect(Grape::Validations::Types::ArrayCoercer).to(
1255
+ receive(:new).at_most(:once).and_call_original
1256
+ )
1257
+
917
1258
  10.times { get '/' }
918
1259
  end
919
1260
  end