grape 3.3.4 → 4.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 (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +128 -0
  3. data/CONTRIBUTING.md +16 -0
  4. data/README.md +208 -8
  5. data/UPGRADING.md +756 -6
  6. data/lib/grape/api/instance.rb +40 -33
  7. data/lib/grape/content_types.rb +47 -1
  8. data/lib/grape/dry_types.rb +1 -1
  9. data/lib/grape/dsl/callbacks.rb +3 -9
  10. data/lib/grape/dsl/declared.rb +4 -4
  11. data/lib/grape/dsl/desc.rb +18 -8
  12. data/lib/grape/dsl/entity.rb +37 -19
  13. data/lib/grape/dsl/helpers.rb +3 -3
  14. data/lib/grape/dsl/inside_route.rb +20 -8
  15. data/lib/grape/dsl/middleware.rb +3 -3
  16. data/lib/grape/dsl/parameters.rb +54 -28
  17. data/lib/grape/dsl/request_response.rb +37 -39
  18. data/lib/grape/dsl/rescue_options.rb +6 -5
  19. data/lib/grape/dsl/routing.rb +116 -49
  20. data/lib/grape/dsl/settings.rb +1 -1
  21. data/lib/grape/dsl/validations.rb +3 -3
  22. data/lib/grape/dsl/version_options.rb +3 -3
  23. data/lib/grape/endpoint/options.rb +13 -7
  24. data/lib/grape/endpoint.rb +141 -118
  25. data/lib/grape/env.rb +7 -0
  26. data/lib/grape/error_formatter/base.rb +6 -5
  27. data/lib/grape/error_formatter.rb +6 -2
  28. data/lib/grape/exceptions/error_response.rb +4 -1
  29. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  30. data/lib/grape/exceptions/validation.rb +9 -8
  31. data/lib/grape/formatter/json.rb +1 -0
  32. data/lib/grape/formatter/serializable_hash.rb +1 -0
  33. data/lib/grape/locale/en.yml +1 -0
  34. data/lib/grape/middleware/auth/dsl.rb +5 -33
  35. data/lib/grape/middleware/error.rb +153 -46
  36. data/lib/grape/middleware/formatter.rb +95 -26
  37. data/lib/grape/middleware/precomputed_content_types.rb +12 -7
  38. data/lib/grape/middleware/stack.rb +13 -4
  39. data/lib/grape/middleware/versioner/base.rb +3 -9
  40. data/lib/grape/middleware/versioner/path.rb +49 -7
  41. data/lib/grape/namespace.rb +3 -2
  42. data/lib/grape/path.rb +8 -69
  43. data/lib/grape/precompiled_json.rb +50 -0
  44. data/lib/grape/request.rb +36 -4
  45. data/lib/grape/router/base_route.rb +47 -10
  46. data/lib/grape/router/greedy_route.rb +5 -1
  47. data/lib/grape/router/pattern/path.rb +78 -0
  48. data/lib/grape/router/pattern.rb +60 -14
  49. data/lib/grape/router/route.rb +62 -10
  50. data/lib/grape/router.rb +100 -57
  51. data/lib/grape/serve_stream/file_body.rb +6 -0
  52. data/lib/grape/serve_stream/stream_response.rb +6 -0
  53. data/lib/grape/util/api_description.rb +10 -1
  54. data/lib/grape/util/cache.rb +21 -2
  55. data/lib/grape/util/deep_freeze.rb +1 -2
  56. data/lib/grape/util/freeze_on_new.rb +20 -0
  57. data/lib/grape/util/inheritable_setting.rb +751 -37
  58. data/lib/grape/util/media_type.rb +10 -3
  59. data/lib/grape/util/path_normalizer.rb +12 -9
  60. data/lib/grape/util/registry.rb +12 -2
  61. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  62. data/lib/grape/util/stackable_values.rb +32 -6
  63. data/lib/grape/validations/attributes_iterator.rb +21 -4
  64. data/lib/grape/validations/contract_scope.rb +8 -7
  65. data/lib/grape/validations/oneof_collector.rb +6 -10
  66. data/lib/grape/validations/params_documentation.rb +2 -2
  67. data/lib/grape/validations/params_scope.rb +66 -26
  68. data/lib/grape/validations/single_attribute_iterator.rb +5 -3
  69. data/lib/grape/validations/types/array_coercer.rb +4 -6
  70. data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
  71. data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
  72. data/lib/grape/validations/types/json.rb +1 -3
  73. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  74. data/lib/grape/validations/types/primitive_coercer.rb +12 -9
  75. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
  76. data/lib/grape/validations/types.rb +5 -5
  77. data/lib/grape/validations/validations_spec.rb +14 -4
  78. data/lib/grape/validations/validators/base.rb +4 -7
  79. data/lib/grape/validations/validators/coerce_validator.rb +8 -2
  80. data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
  81. data/lib/grape/validations/validators/length_validator.rb +4 -2
  82. data/lib/grape/validations/validators/oneof_validator.rb +2 -0
  83. data/lib/grape/validations/validators/same_as_validator.rb +1 -0
  84. data/lib/grape/version.rb +2 -1
  85. data/lib/grape.rb +23 -2
  86. metadata +10 -9
  87. data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
  88. data/lib/grape/util/base_inheritable.rb +0 -43
  89. data/lib/grape/util/inheritable_values.rb +0 -33
  90. data/lib/grape/util/reverse_stackable_values.rb +0 -15
@@ -6,66 +6,76 @@ module Grape
6
6
  # Specify the default format for the API's serializers.
7
7
  # May be `:json` or `:txt` (default).
8
8
  def default_format(new_format = nil)
9
- return inheritable_setting.namespace_inheritable[:default_format] if new_format.nil?
9
+ return inheritable_setting.default_format if new_format.nil?
10
10
 
11
- inheritable_setting.namespace_inheritable[:default_format] = new_format.to_sym
11
+ inheritable_setting.default_format = new_format.to_sym
12
12
  end
13
13
 
14
14
  # Specify the format for the API's serializers.
15
15
  # May be `:json`, `:xml`, `:txt`, etc.
16
16
  def format(new_format = nil)
17
- return inheritable_setting.namespace_inheritable[:format] if new_format.nil?
17
+ return inheritable_setting.format if new_format.nil?
18
18
 
19
19
  symbolic_new_format = new_format.to_sym
20
- inheritable_setting.namespace_inheritable[:format] = symbolic_new_format
21
- inheritable_setting.namespace_inheritable[:default_error_formatter] = Grape::ErrorFormatter.formatter_for(symbolic_new_format)
20
+ inheritable_setting.format = symbolic_new_format
21
+ inheritable_setting.default_error_formatter = Grape::ErrorFormatter.formatter_for(symbolic_new_format)
22
22
 
23
23
  content_type = content_types[symbolic_new_format]
24
24
  raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type
25
25
 
26
- inheritable_setting.namespace_stackable[:content_types] = { symbolic_new_format => content_type }
26
+ inheritable_setting.add_content_type(symbolic_new_format, content_type)
27
27
  end
28
28
 
29
29
  # Specify a custom formatter for a content-type.
30
30
  def formatter(content_type, new_formatter)
31
- inheritable_setting.namespace_stackable[:formatters] = { content_type.to_sym => new_formatter }
31
+ inheritable_setting.add_formatter(content_type.to_sym, new_formatter)
32
32
  end
33
33
 
34
34
  # Specify a custom parser for a content-type.
35
35
  def parser(content_type, new_parser)
36
- inheritable_setting.namespace_stackable[:parsers] = { content_type.to_sym => new_parser }
36
+ inheritable_setting.add_parser(content_type.to_sym, new_parser)
37
37
  end
38
38
 
39
- # Specify a default error formatter.
39
+ # Specify a default error formatter, by the name it is registered under.
40
+ # A name nothing is registered for used to be stored as
41
+ # +ErrorFormatter::Txt+ — the fallback the lookup applied — so a typo
42
+ # read back as a working setting and silently rendered errors as text.
40
43
  def default_error_formatter(new_formatter_name = nil)
41
- return inheritable_setting.namespace_inheritable[:default_error_formatter] if new_formatter_name.nil?
44
+ return inheritable_setting.default_error_formatter if new_formatter_name.nil?
42
45
 
43
- new_formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name)
44
- inheritable_setting.namespace_inheritable[:default_error_formatter] = new_formatter
46
+ formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name)
47
+ raise Grape::Exceptions::UnknownErrorFormatter.new(new_formatter_name) if formatter.nil?
48
+
49
+ inheritable_setting.default_error_formatter = formatter
45
50
  end
