grape 2.3.0 → 3.0.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/CONTRIBUTING.md +2 -10
  4. data/README.md +106 -43
  5. data/UPGRADING.md +90 -1
  6. data/grape.gemspec +4 -4
  7. data/lib/grape/api/instance.rb +51 -73
  8. data/lib/grape/api.rb +56 -89
  9. data/lib/grape/cookies.rb +31 -25
  10. data/lib/grape/dry_types.rb +48 -4
  11. data/lib/grape/dsl/callbacks.rb +8 -58
  12. data/lib/grape/dsl/desc.rb +8 -67
  13. data/lib/grape/dsl/headers.rb +1 -1
  14. data/lib/grape/dsl/helpers.rb +60 -65
  15. data/lib/grape/dsl/inside_route.rb +26 -61
  16. data/lib/grape/dsl/logger.rb +3 -6
  17. data/lib/grape/dsl/middleware.rb +22 -40
  18. data/lib/grape/dsl/parameters.rb +10 -19
  19. data/lib/grape/dsl/request_response.rb +136 -139
  20. data/lib/grape/dsl/routing.rb +230 -194
  21. data/lib/grape/dsl/settings.rb +22 -134
  22. data/lib/grape/dsl/validations.rb +37 -45
  23. data/lib/grape/endpoint.rb +91 -126
  24. data/lib/grape/error_formatter/base.rb +2 -0
  25. data/lib/grape/exceptions/base.rb +1 -1
  26. data/lib/grape/exceptions/conflicting_types.rb +11 -0
  27. data/lib/grape/exceptions/invalid_parameters.rb +11 -0
  28. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  29. data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
  30. data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
  31. data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
  32. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  33. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +2 -5
  34. data/lib/grape/extensions/hash.rb +2 -1
  35. data/lib/grape/extensions/hashie/mash.rb +3 -5
  36. data/lib/grape/locale/en.yml +44 -44
  37. data/lib/grape/middleware/auth/base.rb +11 -32
  38. data/lib/grape/middleware/auth/dsl.rb +22 -29
  39. data/lib/grape/middleware/base.rb +30 -11
  40. data/lib/grape/middleware/error.rb +14 -32
  41. data/lib/grape/middleware/formatter.rb +40 -72
  42. data/lib/grape/middleware/stack.rb +28 -38
  43. data/lib/grape/middleware/versioner/accept_version_header.rb +2 -4
  44. data/lib/grape/middleware/versioner/base.rb +30 -56
  45. data/lib/grape/middleware/versioner/header.rb +2 -2
  46. data/lib/grape/middleware/versioner/param.rb +2 -3
  47. data/lib/grape/middleware/versioner/path.rb +1 -1
  48. data/lib/grape/namespace.rb +11 -0
  49. data/lib/grape/params_builder/base.rb +20 -0
  50. data/lib/grape/params_builder/hash.rb +11 -0
  51. data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
  52. data/lib/grape/params_builder/hashie_mash.rb +11 -0
  53. data/lib/grape/params_builder.rb +32 -0
  54. data/lib/grape/request.rb +161 -22
  55. data/lib/grape/router/route.rb +1 -1
  56. data/lib/grape/router.rb +27 -8
  57. data/lib/grape/util/api_description.rb +56 -0
  58. data/lib/grape/util/base_inheritable.rb +5 -2
  59. data/lib/grape/util/inheritable_setting.rb +7 -0
  60. data/lib/grape/util/media_type.rb +1 -1
  61. data/lib/grape/util/registry.rb +1 -1
  62. data/lib/grape/validations/contract_scope.rb +2 -2
  63. data/lib/grape/validations/params_documentation.rb +50 -0
  64. data/lib/grape/validations/params_scope.rb +46 -56
  65. data/lib/grape/validations/types/array_coercer.rb +2 -3
  66. data/lib/grape/validations/types/dry_type_coercer.rb +4 -11
  67. data/lib/grape/validations/types/primitive_coercer.rb +1 -28
  68. data/lib/grape/validations/types.rb +10 -25
  69. data/lib/grape/validations/validators/base.rb +2 -9
  70. data/lib/grape/validations/validators/except_values_validator.rb +1 -1
  71. data/lib/grape/validations/validators/presence_validator.rb +1 -1
  72. data/lib/grape/validations/validators/regexp_validator.rb +1 -1
  73. data/lib/grape/version.rb +1 -1
  74. data/lib/grape.rb +18 -9
  75. metadata +35 -20
  76. data/lib/grape/api/helpers.rb +0 -9
  77. data/lib/grape/dsl/api.rb +0 -19
  78. data/lib/grape/dsl/configuration.rb +0 -15
  79. data/lib/grape/error_formatter/jsonapi.rb +0 -7
  80. data/lib/grape/http/headers.rb +0 -56
  81. data/lib/grape/middleware/helpers.rb +0 -12
  82. data/lib/grape/parser/jsonapi.rb +0 -7
  83. data/lib/grape/types/invalid_value.rb +0 -8
  84. data/lib/grape/util/lazy/object.rb +0 -45
  85. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  86. data/lib/grape/validations/attributes_doc.rb +0 -60
