grape 3.2.1 → 3.3.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 +80 -0
  3. data/README.md +116 -43
  4. data/UPGRADING.md +336 -1
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +7 -7
  7. data/lib/grape/api.rb +22 -25
  8. data/lib/grape/cookies.rb +2 -6
  9. data/lib/grape/declared_params_handler.rb +48 -50
  10. data/lib/grape/dsl/callbacks.rb +9 -3
  11. data/lib/grape/dsl/desc.rb +8 -2
  12. data/lib/grape/dsl/entity.rb +88 -0
  13. data/lib/grape/dsl/helpers.rb +27 -7
  14. data/lib/grape/dsl/inside_route.rb +38 -129
  15. data/lib/grape/dsl/logger.rb +3 -5
  16. data/lib/grape/dsl/parameters.rb +32 -38
  17. data/lib/grape/dsl/request_response.rb +53 -48
  18. data/lib/grape/dsl/rescue_options.rb +24 -0
  19. data/lib/grape/dsl/routing.rb +51 -35
  20. data/lib/grape/dsl/settings.rb +14 -8
  21. data/lib/grape/dsl/version_options.rb +23 -0
  22. data/lib/grape/endpoint/options.rb +19 -0
  23. data/lib/grape/endpoint.rb +96 -68
  24. data/lib/grape/env.rb +1 -3
  25. data/lib/grape/error_formatter/base.rb +23 -20
  26. data/lib/grape/error_formatter/json.rb +8 -4
  27. data/lib/grape/error_formatter/txt.rb +10 -10
  28. data/lib/grape/exceptions/base.rb +3 -1
  29. data/lib/grape/exceptions/error_response.rb +45 -0
  30. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  31. data/lib/grape/exceptions/validation.rb +14 -0
  32. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  33. data/lib/grape/exceptions/validation_errors.rb +12 -20
  34. data/lib/grape/formatter/serializable_hash.rb +5 -9
  35. data/lib/grape/json.rb +38 -2
  36. data/lib/grape/locale/en.yml +2 -0
  37. data/lib/grape/middleware/auth/base.rb +2 -3
  38. data/lib/grape/middleware/auth/dsl.rb +23 -8
  39. data/lib/grape/middleware/base.rb +22 -33
  40. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  41. data/lib/grape/middleware/error.rb +152 -62
  42. data/lib/grape/middleware/formatter.rb +66 -50
  43. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  44. data/lib/grape/middleware/stack.rb +5 -6
  45. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  46. data/lib/grape/middleware/versioner/base.rb +34 -38
  47. data/lib/grape/middleware/versioner/header.rb +3 -5
  48. data/lib/grape/middleware/versioner/path.rb +8 -3
  49. data/lib/grape/namespace.rb +3 -3
  50. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  51. data/lib/grape/parser/json.rb +1 -1
  52. data/lib/grape/path.rb +14 -17
  53. data/lib/grape/request.rb +15 -8
  54. data/lib/grape/router/mustermann_pattern.rb +44 -0
  55. data/lib/grape/router/pattern.rb +6 -10
  56. data/lib/grape/router.rb +28 -42
  57. data/lib/grape/serve_stream/file_body.rb +1 -0
  58. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  59. data/lib/grape/serve_stream/stream_response.rb +1 -0
  60. data/lib/grape/testing.rb +33 -0
  61. data/lib/grape/util/base_inheritable.rb +13 -16
  62. data/lib/grape/util/inheritable_setting.rb +44 -27
  63. data/lib/grape/util/inheritable_values.rb +7 -3
  64. data/lib/grape/util/lazy/base.rb +16 -0
  65. data/lib/grape/util/lazy/block.rb +2 -9
  66. data/lib/grape/util/lazy/value.rb +2 -9
  67. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  68. data/lib/grape/util/media_type.rb +1 -4
  69. data/lib/grape/util/path_normalizer.rb +34 -0
  70. data/lib/grape/util/registry.rb +1 -1
  71. data/lib/grape/util/stackable_values.rb +11 -8
  72. data/lib/grape/validations/attributes_iterator.rb +13 -13
  73. data/lib/grape/validations/coerce_options.rb +21 -0
  74. data/lib/grape/validations/oneof_collector.rb +39 -0
  75. data/lib/grape/validations/param_scope_tracker.rb +14 -9
  76. data/lib/grape/validations/params_documentation.rb +25 -23
  77. data/lib/grape/validations/params_scope.rb +54 -172
  78. data/lib/grape/validations/shared_options.rb +19 -0
  79. data/lib/grape/validations/types/array_coercer.rb +2 -2
  80. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  81. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  82. data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
  83. data/lib/grape/validations/types/primitive_coercer.rb +10 -5
  84. data/lib/grape/validations/types/set_coercer.rb +1 -1
  85. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
  86. data/lib/grape/validations/types.rb +23 -30
  87. data/lib/grape/validations/validations_spec.rb +149 -0
  88. data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
  89. data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
  90. data/lib/grape/validations/validators/base.rb +39 -22
  91. data/lib/grape/validations/validators/coerce_validator.rb +5 -3
  92. data/lib/grape/validations/validators/default_validator.rb +7 -8
  93. data/lib/grape/validations/validators/except_values_validator.rb +3 -2
  94. data/lib/grape/validations/validators/length_validator.rb +1 -1
  95. data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
  96. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  97. data/lib/grape/validations/validators/values_validator.rb +5 -5
  98. data/lib/grape/version.rb +1 -1
  99. data/lib/grape/xml.rb +8 -1
  100. data/lib/grape.rb +6 -6
  101. metadata +34 -18
  102. data/lib/grape/middleware/globals.rb +0 -14