46
51
 
52
+ # Specify a custom error formatter for a format, passed positionally or
53
+ # as +with:+. A nil formatter used to be registered as-is, which is the
54
+ # one thing the registration cannot mean: the format then resolves as if
55
+ # the call had never been made. Reject it here, where the mistake is.
47
56
  def error_formatter(format, options = nil, with: nil)
48
57
  formatter = with || options
49
- inheritable_setting.namespace_stackable[:error_formatters] = { format.to_sym => formatter }
58
+ raise ArgumentError, "error_formatter `#{format.inspect}` requires a formatter, given positionally or as `with:`" if formatter.nil?
59
+
60
+ inheritable_setting.add_error_formatter(format.to_sym, formatter)
50
61
  end
51
62
 
52
63
  # Specify additional content-types, e.g.:
53
64
  # content_type :xls, 'application/vnd.ms-excel'
54
65
  def content_type(key, val)
55
- inheritable_setting.namespace_stackable[:content_types] = { key.to_sym => val }
66
+ inheritable_setting.add_content_type(key.to_sym, val)
56
67
  end
57
68
 
58
69
  # All available content types.
59
70
  def content_types
60
- c_types = inheritable_setting.namespace_stackable_with_hash(:content_types)
61
- Grape::ContentTypes.content_types_for c_types
71
+ Grape::ContentTypes.content_types_for(inheritable_setting.content_types)
62
72
  end
