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
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'grape/dsl/headers'
4
-
5
3
  module Grape
6
4
  module DSL
7
5
  module InsideRoute
@@ -28,14 +26,20 @@ module Grape
28
26
  # has completed
29
27
  module PostBeforeFilter
30
28
  def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
31
- options = options.reverse_merge(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
32
- declared_params ||= optioned_declared_params(**options)
29
+ options.reverse_merge!(include_missing: true, include_parent_namespaces: true, evaluate_given: false)
30
+ declared_params ||= optioned_declared_params(options[:include_parent_namespaces])
33
31
 
34
- if passed_params.is_a?(Array)
35
- declared_array(passed_params, options, declared_params, params_nested_path)
36
- else
37
- declared_hash(passed_params, options, declared_params, params_nested_path)
32
+ res = if passed_params.is_a?(Array)
33
+ declared_array(passed_params, options, declared_params, params_nested_path)
34
+ else
35
+ declared_hash(passed_params, options, declared_params, params_nested_path)
36
+ end
37
+
38
+ if (key_maps = namespace_stackable(:contract_key_map))
39
+ key_maps.each { |key_map| key_map.write(passed_params, res) }
38
40
  end
41
+
42
+ res
39
43
  end
40
44
 
41
45
  private
@@ -75,7 +79,7 @@ module Grape
75
79
  else
76
80
  # If it is not a Hash then it does not have children.
77
81
  # Find its value or set it to nil.
78
- return unless options[:include_missing] || passed_params.key?(declared_param)
82
+ return unless options[:include_missing] || passed_params.try(:key?, declared_param)
79
83
 
80
84
  rename_path = params_nested_path + [declared_param.to_s]
81
85
  renamed_param_name = renamed_params[rename_path]
@@ -99,11 +103,11 @@ module Grape
99
103
 
100
104
  route_options_params = options[:route_options][:params] || {}
101
105
  type = route_options_params.dig(key, :type)
102
- has_children = route_options_params.keys.any? { |k| k != key && k.start_with?(key) }
106
+ has_children = route_options_params.keys.any? { |k| k != key && k.start_with?("#{key}[") }
103
107
 
104
108
  if type == 'Hash' && !has_children
105
109
  {}
106
- elsif type == 'Array' || (type&.start_with?('[') && type&.exclude?(','))
110
+ elsif type == 'Array' || (type&.start_with?('[') && type.exclude?(','))
107
111
  []
108
112
  elsif type == 'Set' || type&.start_with?('#<Set')
109
113
  Set.new
@@ -116,8 +120,8 @@ module Grape
116
120
  options[:stringify] ? declared_param.to_s : declared_param.to_sym
117
121
  end
118
122
 
119
- def optioned_declared_params(**options)
120
- declared_params = if options[:include_parent_namespaces]
123
+ def optioned_declared_params(include_parent_namespaces)
124
+ declared_params = if include_parent_namespaces
121
125
  # Declared params including parent namespaces
122
126
  route_setting(:declared_params)
123
127
  else
@@ -159,33 +163,57 @@ module Grape
159
163
  # end user with the specified message.
160
164
  #
161
165
  # @param message [String] The message to display.
162
- # @param status [Integer] the HTTP Status Code. Defaults to default_error_status, 500 if not set.
166
+ # @param status [Integer] The HTTP Status Code. Defaults to default_error_status, 500 if not set.
163
167
  # @param additional_headers [Hash] Addtional headers for the response.
164
- def error!(message, status = nil, additional_headers = nil)
165
- self.status(status || namespace_inheritable(:default_error_status))
168
+ # @param backtrace [Array<String>] The backtrace of the exception that caused the error.
169
+ # @param original_exception [Exception] The original exception that caused the error.
170
+ def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil)
171
+ status = self.status(status || namespace_inheritable(:default_error_status))
166
172
  headers = additional_headers.present? ? header.merge(additional_headers) : header
