grape 2.0.0 → 2.4.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.
Files changed (130) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +151 -1
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +404 -334
  5. data/UPGRADING.md +279 -7
  6. data/grape.gemspec +8 -8
  7. data/lib/grape/api/instance.rb +34 -66
  8. data/lib/grape/api.rb +47 -70
  9. data/lib/grape/content_types.rb +13 -10
  10. data/lib/grape/cookies.rb +31 -24
  11. data/lib/grape/dry_types.rb +0 -2
  12. data/lib/grape/dsl/api.rb +0 -2
  13. data/lib/grape/dsl/desc.rb +49 -44
  14. data/lib/grape/dsl/headers.rb +2 -2
  15. data/lib/grape/dsl/helpers.rb +8 -4
  16. data/lib/grape/dsl/inside_route.rb +67 -54
  17. data/lib/grape/dsl/parameters.rb +10 -9
  18. data/lib/grape/dsl/request_response.rb +14 -18
  19. data/lib/grape/dsl/routing.rb +34 -17
  20. data/lib/grape/dsl/validations.rb +13 -0
  21. data/lib/grape/endpoint.rb +120 -118
  22. data/lib/grape/{util/env.rb → env.rb} +0 -5
  23. data/lib/grape/error_formatter/base.rb +51 -21
  24. data/lib/grape/error_formatter/json.rb +7 -15
  25. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  26. data/lib/grape/error_formatter/txt.rb +11 -17
  27. data/lib/grape/error_formatter/xml.rb +3 -13
  28. data/lib/grape/error_formatter.rb +5 -25
  29. data/lib/grape/exceptions/base.rb +18 -30
  30. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  31. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  32. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  33. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  34. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  35. data/lib/grape/exceptions/validation.rb +5 -6
  36. data/lib/grape/exceptions/validation_array_errors.rb +1 -0
  37. data/lib/grape/exceptions/validation_errors.rb +4 -6
  38. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +2 -5
  39. data/lib/grape/extensions/hash.rb +7 -2
  40. data/lib/grape/extensions/hashie/mash.rb +3 -5
  41. data/lib/grape/formatter/base.rb +16 -0
  42. data/lib/grape/formatter/json.rb +4 -6
  43. data/lib/grape/formatter/serializable_hash.rb +1 -1
  44. data/lib/grape/formatter/txt.rb +3 -5
  45. data/lib/grape/formatter/xml.rb +4 -6
  46. data/lib/grape/formatter.rb +7 -25
  47. data/lib/grape/{util/json.rb → json.rb} +1 -3
  48. data/lib/grape/locale/en.yml +46 -42
  49. data/lib/grape/middleware/auth/base.rb +11 -34
  50. data/lib/grape/middleware/auth/dsl.rb +23 -31
  51. data/lib/grape/middleware/base.rb +41 -23
  52. data/lib/grape/middleware/error.rb +77 -76
  53. data/lib/grape/middleware/formatter.rb +48 -79
  54. data/lib/grape/middleware/globals.rb +1 -3
  55. data/lib/grape/middleware/stack.rb +26 -37
  56. data/lib/grape/middleware/versioner/accept_version_header.rb +6 -33
  57. data/lib/grape/middleware/versioner/base.rb +74 -0
  58. data/lib/grape/middleware/versioner/header.rb +59 -126
  59. data/lib/grape/middleware/versioner/param.rb +4 -25
  60. data/lib/grape/middleware/versioner/path.rb +10 -34
  61. data/lib/grape/middleware/versioner.rb +7 -14
  62. data/lib/grape/namespace.rb +4 -5
  63. data/lib/grape/params_builder/base.rb +18 -0
  64. data/lib/grape/params_builder/hash.rb +11 -0
  65. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  66. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  67. data/lib/grape/params_builder.rb +32 -0
  68. data/lib/grape/parser/base.rb +16 -0
  69. data/lib/grape/parser/json.rb +6 -8
  70. data/lib/grape/parser/xml.rb +6 -8
  71. data/lib/grape/parser.rb +5 -23
  72. data/lib/grape/path.rb +38 -60
  73. data/lib/grape/request.rb +161 -30
  74. data/lib/grape/router/base_route.rb +39 -0
  75. data/lib/grape/router/greedy_route.rb +20 -0
  76. data/lib/grape/router/pattern.rb +45 -31
  77. data/lib/grape/router/route.rb +28 -57
  78. data/lib/grape/router.rb +56 -43
  79. data/lib/grape/util/base_inheritable.rb +4 -4
  80. data/lib/grape/util/cache.rb +0 -3
  81. data/lib/grape/util/endpoint_configuration.rb +1 -1
  82. data/lib/grape/util/header.rb +13 -0
  83. data/lib/grape/util/inheritable_values.rb +0 -2
  84. data/lib/grape/util/lazy/block.rb +29 -0
  85. data/lib/grape/util/lazy/value.rb +38 -0
  86. data/lib/grape/util/lazy/value_array.rb +21 -0
  87. data/lib/grape/util/lazy/value_enumerable.rb +34 -0
  88. data/lib/grape/util/lazy/value_hash.rb +21 -0
  89. data/lib/grape/util/media_type.rb +70 -0
  90. data/lib/grape/util/registry.rb +27 -0
  91. data/lib/grape/util/reverse_stackable_values.rb +1 -6
  92. data/lib/grape/util/stackable_values.rb +1 -6
  93. data/lib/grape/util/strict_hash_configuration.rb +3 -3
  94. data/lib/grape/validations/attributes_doc.rb +38 -36
  95. data/lib/grape/validations/attributes_iterator.rb +1 -0
  96. data/lib/grape/validations/contract_scope.rb +34 -0
  97. data/lib/grape/validations/params_scope.rb +36 -32
  98. data/lib/grape/validations/types/array_coercer.rb +0 -2
  99. data/lib/grape/validations/types/dry_type_coercer.rb +9 -15
  100. data/lib/grape/validations/types/json.rb +0 -2
  101. data/lib/grape/validations/types/primitive_coercer.rb +0 -2
  102. data/lib/grape/validations/types/set_coercer.rb +0 -3
  103. data/lib/grape/validations/types.rb +0 -3
  104. data/lib/grape/validations/validator_factory.rb +2 -2
  105. data/lib/grape/validations/validators/allow_blank_validator.rb +1 -1
  106. data/lib/grape/validations/validators/base.rb +8 -11
  107. data/lib/grape/validations/validators/coerce_validator.rb +1 -1
  108. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  109. data/lib/grape/validations/validators/default_validator.rb +6 -2
  110. data/lib/grape/validations/validators/exactly_one_of_validator.rb +1 -1
  111. data/lib/grape/validations/validators/except_values_validator.rb +2 -2
  112. data/lib/grape/validations/validators/length_validator.rb +49 -0
  113. data/lib/grape/validations/validators/presence_validator.rb +1 -1
  114. data/lib/grape/validations/validators/regexp_validator.rb +2 -2
  115. data/lib/grape/validations/validators/values_validator.rb +20 -57
  116. data/lib/grape/validations.rb +8 -21
  117. data/lib/grape/version.rb +1 -1
  118. data/lib/grape/{util/xml.rb → xml.rb} +1 -1
  119. data/lib/grape.rb +42 -274
  120. metadata +45 -44
  121. data/lib/grape/eager_load.rb +0 -20
  122. data/lib/grape/http/headers.rb +0 -71
  123. data/lib/grape/middleware/helpers.rb +0 -12
  124. data/lib/grape/middleware/versioner/parse_media_type_patch.rb +0 -24
  125. data/lib/grape/router/attribute_translator.rb +0 -63
  126. data/lib/grape/util/lazy_block.rb +0 -27
  127. data/lib/grape/util/lazy_object.rb +0 -43
  128. data/lib/grape/util/lazy_value.rb +0 -91
  129. data/lib/grape/util/registrable.rb +0 -15
  130. data/lib/grape/validations/types/build_coercer.rb +0 -94