@@ -38,9 +38,10 @@ module Grape
38
38
  end
39
39
 
40
40
  def self.attr_key(declared_param_attr)
41
- return attr_key(declared_param_attr.key) if declared_param_attr.is_a?(self)
42
-
43
- if declared_param_attr.is_a?(Hash)
41
+ case declared_param_attr
42
+ when self
43
+ attr_key(declared_param_attr.key)
44
+ when Hash
44
45
  declared_param_attr.transform_values { |value| attrs_keys(value) }
45
46
  else
46
47
  declared_param_attr
@@ -86,7 +87,8 @@ module Grape
86
87
  end
87
88
 
88
89
  def configuration
89
- (@api.configuration.respond_to?(:evaluate) && @api.configuration.evaluate) || @api.configuration
90
+ config = @api.configuration
91
+ config.is_a?(Grape::Util::Lazy::Base) ? config.evaluate : config
90
92
  end
91
93
 
92
94
  # @return [Boolean] whether or not this entire scope needs to be
@@ -195,13 +197,10 @@ module Grape
195
197
  private
196
198
 
197
199
  def build_full_path
198
- if nested?
199
- @parent.full_path + [@element]
200
- elsif lateral?
201
- @parent.full_path
202
- else
203
- []
204
- end
200
+ return @parent.full_path + [@element] if nested?
201
+ return @parent.full_path if lateral?
202
+
203
+ []
205
204
  end
206
205
 
207
206
  # Add a new parameter which should be renamed when using the +#declared+
@@ -315,12 +314,9 @@ module Grape
315
314
  # push_declared_params cannot be called on the frozen scope later.
316
315
  def configure_declared_params
317
316
  push_renamed_param(full_path, @element_renamed) if @element_renamed
317
+ return @parent.push_declared_params [{ @element => @declared_params }] if nested?
318
318
 
319
- if nested?
320
- @parent.push_declared_params [@element => @declared_params]
321
- else
322
- @api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params
323
- end
319
+ @api.inheritable_setting.namespace_stackable[:declared_params] = @declared_params
324
320
  ensure
325
321
  @declared_params = nil
326
322
  end
@@ -332,139 +328,67 @@ module Grape
332
328
  end
333
329
 
334
330
  def validates(attrs, validations)
