grape 0.1.1 → 2.4.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 (197) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1167 -0
  3. data/CONTRIBUTING.md +156 -0
  4. data/LICENSE +1 -1
  5. data/README.md +4192 -0
  6. data/UPGRADING.md +1697 -0
  7. data/grape.gemspec +28 -105
  8. data/grape.png +0 -0
  9. data/lib/grape/api/helpers.rb +9 -0
  10. data/lib/grape/api/instance.rb +247 -0
  11. data/lib/grape/api.rb +158 -194
  12. data/lib/grape/content_types.rb +31 -0
  13. data/lib/grape/cookies.rb +50 -0
  14. data/lib/grape/dry_types.rb +10 -0
  15. data/lib/grape/dsl/api.rb +17 -0
  16. data/lib/grape/dsl/callbacks.rb +69 -0
  17. data/lib/grape/dsl/configuration.rb +15 -0
  18. data/lib/grape/dsl/desc.rb +124 -0
  19. data/lib/grape/dsl/headers.rb +21 -0
  20. data/lib/grape/dsl/helpers.rb +108 -0
  21. data/lib/grape/dsl/inside_route.rb +454 -0
  22. data/lib/grape/dsl/logger.rb +22 -0
  23. data/lib/grape/dsl/middleware.rb +54 -0
  24. data/lib/grape/dsl/parameters.rb +266 -0
  25. data/lib/grape/dsl/request_response.rb +171 -0
  26. data/lib/grape/dsl/routing.rb +248 -0
  27. data/lib/grape/dsl/settings.rb +179 -0
  28. data/lib/grape/dsl/validations.rb +57 -0
  29. data/lib/grape/endpoint.rb +398 -68
  30. data/lib/grape/env.rb +20 -0
  31. data/lib/grape/error_formatter/base.rb +68 -0
  32. data/lib/grape/error_formatter/json.rb +28 -0
  33. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  34. data/lib/grape/error_formatter/txt.rb +21 -0
  35. data/lib/grape/error_formatter/xml.rb +11 -0
  36. data/lib/grape/error_formatter.rb +15 -0
  37. data/lib/grape/exceptions/base.rb +76 -0
  38. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  39. data/lib/grape/exceptions/empty_message_body.rb +11 -0
  40. data/lib/grape/exceptions/incompatible_option_values.rb +11 -0
  41. data/lib/grape/exceptions/invalid_accept_header.rb +11 -0
  42. data/lib/grape/exceptions/invalid_formatter.rb +11 -0
  43. data/lib/grape/exceptions/invalid_message_body.rb +11 -0
  44. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  45. data/lib/grape/exceptions/invalid_response.rb +11 -0
  46. data/lib/grape/exceptions/invalid_version_header.rb +11 -0
  47. data/lib/grape/exceptions/invalid_versioner_option.rb +11 -0
  48. data/lib/grape/exceptions/invalid_with_option_for_represent.rb +11 -0
  49. data/lib/grape/exceptions/method_not_allowed.rb +11 -0
  50. data/lib/grape/exceptions/missing_group_type.rb +13 -0
  51. data/lib/grape/exceptions/missing_mime_type.rb +11 -0
  52. data/lib/grape/exceptions/missing_option.rb +11 -0
  53. data/lib/grape/exceptions/missing_vendor_option.rb +11 -0
  54. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  55. data/lib/grape/exceptions/too_many_multipart_files.rb +11 -0
  56. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  57. data/lib/grape/exceptions/unknown_options.rb +11 -0
  58. data/lib/grape/exceptions/unknown_parameter.rb +11 -0
  59. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  60. data/lib/grape/exceptions/unknown_validator.rb +11 -0
  61. data/lib/grape/exceptions/unsupported_group_type.rb +13 -0
  62. data/lib/grape/exceptions/validation.rb +25 -0
  63. data/lib/grape/exceptions/validation_array_errors.rb +14 -0
  64. data/lib/grape/exceptions/validation_errors.rb +53 -0
  65. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +24 -0
  66. data/lib/grape/extensions/hash.rb +27 -0
  67. data/lib/grape/extensions/hashie/mash.rb +24 -0
  68. data/lib/grape/formatter/base.rb +16 -0
  69. data/lib/grape/formatter/json.rb +13 -0
  70. data/lib/grape/formatter/serializable_hash.rb +39 -0
  71. data/lib/grape/formatter/txt.rb +11 -0
  72. data/lib/grape/formatter/xml.rb +13 -0
  73. data/lib/grape/formatter.rb +17 -0
  74. data/lib/grape/json.rb +10 -0
  75. data/lib/grape/locale/en.yml +59 -0
  76. data/lib/grape/middleware/auth/base.rb +23 -0
  77. data/lib/grape/middleware/auth/dsl.rb +39 -0
  78. data/lib/grape/middleware/auth/strategies.rb +25 -0
  79. data/lib/grape/middleware/auth/strategy_info.rb +15 -0
  80. data/lib/grape/middleware/base.rb +89 -18
  81. data/lib/grape/middleware/error.rb +129 -10
  82. data/lib/grape/middleware/filter.rb +19 -0
  83. data/lib/grape/middleware/formatter.rb +124 -82
  84. data/lib/grape/middleware/globals.rb +14 -0
  85. data/lib/grape/middleware/stack.rb +109 -0
  86. data/lib/grape/middleware/versioner/accept_version_header.rb +38 -0
  87. data/lib/grape/middleware/versioner/base.rb +74 -0
  88. data/lib/grape/middleware/versioner/header.rb +127 -0
  89. data/lib/grape/middleware/versioner/param.rb +32 -0
  90. data/lib/grape/middleware/versioner/path.rb +40 -0
  91. data/lib/grape/middleware/versioner.rb +21 -21
  92. data/lib/grape/namespace.rb +46 -0
  93. data/lib/grape/params_builder/base.rb +18 -0
  94. data/lib/grape/params_builder/hash.rb +11 -0
  95. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  96. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  97. data/lib/grape/params_builder.rb +32 -0
  98. data/lib/grape/parser/base.rb +16 -0
  99. data/lib/grape/parser/json.rb +14 -0
  100. data/lib/grape/parser/xml.rb +14 -0
  101. data/lib/grape/parser.rb +15 -0
  102. data/lib/grape/path.rb +76 -0
  103. data/lib/grape/presenters/presenter.rb +11 -0
  104. data/lib/grape/railtie.rb +9 -0
  105. data/lib/grape/request.rb +190 -0
  106. data/lib/grape/router/base_route.rb +39 -0
  107. data/lib/grape/router/greedy_route.rb +20 -0
  108. data/lib/grape/router/pattern.rb +81 -0
  109. data/lib/grape/router/route.rb +62 -0
  110. data/lib/grape/router.rb +190 -0
  111. data/lib/grape/serve_stream/file_body.rb +36 -0
  112. data/lib/grape/serve_stream/sendfile_response.rb +21 -0
  113. data/lib/grape/serve_stream/stream_response.rb +23 -0
  114. data/lib/grape/types/invalid_value.rb +8 -0
  115. data/lib/grape/util/base_inheritable.rb +43 -0
  116. data/lib/grape/util/cache.rb +17 -0
  117. data/lib/grape/util/endpoint_configuration.rb +8 -0
  118. data/lib/grape/util/header.rb +13 -0
  119. data/lib/grape/util/inheritable_setting.rb +100 -0
  120. data/lib/grape/util/inheritable_values.rb +29 -0
  121. data/lib/grape/util/lazy/block.rb +29 -0
  122. data/lib/grape/util/lazy/value.rb +38 -0
  123. data/lib/grape/util/lazy/value_array.rb +21 -0
  124. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  125. data/lib/grape/util/lazy/value_hash.rb +21 -0
  126. data/lib/grape/util/media_type.rb +70 -0
  127. data/lib/grape/util/registry.rb +27 -0
  128. data/lib/grape/util/reverse_stackable_values.rb +15 -0
  129. data/lib/grape/util/stackable_values.rb +36 -0
  130. data/lib/grape/util/strict_hash_configuration.rb +108 -0
  131. data/lib/grape/validations/attributes_doc.rb +60 -0
  132. data/lib/grape/validations/attributes_iterator.rb +62 -0
  133. data/lib/grape/validations/contract_scope.rb +34 -0
  134. data/lib/grape/validations/multiple_attributes_iterator.rb +13 -0
  135. data/lib/grape/validations/params_scope.rb +538 -0
  136. data/lib/grape/validations/single_attribute_iterator.rb +26 -0
  137. data/lib/grape/validations/types/array_coercer.rb +61 -0
  138. data/lib/grape/validations/types/custom_type_coercer.rb +164 -0
  139. data/lib/grape/validations/types/custom_type_collection_coercer.rb +56 -0
  140. data/lib/grape/validations/types/dry_type_coercer.rb +66 -0
  141. data/lib/grape/validations/types/file.rb +31 -0
  142. data/lib/grape/validations/types/invalid_value.rb +17 -0
  143. data/lib/grape/validations/types/json.rb +71 -0
  144. data/lib/grape/validations/types/multiple_type_coercer.rb +57 -0
  145. data/lib/grape/validations/types/primitive_coercer.rb +73 -0
  146. data/lib/grape/validations/types/set_coercer.rb +35 -0
  147. data/lib/grape/validations/types/variant_collection_coercer.rb +51 -0
  148. data/lib/grape/validations/types.rb +213 -0
  149. data/lib/grape/validations/validator_factory.rb +15 -0
  150. data/lib/grape/validations/validators/all_or_none_of_validator.rb +16 -0
  151. data/lib/grape/validations/validators/allow_blank_validator.rb +20 -0
  152. data/lib/grape/validations/validators/as_validator.rb +14 -0
  153. data/lib/grape/validations/validators/at_least_one_of_validator.rb +15 -0
  154. data/lib/grape/validations/validators/base.rb +88 -0
  155. data/lib/grape/validations/validators/coerce_validator.rb +75 -0
  156. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  157. data/lib/grape/validations/validators/default_validator.rb +37 -0
  158. data/lib/grape/validations/validators/exactly_one_of_validator.rb +17 -0
  159. data/lib/grape/validations/validators/except_values_validator.rb +24 -0
  160. data/lib/grape/validations/validators/length_validator.rb +49 -0
  161. data/lib/grape/validations/validators/multiple_params_base.rb +34 -0
  162. data/lib/grape/validations/validators/mutual_exclusion_validator.rb +16 -0
  163. data/lib/grape/validations/validators/presence_validator.rb +15 -0
  164. data/lib/grape/validations/validators/regexp_validator.rb +16 -0
  165. data/lib/grape/validations/validators/same_as_validator.rb +29 -0
  166. data/lib/grape/validations/validators/values_validator.rb +60 -0
  167. data/lib/grape/validations.rb +21 -0
  168. data/lib/grape/version.rb +6 -0
  169. data/lib/grape/xml.rb +10 -0
  170. data/lib/grape.rb +81 -17
  171. metadata +250 -192
  172. data/.document +0 -5
  173. data/.gitignore +0 -23
  174. data/.rspec +0 -2
  175. data/.rvmrc +0 -1
  176. data/Gemfile +0 -21
  177. data/Gemfile.lock +0 -58
  178. data/README.markdown +0 -75
  179. data/Rakefile +0 -70
  180. data/VERSION +0 -1
  181. data/autotest/discover.rb +0 -1
  182. data/lib/grape/middleware/auth/basic.rb +0 -30
  183. data/lib/grape/middleware/auth/oauth2.rb +0 -55
  184. data/lib/grape/middleware/prefixer.rb +0 -21
  185. data/lib/grape/middleware_stack.rb +0 -35
  186. data/spec/grape/api_spec.rb +0 -343
  187. data/spec/grape/endpoint_spec.rb +0 -104
  188. data/spec/grape/middleware/auth/basic_spec.rb +0 -31
  189. data/spec/grape/middleware/auth/oauth2_spec.rb +0 -88
  190. data/spec/grape/middleware/base_spec.rb +0 -63
  191. data/spec/grape/middleware/error_spec.rb +0 -49
  192. data/spec/grape/middleware/formatter_spec.rb +0 -87
  193. data/spec/grape/middleware/prefixer_spec.rb +0 -30
  194. data/spec/grape/middleware/versioner_spec.rb +0 -40
  195. data/spec/grape/middleware_stack_spec.rb +0 -47
  196. data/spec/grape_spec.rb +0 -1
  197. data/spec/spec_helper.rb +0 -20
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # This class will detect type classes that implement
7
+ # a class-level +parse+ method. The method should accept one
8
+ # +String+ argument and should return the value coerced to
9
+ # the appropriate type. The method may raise an exception if
10
+ # there are any problems parsing the string.
11
+ #
12
+ # Alternately an optional +method+ may be supplied (see the
13
+ # +coerce_with+ option of {Grape::Dsl::Parameters#requires}).
14
+ # This may be any class or object implementing +parse+ or +call+,
15
+ # with the same contract as described above.
16
+ #
17
+ # Type Checking
18
+ # -------------
19
+ #
20
+ # Calls to +coerced?+ will consult this class to check
21
+ # that the coerced value produced above is in fact of the
22
+ # expected type. By default this class performs a basic check
23
+ # against the type supplied, but this behaviour will be
24
+ # overridden if the class implements a class-level
25
+ # +coerced?+ or +parsed?+ method. This method
26
+ # will receive a single parameter that is the coerced value
27
+ # and should return +true+ if the value meets type expectations.
28
+ # Arbitrary assertions may be made here but the grape validation
29
+ # system should be preferred.
30
+ #
31
+ # Alternately a proc or other object responding to +call+ may be
32
+ # supplied in place of a type. This should implement the same
33
+ # contract as +coerced?+, and must be supplied with a coercion
34
+ # +method+.
35
+ class CustomTypeCoercer
36
+ # A new coercer for the given type specification
37
+ # and coercion method.
38
+ #
39
+ # @param type [Class,#coerced?,#parsed?,#call?]
40
+ # specifier for the target type. See class docs.
41
+ # @param method [#parse,#call]
42
+ # optional coercion method. See class docs.
43
+ def initialize(type, method = nil)
44
+ coercion_method = infer_coercion_method type, method
45
+ @method = enforce_symbolized_keys type, coercion_method
46
+ @type_check = infer_type_check(type)
47
+ end
48
+
49
+ # Coerces the given value.
50
+ #
51
+ # @param value [String] value to be coerced, in grape
52
+ # this should always be a string.
53
+ # @return [Object] the coerced result
54
+ def call(val)
55
+ coerced_val = @method.call(val)
56
+
57
+ return coerced_val if coerced_val.is_a?(InvalidValue)
58
+ return InvalidValue.new unless coerced?(coerced_val)
59
+
60
+ coerced_val
61
+ end
62
+
63
+ def coerced?(val)
64
+ val.nil? || @type_check.call(val)
65
+ end
66
+
67
+ private
68
+
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
75
+ 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
87
+ end
88
+
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
+ 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
116
+ end
117
+
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
124
+ end
125
+
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
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # See {CustomTypeCoercer} for details on types
7
+ # that will be supported by this by this coercer.
8
+ # This coercer works in the same way as +CustomTypeCoercer+
9
+ # except that it expects to receive an array of strings to
10
+ # coerce and will return an array (or optionally, a set)
11
+ # of coerced values.
12
+ #
13
+ # +CustomTypeCoercer+ is already capable of providing type
14
+ # checking for arrays where an independent coercion method
15
+ # is supplied. As such, +CustomTypeCollectionCoercer+ does
16
+ # not allow for such a method to be supplied independently
17
+ # of the type.
18
+ class CustomTypeCollectionCoercer < CustomTypeCoercer
19
+ # A new coercer for collections of the given type.
20
+ #
21
+ # @param type [Class,#parse]
22
+ # type to which items in the array should be coerced.
23
+ # Must implement a +parse+ method which accepts a string,
24
+ # and for the purposes of type-checking it may either be
25
+ # a class, or it may implement a +coerced?+, +parsed?+ or
26
+ # +call+ method (in that order of precedence) which
27
+ # accepts a single argument and returns true if the given
28
+ # array item has been coerced correctly.
29
+ # @param set [Boolean]
30
+ # when true, a +Set+ will be returned by {#call} instead
31
+ # of an +Array+ and duplicate items will be discarded.
32
+ def initialize(type, set = false)
33
+ super(type)
34
+ @set = set
35
+ end
36
+
37
+ # Coerces the given value.
38
+ #
39
+ # @param value [Array<String>] an array of values to be coerced
40
+ # @return [Array,Set] the coerced result. May be an +Array+ or a
41
+ # +Set+ depending on the setting given to the constructor
42
+ def call(value)
43
+ coerced = value.map do |item|
44
+ coerced_item = super(item)
45
+
46
+ return coerced_item if coerced_item.is_a?(InvalidValue)
47
+
48
+ coerced_item
49
+ end
50
+
51
+ @set ? Set.new(coerced) : coerced
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
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
+ module Grape
11
+ module Validations
12
+ module Types
13
+ # A base class for classes which must identify a coercer to be used.
14
+ # If the +strict+ argument is true, it won't coerce the given value
15
+ # but check its type. More information there
16
+ # https://dry-rb.org/gems/dry-types/1.2/built-in-types/
17
+ class DryTypeCoercer
18
+ class << self
19
+ # Returns a collection coercer which corresponds to a given type.
20
+ # Example:
21
+ #
22
+ # collection_coercer_for(Array)
23
+ # #=> Grape::Validations::Types::ArrayCoercer
24
+ def collection_coercer_for(type)
25
+ case type
26
+ when Array
27
+ ArrayCoercer
28
+ when Set
29
+ SetCoercer
30
+ else
31
+ raise ArgumentError, "Unknown type: #{type}"
32
+ end
33
+ end
34
+
35
+ # Returns an instance of a coercer for a given type
36
+ def coercer_instance_for(type, strict = false)
37
+ klass = type.instance_of?(Class) ? PrimitiveCoercer : collection_coercer_for(type)
38
+ klass.new(type, strict)
39
+ end
40
+ end
41
+
42
+ def initialize(type, strict = false)
43
+ @type = type
44
+ @strict = strict
45
+ @scope = strict ? DryTypes::Strict : DryTypes::Params
46
+ end
47
+
48
+ # Coerces the given value to a type which was specified during
49
+ # initialization as a type argument.
50
+ #
51
+ # @param val [Object]
52
+ def call(val)
53
+ return if val.nil?
54
+
55
+ @coercer[val]
56
+ rescue Dry::Types::CoercionError => _e
57
+ InvalidValue.new
58
+ end
59
+
60
+ protected
61
+
62
+ attr_reader :scope, :type, :strict
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # Implementation for parameters that are multipart file objects.
7
+ # Actual handling of these objects is provided by +Rack::Request+;
8
+ # this class is here only to assert that rack's handling has succeeded.
9
+ class File
10
+ class << self
11
+ def parse(input)
12
+ return if input.nil?
13
+ return InvalidValue.new unless parsed?(input)
14
+
15
+ # Processing of multipart file objects
16
+ # is already taken care of by Rack::Request.
17
+ # Nothing to do here.
18
+ input
19
+ end
20
+
21
+ def parsed?(value)
22
+ # Rack::Request creates a Hash with filename,
23
+ # content type and an IO object. Do a bit of basic
24
+ # duck-typing.
25
+ value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # Instances of this class may be used as tokens to denote that a parameter value could not be
7
+ # coerced. The given message will be used as a validation error.
8
+ class InvalidValue
9
+ attr_reader :message
10
+
11
+ def initialize(message = nil)
12
+ @message = message
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # Handles coercion and type checking for parameters that are complex
7
+ # types given as JSON-encoded strings. It accepts both JSON objects
8
+ # and arrays of objects, and will coerce the input to a +Hash+
9
+ # or +Array+ object respectively. In either case the Grape
10
+ # validation system will apply nested validation rules to
11
+ # all returned objects.
12
+ class Json
13
+ class << self
14
+ # Coerce the input into a JSON-like data structure.
15
+ #
16
+ # @param input [String] a JSON-encoded parameter value
17
+ # @return [Hash,Array<Hash>,nil]
18
+ def parse(input)
19
+ return input if parsed?(input)
20
+
21
+ # Allow nulls and blank strings
22
+ return if input.nil? || input.match?(/^\s*$/)
23
+
24
+ JSON.parse(input, symbolize_names: true)
25
+ end
26
+
27
+ # Checks that the input was parsed successfully
28
+ # and isn't something odd such as an array of primitives.
29
+ #
30
+ # @param value [Object] result of {#parse}
31
+ # @return [true,false]
32
+ def parsed?(value)
33
+ value.is_a?(::Hash) || coerced_collection?(value)
34
+ end
35
+
36
+ protected
37
+
38
+ # Is the value an array of JSON-like objects?
39
+ #
40
+ # @param value [Object] result of {#parse}
41
+ # @return [true,false]
42
+ def coerced_collection?(value)
43
+ value.is_a?(::Array) && value.all?(::Hash)
44
+ end
45
+ end
46
+ end
47
+
48
+ # Specialization of the {Json} attribute that is guaranteed
49
+ # to return an array of objects. Accepts both JSON-encoded
50
+ # objects and arrays of objects, but wraps single objects
51
+ # in an Array.
52
+ class JsonArray < Json
53
+ class << self
54
+ # See {Json#parse}. Wraps single objects in an array.
55
+ #
56
+ # @param input [String] JSON-encoded parameter value
57
+ # @return [Array<Hash>]
58
+ def parse(input)
59
+ json = super
60
+ Array.wrap(json) unless json.nil?
61
+ end
62
+
63
+ # See {Json#coerced_collection?}
64
+ def parsed?(value)
65
+ coerced_collection? value
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # This class is intended for use with Grape endpoint parameters that
7
+ # have been declared to be of variant-type using the +:types+ option.
8
+ # +MultipleTypeCoercer+ will build a coercer for each type declared
9
+ # in the array passed to +:types+ using {Types.build_coercer}. It will
10
+ # apply these coercers to parameter values in the order given to
11
+ # +:types+, and will return the value returned by the first coercer
12
+ # to successfully coerce the parameter value. Therefore if +String+ is
13
+ # an allowed type it should be declared last, since it will always
14
+ # successfully "coerce" the value.
15
+ class MultipleTypeCoercer
16
+ # Construct a new coercer that will attempt to coerce
17
+ # values to the given list of types in the given order.
18
+ #
19
+ # @param types [Array<Class>] list of allowed types
20
+ # @param method [#call,#parse] method by which values should be
21
+ # coerced. See class docs for default behaviour.
22
+ def initialize(types, method = nil)
23
+ @method = method.respond_to?(:parse) ? method.method(:parse) : method
24
+
25
+ @type_coercers = types.map do |type|
26
+ if Types.multiple? type
27
+ VariantCollectionCoercer.new type, @method
28
+ else
29
+ Types.build_coercer type, strict: !@method.nil?
30
+ end
31
+ end
32
+ end
33
+
34
+ # Coerces the given value.
35
+ #
36
+ # @param val [String] value to be coerced, in grape
37
+ # this should always be a string.
38
+ # @return [Object,InvalidValue] the coerced result, or an instance
39
+ # of {InvalidValue} if the value could not be coerced.
40
+ def call(val)
41
+ # once the value is coerced by the custom method, its type should be checked
42
+ val = @method.call(val) if @method
43
+
44
+ coerced_val = InvalidValue.new
45
+
46
+ @type_coercers.each do |coercer|
47
+ coerced_val = coercer.call(val)
48
+
49
+ return coerced_val unless coerced_val.is_a?(InvalidValue)
50
+ end
51
+
52
+ coerced_val
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # Coerces the given value to a type defined via a +type+ argument during
7
+ # initialization. When +strict+ is true, it doesn't coerce a value but check
8
+ # that it has the proper type.
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),
16
+
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)
30
+ super
31
+
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
41
+ end
42
+
43
+ def call(val)
44
+ return InvalidValue.new if reject?(val)
45
+ return nil if val.nil? || treat_as_nil?(val)
46
+
47
+ super
48
+ end
49
+
50
+ protected
51
+
52
+ attr_reader :type
53
+
54
+ # This method maintains logic which was defined by Virtus. For example,
55
+ # dry-types is ok to convert an array or a hash to a string, it is supported,
56
+ # but Virtus wouldn't accept it. So, this method only exists to not introduce
57
+ # breaking changes.
58
+ def reject?(val)
59
+ (val.is_a?(Array) && type == String) ||
60
+ (val.is_a?(String) && type == Hash) ||
61
+ (val.is_a?(Hash) && type == String)
62
+ end
63
+
64
+ # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
65
+ # absence of a value and coerces it into nil. See a discussion there
66
+ # https://github.com/ruby-grape/grape/pull/2045
67
+ def treat_as_nil?(val)
68
+ val == '' && type != String
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # Takes the given array and converts it to a set. Every element of the set
7
+ # is also coerced.
8
+ class SetCoercer < ArrayCoercer
9
+ def initialize(type, strict = false)
10
+ super
11
+
12
+ @coercer = nil
13
+ end
14
+
15
+ def call(value)
16
+ return InvalidValue.new unless value.is_a?(Array)
17
+
18
+ coerce_elements(value)
19
+ end
20
+
21
+ protected
22
+
23
+ def coerce_elements(collection)
24
+ collection.each_with_object(Set.new) do |elem, memo|
25
+ coerced_elem = elem_coercer.call(elem)
26
+
27
+ return coerced_elem if coerced_elem.is_a?(InvalidValue)
28
+
29
+ memo.add(coerced_elem)
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Validations
5
+ module Types
6
+ # This class wraps {MultipleTypeCoercer}, for use with collections
7
+ # that allow members of more than one type.
8
+ class VariantCollectionCoercer
9
+ # Construct a new coercer that will attempt to coerce
10
+ # a list of values such that all members are of one of
11
+ # the given types. The container may also optionally be
12
+ # coerced to a +Set+. An arbitrary coercion +method+ may
13
+ # be supplied, which will be passed the entire collection
14
+ # as a parameter and should return a new collection, or
15
+ # may return the same one if no coercion was required.
16
+ #
17
+ # @param types [Array<Class>,Set<Class>] list of allowed types,
18
+ # also specifying the container type
19
+ # @param method [#call,#parse] method by which values should be coerced
20
+ def initialize(types, method = nil)
21
+ @types = types
22
+ @method = method.respond_to?(:parse) ? method.method(:parse) : method
23
+
24
+ # If we have a coercion method, pass it in here to save
25
+ # building another one, even though we call it directly.
26
+ @member_coercer = MultipleTypeCoercer.new types, method
27
+ end
28
+
29
+ # Coerce the given value.
30
+ #
31
+ # @param value [Array<String>] collection of values to be coerced
32
+ # @return [Array<Object>,Set<Object>,InvalidValue]
33
+ # the coerced result, or an instance
34
+ # of {InvalidValue} if the value could not be coerced.
35
+ def call(value)
36
+ return unless value.is_a? Array
37
+
38
+ value =
39
+ if @method
40
+ @method.call(value)
41
+ else
42
+ value.map { |v| @member_coercer.call(v) }
43
+ end
44
+ return Set.new value if @types.is_a? Set
45
+
46
+ value
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end