grape 2.4.0 → 4.0.1

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +351 -0
  3. data/CONTRIBUTING.md +18 -10
  4. data/README.md +457 -228
  5. data/UPGRADING.md +1240 -1
  6. data/grape.gemspec +7 -7
  7. data/lib/grape/api/instance.rb +65 -116
  8. data/lib/grape/api.rb +53 -62
  9. data/lib/grape/content_types.rb +58 -3
  10. data/lib/grape/cookies.rb +2 -6
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -4
  13. data/lib/grape/dsl/callbacks.rb +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +24 -67
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/helpers.rb +80 -65
  18. data/lib/grape/dsl/inside_route.rb +56 -296
  19. data/lib/grape/dsl/logger.rb +4 -9
  20. data/lib/grape/dsl/middleware.rb +22 -40
  21. data/lib/grape/dsl/parameters.rb +86 -93
  22. data/lib/grape/dsl/request_response.rb +139 -139
  23. data/lib/grape/dsl/rescue_options.rb +25 -0
  24. data/lib/grape/dsl/routing.rb +328 -203
  25. data/lib/grape/dsl/settings.rb +29 -135
  26. data/lib/grape/dsl/validations.rb +38 -44
  27. data/lib/grape/dsl/version_options.rb +23 -0
  28. data/lib/grape/endpoint/options.rb +25 -0
  29. data/lib/grape/endpoint.rb +285 -245
  30. data/lib/grape/env.rb +8 -3
  31. data/lib/grape/error_formatter/base.rb +27 -21
  32. data/lib/grape/error_formatter/json.rb +6 -0
  33. data/lib/grape/error_formatter/txt.rb +10 -10
  34. data/lib/grape/error_formatter.rb +6 -2
  35. data/lib/grape/exceptions/base.rb +20 -44
  36. data/lib/grape/exceptions/error_response.rb +48 -0
  37. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  38. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  39. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  40. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  41. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  42. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  44. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  45. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  46. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  47. data/lib/grape/exceptions/request_error.rb +11 -0
  48. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  49. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  50. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  51. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  52. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  53. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  54. data/lib/grape/exceptions/validation.rb +25 -7
  55. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  56. data/lib/grape/exceptions/validation_errors.rb +21 -23
  57. data/lib/grape/formatter/json.rb +1 -0
  58. data/lib/grape/formatter/serializable_hash.rb +6 -9
  59. data/lib/grape/json.rb +38 -2
  60. data/lib/grape/locale/en.yml +3 -5
  61. data/lib/grape/middleware/auth/base.rb +4 -3
  62. data/lib/grape/middleware/auth/dsl.rb +9 -23
  63. data/lib/grape/middleware/base.rb +41 -36
  64. data/lib/grape/middleware/error.rb +271 -73
  65. data/lib/grape/middleware/formatter.rb +152 -63
  66. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  67. data/lib/grape/middleware/stack.rb +20 -11
  68. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  69. data/lib/grape/middleware/versioner/base.rb +40 -48
  70. data/lib/grape/middleware/versioner/header.rb +5 -23
  71. data/lib/grape/middleware/versioner/param.rb +2 -2
  72. data/lib/grape/middleware/versioner/path.rb +54 -7
  73. data/lib/grape/mountable.rb +22 -0
  74. data/lib/grape/namespace.rb +19 -11
  75. data/lib/grape/params_builder/base.rb +2 -0
  76. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  77. data/lib/grape/params_builder.rb +2 -19
  78. data/lib/grape/parser/json.rb +1 -1
  79. data/lib/grape/path.rb +8 -72
  80. data/lib/grape/precompiled_json.rb +50 -0
  81. data/lib/grape/request.rb +57 -20
  82. data/lib/grape/router/base_route.rb +53 -7
  83. data/lib/grape/router/greedy_route.rb +16 -6
  84. data/lib/grape/router/mustermann_pattern.rb +44 -0
  85. data/lib/grape/router/pattern/path.rb +78 -0
  86. data/lib/grape/router/pattern.rb +65 -37
  87. data/lib/grape/router/route.rb +66 -18
  88. data/lib/grape/router.rb +126 -115
  89. data/lib/grape/serve_stream/file_body.rb +7 -0
  90. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  91. data/lib/grape/serve_stream/stream_response.rb +7 -0
  92. data/lib/grape/testing.rb +33 -0
  93. data/lib/grape/util/api_description.rb +67 -0
  94. data/lib/grape/util/cache.rb +22 -2
  95. data/lib/grape/util/deep_freeze.rb +34 -0
  96. data/lib/grape/util/freeze_on_new.rb +20 -0
  97. data/lib/grape/util/inheritable_setting.rb +777 -39
  98. data/lib/grape/util/lazy/base.rb +16 -0
  99. data/lib/grape/util/lazy/block.rb +2 -9
  100. data/lib/grape/util/lazy/value.rb +2 -9
  101. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  102. data/lib/grape/util/media_type.rb +13 -9
  103. data/lib/grape/util/path_normalizer.rb +37 -0
  104. data/lib/grape/util/registry.rb +13 -3
  105. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  106. data/lib/grape/util/stackable_values.rb +39 -10
  107. data/lib/grape/util/translation.rb +42 -0
  108. data/lib/grape/validations/attributes_iterator.rb +61 -29
  109. data/lib/grape/validations/coerce_options.rb +21 -0
  110. data/lib/grape/validations/contract_scope.rb +8 -13
  111. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  112. data/lib/grape/validations/oneof_collector.rb +35 -0
  113. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  114. data/lib/grape/validations/params_documentation.rb +52 -0
  115. data/lib/grape/validations/params_scope.rb +217 -304
  116. data/lib/grape/validations/shared_options.rb +19 -0
  117. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  118. data/lib/grape/validations/types/array_coercer.rb +7 -10
  119. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  120. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  121. data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
  122. data/lib/grape/validations/types/json.rb +1 -3
  123. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  124. data/lib/grape/validations/types/primitive_coercer.rb +14 -33
  125. data/lib/grape/validations/types/set_coercer.rb +1 -1
  126. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  127. data/lib/grape/validations/types.rb +29 -51
  128. data/lib/grape/validations/validations_spec.rb +164 -0
  129. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  130. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  131. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  132. data/lib/grape/validations/validators/base.rb +118 -34
  133. data/lib/grape/validations/validators/coerce_validator.rb +25 -37
  134. data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
  135. data/lib/grape/validations/validators/default_validator.rb +13 -20
  136. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  137. data/lib/grape/validations/validators/except_values_validator.rb +14 -4
  138. data/lib/grape/validations/validators/length_validator.rb +23 -22
  139. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  140. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  141. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  142. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  143. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  144. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  145. data/lib/grape/validations/validators/values_validator.rb +34 -26
  146. data/lib/grape/version.rb +1 -2
  147. data/lib/grape/xml.rb +8 -1
  148. data/lib/grape.rb +58 -31
  149. metadata +62 -39
  150. data/lib/grape/api/helpers.rb +0 -9
  151. data/lib/grape/dsl/api.rb +0 -17
  152. data/lib/grape/dsl/configuration.rb +0 -15
  153. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  154. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  155. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  156. data/lib/grape/exceptions/missing_option.rb +0 -11
  157. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  158. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  159. data/lib/grape/exceptions/unknown_options.rb +0 -11
  160. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  161. data/lib/grape/extensions/hash.rb +0 -27
  162. data/lib/grape/extensions/hashie/mash.rb +0 -24
  163. data/lib/grape/middleware/globals.rb +0 -14
  164. data/lib/grape/types/invalid_value.rb +0 -8
  165. data/lib/grape/util/base_inheritable.rb +0 -43
  166. data/lib/grape/util/inheritable_values.rb +0 -29
  167. data/lib/grape/util/reverse_stackable_values.rb +0 -15
  168. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  169. data/lib/grape/validations/attributes_doc.rb +0 -60
  170. data/lib/grape/validations/validator_factory.rb +0 -15