335
- coerce_type = infer_coercion(validations)
336
- required = validations.key?(:presence)
337
- default = validations[:default]
338
- values = validations[:values].is_a?(Hash) ? validations.dig(:values, :value) : validations[:values]
339
- except_values = validations[:except_values].is_a?(Hash) ? validations.dig(:except_values, :value) : validations[:except_values]
340
-
341
- # NB. values and excepts should be nil, Proc, Array, or Range.
342
- # Specifically, values should NOT be a Hash
343
- # use values or excepts to guess coerce type when stated type is Array
344
- coerce_type = guess_coerce_type(coerce_type, values, except_values)
345
-
346
- # default value should be present in values array, if both exist and are not procs
347
- check_incompatible_option_values(default, values, except_values)
331
+ process_oneof!(validations) if validations.key?(:oneof)
332
+ spec = ValidationsSpec.from(validations)
348
333
 
349
- # type should be compatible with values array, if both exist
350
- validate_value_coercion(coerce_type, values, except_values)
334
+ document_params(attrs, spec)
351
335
 
352
- document_params attrs, validations, coerce_type, values, except_values
336
+ # Presence runs first `required` is forwarded to every subsequent
337
+ # validator (some short-circuit on it).
338
+ validate_presence(spec, attrs)
353
339
 
354
- opts = derive_validator_options(validations).freeze
340
+ # Coerce runs second — later validators see the typed value.
341
+ validate_coerce(spec, attrs)
355
342
 
356
- # Validate for presence before any other validators
357
- validates_presence(validations, attrs, opts)
358
-
359
- # Before we run the rest of the validators, let's handle
360
- # whatever coercion so that we are working with correctly
361
- # type casted values
362
- coerce_type validations.extract!(:coerce, :coerce_with, :coerce_message), attrs, required, opts
363
-
364
- validations.each do |type, options|
365
- # Don't try to look up validators for documentation params that don't have one.
366
- next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type)
367
-
368
- validate(type, options, attrs, required, opts)
369
- end
370
- end
371
-
372
- # Validate and comprehend the +:type+, +:types+, and +:coerce_with+
373
- # options that have been supplied to the parameter declaration.
374
- # The +:type+ and +:types+ options will be removed from the
375
- # validations list, replaced appropriately with +:coerce+ and
376
- # +:coerce_with+ options that will later be passed to
377
- # {Validators::CoerceValidator}. The type that is returned may be
378
- # used for documentation and further validation of parameter
379
- # options.
380
- #
381
- # @param validations [Hash] list of validations supplied to the
382
- # parameter declaration
383
- # @return [class-like] type to which the parameter will be coerced
384
- # @raise [ArgumentError] if the given type options are invalid
385
- def infer_coercion(validations)
386
- raise ArgumentError, ':type may not be supplied with :types' if validations.key?(:type) && validations.key?(:types)
387
-
388
- validations[:coerce] = (options_key?(:type, :value, validations) ? validations[:type][:value] : validations[:type]) if validations.key?(:type)
389
- validations[:coerce_message] = (options_key?(:type, :message, validations) ? validations[:type][:message] : nil) if validations.key?(:type)
390
- validations[:coerce] = (options_key?(:types, :value, validations) ? validations[:types][:value] : validations[:types]) if validations.key?(:types)
391
- validations[:coerce_message] = (options_key?(:types, :message, validations) ? validations[:types][:message] : nil) if validations.key?(:types)
392
-
393
- validations.delete(:types) if validations.key?(:types)
394
-
395
- coerce_type = validations[:coerce]
396
-
397
- # Special case - when the argument is a single type that is a
398
- # variant-type collection.
399
- if Types.multiple?(coerce_type) && validations.key?(:type)
400
- validations[:coerce] = Types::VariantCollectionCoercer.new(
401
- coerce_type,
402
- validations.delete(:coerce_with)
403
- )
343
+ spec.validator_entries.each do |type, options|
344
+ validate(type, options, attrs, spec.required?, spec.shared_opts)
404
345
  end
405
- validations.delete(:type)
406
-
407
- coerce_type
408
346
  end
409
347
 