63
73
 
64
74
  # Specify the default status code for errors.
65
75
  def default_error_status(new_status = nil)
66
- return inheritable_setting.namespace_inheritable[:default_error_status] if new_status.nil?
76
+ return inheritable_setting.default_error_status if new_status.nil?
67
77
 
68
- inheritable_setting.namespace_inheritable[:default_error_status] = new_status
78
+ inheritable_setting.default_error_status = new_status
69
79
  end
70
80
 
71
81
  # Allows you to rescue certain exceptions that occur to return
@@ -99,32 +109,20 @@ module Grape
99
109
  def rescue_from(*args, with: nil, rescue_subclasses: true, backtrace: false, original_exception: false, &block)
100
110
  handler = extract_handler(args, with:, block:)
101
111
  meta_selector = (args & META_RESCUE_SELECTORS).first
102
- raise ArgumentError, "rescue_from #{meta_selector.inspect} does not accept additional arguments" if meta_selector && args.size > 1
103
-
104
- namespace_inheritable = nil
105
- arg = nil
106
-
107
- if args.one?
108
- arg = args.first
109
- namespace_inheritable = inheritable_setting.namespace_inheritable
110
- end
112
+ raise ArgumentError, "rescue_from `#{meta_selector.inspect}` does not accept additional arguments" if meta_selector && args.size > 1
111
113
 
112
- case arg
114
+ case meta_selector
113
115
  when :all
114
- namespace_inheritable[:rescue_all] = true
115
- namespace_inheritable[:all_rescue_handler] = handler
116
+ inheritable_setting.add_all_rescue_handler(handler)
116
117
  when :grape_exceptions
117
- namespace_inheritable[:rescue_all] = true
118
- namespace_inheritable[:rescue_grape_exceptions] = true
119
- namespace_inheritable[:grape_exceptions_rescue_handler] = handler
118
+ inheritable_setting.add_grape_exceptions_rescue_handler(handler)
120
119
  when :internal_grape_exceptions
121
- namespace_inheritable[:internal_grape_exceptions_rescue_handler] = handler
120
+ inheritable_setting.add_internal_grape_exceptions_rescue_handler(handler)
122
121
  else
123
- handler_type = rescue_subclasses ? :rescue_handlers : :base_only_rescue_handlers
124
- inheritable_setting.namespace_reverse_stackable[handler_type] = args.to_h { |klass| [klass, handler] }
122
+ inheritable_setting.add_rescue_handlers(args.to_h { |klass| [klass, handler] }, subclasses: rescue_subclasses)
125
123
  end