167
- throw :error, message: message, status: self.status, headers: headers
173
+ throw :error,
174
+ message: message,
175
+ status: status,
176
+ headers: headers,
177
+ backtrace: backtrace,
178
+ original_exception: original_exception
179
+ end
180
+
181
+ # Creates a Rack response based on the provided message, status, and headers.
182
+ # The content type in the headers is set to the default content type unless provided.
183
+ # The message is HTML-escaped if the content type is 'text/html'.
184
+ #
185
+ # @param message [String] The content of the response.
186
+ # @param status [Integer] The HTTP status code.
187
+ # @params headers [Hash] (optional) Headers for the response
188
+ # (default: {Rack::CONTENT_TYPE => content_type}).
189
+ #
190
+ # Returns:
191
+ # A Rack::Response object containing the specified message, status, and headers.
192
+ #
193
+ def rack_response(message, status = 200, headers = { Rack::CONTENT_TYPE => content_type })
194
+ Grape.deprecator.warn('The rack_response method has been deprecated, use error! instead.')
195
+ message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html'
196
+ Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), headers)
168
197
  end
169
198
 
170
199
  # Redirect to a new url.
171
200
  #
172
201
  # @param url [String] The url to be redirect.
173
- # @param options [Hash] The options used when redirect.
174
- # :permanent, default false.
175
- # :body, default a short message including the URL.
176
- def redirect(url, permanent: false, body: nil, **_options)
202
+ # @param permanent [Boolean] default false.
203
+ # @param body default a short message including the URL.
204
+ def redirect(url, permanent: false, body: nil)
177
205
  body_message = body
178
206
  if permanent
179
207
  status 301
180
208
  body_message ||= "This resource has been moved permanently to #{url}."
181
- elsif env[Grape::Http::Headers::HTTP_VERSION] == 'HTTP/1.1' && request.request_method.to_s.upcase != Grape::Http::Headers::GET
209
+ elsif http_version == 'HTTP/1.1' && !request.get?
182
210
  status 303
183
211
  body_message ||= "An alternate resource is located at #{url}."
184
212
  else
185
213
  status 302
186
214
  body_message ||= "This resource has been moved temporarily to #{url}."
187
215
  end
188
- header Grape::Http::Headers::LOCATION, url
216
+ header 'Location', url
189
217
  content_type 'text/plain'
190
218
  body body_message
191
219
  end
@@ -204,10 +232,9 @@ module Grape
204
232
  when nil
205
233
  return @status if instance_variable_defined?(:@status) && @status
206
234
 
207
- case request.request_method.to_s.upcase
208
- when Grape::Http::Headers::POST
235
+ if request.post?
209
236
  201
210
- when Grape::Http::Headers::DELETE
237
+ elsif request.delete?
211
238
  if instance_variable_defined?(:@body) && @body.present?
212
239
  200
213
240
  else
@@ -230,18 +257,6 @@ module Grape
230
257
  end
231
258
  end
232
259
 
233
- # Set or get a cookie
234
- #
235
- # @example
236
- # cookies[:mycookie] = 'mycookie val'
237
- # cookies['mycookie-string'] = 'mycookie string val'
238
- # cookies[:more] = { value: '123', expires: Time.at(0) }
239
- # cookies.delete :more
240
- #
241
- def cookies
242
- @cookies ||= Cookies.new
243
- end
244
-
245
260
  # Allows you to define the response body as something other than the
246
261
  # return value.
247
262
  #
@@ -277,20 +292,6 @@ module Grape
277
292
  body false
278
293
  end
279
294
 
280
- # Deprecated method to send files to the client. Use `sendfile` or `stream`
281
- def file(value = nil)
282
- if value.is_a?(String)
283
- Grape.deprecator.warn('Use sendfile or stream to send files.')
284
- sendfile(value)
285
- elsif !value.is_a?(NilClass)
286
- Grape.deprecator.warn('Use stream to use a Stream object.')
287
- stream(value)
288
- else
289
- Grape.deprecator.warn('Use sendfile or stream to send files.')
290
- sendfile
291
- end
292
- end
293
-
294
295
  # Allows you to send a file to the client via sendfile.