@@ -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
@@ -5,12 +5,14 @@ module Grape
5
5
  class SingleAttributeIterator < AttributesIterator
6
6
  private
7
7
 
8
- def yield_attributes(val, attrs)
8
+ def yield_attributes(val)
9
9
  return if skip?(val)
10
10
 
11
- attrs.each do |attr_name|
12
- yield val, attr_name, empty?(val)
13
- end
11
+ # The emptiness of the scope's params is the same answer for every
12
+ # attribute in it, and answering it costs a +respond_to?+, so it is
13
+ # asked once for the whole list rather than once per attribute.
14
+ empty_val = empty?(val)
15
+ @attrs.each { |attr_name| yield val, attr_name, empty_val }
14
16
  end
15
17
 
16
18
  # Primitives like Integers and Booleans don't respond to +empty?+.
@@ -7,16 +7,17 @@ module Grape
7
7
  # an array of arrays of integers.
8
8
  #
9
9
  # It could've been possible to use an +of+
10
- # method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
10
+ # method (https://dry-rb.org/gems/dry-types/main/array-with-member/)
11
11
  # provided by dry-types. Unfortunately, it doesn't work for Grape because of
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
-
18
- @coercer = scope::Array
19
- @subtype = type.first
17
+ @coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array
18
+ # Built eagerly: instances are shared across requests (and cached in
19
+ # Types::CoercerCache), so no lazy state may be created at call time.
20
+ @elem_coercer = DryTypeCoercer.coercer_instance_for(type.first, strict:)
20
21
  end