@@ -2,14 +2,12 @@
2
2
 
3
3
  module Grape
4
4
  module Parser
5
- module Json
6
- class << self
7
- def call(object, _env)
8
- ::Grape::Json.load(object)
9
- rescue ::Grape::Json::ParseError
10
- # handle JSON parsing errors via the rescue handlers or provide error message
11
- raise Grape::Exceptions::InvalidMessageBody.new('application/json')
12
- end
5
+ class Json < Base
6
+ def self.call(object, _env)
7
+ ::Grape::Json.load(object)
8
+ rescue ::Grape::Json::ParseError
9
+ # handle JSON parsing errors via the rescue handlers or provide error message
10
+ raise Grape::Exceptions::InvalidMessageBody.new('application/json')
13
11
  end
14
12
  end
15
13
  end
@@ -2,14 +2,12 @@
2
2
 
3
3
  module Grape
4
4
  module Parser
5
- module Xml
6
- class << self
7
- def call(object, _env)
8
- ::Grape::Xml.parse(object)
9
- rescue ::Grape::Xml::ParseError
10
- # handle XML parsing errors via the rescue handlers or provide error message
11
- raise Grape::Exceptions::InvalidMessageBody.new('application/xml')
12
- end
5
+ class Xml < Base
6
+ def self.call(object, _env)
7
+ ::Grape::Xml.parse(object)
8
+ rescue ::Grape::Xml::ParseError
9
+ # handle XML parsing errors via the rescue handlers or provide error message
10
+ raise Grape::Exceptions::InvalidMessageBody.new('application/xml')
13
11
  end
