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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +351 -0
- data/CONTRIBUTING.md +18 -10
- data/README.md +457 -228
- data/UPGRADING.md +1240 -1
- data/grape.gemspec +7 -7
- data/lib/grape/api/instance.rb +65 -116
- data/lib/grape/api.rb +53 -62
- data/lib/grape/content_types.rb +58 -3
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +116 -0
- data/lib/grape/dry_types.rb +48 -4
- data/lib/grape/dsl/callbacks.rb +8 -58
- data/lib/grape/dsl/declared.rb +35 -0
- data/lib/grape/dsl/desc.rb +24 -67
- data/lib/grape/dsl/entity.rb +106 -0
- data/lib/grape/dsl/helpers.rb +80 -65
- data/lib/grape/dsl/inside_route.rb +56 -296
- data/lib/grape/dsl/logger.rb +4 -9
- data/lib/grape/dsl/middleware.rb +22 -40
- data/lib/grape/dsl/parameters.rb +86 -93
- data/lib/grape/dsl/request_response.rb +139 -139
- data/lib/grape/dsl/rescue_options.rb +25 -0
- data/lib/grape/dsl/routing.rb +328 -203
- data/lib/grape/dsl/settings.rb +29 -135
- data/lib/grape/dsl/validations.rb +38 -44
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +25 -0
- data/lib/grape/endpoint.rb +285 -245
- data/lib/grape/env.rb +8 -3
- data/lib/grape/error_formatter/base.rb +27 -21
- data/lib/grape/error_formatter/json.rb +6 -0
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/base.rb +20 -44
- data/lib/grape/exceptions/error_response.rb +48 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
- data/lib/grape/exceptions/invalid_formatter.rb +1 -1
- data/lib/grape/exceptions/invalid_message_body.rb +1 -1
- data/lib/grape/exceptions/invalid_version_header.rb +1 -1
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
- data/lib/grape/exceptions/method_not_allowed.rb +1 -1
- data/lib/grape/exceptions/missing_group_type.rb +0 -2
- data/lib/grape/exceptions/missing_mime_type.rb +1 -1
- data/lib/grape/exceptions/request_error.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/unknown_parameter.rb +1 -1
- data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
- data/lib/grape/exceptions/unknown_validator.rb +1 -1
- data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
- data/lib/grape/exceptions/validation.rb +25 -7
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +21 -23
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +6 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +3 -5
- data/lib/grape/middleware/auth/base.rb +4 -3
- data/lib/grape/middleware/auth/dsl.rb +9 -23
- data/lib/grape/middleware/base.rb +41 -36
- data/lib/grape/middleware/error.rb +271 -73
- data/lib/grape/middleware/formatter.rb +152 -63
- data/lib/grape/middleware/precomputed_content_types.rb +51 -0
- data/lib/grape/middleware/stack.rb +20 -11
- data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
- data/lib/grape/middleware/versioner/base.rb +40 -48
- data/lib/grape/middleware/versioner/header.rb +5 -23
- data/lib/grape/middleware/versioner/param.rb +2 -2
- data/lib/grape/middleware/versioner/path.rb +54 -7
- data/lib/grape/mountable.rb +22 -0
- data/lib/grape/namespace.rb +19 -11
- data/lib/grape/params_builder/base.rb +2 -0
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/params_builder.rb +2 -19
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +8 -72
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +57 -20
- data/lib/grape/router/base_route.rb +53 -7
- data/lib/grape/router/greedy_route.rb +16 -6
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +65 -37
- data/lib/grape/router/route.rb +66 -18
- data/lib/grape/router.rb +126 -115
- data/lib/grape/serve_stream/file_body.rb +7 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +7 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/api_description.rb +67 -0
- data/lib/grape/util/cache.rb +22 -2
- data/lib/grape/util/deep_freeze.rb +34 -0
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +777 -39
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +2 -9
- data/lib/grape/util/lazy/value.rb +2 -9
- data/lib/grape/util/lazy/value_enumerable.rb +13 -16
- data/lib/grape/util/media_type.rb +13 -9
- data/lib/grape/util/path_normalizer.rb +37 -0
- data/lib/grape/util/registry.rb +13 -3
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +39 -10
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +61 -29
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/contract_scope.rb +8 -13
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/oneof_collector.rb +35 -0
- data/lib/grape/validations/param_scope_tracker.rb +62 -0
- data/lib/grape/validations/params_documentation.rb +52 -0
- data/lib/grape/validations/params_scope.rb +217 -304
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/single_attribute_iterator.rb +6 -4
- data/lib/grape/validations/types/array_coercer.rb +7 -10
- data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
- data/lib/grape/validations/types/json.rb +1 -3
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +14 -33
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
- data/lib/grape/validations/types.rb +29 -51
- data/lib/grape/validations/validations_spec.rb +164 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
- data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
- data/lib/grape/validations/validators/base.rb +118 -34
- data/lib/grape/validations/validators/coerce_validator.rb +25 -37
- data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
- data/lib/grape/validations/validators/default_validator.rb +13 -20
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
- data/lib/grape/validations/validators/except_values_validator.rb +14 -4
- data/lib/grape/validations/validators/length_validator.rb +23 -22
- data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
- data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +51 -0
- data/lib/grape/validations/validators/presence_validator.rb +4 -2
- data/lib/grape/validations/validators/regexp_validator.rb +11 -3
- data/lib/grape/validations/validators/same_as_validator.rb +7 -15
- data/lib/grape/validations/validators/values_validator.rb +34 -26
- data/lib/grape/version.rb +1 -2
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +58 -31
- metadata +62 -39
- data/lib/grape/api/helpers.rb +0 -9
- data/lib/grape/dsl/api.rb +0 -17
- data/lib/grape/dsl/configuration.rb +0 -15
- data/lib/grape/exceptions/conflicting_types.rb +0 -11
- data/lib/grape/exceptions/empty_message_body.rb +0 -11
- data/lib/grape/exceptions/invalid_parameters.rb +0 -11
- data/lib/grape/exceptions/missing_option.rb +0 -11
- data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
- data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
- data/lib/grape/exceptions/unknown_options.rb +0 -11
- data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
- data/lib/grape/extensions/hash.rb +0 -27
- data/lib/grape/extensions/hashie/mash.rb +0 -24
- data/lib/grape/middleware/globals.rb +0 -14
- data/lib/grape/types/invalid_value.rb +0 -8
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -29
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
- data/lib/grape/util/strict_hash_configuration.rb +0 -108
- data/lib/grape/validations/attributes_doc.rb +0 -60
- 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
|
|
8
|
+
def yield_attributes(val)
|
|
9
9
|
return if skip?(val)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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/
|
|
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
|
|
15
|
+
def initialize(type, strict: false)
|
|
16
16
|
super
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 :
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
119
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
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/
|
|
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, "
|
|
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
|
|
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
|
|
37
|
+
def initialize(type, strict: false)
|
|
43
38
|
@type = type
|
|
44
39
|
@strict = strict
|
|
45
|
-
@
|
|
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
|
|
51
|
+
rescue Dry::Types::CoercionError
|
|
57
52
|
InvalidValue.new
|
|
58
53
|
end
|
|
59
54
|
|
|
60
55
|
protected
|
|
61
56
|
|
|
62
|
-
attr_reader :
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
@
|
|
33
|
-
|
|
34
|
-
@
|
|
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
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 == ''
|
|
49
|
+
@treat_empty_as_nil && val == ''
|
|
69
50
|
end
|
|
70
51
|
end
|
|
71
52
|
end
|
|
@@ -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
|
-
|
|
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
|
|
57
|
+
return Set.new coerced if @types.is_a? Set
|
|
45
58
|
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
|
188
|
-
|
|
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
|
-
|
|
167
|
+
# Multiply-typed parameters, e.g. types: [Integer, String].
|
|
168
|
+
return MultipleTypeCoercer.new(mapped, method) if multiple?(mapped)
|
|
191
169
|
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
177
|
+
# Fallback: let dry-types handle primitives, structures, and known specials.
|
|
178
|
+
DryTypeCoercer.coercer_instance_for(mapped, strict:)
|
|
199
179
|
end
|
|
200
180
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|