grape 2.0.0 → 2.1.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 +61 -1
- data/README.md +362 -316
- data/UPGRADING.md +197 -7
- data/grape.gemspec +5 -6
- data/lib/grape/api/instance.rb +13 -10
- data/lib/grape/api.rb +17 -8
- data/lib/grape/content_types.rb +0 -2
- data/lib/grape/cookies.rb +2 -1
- data/lib/grape/dry_types.rb +0 -2
- data/lib/grape/dsl/desc.rb +22 -20
- data/lib/grape/dsl/headers.rb +1 -1
- data/lib/grape/dsl/inside_route.rb +42 -13
- data/lib/grape/dsl/parameters.rb +4 -3
- data/lib/grape/dsl/routing.rb +20 -4
- data/lib/grape/dsl/validations.rb +13 -0
- data/lib/grape/endpoint.rb +12 -15
- data/lib/grape/{util/env.rb → env.rb} +0 -5
- data/lib/grape/error_formatter/txt.rb +11 -10
- data/lib/grape/exceptions/base.rb +3 -3
- data/lib/grape/exceptions/validation.rb +0 -2
- data/lib/grape/exceptions/validation_array_errors.rb +1 -0
- data/lib/grape/exceptions/validation_errors.rb +1 -3
- data/lib/grape/extensions/hash.rb +5 -1
- data/lib/grape/http/headers.rb +18 -34
- data/lib/grape/{util/json.rb → json.rb} +1 -3
- data/lib/grape/locale/en.yml +3 -0
- data/lib/grape/middleware/auth/base.rb +0 -2
- data/lib/grape/middleware/auth/dsl.rb +0 -2
- data/lib/grape/middleware/base.rb +0 -2
- data/lib/grape/middleware/error.rb +55 -50
- data/lib/grape/middleware/formatter.rb +16 -13
- data/lib/grape/middleware/globals.rb +1 -3
- data/lib/grape/middleware/stack.rb +2 -3
- data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
- data/lib/grape/middleware/versioner/header.rb +17 -163
- data/lib/grape/middleware/versioner/param.rb +2 -4
- data/lib/grape/middleware/versioner/path.rb +1 -3
- data/lib/grape/namespace.rb +3 -4
- data/lib/grape/path.rb +24 -29
- data/lib/grape/request.rb +4 -12
- data/lib/grape/router/base_route.rb +39 -0
- data/lib/grape/router/greedy_route.rb +20 -0
- data/lib/grape/router/pattern.rb +39 -30
- data/lib/grape/router/route.rb +22 -59
- data/lib/grape/router.rb +32 -37
- data/lib/grape/util/accept/header.rb +19 -0
- data/lib/grape/util/accept_header_handler.rb +105 -0
- data/lib/grape/util/base_inheritable.rb +4 -4
- data/lib/grape/util/cache.rb +0 -3
- data/lib/grape/util/endpoint_configuration.rb +1 -1
- data/lib/grape/util/header.rb +13 -0
- data/lib/grape/util/inheritable_values.rb +0 -2
- data/lib/grape/util/lazy/block.rb +29 -0
- data/lib/grape/util/lazy/object.rb +45 -0
- data/lib/grape/util/lazy/value.rb +38 -0
- data/lib/grape/util/lazy/value_array.rb +21 -0
- data/lib/grape/util/lazy/value_enumerable.rb +34 -0
- data/lib/grape/util/lazy/value_hash.rb +21 -0
- data/lib/grape/util/media_type.rb +70 -0
- data/lib/grape/util/reverse_stackable_values.rb +1 -6
- data/lib/grape/util/stackable_values.rb +1 -6
- data/lib/grape/util/strict_hash_configuration.rb +3 -3
- data/lib/grape/validations/attributes_doc.rb +38 -36
- data/lib/grape/validations/contract_scope.rb +71 -0
- data/lib/grape/validations/params_scope.rb +10 -9
- data/lib/grape/validations/types/array_coercer.rb +0 -2
- data/lib/grape/validations/types/build_coercer.rb +69 -71
- data/lib/grape/validations/types/dry_type_coercer.rb +1 -11
- data/lib/grape/validations/types/json.rb +0 -2
- data/lib/grape/validations/types/primitive_coercer.rb +0 -2
- data/lib/grape/validations/types/set_coercer.rb +0 -3
- data/lib/grape/validations/types.rb +0 -3
- data/lib/grape/validations/validators/base.rb +1 -0
- data/lib/grape/validations/validators/default_validator.rb +5 -1
- data/lib/grape/validations/validators/length_validator.rb +42 -0
- data/lib/grape/validations/validators/values_validator.rb +6 -1
- data/lib/grape/validations.rb +3 -7
- data/lib/grape/version.rb +1 -1
- data/lib/grape/{util/xml.rb → xml.rb} +1 -1
- data/lib/grape.rb +30 -274
- metadata +31 -37
- data/lib/grape/eager_load.rb +0 -20
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
- data/lib/grape/router/attribute_translator.rb +0 -63
- data/lib/grape/util/lazy_block.rb +0 -27
- data/lib/grape/util/lazy_object.rb +0 -43
- data/lib/grape/util/lazy_value.rb +0 -91
@@ -1,8 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'grape/middleware/base'
|
4
|
-
require 'grape/middleware/versioner/parse_media_type_patch'
|
5
|
-
|
6
3
|
module Grape
|
7
4
|
module Middleware
|
8
5
|
module Versioner
|
@@ -25,169 +22,26 @@ module Grape
|
|
25
22
|
# X-Cascade header to alert Grape::Router to attempt the next matched
|
26
23
|
# route.
|
27
24
|
class Header < Base
|
28
|
-
VENDOR_VERSION_HEADER_REGEX =
|
29
|
-
/\Avnd\.([a-z0-9.\-_!#{Regexp.last_match(0)}\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/.freeze
|
30
|
-
|
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
|
-
|
34
25
|
def before
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
def strict_accept_header_presence_check
|
54
|
-
return unless header.qvalues.empty?
|
55
|
-
|
56
|
-
fail_with_invalid_accept_header!('Accept header must be set.')
|
57
|
-
end
|
58
|
-
|
59
|
-
def strict_version_vendor_accept_header_presence_check
|
60
|
-
return if versions.blank? || an_accept_header_with_version_and_vendor_is_present?
|
61
|
-
|
62
|
-
fail_with_invalid_accept_header!('API vendor or version not found.')
|
63
|
-
end
|
64
|
-
|
65
|
-
def an_accept_header_with_version_and_vendor_is_present?
|
66
|
-
header.qvalues.keys.any? do |h|
|
67
|
-
VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', ''))
|
26
|
+
handler = Grape::Util::AcceptHeaderHandler.new(
|
27
|
+
accept_header: env[Grape::Http::Headers::HTTP_ACCEPT],
|
28
|
+
versions: options[:versions],
|
29
|
+
**options.fetch(:version_options) { {} }
|
30
|
+
)
|
31
|
+
|
32
|
+
handler.match_best_quality_media_type!(
|
33
|
+
content_types: content_types,
|
34
|
+
allowed_methods: env[Grape::Env::GRAPE_ALLOWED_METHODS]
|
35
|
+
) do |media_type|
|
36
|
+
env.update(
|
37
|
+
Grape::Env::API_TYPE => media_type.type,
|
38
|
+
Grape::Env::API_SUBTYPE => media_type.subtype,
|
39
|
+
Grape::Env::API_VENDOR => media_type.vendor,
|
40
|
+
Grape::Env::API_VERSION => media_type.version,
|
41
|
+
Grape::Env::API_FORMAT => media_type.format
|
42
|
+
)
|
68
43
|
end
|
69
44
|
end
|
70
|
-
|
71
|
-
def header
|
72
|
-
@header ||= rack_accept_header
|
73
|
-
end
|
74
|
-
|
75
|
-
def media_type
|
76
|
-
@media_type ||= header.best_of(available_media_types)
|
77
|
-
end
|
78
|
-
|
79
|
-
def media_type_header_handler
|
80
|
-
type, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
81
|
-
env[Grape::Env::API_TYPE] = type
|
82
|
-
env[Grape::Env::API_SUBTYPE] = subtype
|
83
|
-
|
84
|
-
return unless VENDOR_VERSION_HEADER_REGEX =~ subtype
|
85
|
-
|
86
|
-
env[Grape::Env::API_VENDOR] = Regexp.last_match[1]
|
87
|
-
env[Grape::Env::API_VERSION] = Regexp.last_match[2]
|
88
|
-
# weird that Grape::Middleware::Formatter also does this
|
89
|
-
env[Grape::Env::API_FORMAT] = Regexp.last_match[3]
|
90
|
-
end
|
91
|
-
|
92
|
-
def fail_with_invalid_accept_header!(message)
|
93
|
-
raise Grape::Exceptions::InvalidAcceptHeader
|
94
|
-
.new(message, error_headers)
|
95
|
-
end
|
96
|
-
|
97
|
-
def fail_with_invalid_version_header!(message)
|
98
|
-
raise Grape::Exceptions::InvalidVersionHeader
|
99
|
-
.new(message, error_headers)
|
100
|
-
end
|
101
|
-
|
102
|
-
def available_media_types
|
103
|
-
[].tap do |available_media_types|
|
104
|
-
content_types.each_key do |extension|
|
105
|
-
versions.reverse_each do |version|
|
106
|
-
available_media_types << "application/vnd.#{vendor}-#{version}+#{extension}"
|
107
|
-
available_media_types << "application/vnd.#{vendor}-#{version}"
|
108
|
-
end
|
109
|
-
available_media_types << "application/vnd.#{vendor}+#{extension}"
|
110
|
-
end
|
111
|
-
|
112
|
-
available_media_types << "application/vnd.#{vendor}"
|
113
|
-
available_media_types.concat(content_types.values.flatten)
|
114
|
-
end
|
115
|
-
end
|
116
|
-
|
117
|
-
def headers_contain_wrong_vendor?
|
118
|
-
header.values.all? do |header_value|
|
119
|
-
vendor?(header_value) && request_vendor(header_value) != vendor
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
def headers_contain_wrong_version?
|
124
|
-
header.values.all? do |header_value|
|
125
|
-
version?(header_value) && versions.exclude?(request_version(header_value))
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
def rack_accept_header
|
130
|
-
Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
|
131
|
-
rescue RuntimeError => e
|
132
|
-
fail_with_invalid_accept_header!(e.message)
|
133
|
-
end
|
134
|
-
|
135
|
-
def versions
|
136
|
-
options[:versions] || []
|
137
|
-
end
|
138
|
-
|
139
|
-
def vendor
|
140
|
-
version_options && version_options[:vendor]
|
141
|
-
end
|
142
|
-
|
143
|
-
def strict?
|
144
|
-
version_options && version_options[:strict]
|
145
|
-
end
|
146
|
-
|
147
|
-
def version_options
|
148
|
-
options[:version_options]
|
149
|
-
end
|
150
|
-
|
151
|
-
# By default those errors contain an `X-Cascade` header set to `pass`,
|
152
|
-
# which allows nesting and stacking of routes
|
153
|
-
# (see Grape::Router for more
|
154
|
-
# information). To prevent # this behavior, and not add the `X-Cascade`
|
155
|
-
# header, one can set the `:cascade` option to `false`.
|
156
|
-
def cascade?
|
157
|
-
if version_options&.key?(:cascade)
|
158
|
-
version_options[:cascade]
|
159
|
-
else
|
160
|
-
true
|
161
|
-
end
|
162
|
-
end
|
163
|
-
|
164
|
-
def error_headers
|
165
|
-
cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
|
166
|
-
end
|
167
|
-
|
168
|
-
# @param [String] media_type a content type
|
169
|
-
# @return [Boolean] whether the content type sets a vendor
|
170
|
-
def vendor?(media_type)
|
171
|
-
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
172
|
-
subtype.present? && subtype[HAS_VENDOR_REGEX]
|
173
|
-
end
|
174
|
-
|
175
|
-
def request_vendor(media_type)
|
176
|
-
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
177
|
-
subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
|
178
|
-
end
|
179
|
-
|
180
|
-
def request_version(media_type)
|
181
|
-
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
182
|
-
subtype.match(VENDOR_VERSION_HEADER_REGEX)[2]
|
183
|
-
end
|
184
|
-
|
185
|
-
# @param [String] media_type a content type
|
186
|
-
# @return [Boolean] whether the content type sets an API version
|
187
|
-
def version?(media_type)
|
188
|
-
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
189
|
-
subtype.present? && subtype[HAS_VERSION_REGEX]
|
190
|
-
end
|
191
45
|
end
|
192
46
|
end
|
193
47
|
end
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'grape/middleware/base'
|
4
|
-
|
5
3
|
module Grape
|
6
4
|
module Middleware
|
7
5
|
module Versioner
|
@@ -30,12 +28,12 @@ module Grape
|
|
30
28
|
end
|
31
29
|
|
32
30
|
def before
|
33
|
-
potential_version = Rack::Utils.parse_nested_query(env[
|
31
|
+
potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[paramkey]
|
34
32
|
return if potential_version.nil?
|
35
33
|
|
36
34
|
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 }
|
37
35
|
env[Grape::Env::API_VERSION] = potential_version
|
38
|
-
env[
|
36
|
+
env[Rack::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Rack::RACK_REQUEST_QUERY_HASH
|
39
37
|
end
|
40
38
|
|
41
39
|
private
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'grape/middleware/base'
|
4
|
-
|
5
3
|
module Grape
|
6
4
|
module Middleware
|
7
5
|
module Versioner
|
@@ -26,7 +24,7 @@ module Grape
|
|
26
24
|
end
|
27
25
|
|
28
26
|
def before
|
29
|
-
path = env[
|
27
|
+
path = env[Rack::PATH_INFO].dup
|
30
28
|
path.sub!(mount_path, '') if mounted_path?(path)
|
31
29
|
|
32
30
|
if prefix && path.index(prefix) == 0 # rubocop:disable all
|
data/lib/grape/namespace.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'grape/util/cache'
|
4
|
-
|
5
3
|
module Grape
|
6
4
|
# A container for endpoints or other namespaces, which allows for both
|
7
5
|
# logical grouping of endpoints as well as sharing common configuration.
|
@@ -33,13 +31,14 @@ module Grape
|
|
33
31
|
# Join the namespaces from a list of settings to create a path prefix.
|
34
32
|
# @param settings [Array] list of Grape::Util::InheritableSettings.
|
35
33
|
def self.joined_space_path(settings)
|
36
|
-
|
34
|
+
JoinedSpaceCache[joined_space(settings)]
|
37
35
|
end
|
38
36
|
|
39
37
|
class JoinedSpaceCache < Grape::Util::Cache
|
40
38
|
def initialize
|
39
|
+
super
|
41
40
|
@cache = Hash.new do |h, joined_space|
|
42
|
-
h[joined_space] =
|
41
|
+
h[joined_space] = Grape::Router.normalize_path(joined_space.join('/'))
|
43
42
|
end
|
44
43
|
end
|
45
44
|
end
|
data/lib/grape/path.rb
CHANGED
@@ -1,14 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'grape/util/cache'
|
4
|
-
|
5
3
|
module Grape
|
6
4
|
# Represents a path to an endpoint.
|
7
5
|
class Path
|
8
|
-
def self.prepare(raw_path, namespace, settings)
|
9
|
-
Path.new(raw_path, namespace, settings)
|
10
|
-
end
|
11
|
-
|
12
6
|
attr_reader :raw_path, :namespace, :settings
|
13
7
|
|
14
8
|
def initialize(raw_path, namespace, settings)
|
@@ -22,31 +16,27 @@ module Grape
|
|
22
16
|
end
|
23
17
|
|
24
18
|
def root_prefix
|
25
|
-
|
19
|
+
settings[:root_prefix]
|
26
20
|
end
|
27
21
|
|
28
22
|
def uses_specific_format?
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
false
|
33
|
-
end
|
23
|
+
return false unless settings.key?(:format) && settings.key?(:content_types)
|
24
|
+
|
25
|
+
settings[:format] && Array(settings[:content_types]).size == 1
|
34
26
|
end
|
35
27
|
|
36
28
|
def uses_path_versioning?
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
false
|
41
|
-
end
|
29
|
+
return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
|
30
|
+
|
31
|
+
settings[:version] && settings[:version_options][:using] == :path
|
42
32
|
end
|
43
33
|
|
44
34
|
def namespace?
|
45
|
-
namespace&.match?(/^\S/) && namespace
|
35
|
+
namespace&.match?(/^\S/) && not_slash?(namespace)
|
46
36
|
end
|
47
37
|
|
48
38
|
def path?
|
49
|
-
raw_path&.match?(/^\S/) && raw_path
|
39
|
+
raw_path&.match?(/^\S/) && not_slash?(raw_path)
|
50
40
|
end
|
51
41
|
|
52
42
|
def suffix
|
@@ -60,7 +50,7 @@ module Grape
|
|
60
50
|
end
|
61
51
|
|
62
52
|
def path
|
63
|
-
|
53
|
+
PartsCache[parts]
|
64
54
|
end
|
65
55
|
|
66
56
|
def path_with_suffix
|
@@ -75,24 +65,29 @@ module Grape
|
|
75
65
|
|
76
66
|
class PartsCache < Grape::Util::Cache
|
77
67
|
def initialize
|
68
|
+
super
|
78
69
|
@cache = Hash.new do |h, parts|
|
79
|
-
h[parts] =
|
70
|
+
h[parts] = Grape::Router.normalize_path(parts.join('/'))
|
80
71
|
end
|
81
72
|
end
|
82
73
|
end
|
83
74
|
|
84
75
|
def parts
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
76
|
+
[].tap do |parts|
|
77
|
+
add_part(parts, mount_path)
|
78
|
+
add_part(parts, root_prefix)
|
79
|
+
parts << ':version' if uses_path_versioning?
|
80
|
+
add_part(parts, namespace)
|
81
|
+
add_part(parts, raw_path)
|
82
|
+
end
|
90
83
|
end
|
91
84
|
|
92
|
-
def
|
93
|
-
|
85
|
+
def add_part(parts, value)
|
86
|
+
parts << value if value && not_slash?(value)
|
87
|
+
end
|
94
88
|
|
95
|
-
|
89
|
+
def not_slash?(value)
|
90
|
+
value != '/'
|
96
91
|
end
|
97
92
|
end
|
98
93
|
end
|
data/lib/grape/request.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'grape/util/lazy_object'
|
4
|
-
|
5
3
|
module Grape
|
6
4
|
class Request < Rack::Request
|
7
5
|
HTTP_PREFIX = 'HTTP_'
|
@@ -36,8 +34,8 @@ module Grape
|
|
36
34
|
end
|
37
35
|
|
38
36
|
def build_headers
|
39
|
-
Grape::Util::
|
40
|
-
env.each_pair.with_object(
|
37
|
+
Grape::Util::Lazy::Object.new do
|
38
|
+
env.each_pair.with_object(Grape::Util::Header.new) do |(k, v), headers|
|
41
39
|
next unless k.to_s.start_with? HTTP_PREFIX
|
42
40
|
|
43
41
|
transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k)
|
@@ -46,14 +44,8 @@ module Grape
|
|
46
44
|
end
|
47
45
|
end
|
48
46
|
|
49
|
-
|
50
|
-
|
51
|
-
-header[5..].tr('_', '-').downcase
|
52
|
-
end
|
53
|
-
else
|
54
|
-
def transform_header(header)
|
55
|
-
-header[5..].split('_').map(&:capitalize).join('-')
|
56
|
-
end
|
47
|
+
def transform_header(header)
|
48
|
+
-header[5..].tr('_', '-').downcase
|
57
49
|
end
|
58
50
|
end
|
59
51
|
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
class Router
|
5
|
+
class BaseRoute
|
6
|
+
delegate_missing_to :@options
|
7
|
+
|
8
|
+
attr_reader :index, :pattern, :options
|
9
|
+
|
10
|
+
def initialize(**options)
|
11
|
+
@options = ActiveSupport::OrderedOptions.new.update(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
alias attributes options
|
15
|
+
|
16
|
+
def regexp_capture_index
|
17
|
+
CaptureIndexCache[index]
|
18
|
+
end
|
19
|
+
|
20
|
+
def pattern_regexp
|
21
|
+
pattern.to_regexp
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_regexp(index)
|
25
|
+
@index = index
|
26
|
+
Regexp.new("(?<#{regexp_capture_index}>#{pattern_regexp})")
|
27
|
+
end
|
28
|
+
|
29
|
+
class CaptureIndexCache < Grape::Util::Cache
|
30
|
+
def initialize
|
31
|
+
super
|
32
|
+
@cache = Hash.new do |h, index|
|
33
|
+
h[index] = "_#{index}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Act like a Grape::Router::Route but for greedy_match
|
4
|
+
# see @neutral_map
|
5
|
+
|
6
|
+
module Grape
|
7
|
+
class Router
|
8
|
+
class GreedyRoute < BaseRoute
|
9
|
+
def initialize(pattern:, **options)
|
10
|
+
@pattern = pattern
|
11
|
+
super(**options)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Grape::Router:Route defines params as a function
|
15
|
+
def params(_input = nil)
|
16
|
+
options[:params] || {}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/grape/router/pattern.rb
CHANGED
@@ -1,62 +1,71 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'forwardable'
|
4
|
-
require 'mustermann/grape'
|
5
|
-
require 'grape/util/cache'
|
6
|
-
|
7
3
|
module Grape
|
8
4
|
class Router
|
9
5
|
class Pattern
|
10
|
-
|
11
|
-
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
DEFAULT_CAPTURES = %w[format version].freeze
|
12
9
|
|
13
10
|
attr_reader :origin, :path, :pattern, :to_regexp
|
14
11
|
|
15
|
-
extend Forwardable
|
16
12
|
def_delegators :pattern, :named_captures, :params
|
17
13
|
def_delegators :to_regexp, :===
|
18
14
|
alias match? ===
|
19
15
|
|
20
16
|
def initialize(pattern, **options)
|
21
|
-
@origin
|
22
|
-
@path
|
23
|
-
@pattern =
|
17
|
+
@origin = pattern
|
18
|
+
@path = build_path(pattern, anchor: options[:anchor], suffix: options[:suffix])
|
19
|
+
@pattern = build_pattern(@path, options)
|
24
20
|
@to_regexp = @pattern.to_regexp
|
25
21
|
end
|
26
22
|
|
23
|
+
def captures_default
|
24
|
+
to_regexp.names
|
25
|
+
.delete_if { |n| DEFAULT_CAPTURES.include?(n) }
|
26
|
+
.to_h { |k| [k, ''] }
|
27
|
+
end
|
28
|
+
|
27
29
|
private
|
28
30
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
31
|
+
def build_pattern(path, options)
|
32
|
+
Mustermann::Grape.new(
|
33
|
+
path,
|
34
|
+
uri_decode: true,
|
35
|
+
params: options[:params],
|
36
|
+
capture: extract_capture(**options)
|
37
|
+
)
|
34
38
|
end
|
35
39
|
|
36
|
-
def build_path(pattern, anchor: false, suffix: nil
|
37
|
-
|
38
|
-
|
39
|
-
pattern << '/' unless pattern.end_with?('/')
|
40
|
-
pattern << '*path'
|
41
|
-
end
|
40
|
+
def build_path(pattern, anchor: false, suffix: nil)
|
41
|
+
PatternCache[[build_path_from_pattern(pattern, anchor: anchor), suffix]]
|
42
|
+
end
|
42
43
|
|
43
|
-
|
44
|
-
|
45
|
-
|
44
|
+
def extract_capture(**options)
|
45
|
+
sliced_options = options
|
46
|
+
.slice(:format, :version)
|
47
|
+
.delete_if { |_k, v| v.blank? }
|
48
|
+
.transform_values { |v| Array.wrap(v).map(&:to_s) }
|
49
|
+
return sliced_options if options[:requirements].blank?
|
46
50
|
|
47
|
-
|
51
|
+
options[:requirements].merge(sliced_options)
|
48
52
|
end
|
49
53
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
54
|
+
def build_path_from_pattern(pattern, anchor: false)
|
55
|
+
if pattern.end_with?('*path')
|
56
|
+
pattern.dup.insert(pattern.rindex('/') + 1, '?')
|
57
|
+
elsif anchor
|
58
|
+
pattern
|
59
|
+
elsif pattern.end_with?('/')
|
60
|
+
"#{pattern}?*path"
|
61
|
+
else
|
62
|
+
"#{pattern}/?*path"
|
55
63
|
end
|
56
64
|
end
|
57
65
|
|
58
66
|
class PatternCache < Grape::Util::Cache
|
59
67
|
def initialize
|
68
|
+
super
|
60
69
|
@cache = Hash.new do |h, (pattern, suffix)|
|
61
70
|
h[[pattern, suffix]] = -"#{pattern}#{suffix}"
|
62
71
|
end
|
data/lib/grape/router/route.rb
CHANGED
@@ -1,57 +1,18 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'grape/router/pattern'
|
4
|
-
require 'grape/router/attribute_translator'
|
5
|
-
require 'forwardable'
|
6
|
-
require 'pathname'
|
7
|
-
|
8
3
|
module Grape
|
9
4
|
class Router
|
10
|
-
class Route
|
11
|
-
ROUTE_ATTRIBUTE_REGEXP = /route_([_a-zA-Z]\w*)/.freeze
|
12
|
-
SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze
|
13
|
-
FIXED_NAMED_CAPTURES = %w[format version].freeze
|
14
|
-
|
15
|
-
attr_accessor :pattern, :translator, :app, :index, :options
|
16
|
-
|
17
|
-
alias attributes translator
|
18
|
-
|
5
|
+
class Route < BaseRoute
|
19
6
|
extend Forwardable
|
20
|
-
def_delegators :pattern, :path, :origin
|
21
|
-
delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes
|
22
7
|
|
23
|
-
|
24
|
-
match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
|
25
|
-
if match
|
26
|
-
method_name = match.captures.last.to_sym
|
27
|
-
warn_route_methods(method_name, caller(1).shift)
|
28
|
-
@options[method_name]
|
29
|
-
else
|
30
|
-
super
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
def respond_to_missing?(method_id, _)
|
35
|
-
ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s)
|
36
|
-
end
|
37
|
-
|
38
|
-
def route_method
|
39
|
-
warn_route_methods(:method, caller(1).shift, :request_method)
|
40
|
-
request_method
|
41
|
-
end
|
8
|
+
attr_reader :app, :request_method
|
42
9
|
|
43
|
-
|
44
|
-
warn_route_methods(:path, caller(1).shift)
|
45
|
-
pattern.path
|
46
|
-
end
|
10
|
+
def_delegators :pattern, :path, :origin
|
47
11
|
|
48
12
|
def initialize(method, pattern, **options)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
@options = options.merge(method: method_upcase)
|
53
|
-
@pattern = Pattern.new(pattern, **options)
|
54
|
-
@translator = AttributeTranslator.new(**options, request_method: method_upcase)
|
13
|
+
@request_method = upcase_method(method)
|
14
|
+
@pattern = Grape::Router::Pattern.new(pattern, **options)
|
15
|
+
super(**options)
|
55
16
|
end
|
56
17
|
|
57
18
|
def exec(env)
|
@@ -64,27 +25,29 @@ module Grape
|
|
64
25
|
end
|
65
26
|
|
66
27
|
def match?(input)
|
67
|
-
|
28
|
+
return false if input.blank?
|
29
|
+
|
30
|
+
options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input)
|
68
31
|
end
|
69
32
|
|
70
33
|
def params(input = nil)
|
71
|
-
if input.
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
parsed ? parsed.delete_if { |_, value| value.nil? }.symbolize_keys : {}
|
78
|
-
end
|
34
|
+
return params_without_input if input.blank?
|
35
|
+
|
36
|
+
parsed = pattern.params(input)
|
37
|
+
return {} unless parsed
|
38
|
+
|
39
|
+
parsed.compact.symbolize_keys
|
79
40
|
end
|
80
41
|
|
81
42
|
private
|
82
43
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
44
|
+
def params_without_input
|
45
|
+
pattern.captures_default.merge(attributes.params)
|
46
|
+
end
|
47
|
+
|
48
|
+
def upcase_method(method)
|
49
|
+
method_s = method.to_s
|
50
|
+
Grape::Http::Headers::SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase
|
88
51
|
end
|
89
52
|
end
|
90
53
|
end
|