410
- # Enforce correct usage of :coerce_with parameter.
411
- # We do not allow coercion without a type, nor with
412
- # +JSON+ as a type since this defines its own coercion
413
- # method.
414
- def check_coerce_with(validations)
415
- return unless validations.key?(:coerce_with)
416
- # type must be supplied for coerce_with..
417
- raise ArgumentError, 'must supply type for coerce_with' unless validations.key?(:coerce)
418
-
419
- # but not special JSON types, which
420
- # already imply coercion method
421
- return unless SPECIAL_JSON.include?(validations[:coerce])
348
+ # Enforce correct usage of :coerce_with on a CoerceOptions.
349
+ # We do not allow coercion without a type, nor with +JSON+ as a type
350
+ # since that defines its own coercion method.
351
+ def check_coerce_with(coerce_options)
352
+ return unless coerce_options.coerce_method
353
+ raise ArgumentError, 'must supply type for coerce_with' unless coerce_options.type
354
+ return unless SPECIAL_JSON.include?(coerce_options.type)
422
355
 
423
356
  raise ArgumentError, 'coerce_with disallowed for type: JSON'
424
357
  end
425
358
 
426
- # Add type coercion validation to this scope,
427
- # if any has been specified.
428
- # This validation has special handling since it is
429
- # composited from more than one +requires+/+optional+
430
- # parameter, and needs to be run before most other
431
- # validations.
432
- def coerce_type(validations, attrs, required, opts)
433
- check_coerce_with(validations)
359
+ def validate_presence(spec, attrs)
360
+ return unless spec.required?
434
361
 
435
- # Falsy check (not key?) is intentional: when a remountable API is first
436
- # evaluated on its base instance (no configuration supplied yet),
437
- # configuration[:some_type] evaluates to nil. Skipping instantiation
438
- # here is correct — the real mounted instance will replay this step with
439
- # the actual type value.
440
- return unless validations[:coerce]
441
-
442
- coerce_options = {
443
- type: validations[:coerce],
444
- method: validations[:coerce_with],
445
- message: validations[:coerce_message]
446
- }
447
- validate('coerce', coerce_options, attrs, required, opts)
362
+ validate('presence', spec.presence_options, attrs, true, spec.shared_opts)
448
363
  end
449
364
 
450
- def guess_coerce_type(coerce_type, *values_list)
451
- return coerce_type unless coerce_type == Array
365
+ def validate_coerce(spec, attrs)
366
+ coerce_options = spec.coerce_options
367
+ check_coerce_with(coerce_options)
368
+ # Falsy check is intentional: when a remountable API is first evaluated
369
+ # on its base instance (no configuration supplied yet),
370
+ # configuration[:some_type] evaluates to nil. Skipping instantiation
371
+ # here is correct — the real mounted instance will replay this step
372
+ # with the actual type value.
373
+ return unless coerce_options.type
452
374
 
453
- values_list.each do |values|
454
- next if !values || values.is_a?(Proc)
455
- return values.first.class if values.is_a?(Range) || !values.empty?
456
- end
457
- coerce_type
375
+ validate('coerce', coerce_options, attrs, spec.required?, spec.shared_opts)
458
376
  end
459
377
 
460
- def check_incompatible_option_values(default, values, except_values)
461
- return unless default && !default.is_a?(Proc)
462
-
463
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && !Array(default).all? { |def_val| values.include?(def_val) }
378
+ # Translate a `oneof: [proc, proc, ...]` declaration into a list of
379
+ # captured validator arrays — one array per variant. Each variant's
380
+ # block is evaluated in its own +ParamsScope+ backed by an
381
+ # {OneofCollector} so the full params DSL is available inside variants
382
+ # and the resulting validators are kept out of the real API's
383
+ # registration list.
384
+ def process_oneof!(validations)
385
+ raise ArgumentError, 'oneof: requires type: Hash' unless validations[:type] == Hash
464
386
 
465
- return unless except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
387
+ variants = validations[:oneof]
388
+ raise ArgumentError, 'oneof: must be a non-empty Array of blocks' unless variants.is_a?(Array) && variants.any?
389
+ raise ArgumentError, 'oneof: each variant must be a Proc' unless variants.all?(Proc)
466
390
 
467
- raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
391
+ validations[:oneof] = variants.map { |block| OneofCollector.collect(block) }
468
392
  end
469
393
 
470
394
  def validate(type, options, attrs, required, opts)
