grape 2.2.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -0
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +41 -18
  5. data/UPGRADING.md +75 -1
  6. data/grape.gemspec +5 -5
  7. data/lib/grape/api/instance.rb +25 -60
  8. data/lib/grape/api.rb +44 -76
  9. data/lib/grape/cookies.rb +31 -25
  10. data/lib/grape/dsl/api.rb +0 -2
  11. data/lib/grape/dsl/desc.rb +27 -24
  12. data/lib/grape/dsl/headers.rb +1 -1
  13. data/lib/grape/dsl/helpers.rb +1 -1
  14. data/lib/grape/dsl/inside_route.rb +17 -40
  15. data/lib/grape/dsl/parameters.rb +5 -5
  16. data/lib/grape/dsl/routing.rb +14 -13
  17. data/lib/grape/endpoint.rb +100 -106
  18. data/lib/grape/error_formatter/base.rb +51 -21
  19. data/lib/grape/error_formatter/json.rb +7 -24
  20. data/lib/grape/error_formatter/serializable_hash.rb +7 -0
  21. data/lib/grape/error_formatter/txt.rb +13 -20
  22. data/lib/grape/error_formatter/xml.rb +3 -13
  23. data/lib/grape/error_formatter.rb +4 -12
  24. data/lib/grape/exceptions/base.rb +18 -30
  25. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  26. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  27. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  28. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  29. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  30. data/lib/grape/exceptions/validation.rb +5 -4
  31. data/lib/grape/exceptions/validation_errors.rb +2 -2
  32. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +2 -5
  33. data/lib/grape/extensions/hash.rb +2 -1
  34. data/lib/grape/extensions/hashie/mash.rb +3 -5
  35. data/lib/grape/formatter/base.rb +16 -0
  36. data/lib/grape/formatter/json.rb +4 -6
  37. data/lib/grape/formatter/serializable_hash.rb +1 -1
  38. data/lib/grape/formatter/txt.rb +3 -5
  39. data/lib/grape/formatter/xml.rb +4 -6
  40. data/lib/grape/formatter.rb +4 -12
  41. data/lib/grape/locale/en.yml +44 -44
  42. data/lib/grape/middleware/auth/base.rb +11 -32
  43. data/lib/grape/middleware/auth/dsl.rb +23 -29
  44. data/lib/grape/middleware/base.rb +30 -11
  45. data/lib/grape/middleware/error.rb +18 -24
  46. data/lib/grape/middleware/formatter.rb +39 -73
  47. data/lib/grape/middleware/stack.rb +26 -36
  48. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -3
  49. data/lib/grape/middleware/versioner/base.rb +74 -0
  50. data/lib/grape/middleware/versioner/header.rb +4 -10
  51. data/lib/grape/middleware/versioner/param.rb +2 -5
  52. data/lib/grape/middleware/versioner/path.rb +0 -2
  53. data/lib/grape/middleware/versioner.rb +5 -3
  54. data/lib/grape/namespace.rb +1 -1
  55. data/lib/grape/params_builder/base.rb +18 -0
  56. data/lib/grape/params_builder/hash.rb +11 -0
  57. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  58. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  59. data/lib/grape/params_builder.rb +32 -0
  60. data/lib/grape/parser/base.rb +16 -0
  61. data/lib/grape/parser/json.rb +6 -8
  62. data/lib/grape/parser/xml.rb +6 -8
  63. data/lib/grape/parser.rb +5 -7
  64. data/lib/grape/path.rb +39 -56
  65. data/lib/grape/request.rb +162 -23
  66. data/lib/grape/router/base_route.rb +2 -2
  67. data/lib/grape/router/greedy_route.rb +2 -2
  68. data/lib/grape/router/pattern.rb +23 -18
  69. data/lib/grape/router/route.rb +14 -6
  70. data/lib/grape/router.rb +30 -12
  71. data/lib/grape/util/registry.rb +27 -0
  72. data/lib/grape/validations/contract_scope.rb +2 -39
  73. data/lib/grape/validations/params_scope.rb +15 -14
  74. data/lib/grape/validations/types/dry_type_coercer.rb +10 -6
  75. data/lib/grape/validations/validator_factory.rb +2 -2
  76. data/lib/grape/validations/validators/allow_blank_validator.rb +1 -1
  77. data/lib/grape/validations/validators/base.rb +7 -11
  78. data/lib/grape/validations/validators/coerce_validator.rb +1 -1
  79. data/lib/grape/validations/validators/contract_scope_validator.rb +41 -0
  80. data/lib/grape/validations/validators/default_validator.rb +1 -1
  81. data/lib/grape/validations/validators/except_values_validator.rb +2 -2
  82. data/lib/grape/validations/validators/length_validator.rb +1 -1
  83. data/lib/grape/validations/validators/presence_validator.rb +1 -1
  84. data/lib/grape/validations/validators/regexp_validator.rb +2 -2
  85. data/lib/grape/validations/validators/values_validator.rb +15 -57
  86. data/lib/grape/validations.rb +8 -17
  87. data/lib/grape/version.rb +1 -1
  88. data/lib/grape.rb +14 -2
  89. metadata +24 -16
  90. data/lib/grape/http/headers.rb +0 -55
  91. data/lib/grape/middleware/helpers.rb +0 -12
  92. data/lib/grape/middleware/versioner_helpers.rb +0 -75
  93. data/lib/grape/util/lazy/object.rb +0 -45
  94. data/lib/grape/validations/types/build_coercer.rb +0 -92