14
12
  end
15
13
  end
data/lib/grape/parser.rb CHANGED
@@ -2,32 +2,14 @@
2
2
 
3
3
  module Grape
4
4
  module Parser
5
- extend Util::Registrable
5
+ extend Grape::Util::Registry
6
6
 
7
- class << self
8
- def builtin_parsers
9
- @builtin_parsers ||= {
10
- json: Grape::Parser::Json,
11
- jsonapi: Grape::Parser::Json,
12
- xml: Grape::Parser::Xml
13
- }
14
- end
7
+ module_function
15
8
 
16
- def parsers(**options)
17
- builtin_parsers.merge(default_elements).merge!(options[:parsers] || {})
18
- end
9
+ def parser_for(format, parsers = nil)
10
+ return parsers[format] if parsers&.key?(format)
19
11
 
20
- def parser_for(api_format, **options)
21
- spec = parsers(**options)[api_format]
22
- case spec
23
- when nil
24
- nil
25
- when Symbol
26
- method(spec)
27
- else
28
- spec
29
- end
30
- end
12
+ registry[format]
31
13
  end
32
14
  end
33
15
  end
data/lib/grape/path.rb CHANGED
@@ -1,98 +1,76 @@
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
6
+ DEFAULT_FORMAT_SEGMENT = '(/.:format)'
7
+ NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)'
8
+ VERSION_SEGMENT = ':version'
11
9
 
12
- attr_reader :raw_path, :namespace, :settings
10
+ attr_reader :origin, :suffix
13
11
 
14
- def initialize(raw_path, namespace, settings)
15
- @raw_path = raw_path
16
- @namespace = namespace
17
- @settings = settings
12
+ def initialize(raw_path, raw_namespace, settings)
13
+ @origin = PartsCache[build_parts(raw_path, raw_namespace, settings)]
14
+ @suffix = build_suffix(raw_path, raw_namespace, settings)
18
15
  end
19
16
 
20
- def mount_path
21
- settings[:mount_path]
17
+ def to_s
18
+ "#{origin}#{suffix}"
22
19
  end
23
20
 
24
- def root_prefix
25
- split_setting(:root_prefix)
26
- end
21
+ private
27
22
 
28
- def uses_specific_format?
29
- if settings.key?(:format) && settings.key?(:content_types)
30
- (settings[:format] && Array(settings[:content_types]).size == 1)
23
+ def build_suffix(raw_path, raw_namespace, settings)
24
+ if uses_specific_format?(settings)
25
+ "(.#{settings[:format]})"
26
+ elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path))
27
+ NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT
31
28
  else
32
- false
29
+ DEFAULT_FORMAT_SEGMENT
33
30
  end
34
31
  end
35
32
 