@@ -479,51 +403,9 @@ module Grape
479
403
  @api.inheritable_setting.namespace_stackable[:validations] = validator_instance
480
404
  end
481
405
 
482
- def validate_value_coercion(coerce_type, *values_list)
483
- return unless coerce_type
484
-
485
- coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable)
486
- values_list.each do |values|
487
- next if !values || values.is_a?(Proc)
488
-
489
- value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
490
- value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
491
- raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
492
- end
493
- end
494
-
495
- def extract_message_option(attrs)
496
- return nil unless attrs.is_a?(Array)
497
-
498
- opts = attrs.last.is_a?(Hash) ? attrs.pop : {}
499
- opts.key?(:message) && !opts[:message].nil? ? opts.delete(:message) : nil
500
- end
501
-
502
- def options_key?(type, key, validations)
503
- validations[type].respond_to?(:key?) && validations[type].key?(key) && !validations[type][key].nil?
504
- end
505
-
506
406
  def all_element_blank?(scoped_params)
507
407
  scoped_params.respond_to?(:all?) && scoped_params.all?(&:blank?)
508
408
  end
509
-
510
- # Validators don't have access to each other and they don't need, however,
511
- # some validators might influence others, so their options should be shared
512
- def derive_validator_options(validations)
513
- allow_blank = validations[:allow_blank]
514
-
515
- {
516
- allow_blank: allow_blank.is_a?(Hash) ? allow_blank[:value] : allow_blank,
517
- fail_fast: validations.delete(:fail_fast) || false
518
- }
519
- end
520
-
521
- def validates_presence(validations, attrs, opts)
522
- return unless validations.key?(:presence) && validations[:presence]
523
-
524
- validate('presence', validations.delete(:presence), attrs, true, opts)
525
- validations.delete(:message) if validations.key?(:message)
526
- end
527
409
  end
528
410
  end
529
411
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ # Immutable value object holding the two options every validator reads at
6
+ # construction time: +allow_blank+ and +fail_fast+. Internal to
7
+ # {Validators::Base}, which builds it from the +opts+ Hash so the public
8
+ # 5th-argument contract stays a plain Hash — not part of any wire contract.
9
+ #
10
+ # Defaults mirror the prior +opts.values_at+ behaviour: +allow_blank+ is
11
+ # +nil+ when the declaration didn't supply it (validators treat nil as
12
+ # "not set"), +fail_fast+ defaults to +false+.
13
+ SharedOptions = Data.define(:allow_blank, :fail_fast) do
14
+ def initialize(allow_blank: nil, fail_fast: false)
15
+ super
16
+ end
17
+ end
18
+ end
19
+ end
@@ -12,7 +12,7 @@ module Grape
12
12
  # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
13
13
  # maintains Virtus behavior in coercing.
14
14
  class ArrayCoercer < DryTypeCoercer
15
- def initialize(type, strict = false)
15
+ def initialize(type, strict: false)
16
16
  super
17
17
  @coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array
18
18
  @subtype = type.first
@@ -52,7 +52,7 @@ module Grape
52
52
  end
53
53
 
54
54
  def elem_coercer
55
- @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
55
+ @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict:)
56
56
  end
57
57
  end
58
58
  end
@@ -33,6 +33,10 @@ module Grape
33
33
  # contract as +coerced?+, and must be supplied with a coercion
34
34
  # +method+.
35
35
  class CustomTypeCoercer
36
+ TYPE_CHECK_METHODS = %i[coerced? parsed?].freeze
37
+ COLLECTION_TYPES = [Array, Set].freeze
38
+ private_constant :TYPE_CHECK_METHODS, :COLLECTION_TYPES
39
+
36
40
  # A new coercer for the given type specification
37
41
  # and coercion method.
38
42
  #
@@ -41,8 +45,7 @@ module Grape
41
45
  # @param method [#parse,#call]
42
46
  # optional coercion method. See class docs.
43
47
  def initialize(type, method = nil)
44
- coercion_method = infer_coercion_method type, method
45
- @method = enforce_symbolized_keys type, coercion_method
48
+ @method = build_coercion_method(type, method)
46
49
  @type_check = infer_type_check(type)
