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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -1
  3. data/README.md +362 -316
  4. data/UPGRADING.md +197 -7
  5. data/grape.gemspec +5 -6
  6. data/lib/grape/api/instance.rb +13 -10
  7. data/lib/grape/api.rb +17 -8
  8. data/lib/grape/content_types.rb +0 -2
  9. data/lib/grape/cookies.rb +2 -1
  10. data/lib/grape/dry_types.rb +0 -2
  11. data/lib/grape/dsl/desc.rb +22 -20
  12. data/lib/grape/dsl/headers.rb +1 -1
  13. data/lib/grape/dsl/inside_route.rb +42 -13
  14. data/lib/grape/dsl/parameters.rb +4 -3
  15. data/lib/grape/dsl/routing.rb +20 -4
  16. data/lib/grape/dsl/validations.rb +13 -0
  17. data/lib/grape/endpoint.rb +12 -15
  18. data/lib/grape/{util/env.rb → env.rb} +0 -5
  19. data/lib/grape/error_formatter/txt.rb +11 -10
  20. data/lib/grape/exceptions/base.rb +3 -3
  21. data/lib/grape/exceptions/validation.rb +0 -2
  22. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  23. data/lib/grape/exceptions/validation_errors.rb +1 -3
  24. data/lib/grape/extensions/hash.rb +5 -1
  25. data/lib/grape/http/headers.rb +18 -34
  26. data/lib/grape/{util/json.rb → json.rb} +1 -3
  27. data/lib/grape/locale/en.yml +3 -0
  28. data/lib/grape/middleware/auth/base.rb +0 -2
  29. data/lib/grape/middleware/auth/dsl.rb +0 -2
  30. data/lib/grape/middleware/base.rb +0 -2
  31. data/lib/grape/middleware/error.rb +55 -50
  32. data/lib/grape/middleware/formatter.rb +16 -13
  33. data/lib/grape/middleware/globals.rb +1 -3
  34. data/lib/grape/middleware/stack.rb +2 -3
  35. data/lib/grape/middleware/versioner/accept_version_header.rb +0 -2
  36. data/lib/grape/middleware/versioner/header.rb +17 -163
  37. data/lib/grape/middleware/versioner/param.rb +2 -4
  38. data/lib/grape/middleware/versioner/path.rb +1 -3
  39. data/lib/grape/namespace.rb +3 -4
  40. data/lib/grape/path.rb +24 -29
  41. data/lib/grape/request.rb +4 -12
  42. data/lib/grape/router/base_route.rb +39 -0
  43. data/lib/grape/router/greedy_route.rb +20 -0
  44. data/lib/grape/router/pattern.rb +39 -30
  45. data/lib/grape/router/route.rb +22 -59
  46. data/lib/grape/router.rb +32 -37
  47. data/lib/grape/util/accept/header.rb +19 -0
  48. data/lib/grape/util/accept_header_handler.rb +105 -0
  49. data/lib/grape/util/base_inheritable.rb +4 -4
  50. data/lib/grape/util/cache.rb +0 -3
  51. data/lib/grape/util/endpoint_configuration.rb +1 -1
  52. data/lib/grape/util/header.rb +13 -0
  53. data/lib/grape/util/inheritable_values.rb +0 -2
  54. data/lib/grape/util/lazy/block.rb +29 -0
  55. data/lib/grape/util/lazy/object.rb +45 -0
  56. data/lib/grape/util/lazy/value.rb +38 -0
  57. data/lib/grape/util/lazy/value_array.rb +21 -0
  58. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  59. data/lib/grape/util/lazy/value_hash.rb +21 -0
  60. data/lib/grape/util/media_type.rb +70 -0
  61. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  62. data/lib/grape/util/stackable_values.rb +1 -6
  63. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  64. data/lib/grape/validations/attributes_doc.rb +38 -36
  65. data/lib/grape/validations/contract_scope.rb +71 -0
  66. data/lib/grape/validations/params_scope.rb +10 -9
  67. data/lib/grape/validations/types/array_coercer.rb +0 -2
  68. data/lib/grape/validations/types/build_coercer.rb +69 -71
  69. data/lib/grape/validations/types/dry_type_coercer.rb +1 -11
  70. data/lib/grape/validations/types/json.rb +0 -2
  71. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  72. data/lib/grape/validations/types/set_coercer.rb +0 -3
  73. data/lib/grape/validations/types.rb +0 -3
  74. data/lib/grape/validations/validators/base.rb +1 -0
  75. data/lib/grape/validations/validators/default_validator.rb +5 -1
  76. data/lib/grape/validations/validators/length_validator.rb +42 -0
  77. data/lib/grape/validations/validators/values_validator.rb +6 -1
  78. data/lib/grape/validations.rb +3 -7
  79. data/lib/grape/version.rb +1 -1
  80. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  81. data/lib/grape.rb +30 -274
  82. metadata +31 -37
  83. data/lib/grape/eager_load.rb +0 -20
  84. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  85. data/lib/grape/router/attribute_translator.rb +0 -63
  86. data/lib/grape/util/lazy_block.rb +0 -27
  87. data/lib/grape/util/lazy_object.rb +0 -43
  88. 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