21
22
 
22
23
  def call(_val)
@@ -28,7 +29,7 @@ module Grape
28
29
 
29
30
  protected
30
31
 
31
- attr_reader :subtype
32
+ attr_reader :elem_coercer
32
33
 
33
34
  def coerce_elements(collection)
34
35
  return if collection.nil?
@@ -51,10 +52,6 @@ module Grape
51
52
  def reject?(val)
52
53
  val.nil?
53
54
  end
54
-
55
- def elem_coercer
56
- @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
57
- end
58
55
  end
59
56
  end
60
57
  end
@@ -33,6 +33,12 @@ module Grape
33
33
  # contract as +coerced?+, and must be supplied with a coercion
34
34
  # +method+.
35
35
  class CustomTypeCoercer
36
+ extend Grape::Util::FreezeOnNew
37
+
38
+ TYPE_CHECK_METHODS = %i[coerced? parsed?].freeze
39
+ COLLECTION_TYPES = [Array, Set].freeze
40
+ private_constant :TYPE_CHECK_METHODS, :COLLECTION_TYPES
41
+
36
42
  # A new coercer for the given type specification
37
43
  # and coercion method.
38
44
  #
@@ -41,8 +47,7 @@ module Grape
41
47
  # @param method [#parse,#call]
42
48
  # optional coercion method. See class docs.
43
49
  def initialize(type, method = nil)
44
- coercion_method = infer_coercion_method type, method
45
- @method = enforce_symbolized_keys type, coercion_method
50
+ @method = build_coercion_method(type, method)
46
51
  @type_check = infer_type_check(type)
47
52
  end
48
53
 
@@ -66,97 +71,54 @@ module Grape
66
71
 
67
72
  private
68
73
 
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
74
+ def build_coercion_method(type, method)
75
+ coercion_method = infer_coercion_method(type, method)
76
+ return hash_symbolizer(coercion_method) if type == Hash
77
+ return collection_symbolizer(coercion_method) if COLLECTION_TYPES.include?(type)
78
+
79
+ coercion_method
80
+ end
81
+
75
82
  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
83
+ return type.method(:parse) unless method
84
+ return method unless method.respond_to?(:parse)
85
+
86
+ method.method(:parse)
87
+ end
88
+
89
+ def hash_symbolizer(method)
90
+ ->(val) { method.call(val).deep_symbolize_keys }
91
+ end
92
+
93
+ # +dup+ before +map!+: the collection is whatever the user's coercion
94
+ # method returned — possibly frozen, possibly the input itself — so we
95
+ # must not mutate it in place. +dup+ also preserves the class, keeping a
96
+ # +Set+ a +Set+ (unlike +map+, which would return an +Array+).
97
+ def collection_symbolizer(method)
98
+ ->(val) { method.call(val).dup.map! { |item| symbolize_if_hash(item) } }
99
+ end
100
+
101
+ def symbolize_if_hash(item)
102
+ item.is_a?(Hash) ? item.deep_symbolize_keys : item
87
103
  end
88
104
 
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
105
  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
106
+ method_name = TYPE_CHECK_METHODS.detect { |m| type.respond_to?(m) }
107
+ return type.method(method_name) if method_name
108
+ return type if type.respond_to?(:call)
109
+ return enumerable_type_check(type) if type.is_a?(Enumerable)
110
+
111
+ ->(value) { value.is_a? type }
116
112
  end
117
113
 
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
114
+ def enumerable_type_check(type)
115
+ ->(value) { value.is_a?(Enumerable) && value.all? { |val| recursive_type_check(type.first, val) } }
124
116
  end