data/lib/grape/path.rb CHANGED
@@ -3,65 +3,66 @@
3
3
  module Grape
4
4
  # Represents a path to an endpoint.
5
5
  class Path
6
- attr_reader :raw_path, :namespace, :settings
6
+ DEFAULT_FORMAT_SEGMENT = '(/.:format)'
7
+ NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)'
8
+ VERSION_SEGMENT = ':version'
7
9
 
8
- def initialize(raw_path, namespace, settings)
9
- @raw_path = raw_path
10
- @namespace = namespace
11
- @settings = settings
12
- end
10
+ attr_reader :origin, :suffix
13
11
 
14
- def mount_path
15
- settings[:mount_path]
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)
16
15
  end
17
16
 
18
- def root_prefix
19
- settings[:root_prefix]
17
+ def to_s
18
+ "#{origin}#{suffix}"
20
19
  end
21
20
 
22
- def uses_specific_format?
23
- return false unless settings.key?(:format) && settings.key?(:content_types)
21
+ private
24
22
 
25
- 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
28
+ else
29
+ DEFAULT_FORMAT_SEGMENT
30
+ end
26
31
  end
27
32
 
28
- def uses_path_versioning?
29
- return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
30
-
31
- settings[:version] && settings[:version_options][:using] == :path
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)
40
+ end
32
41
  end
33
42
 
34
- def namespace?
35
- namespace&.match?(/^\S/) && not_slash?(namespace)
43
+ def add_part(parts, value)
44
+ parts << value if value && not_slash?(value)
36
45
  end
37
46
 
38
- def path?
39
- raw_path&.match?(/^\S/) && not_slash?(raw_path)
47
+ def not_slash?(value)
48
+ value != '/'
40
49
  end
41
50
 
42
- def suffix
43
- if uses_specific_format?
44
- "(.#{settings[:format]})"
45
- elsif !uses_path_versioning? || (namespace? || path?)
46
- '(.:format)'
47
- else
48
- '(/.:format)'
49
- end
50
- end
51
+ def uses_specific_format?(settings)
52
+ return false unless settings.key?(:format) && settings.key?(:content_types)
51
53
 
52
- def path
53
- PartsCache[parts]
54
+ settings[:format] && Array(settings[:content_types]).size == 1
54
55
  end
55
56
 
56
- def path_with_suffix
57
- "#{path}#{suffix}"
58
- end
57
+ def uses_path_versioning?(settings)
58
+ return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
59
59
 
60
- def to_s
61
- path_with_suffix
60
+ settings[:version] && settings[:version_options][:using] == :path
62
61
  end
63
62
 
64
- private
63
+ def valid_part?(part)
64
+ part&.match?(/^\S/) && not_slash?(part)
65
+ end
65
66
 
66
67
  class PartsCache < Grape::Util::Cache
67
68
  def initialize
@@ -71,23 +72,5 @@ module Grape
71
72
  end
72
73
  end
73
74
  end
74
-
75
- def parts
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
83
- end
84
-
85
- def add_part(parts, value)
86
- parts << value if value && not_slash?(value)
87
- end
88
-
89
- def not_slash?(value)
90
- value != '/'
91
- end
92
75
  end
93
76
  end
data/lib/grape/request.rb CHANGED
@@ -2,50 +2,189 @@
2
2
 
3
3
  module Grape