295
296
  #
296
297
  # @example
@@ -329,7 +330,7 @@ module Grape
329
330
  return if value.nil? && @stream.nil?
330
331
 
331
332
  header Rack::CONTENT_LENGTH, nil
332
- header Grape::Http::Headers::TRANSFER_ENCODING, nil
333
+ header 'Transfer-Encoding', nil
333
334
  header Rack::CACHE_CONTROL, 'no-cache' # Skips ETag generation (reading the response up front)
334
335
  if value.is_a?(String)
335
336
  file_body = Grape::ServeStream::FileBody.new(value)
@@ -434,7 +435,19 @@ module Grape
434
435
  def entity_representation_for(entity_class, object, options)
435
436
  embeds = { env: env }
436
437
  embeds[:version] = env[Grape::Env::API_VERSION] if env.key?(Grape::Env::API_VERSION)
437
- entity_class.represent(object, **embeds.merge(options))
438
+ entity_class.represent(object, **embeds, **options)
439
+ end
440
+
441
+ def http_version
442
+ env.fetch('HTTP_VERSION') { env[Rack::SERVER_PROTOCOL] }
443
+ end
444
+
445
+ def api_format(format)
446
+ env[Grape::Env::API_FORMAT] = format
447
+ end
448
+
449
+ def context
450
+ self
438
451
  end
439
452
  end
440
453
  end
@@ -23,14 +23,14 @@ module Grape
23
23
  # class API < Grape::API
24
24
  # desc "Get collection"
25
25
  # params do
26
- # build_with Grape::Extensions::Hashie::Mash::ParamBuilder
26
+ # build_with :hashie_mash
27
27
  # requires :user_id, type: Integer
28
28
  # end
29
29
  # get do
30
30
  # params['user_id']
31
31
  # end
32
32
  # end
33
- def build_with(build_with = nil)
33
+ def build_with(build_with)
34
34
  @api.namespace_inheritable(:build_params_with, build_with)
35
35
  end
36
36
 
@@ -130,13 +130,13 @@ module Grape
130
130
 
131
131
  opts = attrs.extract_options!.clone
132
132
  opts[:presence] = { value: true, message: opts[:message] }
133
- opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
133
+ opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
134
134
 
135
135
  if opts[:using]
136
136
  require_required_and_optional_fields(attrs.first, opts)
137
137
  else
138
138
  validate_attributes(attrs, opts, &block)
139
- block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, **opts.slice(:as))
139
+ block ? new_scope(orig_attrs, &block) : push_declared_params(attrs, opts.slice(:as))
140
140
  end
141
141
  end
142
142
 
@@ -149,7 +149,7 @@ module Grape
149
149
 
150
150
  opts = attrs.extract_options!.clone
151
151
  type = opts[:type]
152
- opts = @group.merge(opts) if instance_variable_defined?(:@group) && @group
152
+ opts = @group.deep_merge(opts) if instance_variable_defined?(:@group) && @group
153
153
 
154
154
  # check type for optional parameter group
155
155
  if attrs && block
@@ -162,7 +162,7 @@ module Grape
162
162
  else
163
163
  validate_attributes(attrs, opts, &block)
164
164
 
165
- block ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, **opts.slice(:as))
165
+ block ? new_scope(orig_attrs, true, &block) : push_declared_params(attrs, opts.slice(:as))
166
166
  end
167
167
  end
168
168
 
@@ -170,7 +170,8 @@ module Grape
170
170
  # @param (see #requires)
171
171
  # @option (see #requires)
172
172
  def with(*attrs, &block)
173
- new_group_scope(attrs.clone, &block)
173
+ new_group_attrs = [@group, attrs.clone.first].compact.reduce(&:deep_merge)
174
+ new_group_scope([new_group_attrs], &block)
174
175
  end
175
176
 
176
177
  # Disallow the given parameters to be present in the same request.
@@ -230,7 +231,7 @@ module Grape
230
231
 
231
232
  alias group requires
232
233
 