36
- 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
33
+ def build_parts(raw_path, raw_namespace, settings)
34
+ [].tap do |parts|
35
+ add_part(parts, settings[:mount_path])
36
+ add_part(parts, settings[:root_prefix])
37
+ parts << VERSION_SEGMENT if uses_path_versioning?(settings)
38
+ add_part(parts, raw_namespace)
39
+ add_part(parts, raw_path)
41
40
  end
42
41
  end
43
42
 
44
- def namespace?
45
- namespace&.match?(/^\S/) && namespace != '/'
43
+ def add_part(parts, value)
44
+ parts << value if value && not_slash?(value)
46
45
  end
47
46
 
48
- def path?
49
- raw_path&.match?(/^\S/) && raw_path != '/'
47
+ def not_slash?(value)
48
+ value != '/'
50
49
  end
51
50
 
52
- def suffix
53
- if uses_specific_format?
54
- "(.#{settings[:format]})"
55
- elsif !uses_path_versioning? || (namespace? || path?)
56
- '(.:format)'
57
- else
58
- '(/.:format)'
59
- end
60
- end
51
+ def uses_specific_format?(settings)
52
+ return false unless settings.key?(:format) && settings.key?(:content_types)
61
53
 
62
- def path
63
- Grape::Router.normalize_path(PartsCache[parts])
54
+ settings[:format] && Array(settings[:content_types]).size == 1
64
55
  end
65
56
 
66
- def path_with_suffix
67
- "#{path}#{suffix}"
68
- end
57
+ def uses_path_versioning?(settings)
58
+ return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
69
59
 
70
- def to_s
71
- path_with_suffix
60
+ settings[:version] && settings[:version_options][:using] == :path
72
61
  end
73
62
 
74
- private
63
+ def valid_part?(part)
64
+ part&.match?(/^\S/) && not_slash?(part)
65
+ end
75
66
 
76
67
  class PartsCache < Grape::Util::Cache
77
68
  def initialize
69
+ super
78
70
  @cache = Hash.new do |h, parts|
79
- h[parts] = -parts.join('/')
71
+ h[parts] = Grape::Router.normalize_path(parts.join('/'))
80
72
  end
81
73
  end
82
74
  end
83
-
84
- 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 == '/' }
90
- end
91
-
92
- def split_setting(key)
93
- return if settings[key].nil?
94
-
95
- settings[key].to_s.split('/')
96
- end
97
75
  end
98
76
  end