125
117
 
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
118
+ def recursive_type_check(type, value)
119
+ return value.all? { |val| recursive_type_check(type.first, val) } if type.is_a?(Enumerable) && value.is_a?(Enumerable)
120
+
121
+ !type.is_a?(Enumerable) && value.is_a?(type)
160
122
  end
161
123
  end
162
124
  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
@@ -1,20 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module DryTypes
4
- # Call +Dry.Types()+ to add all registered types to +DryTypes+ which is
5
- # a container in this case. Check documentation for more information
6
- # https://dry-rb.org/gems/dry-types/1.2/getting-started/
7
- include Dry.Types()
8
- end
9
-
10
3
  module Grape
11
4
  module Validations
12
5
  module Types
13
6
  # A base class for classes which must identify a coercer to be used.
14
7
  # If the +strict+ argument is true, it won't coerce the given value
15
8
  # but check its type. More information there
16
- # https://dry-rb.org/gems/dry-types/1.2/built-in-types/
9
+ # https://dry-rb.org/gems/dry-types/main/built-in-types/
17
10
  class DryTypeCoercer
11
+ extend Grape::Util::FreezeOnNew
12
+
18
13
  class << self
19
14
  # Returns a collection coercer which corresponds to a given type.
20
15
  # Example:
@@ -28,21 +23,21 @@ module Grape
28
23
  when Set
29
24
  SetCoercer
30
25
  else
31
- raise ArgumentError, "Unknown type: #{type}"
26
+ raise ArgumentError, "unknown type: `#{type}`"
32
27
  end
33
28
  end
34
29
 
35
30
  # Returns an instance of a coercer for a given type
36
- def coercer_instance_for(type, strict = false)
31
+ def coercer_instance_for(type, strict: false)
37
32
  klass = type.instance_of?(Class) ? PrimitiveCoercer : collection_coercer_for(type)
38
- klass.new(type, strict)
33
+ klass.new(type, strict:)
39
34
  end
40
35
  end
41
36
 
42
- def initialize(type, strict = false)
37
+ def initialize(type, strict: false)
43
38
  @type = type
44
39
  @strict = strict
45
- @scope = strict ? DryTypes::Strict : DryTypes::Params
40
+ @cache_coercer = strict ? DryTypes::StrictCache : DryTypes::ParamsCache
46
41
  end
47
42
 
48
43
  # Coerces the given value to a type which was specified during
@@ -53,13 +48,13 @@ module Grape
53
48
  return if val.nil?
54
49
 
55
50
  @coercer[val]
56
- rescue Dry::Types::CoercionError => _e
51
+ rescue Dry::Types::CoercionError
57
52
  InvalidValue.new
58
53
  end
59
54
 
60
55
  protected
61
56
 
62
- attr_reader :scope, :type, :strict
57
+ attr_reader :type, :strict, :cache_coercer
63
58
  end
64
59
  end
65
60
  end
@@ -17,9 +17,7 @@ module Grape
17
17
  # @return [Hash,Array<Hash>,nil]
18
18
  def parse(input)
19
19
  return input if parsed?(input)
20
-
21
- # Allow nulls and blank strings
22
- return if input.nil? || input.match?(/^\s*$/)
20
+ return if input.blank?
23
21
 
24
22
  JSON.parse(input, symbolize_names: true)
25
23
  end
@@ -13,6 +13,8 @@ module Grape
13
13
  # an allowed type it should be declared last, since it will always
14
14
  # successfully "coerce" the value.
15
15
  class MultipleTypeCoercer
16
+ extend Grape::Util::FreezeOnNew
17
+
16
18
  # Construct a new coercer that will attempt to coerce
17
19
  # values to the given list of types in the given order.
18
20
  #
@@ -28,7 +30,7 @@ module Grape
28
30
  else
29
31
  Types.build_coercer type, strict: !@method.nil?
30
32
  end
31
- end
33
+ end.freeze
32
34
  end
33
35
 
34
36
  # Coerces the given value.
@@ -39,12 +41,12 @@ module Grape
39
41
  # of {InvalidValue} if the value could not be coerced.
40
42
  def call(val)
41
43
  # once the value is coerced by the custom method, its type should be checked
42
- val = @method.call(val) if @method
44
+ candidate = @method ? @method.call(val) : val
43
45
 