4
4
  class Request < Rack::Request
5
- 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
6
138
 
7
139
  alias rack_params params
140
+ alias rack_cookies cookies
8
141
 
9
- def initialize(env, **options)
10
- extend options[:build_params_with] || Grape.config.param_builder
142
+ def initialize(env, build_params_with: nil)
11
143
  super(env)
144
+ @params_builder = Grape::ParamsBuilder.params_builder_for(build_params_with || Grape.config.param_builder)
12
145
  end
13
146
 
14
147
  def params
15
- @params ||= build_params
16
- rescue EOFError
17
- raise Grape::Exceptions::EmptyMessageBody.new(content_type)
18
- rescue Rack::Multipart::MultipartPartLimitError
19
- raise Grape::Exceptions::TooManyMultipartFiles.new(Rack::Utils.multipart_part_limit)
148
+ @params ||= make_params
20
149
  end
21
150
 
22
151
  def headers
23
152
  @headers ||= build_headers
24
153
  end
25
154
 
26
- private
155
+ def cookies
156
+ @cookies ||= Grape::Cookies.new(-> { rack_cookies })
157
+ end
27
158
 
159
+ # needs to be public until extensions param_builder are removed
28
160
  def grape_routing_args
29
- args = env[Grape::Env::GRAPE_ROUTING_ARGS].dup
30
161
  # preserve version from query string parameters
31
- args.delete(:version)
32
- args.delete(:route_info)
33
- args
162
+ env[Grape::Env::GRAPE_ROUTING_ARGS]&.except(:version, :route_info) || {}
34
163
  end
35
164
 
36
- def build_headers
37
- Grape::Util::Lazy::Object.new do
38
- env.each_pair.with_object(Grape::Util::Header.new) do |(k, v), headers|
39
- next unless k.to_s.start_with? HTTP_PREFIX
165
+ private
40
166
 
41
- transformed_header = Grape::Http::Headers::HTTP_HEADERS[k] || transform_header(k)
42
- headers[transformed_header] = v
43
- end
44
- 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
45
179
  end
46
180
 
47
- def transform_header(header)
48
- -header[5..].tr('_', '-').downcase
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
187
+ end
49
188
  end
50
189
  end
51
190
  end
@@ -7,8 +7,8 @@ module Grape
7
7
 
8
8
  attr_reader :index, :pattern, :options
9
9
 
10
- def initialize(**options)
11
- @options = ActiveSupport::OrderedOptions.new.update(options)
10
+ def initialize(options)
11
+ @options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options)
12
12
  end
13
13
 
14
14
  alias attributes options
@@ -6,9 +6,9 @@
6
6
  module Grape
7
7
  class Router
8
8
  class GreedyRoute < BaseRoute
9
- def initialize(pattern:, **options)
9
+ def initialize(pattern, options)
10
10
  @pattern = pattern
11
- super(**options)
11
+ super(options)
12
12
  end
13
13
 
14
14
  # Grape::Router:Route defines params as a function
@@ -9,14 +9,14 @@ module Grape
9
9
 
10
10
  attr_reader :origin, :path, :pattern, :to_regexp
11
11
 
12
- def_delegators :pattern, :named_captures, :params
12
+ def_delegators :pattern, :params
13
13
  def_delegators :to_regexp, :===
14
14
  alias match? ===
15
15
 
16
- def initialize(pattern, **options)
17
- @origin = pattern
18
- @path = build_path(pattern, anchor: options[:anchor], suffix: options[:suffix])
19
- @pattern = build_pattern(@path, 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])
20
20
  @to_regexp = @pattern.to_regexp
21
21
  end
22
22
 
@@ -28,30 +28,31 @@ module Grape
28
28
 
29
29
  private
30
30
 
31
- def build_pattern(path, options)
31
+ def build_pattern(path, params, format, version, requirements)
32
32
  Mustermann::Grape.new(
33
33
  path,
34
34
  uri_decode: true,
35
- params: options[:params],
36
- capture: extract_capture(**options)
35
+ params: params,
36
+ capture: extract_capture(format, version, requirements)
37
37
  )
38
38
  end
39
39
 
40
- def build_path(pattern, anchor: false, suffix: nil)
41
- PatternCache[[build_path_from_pattern(pattern, anchor: anchor), suffix]]
40
+ def build_path(pattern, anchor, suffix)
41
+ PatternCache[[build_path_from_pattern(pattern, anchor), suffix]]
42
42
  end