233
- class EmptyOptionalValue; end
234
+ class EmptyOptionalValue; end # rubocop:disable Lint/EmptyClass
234
235
 
235
236
  def map_params(params, element, is_array = false)
236
237
  if params.is_a?(Array)
@@ -250,7 +251,7 @@ module Grape
250
251
  # @return hash of parameters relevant for the current scope
251
252
  # @api private
252
253
  def params(params)
253
- params = @parent.params(params) if instance_variable_defined?(:@parent) && @parent
254
+ params = @parent.params_meeting_dependency.presence || @parent.params(params) if instance_variable_defined?(:@parent) && @parent
254
255
  params = map_params(params, @element) if instance_variable_defined?(:@element) && @element
255
256
  params
256
257
  end
@@ -17,18 +17,16 @@ module Grape
17
17
  # Specify the format for the API's serializers.
18
18
  # May be `:json`, `:xml`, `:txt`, etc.
19
19
  def format(new_format = nil)
20
- if new_format
21
- namespace_inheritable(:format, new_format.to_sym)
22
- # define the default error formatters
23
- namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(new_format, **{}))
24
- # define a single mime type
25
- mime_type = content_types[new_format.to_sym]
26
- raise Grape::Exceptions::MissingMimeType.new(new_format) unless mime_type
27
-
28
- namespace_stackable(:content_types, new_format.to_sym => mime_type)
29
- else
30
- namespace_inheritable(:format)
31
- end
20
+ return namespace_inheritable(:format) unless new_format
21
+
22
+ symbolic_new_format = new_format.to_sym
23
+ namespace_inheritable(:format, symbolic_new_format)
24
+ namespace_inheritable(:default_error_formatter, Grape::ErrorFormatter.formatter_for(symbolic_new_format))
25
+
26
+ content_type = content_types[symbolic_new_format]
27
+ raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type
28
+
29
+ namespace_stackable(:content_types, symbolic_new_format => content_type)
32
30
  end
33
31
 
34
32
  # Specify a custom formatter for a content-type.
@@ -43,12 +41,10 @@ module Grape
43
41
 
44
42
  # Specify a default error formatter.
45
43
  def default_error_formatter(new_formatter_name = nil)
46
- if new_formatter_name
47
- new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name, **{})
48
- namespace_inheritable(:default_error_formatter, new_formatter)
49
- else
50
- namespace_inheritable(:default_error_formatter)
51
- end
44
+ return namespace_inheritable(:default_error_formatter) unless new_formatter_name
45
+
46
+ new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name)
47
+ namespace_inheritable(:default_error_formatter, new_formatter)
52
48
  end
53
49
 
54
50
  def error_formatter(format, options)
@@ -30,7 +30,7 @@ module Grape
30
30
  if args.any?
31
31
  options = args.extract_options!
32
32
  options = options.reverse_merge(using: :path)
33
- requested_versions = args.flatten
33
+ requested_versions = args.flatten.map(&:to_s)
34
34
 
35
35
  raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
36
36
 
@@ -54,7 +54,7 @@ module Grape
54
54
 
55
55
  # Define a root URL prefix for your entire API.
56
56
  def prefix(prefix = nil)
57
- namespace_inheritable(:root_prefix, prefix)
57
+ namespace_inheritable(:root_prefix, prefix&.to_s)
58
58
  end
59
59
 
60
60
  # Create a scope without affecting the URL.
@@ -67,6 +67,10 @@ module Grape
67
67
  end
68
68
  end
69
69
 
70
+ def build_with(build_with)
71
+ namespace_inheritable(:build_params_with, build_with)
72
+ end
73
+
70
74
  # Do not route HEAD requests to GET requests automatically.
71
75
  def do_not_route_head!
72
76
  namespace_inheritable(:do_not_route_head, true)
@@ -77,6 +81,10 @@ module Grape
77
81
  namespace_inheritable(:do_not_route_options, true)
78
82
  end
79
83
 
84
+ def lint!
85
+ namespace_inheritable(:lint, true)
86
+ end
87
+
80
88
  def do_not_document!