@@ -4,77 +4,51 @@ module Grape
4
4
  module Middleware
5
5
  module Versioner
6
6
  class Base < Grape::Middleware::Base
7
- DEFAULT_PATTERN = /.*/i.freeze
8
- DEFAULT_PARAMETER = 'apiver'
7
+ DEFAULT_OPTIONS = {
8
+ pattern: /.*/i,
9
+ prefix: nil,
10
+ mount_path: nil,
11
+ version_options: {
12
+ strict: false,
13
+ cascade: true,
14
+ parameter: 'apiver',
15
+ vendor: nil
16
+ }.freeze
17
+ }.freeze
9
18
 
10
- def self.inherited(klass)
11
- super
12
- Versioner.register(klass)
13
- end
14
-
15
- def default_options
16
- {
17
- versions: nil,
18
- prefix: nil,
19
- mount_path: nil,
20
- pattern: DEFAULT_PATTERN,
21
- version_options: {
22
- strict: false,
23
- cascade: true,
24
- parameter: DEFAULT_PARAMETER
25
- }
26
- }
27
- end
19
+ CASCADE_PASS_HEADER = { 'X-Cascade' => 'pass' }.freeze
28
20
 
29
- def versions
30
- options[:versions]
21
+ DEFAULT_OPTIONS.each_key do |key|
22
+ define_method key do
23
+ options[key]
24
+ end
31
25
  end
32
26
 
33
- def prefix
34
- options[:prefix]
27
+ DEFAULT_OPTIONS[:version_options].each_key do |key|
28
+ define_method key do
29
+ options[:version_options][key]
30
+ end
35
31
  end
36
32
 
37
- def mount_path
38
- options[:mount_path]
39
- end
40
-
41
- def pattern
42
- options[:pattern]
43
- end
44
-
45
- def version_options
46
- options[:version_options]
47
- end
48
-
49
- def strict?
50
- version_options[:strict]
51
- end
52
-
53
- # By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
54
- # of routes (see Grape::Router) for more information). To prevent
55
- # this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
56
- def cascade?
57
- version_options[:cascade]
58
- end
59
-
60
- def parameter_key
61
- version_options[:parameter]
33
+ def self.inherited(klass)
34
+ super
35
+ Versioner.register(klass)
62
36
  end
63
37
 
64
- def vendor
65
- version_options[:vendor]
66
- end
38
+ attr_reader :error_headers, :versions
67
39
 
68
- def error_headers
69
- cascade? ? { Grape::Http::Headers::X_CASCADE => 'pass' } : {}
40
+ def initialize(app, **options)
41
+ super
42
+ @error_headers = cascade ? CASCADE_PASS_HEADER : {}
43
+ @versions = options[:versions]&.map(&:to_s) # making sure versions are strings to ease potential match
70
44
  end
71
45
 
72
46
  def potential_version_match?(potential_version)
73
- versions.blank? || versions.any? { |v| v.to_s == potential_version }
47
+ versions.blank? || versions.include?(potential_version)
74
48
  end
75
49
 
76
50
  def version_not_found!
77
- throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' }
51
+ throw :error, status: 404, message: '404 API Version Not Found', headers: CASCADE_PASS_HEADER
78
52
  end
79
53
  end
80
54
  end
@@ -49,11 +49,11 @@ module Grape
49
49
  end
50
50
 
51
51
  def accept_header
52
- env[Grape::Http::Headers::HTTP_ACCEPT]
52
+ env['HTTP_ACCEPT']
53
53
  end
54
54
 
55
55
  def strict_header_checks!
56
- return unless strict?
56
+ return unless strict
57
57
 
58
58
  accept_header_check!
59
59
  version_and_vendor_check!
@@ -20,12 +20,11 @@ module Grape
20
20
  # env['api.version'] => 'v1'
21
21
  class Param < Base
22
22
  def before
23
- potential_version = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[parameter_key]
23
+ potential_version = query_params[parameter]
24
24
  return if potential_version.blank?
25
25
 
26
26
  version_not_found! unless potential_version_match?(potential_version)
27
- env[Grape::Env::API_VERSION] = potential_version
28
- env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter_key) if env.key? Rack::RACK_REQUEST_QUERY_HASH
27
+ env[Grape::Env::API_VERSION] = env[Rack::RACK_REQUEST_QUERY_HASH].delete(parameter)
29
28
  end