126
124
 
127
- inheritable_setting.namespace_stackable[:rescue_options] = RescueOptions.new(backtrace:, original_exception:)
125
+ inheritable_setting.add_rescue_options(RescueOptions.new(backtrace:, original_exception:))
128
126
  end
129
127
 
130
128
  # Allows you to specify a default representation entity for a
@@ -150,7 +148,7 @@ module Grape
150
148
  def represent(model_class, with:)
151
149
  raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless with.is_a?(Class)
152
150
 
153
- inheritable_setting.namespace_stackable[:representations] = { model_class => with }
151
+ inheritable_setting.add_representation(model_class, with)
154
152
  end
155
153
 
156
154
  private
@@ -165,7 +163,7 @@ module Grape
165
163
  case with
166
164
  when Proc, Symbol then with
167
165
  when String then with.to_sym
168
- else raise ArgumentError, "with: #{with.class}, expected Symbol, String or Proc"
166
+ else raise ArgumentError, "with: `#{with.class}`, expected Symbol, String or Proc"
169
167
  end
170
168
  end
171
169
  end
@@ -3,9 +3,10 @@
3
3
  module Grape
4
4
  module DSL
5
5
  # Immutable value object holding the response-shaping booleans accepted
6
- # by +Grape::DSL::RequestResponse#rescue_from+. Stored on the
7
- # inheritable settings as +namespace_stackable[:rescue_options]+ and
8
- # delegated to by +Grape::Middleware::Error+ (which forwards
6
+ # by +Grape::DSL::RequestResponse#rescue_from+. Recorded on the
7
+ # inheritable settings via +Grape::Util::InheritableSetting#add_rescue_options+
8
+ # (the nearest scope's latest registration wins on read, see
9
+ # +#rescue_options+) and delegated to by +Grape::Middleware::Error+ (which forwards
9
10
  # +backtrace+/+original_exception+ to the formatter as
10
11
  # +include_backtrace+/+include_original_exception+).
11
12
  #
@@ -13,8 +14,8 @@ module Grape
13
14
  # signature on purpose: keeping them on both sides means each entry point
14
15
  # is self-documenting without needing to import a shared constant — the
15
16
  # DSL signature shows what a user sees in the IDE, and the Data object
16
- # has working defaults when constructed directly (middleware
17
- # `DEFAULT_OPTIONS`, spec fixtures, etc.). The two must stay in lockstep.
17
+ # has working defaults when constructed directly (spec fixtures, a
18
+ # middleware built by hand). The two must stay in lockstep.
18
19
  RescueOptions = Data.define(:backtrace, :original_exception) do
19
20
  def initialize(backtrace: false, original_exception: false)
20
21
  super
@@ -16,9 +16,9 @@ module Grape
16
16
  end
17
17
 
18
18
  def cascade(value = nil)
19
- return inheritable_setting.namespace_inheritable.key?(:cascade) ? !inheritable_setting.namespace_inheritable[:cascade].nil? : true if value.nil?
19
+ return inheritable_setting.cascade_defined? ? inheritable_setting.cascade : true if value.nil?
20
20
 
21
- inheritable_setting.namespace_inheritable[:cascade] = value
21
+ inheritable_setting.cascade = value
22
22
  end
23
23
 
24
24
  # Specify an API version.
@@ -72,13 +72,13 @@ module Grape
72
72
 
73
73
  if block
74
74
  within_namespace do
75
- inheritable_setting.namespace_inheritable[:version] = requested_versions
76
- inheritable_setting.namespace_inheritable[:version_options] = options
75
+ inheritable_setting.version = requested_versions
76
+ inheritable_setting.version_options = options
77
77
  instance_eval(&block)
78
78
  end
79
79
  else
80
- inheritable_setting.namespace_inheritable[:version] = requested_versions
81
- inheritable_setting.namespace_inheritable[:version_options] = options
80
+ inheritable_setting.version = requested_versions
81
+ inheritable_setting.version_options = options
82
82
  end
83
83
 