47
50
  end
48
51
 
@@ -66,97 +69,50 @@ module Grape
66
69
 
67
70
  private
68
71
 
69
- # Determine the coercion method we're expected to use
70
- # based on the parameters given.
71
- #
72
- # @param type see #new
73
- # @param method see #new
74
- # @return [#call] coercion method
72
+ def build_coercion_method(type, method)
73
+ coercion_method = infer_coercion_method(type, method)
74
+ return hash_symbolizer(coercion_method) if type == Hash
75
+ return collection_symbolizer(coercion_method) if COLLECTION_TYPES.include?(type)
76
+
77
+ coercion_method
78
+ end
79
+
75
80
  def infer_coercion_method(type, method)
76
- if method
77
- if method.respond_to? :parse
78
- method.method :parse
79
- else
80
- method
81
- end
82
- else
83
- # Try to use parse() declared on the target type.
84
- # This may raise an exception, but we are out of ideas anyway.
85
- type.method :parse
86
- end
81
+ return type.method(:parse) unless method
82
+ return method unless method.respond_to?(:parse)
83
+
84
+ method.method(:parse)
85
+ end
86
+
87
+ def hash_symbolizer(method)
88
+ ->(val) { method.call(val).deep_symbolize_keys }
89
+ end
90
+
91
+ def collection_symbolizer(method)
92
+ ->(val) { method.call(val).map! { |item| symbolize_if_hash(item) } }
93
+ end
94
+
95
+ def symbolize_if_hash(item)
96
+ item.is_a?(Hash) ? item.deep_symbolize_keys : item
87
97
  end
88
98
 
89
- # Determine how the type validity of a coerced
90
- # value should be decided.
91
- #
92
- # @param type see #new
93
- # @return [#call] a procedure which accepts a single parameter
94
- # and returns +true+ if the passed object is of the correct type.
95
99
  def infer_type_check(type)
96
- # First check for special class methods
97
- if type.respond_to? :coerced?
98
- type.method :coerced?
99
- elsif type.respond_to? :parsed?
100
- type.method :parsed?
101
- elsif type.respond_to? :call
102
- # Arbitrary proc passed for type validation.
103
- # Note that this will fail unless a method is also
104
- # passed, or if the type also implements a parse() method.
105
- type
106
- elsif type.is_a?(Enumerable)
107
- lambda do |value|
108
- value.is_a?(Enumerable) && value.all? do |val|
109
- recursive_type_check(type.first, val)
110
- end
111
- end
112
- else
113
- # By default, do a simple type check
114
- ->(value) { value.is_a? type }
115
- end
100
+ method_name = TYPE_CHECK_METHODS.detect { |m| type.respond_to?(m) }
101
+ return type.method(method_name) if method_name
102
+ return type if type.respond_to?(:call)
103
+ return enumerable_type_check(type) if type.is_a?(Enumerable)
104
+
105
+ ->(value) { value.is_a? type }
116
106
  end
117
107
 
118
- def recursive_type_check(type, value)
119
- if type.is_a?(Enumerable) && value.is_a?(Enumerable)
120
- value.all? { |val| recursive_type_check(type.first, val) }
121
- else
122
- !type.is_a?(Enumerable) && value.is_a?(type)
123
- end
108
+ def enumerable_type_check(type)
109
+ ->(value) { value.is_a?(Enumerable) && value.all? { |val| recursive_type_check(type.first, val) } }
124
110
  end
125
111
 