data/lib/grape/request.rb CHANGED
@@ -1,58 +1,189 @@
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
- HTTP_PREFIX = 'HTTP_'
5
+ # Based on rack 3 KNOWN_HEADERS
6
+ # https://github.com/rack/rack/blob/4f15e7b814922af79605be4b02c5b7c3044ba206/lib/rack/headers.rb#L10
7
+
8
+ KNOWN_HEADERS = %w[
9
+ Accept
10
+ Accept-CH
11
+ Accept-Encoding
12
+ Accept-Language
13
+ Accept-Patch
14
+ Accept-Ranges
15
+ Accept-Version
16
+ Access-Control-Allow-Credentials
17
+ Access-Control-Allow-Headers
18
+ Access-Control-Allow-Methods
19
+ Access-Control-Allow-Origin
20
+ Access-Control-Expose-Headers
21
+ Access-Control-Max-Age
22
+ Age
23
+ Allow
24
+ Alt-Svc
25
+ Authorization
26
+ Cache-Control
27
+ Client-Ip
28
+ Connection
29
+ Content-Disposition
30
+ Content-Encoding
31
+ Content-Language
32
+ Content-Length
33
+ Content-Location
34
+ Content-MD5
35
+ Content-Range
36
+ Content-Security-Policy
37
+ Content-Security-Policy-Report-Only
38
+ Content-Type
39
+ Cookie
40
+ Date
41
+ Delta-Base
42
+ Dnt
43
+ ETag
44
+ Expect-CT
45
+ Expires
46
+ Feature-Policy
47
+ Forwarded
48
+ Host
49
+ If-Modified-Since
50
+ If-None-Match
51
+ IM
52
+ Last-Modified
53
+ Link
54
+ Location
55
+ NEL
56
+ P3P
57
+ Permissions-Policy
58
+ Pragma
59
+ Preference-Applied
60
+ Proxy-Authenticate
61
+ Public-Key-Pins
62
+ Range
63
+ Referer
64
+ Referrer-Policy
65
+ Refresh
66
+ Report-To
67
+ Retry-After
68
+ Sec-Fetch-Dest
69
+ Sec-Fetch-Mode
70
+ Sec-Fetch-Site
71
+ Sec-Fetch-User
72
+ Server
73
+ Set-Cookie
74
+ Status
75
+ Strict-Transport-Security
76
+ Timing-Allow-Origin
77
+ Tk
78
+ Trailer
79
+ Transfer-Encoding
80
+ Upgrade
81
+ Upgrade-Insecure-Requests
82
+ User-Agent
83
+ Vary
84
+ Version
85
+ Via
86
+ Warning
87
+ WWW-Authenticate
88
+ X-Accel-Buffering
89
+ X-Accel-Charset
90
+ X-Accel-Expires
91
+ X-Accel-Limit-Rate
92
+ X-Accel-Mapping
93
+ X-Accel-Redirect
94
+ X-Access-Token
95
+ X-Auth-Request-Access-Token
96
+ X-Auth-Request-Email
97
+ X-Auth-Request-Groups
98
+ X-Auth-Request-Preferred-Username
99
+ X-Auth-Request-Redirect
100
+ X-Auth-Request-Token
101
+ X-Auth-Request-User
102
+ X-Cascade
103
+ X-Client-Ip
104
+ X-Content-Duration
105
+ X-Content-Security-Policy
106
+ X-Content-Type-Options
107
+ X-Correlation-Id
108
+ X-Download-Options
109
+ X-Forwarded-Access-Token
110
+ X-Forwarded-Email
111
+ X-Forwarded-For
112
+ X-Forwarded-Groups
113
+ X-Forwarded-Host
114
+ X-Forwarded-Port
115
+ X-Forwarded-Preferred-Username
116
+ X-Forwarded-Proto
117
+ X-Forwarded-Scheme
118
+ X-Forwarded-Ssl
119
+ X-Forwarded-Uri
120
+ X-Forwarded-User
121
+ X-Frame-Options
122
+ X-HTTP-Method-Override
123
+ X-Permitted-Cross-Domain-Policies
124
+ X-Powered-By
125
+ X-Real-IP
126
+ X-Redirect-By
127
+ X-Request-Id
128
+ X-Requested-With
129
+ X-Runtime
130
+ X-Sendfile
131
+ X-Sendfile-Type
132
+ X-UA-Compatible
133
+ X-WebKit-CS
134
+ X-XSS-Protection
135
+ ].each_with_object({}) do |header, response|
136
+ response["HTTP_#{header.upcase.tr('-', '_')}"] = header
137
+ end.freeze
8
138
 
9
139
  alias rack_params params
140
+ alias rack_cookies cookies
10
141
 
11
- def initialize(env, **options)
12
- extend options[:build_params_with] || Grape.config.param_builder
142
+ def initialize(env, build_params_with: nil)
13
143
  super(env)
144
+ @params_builder = Grape::ParamsBuilder.params_builder_for(build_params_with || Grape.config.param_builder)
14
145
  end
15
146
 
16
147
  def params
17
- @params ||= build_params
18
- rescue EOFError
19
- raise Grape::Exceptions::EmptyMessageBody.new(content_type)
20
- rescue Rack::Multipart::MultipartPartLimitError
21
- raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit)
148
+ @params ||= make_params
22
149
  end
23
150
 
24
151
  def headers
25
152
  @headers ||= build_headers
26
153
  end
27
154
 
28
- private
155
+ def cookies
156
+ @cookies ||= Grape::Cookies.new(-> { rack_cookies })
157
+ end
29
158
 
159
+ # needs to be public until extensions param_builder are removed
30
160
  def grape_routing_args
31
- args = env[Grape::Env::GRAPE_ROUTING_ARGS].dup
32
161
  # preserve version from query string parameters
33
- args.delete(:version)
34
- args.delete(:route_info)
35
- args
162
+ env[Grape::Env::GRAPE_ROUTING_ARGS]&.except(:version, :route_info) || {}
36
163
  end