84
84
  @versions&.last
@@ -86,9 +86,9 @@ module Grape
86
86
 
87
87
  # Define a root URL prefix for your entire API.
88
88
  def prefix(prefix = nil)
89
- return inheritable_setting.namespace_inheritable[:root_prefix] if prefix.nil?
89
+ return inheritable_setting.root_prefix if prefix.nil?
90
90
 
91
- inheritable_setting.namespace_inheritable[:root_prefix] = prefix.to_s
91
+ inheritable_setting.root_prefix = prefix.to_s
92
92
  end
93
93
 
94
94
  # Create a scope without affecting the URL.
@@ -102,33 +102,40 @@ module Grape
102
102
  end
103
103
 
104
104
  def build_with(build_with)
105
- inheritable_setting.namespace_inheritable[:build_params_with] = build_with
105
+ inheritable_setting.build_params_with = build_with
106
106
  end
107
107
 
108
108
  # Do not route HEAD requests to GET requests automatically.
109
109
  def do_not_route_head!
110
- inheritable_setting.namespace_inheritable[:do_not_route_head] = true
110
+ inheritable_setting.do_not_route_head!
111
111
  end
112
112
 
113
113
  # Do not automatically route OPTIONS.
114
114
  def do_not_route_options!
115
- inheritable_setting.namespace_inheritable[:do_not_route_options] = true
115
+ inheritable_setting.do_not_route_options!
116
116
  end
117
117
 
118
118
  def lint!
119
- inheritable_setting.namespace_inheritable[:lint] = true
119
+ inheritable_setting.lint!
120
120
  end
121
121
 
122
122
  def do_not_document!
123
- inheritable_setting.namespace_inheritable[:do_not_document] = true
123
+ inheritable_setting.do_not_document!
124
124
  end
125
125
 
126
- def mount(mounts, *opts)
127
- mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
128
- mounts.each_pair do |app, path|
126
+ def mount(mounts, opts = {})
127
+ mount_opts = opts
128
+ if opts[:refresh_already_mounted]
129
+ Grape.deprecator.warn('`refresh_already_mounted` is not a `mount` option and will be ignored in a future release.')
130
+ drop_endpoints_mounted_for(mounts)
131
+ # Dropped before the recursion below re-enters with the same options,
132
+ # so a Grape API does not warn once per mount and once per instance.
133
+ mount_opts = opts.except(:refresh_already_mounted)
134
+ end
135
+
136
+ normalize_mounts(mounts).each_pair do |app, path|
129
137
  if app.respond_to?(:mount_instance)
130
- opts_with = opts.any? ? opts.first[:with] : {}
131
- mount({ app.mount_instance(configuration: opts_with) => path }, *opts)
138
+ mount({ app.mount_instance(configuration: mount_opts[:with] || {}) => path }, mount_opts)
132
139
  next
133
140
  end
134
141
  in_setting = inheritable_setting
@@ -137,7 +144,7 @@ module Grape
137
144
  # instantiated Grape::API::Instance (vs. a bare Rack app).
138
145
  if app.is_a?(Grape::Mountable)
139
146
  mount_path = Grape::Util::PathNormalizer.call(path)
140
- app.top_level_setting.namespace_stackable[:mount_path] = mount_path
147
+ app.top_level_setting.add_mount_path(mount_path)
141
148
 
142
149
  app.inherit_settings(inheritable_setting)
143
150
 
@@ -147,23 +154,13 @@ module Grape
147
154
  change!
148
155
  end
149
156
 
150
- # When trying to mount multiple times the same endpoint, remove the previous ones
151
- # from the list of endpoints if refresh_already_mounted parameter is true
152
- refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false
153
- if refresh_already_mounted && !endpoints.empty?
154
- endpoints.delete_if do |endpoint|
155
- endpoint.options[:app].to_s == app.to_s
156
- end
157
- end
158
-
159
157
  endpoints << Grape::Endpoint.new(
160
158
  in_setting,
161
- method: :any,
159
+ http_methods: :any,
162
160
  path:,
163
161
  app:,
164
- route_options: { anchor: false },
165
- forward_match: !app.respond_to?(:inheritable_setting),
166
- for: self
162
+ anchor: false,
163
+ api: self
167
164
  )