126
- # Enforce symbolized keys for complex types
127
- # by wrapping the coercion method such that
128
- # any Hash objects in the immediate heirarchy
129
- # have their keys recursively symbolized.
130
- # This helps common libs such as JSON to work easily.
131
- #
132
- # @param type see #new
133
- # @param method see #infer_coercion_method
134
- # @return [#call] +method+ wrapped in an additional
135
- # key-conversion step, or just returns +method+
136
- # itself if no conversion is deemed to be
137
- # necessary.
138
- def enforce_symbolized_keys(type, method)
139
- # Collections have all values processed individually
140
- if [Array, Set].include?(type)
141
- lambda do |val|
142
- method.call(val).tap do |new_val|
143
- new_val.map do |item|
144
- item.is_a?(Hash) ? item.deep_symbolize_keys : item
145
- end
146
- end
147
- end
148
-
149
- # Hash objects are processed directly
150
- elsif type == Hash
151
- lambda do |val|
152
- method.call(val).deep_symbolize_keys
153
- end
154
-
155
- # Simple types are not processed.
156
- # This includes Array<primitive> types.
157
- else
158
- method
159
- end
112
+ def recursive_type_check(type, value)
113
+ return value.all? { |val| recursive_type_check(type.first, val) } if type.is_a?(Enumerable) && value.is_a?(Enumerable)
114
+
115
+ !type.is_a?(Enumerable) && value.is_a?(type)
160
116
  end
161
117
  end
162
118
  end
@@ -29,7 +29,7 @@ module Grape
29
29
  # @param set [Boolean]
30
30
  # when true, a +Set+ will be returned by {#call} instead
31
31
  # of an +Array+ and duplicate items will be discarded.
32
- def initialize(type, set = false)
32
+ def initialize(type, set: false)
33
33
  super(type)
34
34
  @set = set
35
35
  end
@@ -26,13 +26,13 @@ module Grape
26
26
  end
27
27
 
28
28
  # Returns an instance of a coercer for a given type
29
- def coercer_instance_for(type, strict = false)
29
+ def coercer_instance_for(type, strict: false)
30
30
  klass = type.instance_of?(Class) ? PrimitiveCoercer : collection_coercer_for(type)
31
- klass.new(type, strict)
31
+ klass.new(type, strict:)
32
32
  end
33
33
  end
34
34
 
35
- def initialize(type, strict = false)
35
+ def initialize(type, strict: false)
36
36
  @type = type
37
37
  @strict = strict
38
38
  @cache_coercer = strict ? DryTypes::StrictCache : DryTypes::ParamsCache
@@ -7,7 +7,7 @@ module Grape
7
7
  # initialization. When +strict+ is true, it doesn't coerce a value but check
8
8
  # that it has the proper type.
9
9
  class PrimitiveCoercer < DryTypeCoercer
10
- def initialize(type, strict = false)
10
+ def initialize(type, strict: false)
11
11
  super
12
12
 
13
13
  @coercer = cache_coercer[type]
@@ -15,7 +15,7 @@ module Grape
15
15
 
16
16
  def call(val)
17
17
  return InvalidValue.new if reject?(val)
18
- return nil if val.nil? || treat_as_nil?(val)
18
+ return if val.nil? || treat_as_nil?(val)
19
19
 
20
20
  super
21
21
  end
@@ -29,9 +29,14 @@ module Grape
29
29
  # but Virtus wouldn't accept it. So, this method only exists to not introduce
30
30
  # breaking changes.
31
31
  def reject?(val)
32
- (val.is_a?(Array) && type == String) ||
33
- (val.is_a?(String) && type == Hash) ||
34
- (val.is_a?(Hash) && type == String)
32
+ case val
33
+ when Array, Hash
34
+ type == String
35
+ when String
36
+ type == Hash
37
+ else
38
+ false
39
+ end
35
40
  end
36
41
 
37
42
  # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
@@ -6,7 +6,7 @@ module Grape
6
6
  # Takes the given array and converts it to a set. Every element of the set
7
7
  # is also coerced.
8
8
  class SetCoercer < ArrayCoercer
9
- def initialize(type, strict = false)
9
+ def initialize(type, strict: false)
10
10
  super
11
11
 
12
12
  @coercer = nil
@@ -26,6 +26,14 @@ module Grape
26
26
  @member_coercer = MultipleTypeCoercer.new types, method
27
27
  end
28
28
 
29
+ # Returns the Grape DSL notation for this coercer, e.g. "Array[Integer, String]".
30
+ # Distinct from the plain-array string "[Integer, String]" produced by the
31
+ # +types:+ keyword, which lets documentation tools tell the two apart.
32
+ def to_s
33
+ container = @types.is_a?(Set) ? 'Set' : 'Array'
34
+ "#{container}[#{@types.join(', ')}]"
35
+ end
36
+
29
37
  # Coerce the given value.