37
164
 
38
- def build_headers
39
- Grape::Util::LazyObject.new do
40
- env.each_pair.with_object({}) do |(k, v), headers|
41
- next unless k.to_s.start_with? HTTP_PREFIX
165
+ private
42
166
 
43
- transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k)
44
- headers[transformed_header] = v
45
- end
46
- end
167
+ def make_params
168
+ @params_builder.call(rack_params).deep_merge!(grape_routing_args)
169
+ rescue EOFError
170
+ raise Grape::Exceptions::EmptyMessageBody.new(content_type)
171
+ rescue Rack::Multipart::MultipartPartLimitError, Rack::Multipart::MultipartTotalPartLimitError
172
+ raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit)
173
+ rescue Rack::QueryParser::ParamsTooDeepError
174
+ raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit)
175
+ rescue Rack::Utils::ParameterTypeError
176
+ raise Grape::Exceptions::ConflictingTypes
177
+ rescue Rack::Utils::InvalidParameterError
178
+ raise Grape::Exceptions::InvalidParameters
47
179
  end
48
180
 
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('-')
181
+ def build_headers
182
+ each_header.with_object(Grape::Util::Header.new) do |(k, v), headers|
183
+ next unless k.start_with? 'HTTP_'
184
+
185
+ transformed_header = KNOWN_HEADERS.fetch(k) { -k[5..].tr('_', '-').downcase }
186
+ headers[transformed_header] = v
56
187
  end
57
188
  end
58
189
  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 = options.is_a?(ActiveSupport::OrderedOptions) ? 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,76 @@
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
- def_delegators :pattern, :named_captures, :params
12
+ def_delegators :pattern, :params
17
13
  def_delegators :to_regexp, :===
18
14
  alias match? ===
19
15
 
20
- def initialize(pattern, **options)
21
- @origin = pattern
22
- @path = build_path(pattern, **options)
23
- @pattern = Mustermann::Grape.new(@path, **pattern_options(options))
16
+ def initialize(origin, suffix, options)
17
+ @origin = origin
18
+ @path = build_path(origin, options[:anchor], suffix)
19
+ @pattern = build_pattern(@path, options[:params], options[:format], options[:version], options[:requirements])
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, params, format, version, requirements)
32
+ Mustermann::Grape.new(
33
+ path,
34
+ uri_decode: true,
35
+ params: params,
36
+ capture: extract_capture(format, version, requirements)
37
+ )
38
+ end
39
+
40
+ def build_path(pattern, anchor, suffix)
41
+ PatternCache[[build_path_from_pattern(pattern, anchor), suffix]]
34
42
  end
35
43
 
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'
44
+ def extract_capture(format, version, requirements)
45
+ capture = {}.tap do |h|
46
+ h[:format] = map_str(format) if format.present?
47
+ h[:version] = map_str(version) if version.present?
41
48
  end
42
49
 
43
- pattern = -pattern.split('/').tap do |parts|
44
- parts[parts.length - 1] = "?#{parts.last}"
45
- end.join('/') if pattern.end_with?('*path')
50
+ return capture if requirements.blank?
46
51
 
47
- PatternCache[[pattern, suffix]]
52
+ requirements.merge(capture)
48
53
  end
49
54
 
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?
55
+ def build_path_from_pattern(pattern, anchor)
56
+ if pattern.end_with?('*path')
57
+ pattern.dup.insert(pattern.rindex('/') + 1, '?')
58
+ elsif anchor
59
+ pattern
60
+ elsif pattern.end_with?('/')
61
+ "#{pattern}?*path"
62
+ else
63
+ "#{pattern}/?*path"
55
64
  end
56
65
  end
57
66
 
67
+ def map_str(value)
68
+ Array.wrap(value).map(&:to_s)
69
+ end
70
+
58
71
  class PatternCache < Grape::Util::Cache
59
72
  def initialize
73
+ super
60
74
  @cache = Hash.new do |h, (pattern, suffix)|
61
75
  h[[pattern, suffix]] = -"#{pattern}#{suffix}"
62
76
  end