168
165
  end
169
166
  end
@@ -173,6 +170,9 @@ module Grape
173
170
  #
174
171
  # @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted.
175
172
  # @param paths [String] One or more strings representing the URL segment(s) for this route.
173
+ # @param requirements [Hash] Regular-expression constraints for named path params; the route matches only when every requirement is satisfied.
174
+ # @param anchor [Boolean] Whether the route is anchored to the whole path. Defaults to `true`; pass `false` for catch-all routes (e.g. `'/(*:path)'`).
175
+ # @param route_options [Hash] Any additional custom options, carried through to `route.options`.
176
176
  #
177
177
  # @example Defining a basic route.
178
178
  # class MyAPI < Grape::API
@@ -180,19 +180,27 @@ module Grape
180
180
  # {hello: 'world'}
181
181
  # end
182
182
  # end
183
- def route(methods, paths = ['/'], route_options = {}, &)
184
- method = methods == :any ? '*' : methods
185
- endpoint_params = inheritable_setting.namespace_stackable_with_hash(:params) || {}
186
- endpoint_description = inheritable_setting.route[:description]
187
- all_route_options = { params: endpoint_params }
188
- all_route_options.deep_merge!(endpoint_description) if endpoint_description
183
+ def route(methods, paths = ['/'], requirements: nil, anchor: true, **route_options, &)
184
+ validate_requirements!(requirements)
185
+
186
+ http_methods = methods == :any ? '*' : methods
187
+ endpoint_description = inheritable_setting.route_description
188
+
189
+ # +params+, +requirements+ and +anchor+ each travel as their own endpoint
190
+ # input; the route-options bag keeps the description's other keys
191
+ # (+success+, +tags+, …) plus any custom options.
192
+ params = prepare_params(endpoint_description[:params])
193
+ all_route_options = endpoint_description.except(:params)
189
194
  all_route_options.deep_merge!(route_options) if route_options.present?
190
195
 
191
196
  new_endpoint = Grape::Endpoint.new(
192
197
  inheritable_setting,
193
- method:,
198
+ http_methods:,
194
199
  path: paths,
195
- for: self,
200
+ api: self,
201
+ params:,
202
+ requirements:,
203
+ anchor:,
196
204
  route_options: all_route_options,
197
205
  &
198
206
  )
@@ -204,7 +212,7 @@ module Grape
204
212
 
205
213
  Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
206
214
  define_method supported_method.downcase do |path = '/', **options, &block|
207
- route(supported_method, path, options, &block)
215
+ route(supported_method, path, **options, &block)
208
216
  end
209
217
  end
210
218
 
@@ -221,11 +229,13 @@ module Grape
221
229
  # end
222
230
  # end
223
231
  def namespace(space = nil, requirements: nil, **options, &block)
224
- return Namespace.joined_space_path(inheritable_setting.namespace_stackable[:namespace]) unless space || block
232
+ return inheritable_setting.namespace_path unless space || block
233
+
234
+ validate_requirements!(requirements)
225
235
 
226
236
  within_namespace do
227
237
  nest(block) do
228
- inheritable_setting.namespace_stackable[:namespace] = Grape::Namespace.new(space, requirements:, **options) if space
238
+ inheritable_setting.add_namespace(Grape::Namespace.new(space, requirements:, **options)) if space
229
239
  end
230
240
  end
231
241
  end
@@ -244,15 +254,20 @@ module Grape
244
254
  # in your API.
245
255
  #
246
256
  # @param param [Symbol] The name of the parameter you wish to declare.
247
- # @option options [Regexp] You may supply a regular expression that the declared parameter must meet.
257
+ # @option options [Regexp, Class, Symbol] The constraint the declared parameter must meet — a Regexp, or a capture type such as +Integer+.
248
258
  def route_param(param, requirements: nil, type: nil, **, &)