30
38
  #
31
39
  # @param value [Array<String>] collection of values to be coerced
@@ -75,7 +75,7 @@ module Grape
75
75
  # @return [Boolean] +true+ if the given value will be treated as
76
76
  # a list of types.
77
77
  def multiple?(type)
78
- (type.is_a?(Array) || type.is_a?(Set)) && type.size > 1
78
+ array_or_set?(type) && type.size > 1
79
79
  end
80
80
 
81
81
  # Does Grape provide special coercion and validation
@@ -98,17 +98,18 @@ module Grape
98
98
  GROUPS.include? type
99
99
  end
100
100
 
101
+ def array_or_set?(type)
102
+ type.is_a?(Array) || type.is_a?(Set)
103
+ end
104
+
101
105
  # A valid custom type must implement a class-level `parse` method, taking
102
106
  # one String argument and returning the parsed value in its correct type.
103
107
  #
104
108
  # @param type [Class] type to check
105
109
  # @return [Boolean] whether or not the type can be used as a custom type
106
110
  def custom?(type)
107
- !primitive?(type) &&
108
- !structure?(type) &&
109
- !multiple?(type) &&
110
- type.respond_to?(:parse) &&
111
- type.method(:parse).arity == 1
111
+ !(primitive?(type) || structure?(type) || multiple?(type)) &&
112
+ type.respond_to?(:parse) && type.method(:parse).arity == 1
112
113
  end
113
114
 
114
115
  # Is the declared type an +Array+ or +Set+ of a {#custom?} type?
@@ -117,9 +118,7 @@ module Grape
117
118
  # @return [Boolean] true if +type+ is a collection of a type that implements
118
119
  # its own +#parse+ method.
119
120
  def collection_of_custom?(type)
120
- (type.is_a?(Array) || type.is_a?(Set)) &&
121
- type.length == 1 &&
122
- (custom?(type.first) || special?(type.first))
121
+ array_or_set?(type) && type.length == 1 && (custom?(type.first) || special?(type.first))
123
122
  end
124
123
 
125
124
  def map_special(type)
@@ -162,27 +161,21 @@ module Grape
162
161
  end
163
162
 
164
163
  def create_coercer_instance(type, method, strict)
165
- # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!!
166
- type = Types.map_special(type)
167
-
168
- # Use a special coercer for multiply-typed parameters.
169
- if Types.multiple?(type)
170
- MultipleTypeCoercer.new(type, method)
171
-
172
- # Use a special coercer for custom types and coercion methods.
173
- elsif method || Types.custom?(type)
174
- CustomTypeCoercer.new(type, method)
175
-
176
- # Special coercer for collections of types that implement a parse method.
177
- # CustomTypeCoercer (above) already handles such types when an explicit coercion
178
- # method is supplied.
179
- elsif Types.collection_of_custom?(type)
180
- Types::CustomTypeCollectionCoercer.new(
181
- Types.map_special(type.first), type.is_a?(Set)
182
- )
183
- else
184
- DryTypeCoercer.coercer_instance_for(type, strict)
185
- end
164
+ # map_special doesn't recurse into collections applied only to the top-level type here.
165
+ type = map_special(type)
166
+
167
+ # Multiply-typed parameters, e.g. types: [Integer, String].
168
+ return MultipleTypeCoercer.new(type, method) if multiple?(type)
169
+
170
+ # User-supplied coercion method, or a custom type with its own #parse.
171
+ return CustomTypeCoercer.new(type, method) if method || custom?(type)
172
+
173
+ # Array/Set of a custom type — CustomTypeCoercer already handles single
174
+ # custom types when an explicit coercion method is supplied.
175
+ return CustomTypeCollectionCoercer.new(map_special(type.first), set: type.is_a?(Set)) if collection_of_custom?(type)
176
+
177
+ # Fallback: let dry-types handle primitives, structures, and known specials.
178
+ DryTypeCoercer.coercer_instance_for(type, strict:)
186
179
  end
187
180
 
188
181
  class CoercerCache < Grape::Util::Cache