grape 2.0.0 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -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 +14 -11
  7. data/lib/grape/api.rb +19 -10
  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 +5 -4
  15. data/lib/grape/dsl/routing.rb +20 -4
  16. data/lib/grape/dsl/validations.rb +13 -0
  17. data/lib/grape/endpoint.rb +14 -17
  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 +2 -4
  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 +1 -3
  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 +4 -5
  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_handler.rb +105 -0
  48. data/lib/grape/util/base_inheritable.rb +4 -4
  49. data/lib/grape/util/cache.rb +0 -3
  50. data/lib/grape/util/endpoint_configuration.rb +1 -1
  51. data/lib/grape/util/header.rb +13 -0
  52. data/lib/grape/util/inheritable_values.rb +0 -2
  53. data/lib/grape/util/lazy/block.rb +29 -0
  54. data/lib/grape/util/lazy/object.rb +45 -0
  55. data/lib/grape/util/lazy/value.rb +38 -0
  56. data/lib/grape/util/lazy/value_array.rb +21 -0
  57. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  58. data/lib/grape/util/lazy/value_hash.rb +21 -0
  59. data/lib/grape/util/media_type.rb +70 -0
  60. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  61. data/lib/grape/util/stackable_values.rb +1 -6
  62. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  63. data/lib/grape/validations/attributes_doc.rb +38 -36
  64. data/lib/grape/validations/attributes_iterator.rb +1 -0
  65. data/lib/grape/validations/contract_scope.rb +71 -0
  66. data/lib/grape/validations/params_scope.rb +15 -18
  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/exactly_one_of_validator.rb +1 -1
  77. data/lib/grape/validations/validators/length_validator.rb +42 -0
  78. data/lib/grape/validations/validators/values_validator.rb +6 -1
  79. data/lib/grape/validations.rb +3 -7
  80. data/lib/grape/version.rb +1 -1
  81. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  82. data/lib/grape.rb +30 -274
  83. metadata +30 -37
  84. data/lib/grape/eager_load.rb +0 -20
  85. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  86. data/lib/grape/router/attribute_translator.rb +0 -63
  87. data/lib/grape/util/lazy_block.rb +0 -27
  88. data/lib/grape/util/lazy_object.rb +0 -43
  89. data/lib/grape/util/lazy_value.rb +0 -91
@@ -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
  class Globals < Base
@@ -9,7 +7,7 @@ module Grape
9
7
  request = Grape::Request.new(@env, build_params_with: @options[:build_params_with])
10
8
  @env[Grape::Env::GRAPE_REQUEST] = request
11
9
  @env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers
12
- @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Grape::Env::RACK_INPUT]
10
+ @env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Rack::RACK_INPUT]
13
11
  end
14
12
  end
15
13
  end
@@ -57,8 +57,8 @@ module Grape
57
57
  middlewares.last
58
58
  end
59
59
 
60
- def [](i)
61
- middlewares[i]
60
+ def [](index)
61
+ middlewares[index]
62
62
  end
63
63
 
64
64
  def insert(index, *args, &block)
@@ -76,11 +76,10 @@ module Grape
76
76
  end
77
77
  ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true)
78
78
 
79
- def use(*args, &block)
80
- middleware = self.class::Middleware.new(*args, &block)
79
+ def use(...)
80
+ middleware = self.class::Middleware.new(...)
81
81
  middlewares.push(middleware)
82
82
  end
83
- ruby2_keywords :use if respond_to?(:ruby2_keywords, true)
84
83
 
85
84
  def merge_with(middleware_specs)
86
85
  middleware_specs.each do |operation, *args|
@@ -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
@@ -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