43
43
 
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?
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?
48
+ end
49
+
50
+ return capture if requirements.blank?
50
51
 
51
- options[:requirements].merge(sliced_options)
52
+ requirements.merge(capture)
52
53
  end
53
54
 
54
- def build_path_from_pattern(pattern, anchor: false)
55
+ def build_path_from_pattern(pattern, anchor)
55
56
  if pattern.end_with?('*path')
56
57
  pattern.dup.insert(pattern.rindex('/') + 1, '?')
57
58
  elsif anchor
@@ -63,6 +64,10 @@ module Grape
63
64
  end
64
65
  end
65
66
 
67
+ def map_str(value)
68
+ Array.wrap(value).map(&:to_s)
69
+ end
70
+
66
71
  class PatternCache < Grape::Util::Cache
67
72
  def initialize
68
73
  super
@@ -5,14 +5,22 @@ module Grape
5
5
  class Route < BaseRoute
6
6
  extend Forwardable
7
7
 
8
+ FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
9
+ NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }
10
+
8
11
  attr_reader :app, :request_method
9
12
 
10
13
  def_delegators :pattern, :path, :origin
11
14
 
12
- def initialize(method, pattern, **options)
15
+ def initialize(method, origin, path, options)
13
16
  @request_method = upcase_method(method)
14
- @pattern = Grape::Router::Pattern.new(pattern, **options)
15
- super(**options)
17
+ @pattern = Grape::Router::Pattern.new(origin, path, options)
18
+ @match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
19
+ super(options)
20
+ end
21
+
22
+ def convert_to_head_request!
23
+ @request_method = Rack::HEAD
16
24
  end
17
25
 
18
26
  def exec(env)
@@ -27,7 +35,7 @@ module Grape
27
35
  def match?(input)
28
36
  return false if input.blank?
29
37
 
30
- options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input)
38
+ @match_function.call(input, pattern)
31
39
  end
32
40
 
33
41
  def params(input = nil)
@@ -42,12 +50,12 @@ module Grape
42
50
  private
43
51
 
44
52
  def params_without_input
45
- pattern.captures_default.merge(attributes.params)
53
+ @params_without_input ||= pattern.captures_default.merge(attributes.params)
46
54
  end
47
55
 
48
56
  def upcase_method(method)
49
57
  method_s = method.to_s
50
- Grape::Http::Headers::SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase
58
+ Grape::HTTP_SUPPORTED_METHODS.detect { |m| m.casecmp(method_s).zero? } || method_s.upcase
51
59
  end
52
60
  end
53
61
  end
data/lib/grape/router.rb CHANGED
@@ -4,12 +4,30 @@ module Grape
4
4
  class Router
5
5
  attr_reader :map, :compiled
6
6
 
7
+ # Taken from Rails
8
+ # normalize_path("/foo") # => "/foo"
9
+ # normalize_path("/foo/") # => "/foo"
10
+ # normalize_path("foo") # => "/foo"
11
+ # normalize_path("") # => "/"
12
+ # normalize_path("/%ab") # => "/%AB"
13
+ # https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19
7
14
  def self.normalize_path(path)
15
+ return +'/' unless path
16
+
17
+ # Fast path for the overwhelming majority of paths that don't need to be normalized
18
+ return path.dup if path == '/' || (path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//})))
19
+
20
+ # Slow path
21
+ encoding = path.encoding
8
22
  path = +"/#{path}"
9
23
  path.squeeze!('/')
10
- path.sub!(%r{/+\Z}, '')
11
- path = '/' if path == ''
12
- path
24
+
25
+ unless path == '/'
26
+ path.delete_suffix!('/')
27
+ path.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase }
28
+ end
29
+
30
+ path.force_encoding(encoding)
13
31
  end
14
32
 
15
33
  def initialize
@@ -24,7 +42,7 @@ module Grape
24
42
 
25
43
  @union = Regexp.union(@neutral_regexes)
26
44
  @neutral_regexes = nil
27
- (Grape::Http::Headers::SUPPORTED_METHODS + ['*']).each do |method|
45
+ (Grape::HTTP_SUPPORTED_METHODS + ['*']).each do |method|
28
46
  next unless map.key?(method)
29
47
 
30
48
  routes = map[method]
@@ -38,8 +56,8 @@ module Grape
38
56
  map[route.request_method] << route
39
57
  end
40
58
 