- strict_header_checks if strict?
36
-
37
- if media_type || env[Grape::Env::GRAPE_ALLOWED_METHODS]
38
- media_type_header_handler
39
- elsif headers_contain_wrong_vendor?
40
- fail_with_invalid_accept_header!('API vendor not found.')
41
- elsif headers_contain_wrong_version?
42
- fail_with_invalid_version_header!('API version not found.')
43
- end
44
- end
45
-
46
- private
47
-
48
- def strict_header_checks
49
- strict_accept_header_presence_check
50
- strict_version_vendor_accept_header_presence_check
51
- end
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[Grape::Http::Headers::QUERY_STRING])[paramkey]
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[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH
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[Grape::Http::Headers::PATH_INFO].dup
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
@@ -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
- Grape::Router.normalize_path(JoinedSpaceCache[joined_space(settings)])
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] = -joined_space.join('/')
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
- split_setting(:root_prefix)
19
+ settings[:root_prefix]
26
20
  end
27
21
 
28
22
  def uses_specific_format?
29
- if settings.key?(:format) && settings.key?(:content_types)
30
- (settings[:format] && Array(settings[:content_types]).size == 1)
31
- else
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
- if settings.key?(:version) && settings[:version_options] && settings[:version_options].key?(:using)
38
- (settings[:version] && settings[:version_options][:using] == :path)
39
- else
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
- Grape::Router.normalize_path(PartsCache[parts])
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] = -parts.join('/')
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
- parts = [mount_path, root_prefix].compact
86
- parts << ':version' if uses_path_versioning?
87
- parts << namespace.to_s
88
- parts << raw_path.to_s
89
- parts.flatten.reject { |part| part == '/' }
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 split_setting(key)
93
- return if settings[key].nil?
85
+ def add_part(parts, value)
86
+ parts << value if value && not_slash?(value)
87
+ end
94
88
 
95
- settings[key].to_s.split('/')
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::LazyObject.new do
40
- env.each_pair.with_object({}) do |(k, v), headers|
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
- if Grape.lowercase_headers?
50
- def transform_header(header)
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
@@ -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
- DEFAULT_PATTERN_OPTIONS = { uri_decode: true }.freeze
11
- DEFAULT_SUPPORTED_CAPTURE = %i[format version].freeze
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 = pattern
22
- @path = build_path(pattern, **options)
23
- @pattern = Mustermann::Grape.new(@path, **pattern_options(options))
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 pattern_options(options)
30
- capture = extract_capture(**options)
31
- options = DEFAULT_PATTERN_OPTIONS.dup
32
- options[:capture] = capture if capture.present?
33
- options
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, **_options)
37
- unless anchor || pattern.end_with?('*path')
38
- pattern = +pattern
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
- pattern = -pattern.split('/').tap do |parts|
44
- parts[parts.length - 1] = "?#{parts.last}"
45
- end.join('/') if pattern.end_with?('*path')
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
- PatternCache[[pattern, suffix]]
51
+ options[:requirements].merge(sliced_options)
48
52
  end
49
53
 
50
- def extract_capture(requirements: {}, **options)
51
- requirements = {}.merge(requirements)
52
- DEFAULT_SUPPORTED_CAPTURE.each_with_object(requirements) do |field, capture|
53
- option = Array(options[field])
54
- capture[field] = option.map(&:to_s) if option.present?
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
@@ -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
- def method_missing(method_id, *arguments)
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
- def route_path
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
- method_s = method.to_s
50
- method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase
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
- translator.respond_to?(:forward_match) && translator.forward_match ? input.start_with?(pattern.origin) : pattern.match?(input)
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.nil?
72
- pattern.named_captures.keys.each_with_object(translator.params) do |(key), defaults|
73
- defaults[key] ||= '' unless FIXED_NAMED_CAPTURES.include?(key) || defaults.key?(key)
74
- end
75
- else
76
- parsed = pattern.params(input)
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 warn_route_methods(name, location, expected = nil)
84
- path, line = *location.scan(SOURCE_LOCATION_REGEXP).first
85
- path = File.realpath(path) if Pathname.new(path).relative?
86
- expected ||= name
87
- Grape.deprecator.warn("#{path}:#{line}: The route_xxx methods such as route_#{name} have been deprecated, please use #{expected}.")
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