249
- requirements = { param.to_sym => requirements } if requirements.is_a?(Regexp)
259
+ # The param is named here, so the constraint is its own: nest whatever
260
+ # it is, not just a Regexp. A Hash would name the param twice, or key a
261
+ # capture this namespace does not introduce, and belongs on +namespace+.
262
+ raise ArgumentError, "route_param :`#{param}` constrains :`#{param}`; pass the constraint itself, or a Hash of requirements to the enclosing namespace" if requirements.respond_to?(:to_hash)
263
+
264
+ param_requirements = requirements ? { param.to_sym => requirements } : requirements
250
265
 
251
266
  Grape::Validations::ParamsScope.new(api: self) do
252
267
  requires param, type: type
253
268
  end if type
254
269
 
255
- namespace(":#{param}", requirements:, **, &)
270
+ namespace(":#{param}", requirements: param_requirements, **, &)
256
271
  end
257
272
 
258
273
  # @return array of defined versions
@@ -262,6 +277,24 @@ module Grape
262
277
 
263
278
  private
264
279
 
280
+ # Requirements are keyed by param name and merged across namespaces, so
281
+ # anything else has nothing to attach to. Rejected here rather than at
282
+ # the merge, which runs on the first request that builds the routes.
283
+ def validate_requirements!(requirements)
284
+ return if requirements.nil? || requirements.respond_to?(:to_hash)
285
+
286
+ raise ArgumentError, "requirements must be a Hash of param name => constraint, got `#{requirements.class}`"
287
+ end
288
+
289
+ # Compose a route's params: the declared params (+params do … end+) deep-merged
290
+ # with any documented alongside +desc ..., params:+ (+description_params+).
291
+ def prepare_params(description_params)
292
+ endpoint_params = inheritable_setting.params_documentation || {}
293
+ return endpoint_params if description_params.blank?
294
+
295
+ endpoint_params.deep_merge(description_params)
296
+ end
297
+
265
298
  # Remove all defined routes.
266
299
  def reset_routes!
267
300
  endpoints.each(&:reset_routes!)
@@ -272,9 +305,43 @@ module Grape
272
305
  @endpoints = []
273
306
  end
274
307
 
275
- def refresh_mounted_api(mounts, *opts)
276
- opts << { refresh_already_mounted: true }
277
- mount(mounts, *opts)
308
+ # Re-mount +mounts+, replacing any endpoint already mounted for the same
309
+ # base API rather than adding a second one. Called by
310
+ # {Grape::API.refresh_mount_step} when a class-level method runs after the
311
+ # API was mounted.
312
+ def refresh_mounted_api(mounts, opts = {})
313
+ drop_endpoints_mounted_for(mounts)
314
+ mount(mounts, opts)
315
+ end
316
+
317
+ # A mounted Grape API is stored as the throwaway instance +mount+ built
318
+ # for it, never as the class that was written, so endpoints are matched on
319
+ # the base API both of them share.
320
+ def drop_endpoints_mounted_for(mounts)
321
+ normalize_mounts(mounts).each_key do |app|
322
+ endpoints.delete_if { |endpoint| same_mounted_app?(endpoint.mounted_app, app) }
323
+ end
324
+ end
325
+
326
+ # A bare app mounts at the root. The test is +Hash+ rather than
327
+ # +respond_to?(:each_pair)+ because a Struct or an OpenStruct answers that
328
+ # too, and neither can express an app => path mapping — their keys are
329
+ # member names. Reading one as a mapping would silently mount nonsense
330
+ # instead of mounting the app itself.
331
+ def normalize_mounts(mounts)
332
+ mounts.is_a?(Hash) ? mounts : { mounts => '/' }
333
+ end
334
+
335
+ # Two mounts refer to the same app when they share the same base Grape
336
+ # API. +mount+ turns every mounted Grape API into a throwaway
337
+ # +mount_instance+ (a fresh +Class.new+ per mount), so object identity
338
+ # never holds across mounts; comparing the base is the real signal.
339
+ # Plain Rack apps have no base and are mounted as-is, so they fall back
340
+ # to object identity.
341
+ def same_mounted_app?(mounted, app)
342
+ return mounted.base.equal?(app.base) if mounted.respond_to?(:base) && app.respond_to?(:base)
343
+
344
+ mounted.equal?(app)
278
345
  end
279
346
 
280
347
  # Execute first the provided block, then each of the
@@ -37,7 +37,7 @@ module Grape
37
37
  end