81
89
  namespace_inheritable(:do_not_document, true)
82
90
  end
@@ -85,8 +93,8 @@ module Grape
85
93
  mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
86
94
  mounts.each_pair do |app, path|
87
95
  if app.respond_to?(:mount_instance)
88
- opts_with = opts.any? ? opts.shift[:with] : {}
89
- mount({ app.mount_instance(configuration: opts_with) => path })
96
+ opts_with = opts.any? ? opts.first[:with] : {}
97
+ mount({ app.mount_instance(configuration: opts_with) => path }, *opts)
90
98
  next
91
99
  end
92
100
  in_setting = inheritable_setting
@@ -103,6 +111,15 @@ module Grape
103
111
  change!
104
112
  end
105
113
 
114
+ # When trying to mount multiple times the same endpoint, remove the previous ones
115
+ # from the list of endpoints if refresh_already_mounted parameter is true
116
+ refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false
117
+ if refresh_already_mounted && !endpoints.empty?
118
+ endpoints.delete_if do |endpoint|
119
+ endpoint.options[:app].to_s == app.to_s
120
+ end
121
+ end
122
+
106
123
  endpoints << Grape::Endpoint.new(
107
124
  in_setting,
108
125
  method: :any,
@@ -145,7 +162,7 @@ module Grape
145
162
  reset_validations!
146
163
  end
147
164
 
148
- Grape::Http::Headers::SUPPORTED_METHODS.each do |supported_method|
165
+ Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
149
166
  define_method supported_method.downcase do |*args, &block|
150
167
  options = args.extract_options!
151
168
  paths = args.first || ['/']
@@ -166,19 +183,12 @@ module Grape
166
183
  # end
167
184
  # end
168
185
  def namespace(space = nil, options = {}, &block)
169
- @namespace_description = nil unless instance_variable_defined?(:@namespace_description) && @namespace_description
170
-
171
- if space || block
172
- within_namespace do
173
- previous_namespace_description = @namespace_description
174
- @namespace_description = (@namespace_description || {}).deep_merge(namespace_setting(:description) || {})
175
- nest(block) do
176
- namespace_stackable(:namespace, Namespace.new(space, **options)) if space
177
- end
178
- @namespace_description = previous_namespace_description
186
+ return Namespace.joined_space_path(namespace_stackable(:namespace)) unless space || block
187
+
188
+ within_namespace do
189
+ nest(block) do
190
+ namespace_stackable(:namespace, Namespace.new(space, options)) if space
179
191
  end
180
- else
181
- Namespace.joined_space_path(namespace_stackable(:namespace))
182
192
  end
183
193
  end
184
194
 
@@ -225,6 +235,13 @@ module Grape
225
235
  def versions
226
236
  @versions ||= []
227
237
  end
238
+
239
+ private
240
+
241
+ def refresh_mounted_api(mounts, *opts)
242
+ opts << { refresh_already_mounted: true }
243
+ mount(mounts, *opts)
244
+ end
228
245
  end
229
246
  end
230
247
  end
@@ -38,6 +38,19 @@ module Grape
38
38
  def params(&block)
39
39
  Grape::Validations::ParamsScope.new(api: self, type: Hash, &block)
40
40
  end
41
+
42
+ # Declare the contract to be used for the endpoint's parameters.
43
+ # @param contract [Class<Dry::Validation::Contract> | Dry::Schema::Processor]
44
+ # The contract or schema to be used for validation. Optional.
45
+ # @yield a block yielding a new instance of Dry::Schema::Params
46
+ # subclass, allowing to define the schema inline. When the
47
+ # +contract+ parameter is a schema, it will be used as a parent. Optional.
48
+ def contract(contract = nil, &block)
49
+ raise ArgumentError, 'Either contract or block must be provided' unless contract || block
50
+ raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
51
+
52
+ Grape::Validations::ContractScope.new(self, contract, &block)
53
+ end
41
54
  end
42
55
  end
43
56
  end