44
46
  coerced_val = InvalidValue.new
45
47
 
46
48
  @type_coercers.each do |coercer|
47
- coerced_val = coercer.call(val)
49
+ coerced_val = coercer.call(candidate)
48
50
 
49
51
  return coerced_val unless coerced_val.is_a?(InvalidValue)
50
52
  end
@@ -7,42 +7,23 @@ 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
- MAPPING = {
11
- Grape::API::Boolean => DryTypes::Params::Bool,
12
- BigDecimal => DryTypes::Params::Decimal,
13
- Numeric => DryTypes::Params::Integer | DryTypes::Params::Float | DryTypes::Params::Decimal,
14
- TrueClass => DryTypes::Params::Bool.constrained(eql: true),
15
- FalseClass => DryTypes::Params::Bool.constrained(eql: false),
10
+ # The input classes Virtus refused for a given declared type, keyed by
11
+ # that type. Resolved once in #initialize rather than re-derived from
12
+ # +type+ on every value: the answer only ever depends on the
13
+ # declaration, and every coerced attribute of every request asks.
14
+ REJECTED_INPUTS = { String => [Array, Hash].freeze, Hash => [String].freeze }.freeze
16
15
 
17
- # unfortunately, a +Params+ scope doesn't contain String
18
- String => DryTypes::Coercible::String
19
- }.freeze
20
-
21
- STRICT_MAPPING = {
22
- Grape::API::Boolean => DryTypes::Strict::Bool,
23
- BigDecimal => DryTypes::Strict::Decimal,
24
- Numeric => DryTypes::Strict::Integer | DryTypes::Strict::Float | DryTypes::Strict::Decimal,
25
- TrueClass => DryTypes::Strict::Bool.constrained(eql: true),
26
- FalseClass => DryTypes::Strict::Bool.constrained(eql: false)
27
- }.freeze
28
-
29
- def initialize(type, strict = false)
16
+ def initialize(type, strict: false)
30
17
  super
31
18
 
32
- @type = type
33
-
34
- @coercer = (strict ? STRICT_MAPPING : MAPPING).fetch(type) do
35
- scope.const_get(type.name, false)
36
- rescue NameError
37
- raise ArgumentError, "type #{type} should support coercion via `[]`" unless type.respond_to?(:[])
38
-
39
- type
40
- end
19
+ @coercer = cache_coercer[type]
20
+ @rejected_inputs = REJECTED_INPUTS[type]
21
+ @treat_empty_as_nil = type != String
41
22
  end
42
23
 
43
24
  def call(val)
44
25
  return InvalidValue.new if reject?(val)
45
- return nil if val.nil? || treat_as_nil?(val)
26
+ return if val.nil? || treat_as_nil?(val)
46
27
 
47
28
  super
48
29
  end
@@ -56,16 +37,16 @@ module Grape
56
37
  # but Virtus wouldn't accept it. So, this method only exists to not introduce
57
38
  # breaking changes.
58
39
  def reject?(val)
59
- (val.is_a?(Array) && type == String) ||
60
- (val.is_a?(String) && type == Hash) ||
61
- (val.is_a?(Hash) && type == String)
40
+ return false unless @rejected_inputs
41
+
42
+ @rejected_inputs.any? { |klass| val.is_a?(klass) }
62
43
  end
63
44
 
64
45
  # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
65
46
  # absence of a value and coerces it into nil. See a discussion there
66
47
  # https://github.com/ruby-grape/grape/pull/2045
67
48
  def treat_as_nil?(val)
68
- val == '' && type != String
49
+ @treat_empty_as_nil && val == ''
69
50
  end
70
51
  end
71
52
  end
@@ -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
@@ -6,6 +6,11 @@ module Grape
6
6
  # This class wraps {MultipleTypeCoercer}, for use with collections
7
7
  # that allow members of more than one type.
8
8
  class VariantCollectionCoercer
9
+ extend Grape::Util::FreezeOnNew
10
+
11
+ # @return [Array<Class>,Set<Class>] the member types as declared in the DSL
12
+ attr_reader :types
13
+
9
14
  # Construct a new coercer that will attempt to coerce
10
15
  # a list of values such that all members are of one of
11
16
  # the given types. The container may also optionally be
@@ -26,6 +31,14 @@ module Grape
26
31
  @member_coercer = MultipleTypeCoercer.new types, method
27
32
  end
28
33
 
