grape 3.2.1 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +80 -0
- data/README.md +116 -43
- data/UPGRADING.md +336 -1
- data/grape.gemspec +5 -5
- data/lib/grape/api/instance.rb +7 -7
- data/lib/grape/api.rb +22 -25
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +48 -50
- data/lib/grape/dsl/callbacks.rb +9 -3
- data/lib/grape/dsl/desc.rb +8 -2
- data/lib/grape/dsl/entity.rb +88 -0
- data/lib/grape/dsl/helpers.rb +27 -7
- data/lib/grape/dsl/inside_route.rb +38 -129
- data/lib/grape/dsl/logger.rb +3 -5
- data/lib/grape/dsl/parameters.rb +32 -38
- data/lib/grape/dsl/request_response.rb +53 -48
- data/lib/grape/dsl/rescue_options.rb +24 -0
- data/lib/grape/dsl/routing.rb +51 -35
- data/lib/grape/dsl/settings.rb +14 -8
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +19 -0
- data/lib/grape/endpoint.rb +96 -68
- data/lib/grape/env.rb +1 -3
- data/lib/grape/error_formatter/base.rb +23 -20
- data/lib/grape/error_formatter/json.rb +8 -4
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/exceptions/base.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +45 -0
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/validation.rb +14 -0
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +12 -20
- data/lib/grape/formatter/serializable_hash.rb +5 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/base.rb +2 -3
- data/lib/grape/middleware/auth/dsl.rb +23 -8
- data/lib/grape/middleware/base.rb +22 -33
- data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
- data/lib/grape/middleware/error.rb +152 -62
- data/lib/grape/middleware/formatter.rb +66 -50
- data/lib/grape/middleware/precomputed_content_types.rb +46 -0
- data/lib/grape/middleware/stack.rb +5 -6
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/base.rb +34 -38
- data/lib/grape/middleware/versioner/header.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +8 -3
- data/lib/grape/namespace.rb +3 -3
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +14 -17
- data/lib/grape/request.rb +15 -8
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern.rb +6 -10
- data/lib/grape/router.rb +28 -42
- data/lib/grape/serve_stream/file_body.rb +1 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +1 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/base_inheritable.rb +13 -16
- data/lib/grape/util/inheritable_setting.rb +44 -27
- data/lib/grape/util/inheritable_values.rb +7 -3
- 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 +1 -4
- data/lib/grape/util/path_normalizer.rb +34 -0
- data/lib/grape/util/registry.rb +1 -1
- data/lib/grape/util/stackable_values.rb +11 -8
- data/lib/grape/validations/attributes_iterator.rb +13 -13
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/oneof_collector.rb +39 -0
- data/lib/grape/validations/param_scope_tracker.rb +14 -9
- data/lib/grape/validations/params_documentation.rb +25 -23
- data/lib/grape/validations/params_scope.rb +54 -172
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/types/array_coercer.rb +2 -2
- data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
- data/lib/grape/validations/types/primitive_coercer.rb +10 -5
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
- data/lib/grape/validations/types.rb +23 -30
- data/lib/grape/validations/validations_spec.rb +149 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
- data/lib/grape/validations/validators/base.rb +39 -22
- data/lib/grape/validations/validators/coerce_validator.rb +5 -3
- data/lib/grape/validations/validators/default_validator.rb +7 -8
- data/lib/grape/validations/validators/except_values_validator.rb +3 -2
- data/lib/grape/validations/validators/length_validator.rb +1 -1
- data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
- data/lib/grape/validations/validators/oneof_validator.rb +49 -0
- data/lib/grape/validations/validators/values_validator.rb +5 -5
- data/lib/grape/version.rb +1 -1
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +6 -6
- metadata +34 -18
- data/lib/grape/middleware/globals.rb +0 -14
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Util
|
|
5
5
|
module Lazy
|
|
6
|
-
class Value
|
|
6
|
+
class Value < Base
|
|
7
7
|
attr_reader :access_keys
|
|
8
8
|
|
|
9
9
|
def initialize(value, access_keys = [])
|
|
10
|
+
super()
|
|
10
11
|
@value = value
|
|
11
12
|
@access_keys = access_keys
|
|
12
13
|
end
|
|
@@ -20,18 +21,10 @@ module Grape
|
|
|
20
21
|
@value
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
def lazy?
|
|
24
|
-
true
|
|
25
|
-
end
|
|
26
|
-
|
|
27
24
|
def reached_by(parent_access_keys, access_key)
|
|
28
25
|
@access_keys = parent_access_keys + [access_key]
|
|
29
26
|
self
|
|
30
27
|
end
|
|
31
|
-
|
|
32
|
-
def to_s
|
|
33
|
-
evaluate.to_s
|
|
34
|
-
end
|
|
35
28
|
end
|
|
36
29
|
end
|
|
37
30
|
end
|
|
@@ -5,28 +5,25 @@ module Grape
|
|
|
5
5
|
module Lazy
|
|
6
6
|
class ValueEnumerable < Value
|
|
7
7
|
def [](key)
|
|
8
|
-
if @value_hash[key].nil?
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@value_hash[key].reached_by(access_keys, key)
|
|
12
|
-
end
|
|
8
|
+
return Value.new(nil).reached_by(access_keys, key) if @value_hash[key].nil?
|
|
9
|
+
|
|
10
|
+
@value_hash[key].reached_by(access_keys, key)
|
|
13
11
|
end
|
|
14
12
|
|
|
15
13
|
def fetch(access_keys)
|
|
16
|
-
|
|
17
|
-
value = self[fetched_keys.shift]
|
|
18
|
-
fetched_keys.any? ? value.fetch(fetched_keys) : value
|
|
14
|
+
access_keys.reduce(self) { |node, key| node[key] }
|
|
19
15
|
end
|
|
20
16
|
|
|
21
17
|
def []=(key, value)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
18
|
+
value_class = case value
|
|
19
|
+
when Hash
|
|
20
|
+
ValueHash
|
|
21
|
+
when Array
|
|
22
|
+
ValueArray
|
|
23
|
+
else
|
|
24
|
+
Value
|
|
25
|
+
end
|
|
26
|
+
@value_hash[key] = value_class.new(value)
|
|
30
27
|
end
|
|
31
28
|
end
|
|
32
29
|
end
|
|
@@ -20,10 +20,6 @@ module Grape
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def ==(other)
|
|
23
|
-
eql?(other)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def eql?(other)
|
|
27
23
|
self.class == other.class &&
|
|
28
24
|
other.type == type &&
|
|
29
25
|
other.subtype == subtype &&
|
|
@@ -31,6 +27,7 @@ module Grape
|
|
|
31
27
|
other.version == version &&
|
|
32
28
|
other.format == format
|
|
33
29
|
end
|
|
30
|
+
alias eql? ==
|
|
34
31
|
|
|
35
32
|
def hash
|
|
36
33
|
[self.class, type, subtype, vendor, version, format].hash
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Util
|
|
5
|
+
module PathNormalizer
|
|
6
|
+
# Taken from Rails
|
|
7
|
+
# call("/foo") # => "/foo"
|
|
8
|
+
# call("/foo/") # => "/foo"
|
|
9
|
+
# call("foo") # => "/foo"
|
|
10
|
+
# call("") # => "/"
|
|
11
|
+
# call("/%ab") # => "/%AB"
|
|
12
|
+
# https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19
|
|
13
|
+
def self.call(path)
|
|
14
|
+
return '/' unless path
|
|
15
|
+
return path if path == '/'
|
|
16
|
+
|
|
17
|
+
# Fast path for the overwhelming majority of paths that don't need to be normalized
|
|
18
|
+
return path if path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//}))
|
|
19
|
+
|
|
20
|
+
# Slow path
|
|
21
|
+
encoding = path.encoding
|
|
22
|
+
path = "/#{path}"
|
|
23
|
+
path.squeeze!('/')
|
|
24
|
+
|
|
25
|
+
unless path == '/'
|
|
26
|
+
path.delete_suffix!('/')
|
|
27
|
+
path.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
path.force_encoding(encoding)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/grape/util/registry.rb
CHANGED
|
@@ -3,24 +3,27 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Util
|
|
5
5
|
class StackableValues < BaseInheritable
|
|
6
|
-
|
|
6
|
+
EMPTY = [].freeze
|
|
7
|
+
|
|
8
|
+
# Even if there is no value, an empty (frozen) array will be returned.
|
|
7
9
|
def [](name)
|
|
8
|
-
inherited_value = inherited_values[name]
|
|
9
|
-
new_value = new_values[name]
|
|
10
|
+
inherited_value = @inherited_values[name]
|
|
11
|
+
new_value = @new_values && @new_values[name]
|
|
10
12
|
|
|
11
|
-
return new_value ||
|
|
13
|
+
return new_value || EMPTY unless inherited_value
|
|
12
14
|
|
|
13
15
|
concat_values(inherited_value, new_value)
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
def []=(name, value)
|
|
17
|
-
new_values
|
|
18
|
-
new_values[name]
|
|
19
|
+
@new_values ||= {}
|
|
20
|
+
@new_values[name] ||= []
|
|
21
|
+
@new_values[name].push value
|
|
19
22
|
end
|
|
20
23
|
|
|
21
24
|
def to_hash
|
|
22
|
-
keys.
|
|
23
|
-
|
|
25
|
+
keys.to_h do |key|
|
|
26
|
+
[key, self[key]]
|
|
24
27
|
end
|
|
25
28
|
end
|
|
26
29
|
|
|
@@ -3,37 +3,37 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Validations
|
|
5
5
|
class AttributesIterator
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def initialize(attrs, scope
|
|
6
|
+
# +attrs+ and +scope+ are static per validator; only +params+ varies
|
|
7
|
+
# per request, so an instance can be built once and reused (it keeps
|
|
8
|
+
# no request-derived state). Reused instances are shared across
|
|
9
|
+
# threads, so +each+ must stay free of mutable instance state.
|
|
10
|
+
def initialize(attrs, scope)
|
|
11
11
|
@attrs = attrs
|
|
12
12
|
@scope = scope
|
|
13
|
-
@original_params = scope.params(params)
|
|
14
|
-
@params = Array.wrap(@original_params)
|
|
15
13
|
end
|
|
16
14
|
|
|
17
|
-
def each(&)
|
|
18
|
-
|
|
15
|
+
def each(params, &)
|
|
16
|
+
original_params = @scope.params(params)
|
|
17
|
+
# because we need recursion for nested arrays
|
|
18
|
+
do_each(Array.wrap(original_params), original_params, &)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
-
def do_each(params_to_process, parent_indices = [], &block)
|
|
23
|
+
def do_each(params_to_process, original_params, parent_indices = [], &block)
|
|
24
24
|
params_to_process.each_with_index do |resource_params, index|
|
|
25
25
|
# when we get arrays of arrays it means that target element located inside array
|
|
26
26
|
# we need this because we want to know parent arrays indices
|
|
27
27
|
if resource_params.is_a?(Array)
|
|
28
|
-
do_each(resource_params, [index] + parent_indices, &block)
|
|
28
|
+
do_each(resource_params, original_params, [index] + parent_indices, &block)
|
|
29
29
|
next
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
if @scope.type == Array
|
|
33
|
-
next unless
|
|
33
|
+
next unless original_params.is_a?(Array) # do not validate content of array if it isn't array
|
|
34
34
|
|
|
35
35
|
store_indices(@scope, index, parent_indices)
|
|
36
|
-
elsif
|
|
36
|
+
elsif original_params.is_a?(Array)
|
|
37
37
|
# Lateral scope (no @element) whose params resolved to an array —
|
|
38
38
|
# delegate index tracking to the nearest array-typed ancestor so
|
|
39
39
|
# that full_name produces the correct bracketed index.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Validations
|
|
5
|
+
# Immutable value object describing how a parameter is coerced. Assembled
|
|
6
|
+
# by {ValidationsSpec#coerce_options} from the parsed +type+/+coerce_with+/
|
|
7
|
+
# +coerce_message+ declaration — never written by the user — and consumed
|
|
8
|
+
# by {ParamsScope#check_coerce_with} / {ParamsScope#validate_coerce} and by
|
|
9
|
+
# {Validators::Validators::CoerceValidator} (which receives it as its
|
|
10
|
+
# +options+ argument).
|
|
11
|
+
#
|
|
12
|
+
# All three fields may be +nil+ (e.g. a remountable API evaluated on its
|
|
13
|
+
# base instance has no resolved +type+ yet).
|
|
14
|
+
# +coerce_method+ (not +method+) avoids shadowing +Object#method+.
|
|
15
|
+
CoerceOptions = Data.define(:type, :coerce_method, :message) do
|
|
16
|
+
def initialize(type: nil, coerce_method: nil, message: nil)
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Validations
|
|
5
|
+
# Stand-in for the +@api+ object that {ParamsScope} normally writes to.
|
|
6
|
+
# Used when evaluating a single variant block of a +oneof:+ schema so that
|
|
7
|
+
# the variant's validators are captured locally rather than registered on
|
|
8
|
+
# the real API. Exposes only the slice of the API surface that
|
|
9
|
+
# ParamsScope and its helpers touch during definition.
|
|
10
|
+
class OneofCollector
|
|
11
|
+
attr_reader :inheritable_setting
|
|
12
|
+
|
|
13
|
+
def initialize
|
|
14
|
+
@inheritable_setting = Grape::Util::InheritableSetting.new
|
|
15
|
+
@inheritable_setting.namespace_inheritable[:do_not_document] = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configuration
|
|
19
|
+
nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def validators
|
|
23
|
+
inheritable_setting.namespace_stackable[:validations]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def declared_params
|
|
27
|
+
inheritable_setting.namespace_stackable[:declared_params]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Evaluate +variant_block+ in a fresh +ParamsScope+ backed by a new
|
|
31
|
+
# collector and return the validators that the block registered.
|
|
32
|
+
def self.collect(variant_block)
|
|
33
|
+
collector = new
|
|
34
|
+
ParamsScope.new(api: collector, type: Hash, &variant_block)
|
|
35
|
+
collector.validators
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -29,28 +29,33 @@ module Grape
|
|
|
29
29
|
Fiber[FIBER_KEY]
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def initialize
|
|
33
|
-
@index_tracker = {}.compare_by_identity
|
|
34
|
-
@qualifying_params_tracker = {}.compare_by_identity
|
|
35
|
-
end
|
|
36
|
-
|
|
37
32
|
def store_index(scope, index)
|
|
38
|
-
|
|
33
|
+
index_tracker.store(scope, index)
|
|
39
34
|
end
|
|
40
35
|
|
|
41
36
|
def index_for(scope)
|
|
42
|
-
|
|
37
|
+
index_tracker[scope]
|
|
43
38
|
end
|
|
44
39
|
|
|
45
40
|
# Returns qualifying params for +scope+, or EMPTY_PARAMS if none were stored.
|
|
46
41
|
# Note: an explicitly stored empty array and "never stored" are treated identically
|
|
47
42
|
# by callers (both yield a blank result that falls through to the parent params).
|
|
48
43
|
def qualifying_params(scope)
|
|
49
|
-
|
|
44
|
+
qualifying_params_tracker.fetch(scope, EMPTY_PARAMS)
|
|
50
45
|
end
|
|
51
46
|
|
|
52
47
|
def store_qualifying_params(scope, params)
|
|
53
|
-
|
|
48
|
+
qualifying_params_tracker.store(scope, params)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def index_tracker
|
|
54
|
+
@index_tracker ||= {}.compare_by_identity
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def qualifying_params_tracker
|
|
58
|
+
@qualifying_params_tracker ||= {}.compare_by_identity
|
|
54
59
|
end
|
|
55
60
|
end
|
|
56
61
|
end
|
|
@@ -2,39 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Validations
|
|
5
|
-
# Documents parameters of an endpoint.
|
|
6
|
-
#
|
|
7
|
-
|
|
5
|
+
# Documents parameters of an endpoint. Reads from a frozen
|
|
6
|
+
# +ValidationsSpec+; never mutates the user's validations hash.
|
|
8
7
|
module ParamsDocumentation
|
|
9
|
-
def document_params(attrs,
|
|
10
|
-
return
|
|
8
|
+
def document_params(attrs, spec)
|
|
9
|
+
return if @api.inheritable_setting.namespace_inheritable[:do_not_document]
|
|
11
10
|
|
|
12
|
-
documented_attrs = attrs.
|
|
13
|
-
|
|
11
|
+
documented_attrs = attrs.to_h do |name|
|
|
12
|
+
[full_name(name), extract_details(spec)]
|
|
14
13
|
end
|
|
15
14
|
@api.inheritable_setting.namespace_stackable[:params] = documented_attrs
|
|
16
15
|
end
|
|
17
16
|
|
|
18
17
|
private
|
|
19
18
|
|
|
20
|
-
def extract_details(
|
|
21
|
-
{}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
def extract_details(spec)
|
|
20
|
+
details = {}
|
|
21
|
+
details[:required] = spec.required?
|
|
22
|
+
details[:type] = TypeCache[spec.coerce_type] if spec.coerce_type
|
|
23
|
+
details[:values] = spec.values if spec.values
|
|
24
|
+
details[:except_values] = spec.except_values if spec.except_values
|
|
25
|
+
details[:default] = spec.default unless spec.default.nil?
|
|
26
|
+
|
|
27
|
+
length = spec.raw[:length]
|
|
28
|
+
if length.is_a?(Hash)
|
|
29
|
+
details[:min_length] = length[:min] if length.key?(:min)
|
|
30
|
+
details[:max_length] = length[:max] if length.key?(:max)
|
|
31
|
+
end
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
33
|
+
desc = spec.raw[:desc] || spec.raw[:description]
|
|
34
|
+
details[:desc] = desc if desc
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
documentation = spec.raw[:documentation]
|
|
37
|
+
details[:documentation] = documentation if documentation
|
|
38
|
+
|
|
39
|
+
details
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
class TypeCache < Grape::Util::Cache
|