30
29
  end
31
30
  end
@@ -28,7 +28,7 @@ module Grape
28
28
  slash_position = path_info.index('/', 1) # omit the first one
29
29
  return unless slash_position
30
30
 
31
- potential_version = path_info[1..slash_position - 1]
31
+ potential_version = path_info[1..(slash_position - 1)]
32
32
  return unless potential_version.match?(pattern)
33
33
 
34
34
  version_not_found! unless potential_version_match?(potential_version)
@@ -28,6 +28,17 @@ module Grape
28
28
  settings&.map(&:space)
29
29
  end
30
30
 
31
+ def eql?(other)
32
+ other.class == self.class &&
33
+ other.space == space &&
34
+ other.options == options
35
+ end
36
+ alias == eql?
37
+
38
+ def hash
39
+ [self.class, space, options].hash
40
+ end
41
+
31
42
  # Join the namespaces from a list of settings to create a path prefix.
32
43
  # @param settings [Array] list of Grape::Util::InheritableSettings.
33
44
  def self.joined_space_path(settings)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ class Base
6
+ class << self
7
+ def call(_params)
8
+ raise NotImplementedError
9
+ end
10
+
11
+ private
12
+
13
+ def inherited(klass)
14
+ super
15
+ ParamsBuilder.register(klass)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ class Hash < Base
6
+ def self.call(params)
7
+ params.deep_symbolize_keys
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ class HashWithIndifferentAccess < Base
6
+ def self.call(params)
7
+ params.with_indifferent_access
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ class HashieMash < Base
6
+ def self.call(params)
7
+ ::Hashie::Mash.new(params)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module ParamsBuilder
5
+ extend Grape::Util::Registry
6
+
7
+ SHORT_NAME_LOOKUP = {
8
+ 'Grape::Extensions::Hash::ParamBuilder' => :hash,
9
+ 'Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder' => :hash_with_indifferent_access,
10
+ 'Grape::Extensions::Hashie::Mash::ParamBuilder' => :hashie_mash
11
+ }.freeze
12
+
13
+ module_function
14
+
15
+ def params_builder_for(short_name)
16
+ verified_short_name = verify_short_name!(short_name)
17
+
18
+ raise Grape::Exceptions::UnknownParamsBuilder, verified_short_name unless registry.key?(verified_short_name)
19
+
20
+ registry[verified_short_name]
21
+ end
22
+
23
+ def verify_short_name!(short_name)
24
+ return short_name if short_name.is_a?(Symbol)
25
+
26
+ class_name = short_name.name
27
+ SHORT_NAME_LOOKUP[class_name].tap do |real_short_name|
28
+ Grape.deprecator.warn "#{class_name} has been deprecated. Use short name :#{real_short_name} instead."
29
+ end
30
+ end
31
+ end
32
+ 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
142
  def initialize(env, build_params_with: nil)
10
- extend build_params_with || Grape.config.param_builder
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
@@ -55,7 +55,7 @@ module Grape
55
55
 
56
56
  def upcase_method(method)
57
57
  method_s = method.to_s
58
- 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
59
59
  end
60
60
  end
61
61
  end
data/lib/grape/router.rb CHANGED
@@ -4,12 +4,31 @@ 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)
8
- path = +"/#{path}"
15
+ return '/' unless path
16
+ return path if path == '/'
17
+
18
+ # Fast path for the overwhelming majority of paths that don't need to be normalized
19
+ return path.dup if path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//}))
20
+
21
+ # Slow path
22
+ encoding = path.encoding
23
+ path = "/#{path}"
9
24
  path.squeeze!('/')
10
- path.sub!(%r{/+\Z}, '')
11
- path = '/' if path == ''
12
- path
25
+
26
+ unless path == '/'
27
+ path.delete_suffix!('/')
28
+ path.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase }
29
+ end
30
+
31
+ path.force_encoding(encoding)
13
32
  end
14
33
 
15
34
  def initialize
@@ -24,7 +43,7 @@ module Grape
24
43
 
25
44
  @union = Regexp.union(@neutral_regexes)
26
45
  @neutral_regexes = nil
27
- (Grape::Http::Headers::SUPPORTED_METHODS + ['*']).each do |method|
46
+ (Grape::HTTP_SUPPORTED_METHODS + ['*']).each do |method|
28
47
  next unless map.key?(method)
29
48
 
30
49
  routes = map[method]
@@ -93,7 +112,7 @@ module Grape
93
112
  return response unless cascade
94
113
 
95
114
  # we need to close the body if possible before dismissing
96
- response[2].close if response[2].respond_to?(:close)
115
+ response[2].try(:close)
97
116
  end
98
117
  end
99
118
  end