34
+ # Returns the Grape DSL notation for this coercer, e.g. "Array[Integer, String]".
35
+ # Distinct from the plain-array string "[Integer, String]" produced by the
36
+ # +types:+ keyword, which lets documentation tools tell the two apart.
37
+ def to_s
38
+ container = @types.is_a?(Set) ? 'Set' : 'Array'
39
+ "#{container}[#{@types.join(', ')}]"
40
+ end
41
+
29
42
  # Coerce the given value.
30
43
  #
31
44
  # @param value [Array<String>] collection of values to be coerced
@@ -35,15 +48,15 @@ module Grape
35
48
  def call(value)
36
49
  return unless value.is_a? Array
37
50
 
38
- value =
51
+ coerced =
39
52
  if @method
40
53
  @method.call(value)
41
54
  else
42
55
  value.map { |v| @member_coercer.call(v) }
43
56
  end
44
- return Set.new value if @types.is_a? Set
57
+ return Set.new coerced if @types.is_a? Set
45
58
 
46
- value
59
+ coerced
47
60
  end
48
61
  end
49
62
  end
@@ -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)
@@ -155,59 +154,38 @@ module Grape
155
154
  # @return [Object] object to be used
156
155
  # for coercion and type validation
157
156
  def build_coercer(type, method: nil, strict: false)
158
- cache_instance(type, method, strict) do
159
- create_coercer_instance(type, method, strict)
160
- end
161
- end
157
+ # no cache since unique
158
+ return create_coercer_instance(type, method, strict) if method.respond_to?(:call)
162
159
 
163
- def create_coercer_instance(type, method, strict)
164
- # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!!
165
- type = Types.map_special(type)
166
-
167
- # Use a special coercer for multiply-typed parameters.
168
- if Types.multiple?(type)
169
- MultipleTypeCoercer.new(type, method)
170
-
171
- # Use a special coercer for custom types and coercion methods.
172
- elsif method || Types.custom?(type)
173
- CustomTypeCoercer.new(type, method)
174
-
175
- # Special coercer for collections of types that implement a parse method.
176
- # CustomTypeCoercer (above) already handles such types when an explicit coercion
177
- # method is supplied.
178
- elsif Types.collection_of_custom?(type)
179
- Types::CustomTypeCollectionCoercer.new(
180
- Types.map_special(type.first), type.is_a?(Set)
181
- )
182
- else
183
- DryTypeCoercer.coercer_instance_for(type, strict)
184
- end
160
+ CoercerCache[[type, method, strict]]
185
161
  end
186
162
 
187
- def cache_instance(type, method, strict, &_block)
188
- key = cache_key(type, method, strict)
163
+ def create_coercer_instance(type, method, strict)
164
+ # map_special doesn't recurse into collections — applied only to the top-level type here.
165
+ mapped = map_special(type)
189
166
 
190
- return @__cache[key] if @__cache.key?(key)
167
+ # Multiply-typed parameters, e.g. types: [Integer, String].
168
+ return MultipleTypeCoercer.new(mapped, method) if multiple?(mapped)
191
169
 
192
- instance = yield
170
+ # User-supplied coercion method, or a custom type with its own #parse.
171
+ return CustomTypeCoercer.new(mapped, method) if method || custom?(mapped)
193
172
 
194
- @__cache_write_lock.synchronize do
195
- @__cache[key] = instance
196
- end
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(mapped.first), set: mapped.is_a?(Set)) if collection_of_custom?(mapped)
197
176
 
198
- instance
177
+ # Fallback: let dry-types handle primitives, structures, and known specials.
178
+ DryTypeCoercer.coercer_instance_for(mapped, strict:)
199
179
  end
200
180
 
201
- def cache_key(type, method, strict)
202
- [type, method, strict].each_with_object(+'_') do |val, memo|
203
- next if val.nil?
204
-
205
- memo << '_' << val.to_s
181
+ class CoercerCache < Grape::Util::Cache
182
+ def initialize
183
+ super
184
+ @cache = Hash.new do |h, (type, method, strict)|
185
+ h[[type, method, strict]] = Grape::Validations::Types.create_coercer_instance(type, method, strict)
186
+ end
206
187
  end
207
188
  end
208
-
209
- instance_variable_set(:@__cache, {})
210
- instance_variable_set(:@__cache_write_lock, Mutex.new)
211
189
  end
212
190
  end
213
191
  end