grape 1.5.0 → 1.6.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 +51 -2
- data/README.md +43 -10
- data/UPGRADING.md +91 -0
- data/grape.gemspec +5 -5
- data/lib/grape/api/instance.rb +13 -17
- data/lib/grape/api.rb +7 -14
- data/lib/grape/cookies.rb +2 -0
- data/lib/grape/dsl/callbacks.rb +1 -1
- data/lib/grape/dsl/desc.rb +3 -5
- data/lib/grape/dsl/helpers.rb +6 -4
- data/lib/grape/dsl/inside_route.rb +18 -9
- data/lib/grape/dsl/middleware.rb +4 -4
- data/lib/grape/dsl/parameters.rb +11 -7
- data/lib/grape/dsl/request_response.rb +9 -6
- data/lib/grape/dsl/routing.rb +7 -6
- data/lib/grape/dsl/settings.rb +5 -5
- data/lib/grape/endpoint.rb +21 -36
- data/lib/grape/error_formatter/json.rb +2 -6
- data/lib/grape/error_formatter/xml.rb +2 -6
- data/lib/grape/exceptions/empty_message_body.rb +11 -0
- data/lib/grape/exceptions/validation.rb +2 -3
- data/lib/grape/exceptions/validation_errors.rb +1 -1
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +2 -1
- data/lib/grape/formatter/xml.rb +1 -0
- data/lib/grape/locale/en.yml +1 -1
- data/lib/grape/middleware/auth/base.rb +3 -3
- data/lib/grape/middleware/base.rb +4 -2
- data/lib/grape/middleware/error.rb +1 -1
- data/lib/grape/middleware/formatter.rb +4 -4
- data/lib/grape/middleware/stack.rb +10 -16
- data/lib/grape/middleware/versioner/accept_version_header.rb +3 -5
- data/lib/grape/middleware/versioner/header.rb +6 -4
- data/lib/grape/middleware/versioner/param.rb +1 -0
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
- data/lib/grape/middleware/versioner/path.rb +2 -0
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/parser/xml.rb +1 -1
- data/lib/grape/path.rb +1 -0
- data/lib/grape/request.rb +3 -0
- data/lib/grape/router/attribute_translator.rb +1 -1
- data/lib/grape/router/pattern.rb +1 -1
- data/lib/grape/router/route.rb +2 -2
- data/lib/grape/router.rb +6 -0
- data/lib/grape/util/inheritable_setting.rb +1 -3
- data/lib/grape/util/lazy_value.rb +3 -2
- data/lib/grape/validations/attributes_iterator.rb +8 -0
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/params_scope.rb +92 -58
- data/lib/grape/validations/single_attribute_iterator.rb +1 -1
- data/lib/grape/validations/types/custom_type_coercer.rb +3 -2
- data/lib/grape/validations/types/dry_type_coercer.rb +1 -1
- data/lib/grape/validations/types/invalid_value.rb +24 -0
- data/lib/grape/validations/types/json.rb +2 -1
- data/lib/grape/validations/types/primitive_coercer.rb +3 -3
- data/lib/grape/validations/types.rb +1 -4
- data/lib/grape/validations/validator_factory.rb +1 -1
- data/lib/grape/validations/validators/all_or_none.rb +1 -0
- data/lib/grape/validations/validators/as.rb +4 -8
- data/lib/grape/validations/validators/at_least_one_of.rb +1 -0
- data/lib/grape/validations/validators/base.rb +12 -7
- data/lib/grape/validations/validators/coerce.rb +8 -9
- data/lib/grape/validations/validators/default.rb +1 -0
- data/lib/grape/validations/validators/exactly_one_of.rb +1 -0
- data/lib/grape/validations/validators/multiple_params_base.rb +5 -2
- data/lib/grape/validations/validators/mutual_exclusion.rb +1 -0
- data/lib/grape/validations/validators/presence.rb +1 -0
- data/lib/grape/validations/validators/regexp.rb +1 -0
- data/lib/grape/validations/validators/same_as.rb +1 -0
- data/lib/grape/validations/validators/values.rb +3 -0
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +3 -1
- data/spec/grape/api/custom_validations_spec.rb +1 -0
- data/spec/grape/api/routes_with_requirements_spec.rb +8 -8
- data/spec/grape/api_remount_spec.rb +9 -4
- data/spec/grape/api_spec.rb +203 -37
- data/spec/grape/dsl/callbacks_spec.rb +1 -1
- data/spec/grape/dsl/middleware_spec.rb +1 -1
- data/spec/grape/dsl/parameters_spec.rb +1 -0
- data/spec/grape/dsl/routing_spec.rb +1 -1
- data/spec/grape/endpoint/declared_spec.rb +259 -1
- data/spec/grape/endpoint_spec.rb +18 -5
- data/spec/grape/entity_spec.rb +10 -10
- data/spec/grape/middleware/auth/dsl_spec.rb +1 -1
- data/spec/grape/middleware/error_spec.rb +1 -2
- data/spec/grape/middleware/formatter_spec.rb +2 -2
- data/spec/grape/middleware/stack_spec.rb +4 -3
- data/spec/grape/request_spec.rb +1 -1
- data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
- data/spec/grape/validations/params_scope_spec.rb +37 -3
- data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
- data/spec/grape/validations/types/primitive_coercer_spec.rb +2 -2
- data/spec/grape/validations/validators/coerce_spec.rb +129 -22
- data/spec/grape/validations/validators/except_values_spec.rb +2 -2
- data/spec/grape/validations/validators/values_spec.rb +15 -11
- data/spec/grape/validations_spec.rb +280 -0
- data/spec/shared/versioning_examples.rb +22 -22
- data/spec/spec_helper.rb +1 -1
- data/spec/support/basic_auth_encode_helpers.rb +1 -1
- data/spec/support/versioned_helpers.rb +1 -1
- metadata +8 -6
@@ -22,6 +22,7 @@ module Grape
|
|
22
22
|
|
23
23
|
def after
|
24
24
|
return unless @app_response
|
25
|
+
|
25
26
|
status, headers, bodies = *@app_response
|
26
27
|
|
27
28
|
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
@@ -79,7 +80,7 @@ module Grape
|
|
79
80
|
(request.post? || request.put? || request.patch? || request.delete?) &&
|
80
81
|
(!request.form_data? || !request.media_type) &&
|
81
82
|
!request.parseable_data? &&
|
82
|
-
(request.content_length.to_i
|
83
|
+
(request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
|
83
84
|
|
84
85
|
return unless (input = env[Grape::Env::RACK_INPUT])
|
85
86
|
|
@@ -96,9 +97,7 @@ module Grape
|
|
96
97
|
def read_rack_input(body)
|
97
98
|
fmt = request.media_type ? mime_types[request.media_type] : options[:default_format]
|
98
99
|
|
99
|
-
unless content_type_for(fmt)
|
100
|
-
throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported."
|
101
|
-
end
|
100
|
+
throw :error, status: 415, message: "The provided content-type '#{request.media_type}' is not supported." unless content_type_for(fmt)
|
102
101
|
parser = Grape::Parser.parser_for fmt, **options
|
103
102
|
if parser
|
104
103
|
begin
|
@@ -145,6 +144,7 @@ module Grape
|
|
145
144
|
fmt = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[Grape::Http::Headers::FORMAT]
|
146
145
|
# avoid symbol memory leak on an unknown format
|
147
146
|
return fmt.to_sym if content_type_for(fmt)
|
147
|
+
|
148
148
|
fmt
|
149
149
|
end
|
150
150
|
|
@@ -6,12 +6,11 @@ module Grape
|
|
6
6
|
# It allows to insert and insert after
|
7
7
|
class Stack
|
8
8
|
class Middleware
|
9
|
-
attr_reader :args, :
|
9
|
+
attr_reader :args, :block, :klass
|
10
10
|
|
11
|
-
def initialize(klass, *args,
|
11
|
+
def initialize(klass, *args, &block)
|
12
12
|
@klass = klass
|
13
|
-
@args
|
14
|
-
@opts = opts
|
13
|
+
@args = args
|
15
14
|
@block = block
|
16
15
|
end
|
17
16
|
|
@@ -32,16 +31,8 @@ module Grape
|
|
32
31
|
klass.to_s
|
33
32
|
end
|
34
33
|
|
35
|
-
|
36
|
-
|
37
|
-
block ? builder.use(klass, *args, **opts, &block) : builder.use(klass, *args, **opts)
|
38
|
-
end
|
39
|
-
else
|
40
|
-
def use_in(builder)
|
41
|
-
args = self.args
|
42
|
-
args += [opts] unless opts.empty?
|
43
|
-
block ? builder.use(klass, *args, &block) : builder.use(klass, *args)
|
44
|
-
end
|
34
|
+
def use_in(builder)
|
35
|
+
builder.use(@klass, *@args, &@block)
|
45
36
|
end
|
46
37
|
end
|
47
38
|
|
@@ -54,8 +45,8 @@ module Grape
|
|
54
45
|
@others = []
|
55
46
|
end
|
56
47
|
|
57
|
-
def each
|
58
|
-
@middlewares.each
|
48
|
+
def each(&block)
|
49
|
+
@middlewares.each(&block)
|
59
50
|
end
|
60
51
|
|
61
52
|
def size
|
@@ -75,6 +66,7 @@ module Grape
|
|
75
66
|
middleware = self.class::Middleware.new(*args, &block)
|
76
67
|
middlewares.insert(index, middleware)
|
77
68
|
end
|
69
|
+
ruby2_keywords :insert if respond_to?(:ruby2_keywords, true)
|
78
70
|
|
79
71
|
alias insert_before insert
|
80
72
|
|
@@ -82,11 +74,13 @@ module Grape
|
|
82
74
|
index = assert_index(index, :after)
|
83
75
|
insert(index + 1, *args, &block)
|
84
76
|
end
|
77
|
+
ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true)
|
85
78
|
|
86
79
|
def use(*args, &block)
|
87
80
|
middleware = self.class::Middleware.new(*args, &block)
|
88
81
|
middlewares.push(middleware)
|
89
82
|
end
|
83
|
+
ruby2_keywords :use if respond_to?(:ruby2_keywords, true)
|
90
84
|
|
91
85
|
def merge_with(middleware_specs)
|
92
86
|
middleware_specs.each do |operation, *args|
|
@@ -22,11 +22,9 @@ module Grape
|
|
22
22
|
def before
|
23
23
|
potential_version = (env[Grape::Http::Headers::HTTP_ACCEPT_VERSION] || '').strip
|
24
24
|
|
25
|
-
if strict?
|
25
|
+
if strict? && potential_version.empty?
|
26
26
|
# If no Accept-Version header:
|
27
|
-
|
28
|
-
throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
|
29
|
-
end
|
27
|
+
throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
|
30
28
|
end
|
31
29
|
|
32
30
|
return if potential_version.empty?
|
@@ -51,7 +49,7 @@ module Grape
|
|
51
49
|
# of routes (see Grape::Router) for more information). To prevent
|
52
50
|
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
53
51
|
def cascade?
|
54
|
-
if options[:version_options]
|
52
|
+
if options[:version_options]&.key?(:cascade)
|
55
53
|
options[:version_options][:cascade]
|
56
54
|
else
|
57
55
|
true
|
@@ -26,10 +26,10 @@ module Grape
|
|
26
26
|
# route.
|
27
27
|
class Header < Base
|
28
28
|
VENDOR_VERSION_HEADER_REGEX =
|
29
|
-
/\Avnd\.([a-z0-9.\-_
|
29
|
+
/\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze
|
30
30
|
|
31
|
-
HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_
|
32
|
-
HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_
|
31
|
+
HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#{Regexp.last_match(0)}\^]+/.freeze
|
32
|
+
HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))+/.freeze
|
33
33
|
|
34
34
|
def before
|
35
35
|
strict_header_checks if strict?
|
@@ -52,12 +52,14 @@ module Grape
|
|
52
52
|
|
53
53
|
def strict_accept_header_presence_check
|
54
54
|
return unless header.qvalues.empty?
|
55
|
+
|
55
56
|
fail_with_invalid_accept_header!('Accept header must be set.')
|
56
57
|
end
|
57
58
|
|
58
59
|
def strict_version_vendor_accept_header_presence_check
|
59
60
|
return unless versions.present?
|
60
61
|
return if an_accept_header_with_version_and_vendor_is_present?
|
62
|
+
|
61
63
|
fail_with_invalid_accept_header!('API vendor or version not found.')
|
62
64
|
end
|
63
65
|
|
@@ -160,7 +162,7 @@ module Grape
|
|
160
162
|
# information). To prevent # this behavior, and not add the `X-Cascade`
|
161
163
|
# header, one can set the `:cascade` option to `false`.
|
162
164
|
def cascade?
|
163
|
-
if version_options
|
165
|
+
if version_options&.key?(:cascade)
|
164
166
|
version_options[:cascade]
|
165
167
|
else
|
166
168
|
true
|
@@ -32,6 +32,7 @@ module Grape
|
|
32
32
|
def before
|
33
33
|
potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey]
|
34
34
|
return if potential_version.nil?
|
35
|
+
|
35
36
|
throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
|
36
37
|
env[Grape::Env::API_VERSION] = potential_version
|
37
38
|
env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'English'
|
3
4
|
module Rack
|
4
5
|
module Accept
|
5
6
|
module Header
|
6
|
-
ALLOWED_CHARACTERS = %r{^([a-z*]+)
|
7
|
+
ALLOWED_CHARACTERS = %r{^([a-z*]+)/([a-z0-9*&\^\-_#{$ERROR_INFO}.+]+)(?:;([a-z0-9=;]+))?$}.freeze
|
7
8
|
class << self
|
8
9
|
# Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44
|
9
10
|
def parse_media_type(media_type)
|
@@ -37,6 +37,7 @@ module Grape
|
|
37
37
|
pieces = path.split('/')
|
38
38
|
potential_version = pieces[1]
|
39
39
|
return unless potential_version&.match?(options[:pattern])
|
40
|
+
|
40
41
|
throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
|
41
42
|
env[Grape::Env::API_VERSION] = potential_version
|
42
43
|
end
|
@@ -45,6 +46,7 @@ module Grape
|
|
45
46
|
|
46
47
|
def mounted_path?(path)
|
47
48
|
return false unless mount_path && path.start_with?(mount_path)
|
49
|
+
|
48
50
|
rest = path.slice(mount_path.length..-1)
|
49
51
|
rest.start_with?('/') || rest.empty?
|
50
52
|
end
|
data/lib/grape/parser/json.rb
CHANGED
@@ -8,7 +8,7 @@ module Grape
|
|
8
8
|
::Grape::Json.load(object)
|
9
9
|
rescue ::Grape::Json::ParseError
|
10
10
|
# handle JSON parsing errors via the rescue handlers or provide error message
|
11
|
-
raise Grape::Exceptions::InvalidMessageBody
|
11
|
+
raise Grape::Exceptions::InvalidMessageBody.new('application/json')
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
data/lib/grape/parser/xml.rb
CHANGED
@@ -8,7 +8,7 @@ module Grape
|
|
8
8
|
::Grape::Xml.parse(object)
|
9
9
|
rescue ::Grape::Xml::ParseError
|
10
10
|
# handle XML parsing errors via the rescue handlers or provide error message
|
11
|
-
raise Grape::Exceptions::InvalidMessageBody
|
11
|
+
raise Grape::Exceptions::InvalidMessageBody.new('application/xml')
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
data/lib/grape/path.rb
CHANGED
data/lib/grape/request.rb
CHANGED
@@ -15,6 +15,8 @@ module Grape
|
|
15
15
|
|
16
16
|
def params
|
17
17
|
@params ||= build_params
|
18
|
+
rescue EOFError
|
19
|
+
raise Grape::Exceptions::EmptyMessageBody.new(content_type)
|
18
20
|
end
|
19
21
|
|
20
22
|
def headers
|
@@ -35,6 +37,7 @@ module Grape
|
|
35
37
|
Grape::Util::LazyObject.new do
|
36
38
|
env.each_pair.with_object({}) do |(k, v), headers|
|
37
39
|
next unless k.to_s.start_with? HTTP_PREFIX
|
40
|
+
|
38
41
|
transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k)
|
39
42
|
headers[transformed_header] = v
|
40
43
|
end
|
@@ -4,7 +4,7 @@ module Grape
|
|
4
4
|
class Router
|
5
5
|
# this could be an OpenStruct, but doesn't work in Ruby 2.3.0, see https://bugs.ruby-lang.org/issues/12251
|
6
6
|
class AttributeTranslator
|
7
|
-
attr_reader :attributes
|
7
|
+
attr_reader :attributes
|
8
8
|
|
9
9
|
ROUTE_ATTRIBUTES = %i[
|
10
10
|
prefix
|
data/lib/grape/router/pattern.rb
CHANGED
data/lib/grape/router/route.rb
CHANGED
@@ -84,8 +84,8 @@ module Grape
|
|
84
84
|
path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
|
85
85
|
path = File.realpath(path) if Pathname.new(path).relative?
|
86
86
|
expected ||= name
|
87
|
-
warn
|
88
|
-
#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.
|
87
|
+
warn <<~WARNING
|
88
|
+
#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.
|
89
89
|
WARNING
|
90
90
|
end
|
91
91
|
end
|
data/lib/grape/router.rb
CHANGED
@@ -28,6 +28,7 @@ module Grape
|
|
28
28
|
|
29
29
|
def compile!
|
30
30
|
return if compiled
|
31
|
+
|
31
32
|
@union = Regexp.union(@neutral_regexes)
|
32
33
|
@neutral_regexes = nil
|
33
34
|
self.class.supported_methods.each do |method|
|
@@ -60,6 +61,7 @@ module Grape
|
|
60
61
|
def recognize_path(input)
|
61
62
|
any = with_optimization { greedy_match?(input) }
|
62
63
|
return if any == default_response
|
64
|
+
|
63
65
|
any.endpoint
|
64
66
|
end
|
65
67
|
|
@@ -80,6 +82,7 @@ module Grape
|
|
80
82
|
map[method].each do |route|
|
81
83
|
next if exact_route == route
|
82
84
|
next unless route.match?(input)
|
85
|
+
|
83
86
|
response = process_route(route, env)
|
84
87
|
break unless cascade?(response)
|
85
88
|
end
|
@@ -91,6 +94,7 @@ module Grape
|
|
91
94
|
response = yield(input, method)
|
92
95
|
|
93
96
|
return response if response && !(cascade = cascade?(response))
|
97
|
+
|
94
98
|
last_neighbor_route = greedy_match?(input)
|
95
99
|
|
96
100
|
# If last_neighbor_route exists and request method is OPTIONS,
|
@@ -139,12 +143,14 @@ module Grape
|
|
139
143
|
def match?(input, method)
|
140
144
|
current_regexp = @optimized_map[method]
|
141
145
|
return unless current_regexp.match(input)
|
146
|
+
|
142
147
|
last_match = Regexp.last_match
|
143
148
|
@map[method].detect { |route| last_match["_#{route.index}"] }
|
144
149
|
end
|
145
150
|
|
146
151
|
def greedy_match?(input)
|
147
152
|
return unless @union.match(input)
|
153
|
+
|
148
154
|
last_match = Regexp.last_match
|
149
155
|
@neutral_map.detect { |route| last_match["_#{route.index}"] }
|
150
156
|
end
|
@@ -5,9 +5,7 @@ module Grape
|
|
5
5
|
# A branchable, inheritable settings object which can store both stackable
|
6
6
|
# and inheritable values (see InheritableValues and StackableValues).
|
7
7
|
class InheritableSetting
|
8
|
-
attr_accessor :route, :api_class, :namespace
|
9
|
-
attr_accessor :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable
|
10
|
-
attr_accessor :parent, :point_in_time_copies
|
8
|
+
attr_accessor :route, :api_class, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent, :point_in_time_copies
|
11
9
|
|
12
10
|
# Retrieve global settings.
|
13
11
|
def self.global
|
@@ -49,9 +49,10 @@ module Grape
|
|
49
49
|
end
|
50
50
|
|
51
51
|
def []=(key, value)
|
52
|
-
@value_hash[key] =
|
52
|
+
@value_hash[key] = case value
|
53
|
+
when Hash
|
53
54
|
LazyValueHash.new(value)
|
54
|
-
|
55
|
+
when Array
|
55
56
|
LazyValueArray.new(value)
|
56
57
|
else
|
57
58
|
LazyValue.new(value)
|
@@ -48,6 +48,14 @@ module Grape
|
|
48
48
|
def yield_attributes(_resource_params, _attrs)
|
49
49
|
raise NotImplementedError
|
50
50
|
end
|
51
|
+
|
52
|
+
# This is a special case so that we can ignore tree's where option
|
53
|
+
# values are missing lower down. Unfortunately we can remove this
|
54
|
+
# are the parameter parsing stage as they are required to ensure
|
55
|
+
# the correct indexing is maintained
|
56
|
+
def skip?(val)
|
57
|
+
val == Grape::DSL::Parameters::EmptyOptionalValue
|
58
|
+
end
|
51
59
|
end
|
52
60
|
end
|
53
61
|
end
|