38
38
 
39
39
  def route_setting(key, value = nil)
40
- get_or_set(inheritable_setting.route, key, value)
40
+ inheritable_setting.route_setting(key, value)
41
41
  end
42
42
 
43
43
  def namespace_setting(key, value = nil)
@@ -17,8 +17,8 @@ module Grape
17
17
  # subclass, allowing to define the schema inline. When the
18
18
  # +contract+ parameter is a schema, it will be used as a parent. Optional.
19
19
  def contract(contract = nil, &block)
20
- raise ArgumentError, 'Either contract or block must be provided' unless contract || block
21
- raise ArgumentError, 'Cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
20
+ raise ArgumentError, 'either contract or block must be provided' unless contract || block
21
+ raise ArgumentError, 'cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
22
22
 
23
23
  Grape::Validations::ContractScope.new(self, contract, &block)
24
24
  end
@@ -44,7 +44,7 @@ module Grape
44
44
  # # whatever
45
45
  # end
46
46
  def reset_validations!
47
- inheritable_setting.namespace_stackable.delete(:declared_params, :params, :validations)
47
+ inheritable_setting.reset_validations!
48
48
  end
49
49
  end
50
50
  end
@@ -4,7 +4,7 @@ module Grape
4
4
  module DSL
5
5
  # Immutable value object holding the resolved options from
6
6
  # +Grape::DSL::Routing#version+. Stored on the inheritable settings as
7
- # +namespace_inheritable[:version_options]+ and read by internal call
7
+ # +Grape::Util::InheritableSetting#version_options+ and read by internal call
8
8
  # sites (`Path`, `Endpoint`, `API::Instance#cascade?`,
9
9
  # `Middleware::Versioner::Base`) via accessors.
10
10
  #
@@ -12,8 +12,8 @@ module Grape
12
12
  # signature on purpose: keeping them on both sides means each entry point
13
13
  # is self-documenting without needing to import a shared constant — the
14
14
  # DSL signature shows what a user sees in the IDE, and the Data object
15
- # has working defaults when constructed directly (middleware
16
- # `DEFAULT_OPTIONS`, spec fixtures, etc.). The two must stay in lockstep.
15
+ # has working defaults when constructed directly (spec fixtures, a
16
+ # middleware built by hand). The two must stay in lockstep.
17
17
  VersionOptions = Data.define(:using, :cascade, :parameter, :strict, :vendor) do
18
18
  def initialize(using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil)
19
19
  super
@@ -6,13 +6,19 @@ module Grape
6
6
  # +Grape::Endpoint.new+. Internal to {Grape::Endpoint}, which builds it
7
7
  # from the +**options+ Hash in #initialize so the public +options+ reader
8
8
  # stays a plain Hash for downstream gems (e.g. grape-swagger).
9
- # +:method+ is renamed to +:http_methods+ on the value object to avoid
10
- # shadowing +Object#method+ via the generated Data accessor.
11
- Options = Data.define(:path, :http_methods, :for, :route_options, :app, :format, :forward_match) do
12
- def initialize(path:, method:, route_options: {}, app: nil, format: nil, forward_match: nil, **rest)
13
- path = Array(path)
14
- path << '/' if path.empty?
15
- super(path:, http_methods: Array(method), route_options:, app:, format:, forward_match:, **rest)
9
+ Options = Data.define(:path, :http_methods, :api, :route_options, :app, :params, :requirements, :anchor) do
10
+ def initialize(path:, http_methods:, api:, route_options: {}, app: nil, params: {}, requirements: nil, anchor: true)
11
+ # +Array()+ hands back the very Array it was given, so defaulting an
12
+ # empty one by appending would append to the caller's Array — and raise
13
+ # FrozenError on a frozen one. Build a new Array instead of growing
14
+ # theirs: nothing about constructing an endpoint should be visible in
15
+ # the path the caller passed in.
16
+ paths = Array(path)
17
+ super(
18
+ path: paths.presence || ['/'],
19
+ http_methods: Array(http_methods),
20
+ api:, route_options:, app:, params:, requirements:, anchor:
21
+ )
16
22
  end
17
23
  end
18
24
  end