@@ -138,7 +157,7 @@ module Grape
138
157
  end
139
158
 
140
159
  def default_response
141
- headers = Grape::Util::Header.new.merge(Grape::Http::Headers::X_CASCADE => 'pass')
160
+ headers = Grape::Util::Header.new.merge('X-Cascade' => 'pass')
142
161
  [404, headers, ['404 Not Found']]
143
162
  end
144
163
 
@@ -162,7 +181,7 @@ module Grape
162
181
  end
163
182
 
164
183
  def cascade?(response)
165
- response && response[1][Grape::Http::Headers::X_CASCADE] == 'pass'
184
+ response && response[1]['X-Cascade'] == 'pass'
166
185
  end
167
186
 
168
187
  def string_for(input)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Util
5
+ class ApiDescription
6
+ def initialize(description, endpoint_configuration, &block)
7
+ @endpoint_configuration = endpoint_configuration
8
+ @attributes = { description: description }
9
+ instance_eval(&block)
10
+ end
11
+
12
+ %i[
13
+ body_name
14
+ consumes
15
+ default
16
+ deprecated
17
+ detail
18
+ entity
19
+ headers
20
+ hidden
21
+ http_codes
22
+ is_array
23
+ named
24
+ nickname
25
+ params
26
+ produces
27
+ security
28
+ summary
29
+ tags
30
+ ].each do |attribute|
31
+ define_method attribute do |value|
32
+ @attributes[attribute] = value
33
+ end
34
+ end
35
+
36
+ alias success entity
37
+ alias failure http_codes
38
+
39
+ def configuration
40
+ @configuration ||= eval_endpoint_config(@endpoint_configuration)
41
+ end
42
+
43
+ def settings
44
+ @attributes
45
+ end
46
+
47
+ private
48
+
49
+ def eval_endpoint_config(configuration)
50
+ return configuration if configuration.is_a?(Hash)
51
+
52
+ configuration.evaluate
53
+ end
54
+ end
55
+ end
56
+ end
@@ -14,8 +14,11 @@ module Grape
14
14
  @new_values = {}
15
15
  end
16
16
 
17
- def delete(key)
18
- new_values.delete key
17
+ def delete(*keys)
18
+ keys.map do |key|
19
+ # since delete returns the deleted value, seems natural to `map` the result
20
+ new_values.delete key
21
+ end
19
22
  end
20
23
 
21
24
  def initialize_copy(other)
@@ -95,6 +95,13 @@ module Grape
95
95
  namespace_reverse_stackable: namespace_reverse_stackable.to_hash
96
96
  }
97
97
  end
98
+
99
+ def namespace_stackable_with_hash(key)
100
+ data = namespace_stackable[key]
101
+ return if data.blank?
102
+
103
+ data.each_with_object({}) { |value, result| result.deep_merge!(value) }
104
+ end
98
105
  end
99
106
  end
100
107
  end
@@ -7,7 +7,7 @@ module Grape
7
7
 
8
8
  # based on the HTTP Accept header with the pattern:
9
9
  # application/vnd.:vendor-:version+:format
10
- VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?<vendor>[a-z0-9.\-_!^]+?)(?:-(?<version>[a-z0-9*.]+))?(?:\+(?<format>[a-z0-9*\-.]+))?\z/.freeze
10
+ VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?<vendor>[a-z0-9.\-_!^]+?)(?:-(?<version>[a-z0-9*.]+))?(?:\+(?<format>[a-z0-9*\-.]+))?\z/
11
11
 
12
12
  def initialize(type:, subtype:)
13
13
  @type = type
@@ -7,7 +7,7 @@ module Grape
7
7
  short_name = build_short_name(klass)
8
8
  return if short_name.nil?
9
9
 
10
- warn "#{short_name} is already registered with class #{klass}" if registry.key?(short_name)
10
+ warn "#{short_name} is already registered with class #{registry[short_name]}. It will be overridden globally with the following: #{klass.name}" if registry.key?(short_name)
11
11
  registry[short_name] = klass
12
12
  end
13
13
 
@@ -20,14 +20,14 @@ module Grape
20
20
  key_map = contract.key_map
21
21
  end
22
22
 
23
- api.namespace_stackable(:contract_key_map, key_map)
23
+ api.inheritable_setting.namespace_stackable[:contract_key_map] = key_map
24
24
 
25
25
  validator_options = {
26
26
  validator_class: Grape::Validations.require_validator(:contract_scope),
27
27
  opts: { schema: contract, fail_fast: false }
28
28
  }
29
29
 
30
- api.namespace_stackable(:validations, validator_options)
30
+ api.inheritable_setting.namespace_stackable[:validations] = validator_options
31
31
  end
32
32
  end
33
33
  end