41
- def associate_routes(pattern, **options)
42
- Grape::Router::GreedyRoute.new(pattern: pattern, **options).then do |greedy_route|
59
+ def associate_routes(pattern, options)
60
+ Grape::Router::GreedyRoute.new(pattern, options).then do |greedy_route|
43
61
  @neutral_regexes << greedy_route.to_regexp(@neutral_map.length)
44
62
  @neutral_map << greedy_route
45
63
  end
@@ -93,7 +111,7 @@ module Grape
93
111
  return response unless cascade
94
112
 
95
113
  # we need to close the body if possible before dismissing
96
- response[2].close if response[2].respond_to?(:close)
114
+ response[2].try(:close)
97
115
  end
98
116
  end
99
117
  end
@@ -107,7 +125,7 @@ module Grape
107
125
 
108
126
  route = match?(input, '*')
109
127
 
110
- return last_neighbor_route.endpoint.call(env) if last_neighbor_route && last_response_cascade && route
128
+ return last_neighbor_route.options[:endpoint].call(env) if last_neighbor_route && last_response_cascade && route
111
129
 
112
130
  last_response_cascade = cascade_or_return_response.call(process_route(route, env)) if route
113
131
 
@@ -138,7 +156,7 @@ module Grape
138
156
  end
139
157
 
140
158
  def default_response
141
- headers = Grape::Util::Header.new.merge(Grape::Http::Headers::X_CASCADE => 'pass')
159
+ headers = Grape::Util::Header.new.merge('X-Cascade' => 'pass')
142
160
  [404, headers, ['404 Not Found']]
143
161
  end
144
162
 
@@ -152,8 +170,8 @@ module Grape
152
170
 
153
171
  def call_with_allow_headers(env, route)
154
172
  prepare_env_from_route(env, route)
155
- env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header.join(', ').freeze
156
- route.endpoint.call(env)
173
+ env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.options[:allow_header]
174
+ route.options[:endpoint].call(env)
157
175
  end
158
176
 
159
177
  def prepare_env_from_route(env, route)
@@ -162,7 +180,7 @@ module Grape
162
180
  end
163
181
 
164
182
  def cascade?(response)
165
- response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
183
+ response && response[1]['X-Cascade'] == 'pass'
166
184
  end
167
185
 
168
186
  def string_for(input)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ module Registry
6
+ def register(klass)
7
+ short_name = build_short_name(klass)
8
+ return if short_name.nil?
9
+
10
+ warn "#{short_name} is already registered with class #{klass}" if registry.key?(short_name)
11
+ registry[short_name] = klass
12
+ end
13
+
14
+ private
15
+
16
+ def build_short_name(klass)
17
+ return if klass.name.blank?
18
+
19
+ klass.name.demodulize.underscore
20
+ end
21
+
22
+ def registry
23
+ @registry ||= {}.with_indifferent_access
24
+ end
25
+ end
26
+ end
27
+ end
@@ -23,49 +23,12 @@ module Grape
23
23
  api.namespace_stackable(:contract_key_map, key_map)
24
24
 
25
25
  validator_options = {
26
- validator_class: Validator,
27
- opts: { schema: contract }
26
+ validator_class: Grape::Validations.require_validator(:contract_scope),
27
+ opts: { schema: contract, fail_fast: false }
28
28
  }
29
29
 
30
30
  api.namespace_stackable(:validations, validator_options)
31
31
  end
32
-
33
- class Validator
34
- attr_reader :schema
35
-
36
- def initialize(*_args, schema:)
37
- @schema = schema
38
- end
39
-
40
- # Validates a given request.
41
- # @param request [Grape::Request] the request currently being handled
42
- # @raise [Grape::Exceptions::ValidationArrayErrors] if validation failed
43
- # @return [void]
44
- def validate(request)
45
- res = schema.call(request.params)
46
-
47
- if res.success?
48
- request.params.deep_merge!(res.to_h)
49
- return
50
- end
51
-
52
- errors = []
53
-
54
- res.errors.messages.each do |message|
55
- full_name = message.path.first.to_s
56
-
57
- full_name += "[#{message.path[1..].join('][')}]" if message.path.size > 1
58
-
59
- errors << Grape::Exceptions::Validation.new(params: [full_name], message: message.text)
60
- end
61
-
62
- raise Grape::Exceptions::ValidationArrayErrors.new(errors)
63
- end
64
-
65
- def fail_fast?
66
- false
67
- end
68
- end
69
32
  end
70
33
  end
71
34
  end