grape 2.4.0 → 4.0.1

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +351 -0
  3. data/CONTRIBUTING.md +18 -10
  4. data/README.md +457 -228
  5. data/UPGRADING.md +1240 -1
  6. data/grape.gemspec +7 -7
  7. data/lib/grape/api/instance.rb +65 -116
  8. data/lib/grape/api.rb +53 -62
  9. data/lib/grape/content_types.rb +58 -3
  10. data/lib/grape/cookies.rb +2 -6
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -4
  13. data/lib/grape/dsl/callbacks.rb +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +24 -67
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/helpers.rb +80 -65
  18. data/lib/grape/dsl/inside_route.rb +56 -296
  19. data/lib/grape/dsl/logger.rb +4 -9
  20. data/lib/grape/dsl/middleware.rb +22 -40
  21. data/lib/grape/dsl/parameters.rb +86 -93
  22. data/lib/grape/dsl/request_response.rb +139 -139
  23. data/lib/grape/dsl/rescue_options.rb +25 -0
  24. data/lib/grape/dsl/routing.rb +328 -203
  25. data/lib/grape/dsl/settings.rb +29 -135
  26. data/lib/grape/dsl/validations.rb +38 -44
  27. data/lib/grape/dsl/version_options.rb +23 -0
  28. data/lib/grape/endpoint/options.rb +25 -0
  29. data/lib/grape/endpoint.rb +285 -245
  30. data/lib/grape/env.rb +8 -3
  31. data/lib/grape/error_formatter/base.rb +27 -21
  32. data/lib/grape/error_formatter/json.rb +6 -0
  33. data/lib/grape/error_formatter/txt.rb +10 -10
  34. data/lib/grape/error_formatter.rb +6 -2
  35. data/lib/grape/exceptions/base.rb +20 -44
  36. data/lib/grape/exceptions/error_response.rb +48 -0
  37. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  38. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  39. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  40. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  41. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  42. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  44. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  45. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  46. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  47. data/lib/grape/exceptions/request_error.rb +11 -0
  48. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  49. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  50. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  51. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  52. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  53. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  54. data/lib/grape/exceptions/validation.rb +25 -7
  55. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  56. data/lib/grape/exceptions/validation_errors.rb +21 -23
  57. data/lib/grape/formatter/json.rb +1 -0
  58. data/lib/grape/formatter/serializable_hash.rb +6 -9
  59. data/lib/grape/json.rb +38 -2
  60. data/lib/grape/locale/en.yml +3 -5
  61. data/lib/grape/middleware/auth/base.rb +4 -3
  62. data/lib/grape/middleware/auth/dsl.rb +9 -23
  63. data/lib/grape/middleware/base.rb +41 -36
  64. data/lib/grape/middleware/error.rb +271 -73
  65. data/lib/grape/middleware/formatter.rb +152 -63
  66. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  67. data/lib/grape/middleware/stack.rb +20 -11
  68. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  69. data/lib/grape/middleware/versioner/base.rb +40 -48
  70. data/lib/grape/middleware/versioner/header.rb +5 -23
  71. data/lib/grape/middleware/versioner/param.rb +2 -2
  72. data/lib/grape/middleware/versioner/path.rb +54 -7
  73. data/lib/grape/mountable.rb +22 -0
  74. data/lib/grape/namespace.rb +19 -11
  75. data/lib/grape/params_builder/base.rb +2 -0
  76. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  77. data/lib/grape/params_builder.rb +2 -19
  78. data/lib/grape/parser/json.rb +1 -1
  79. data/lib/grape/path.rb +8 -72
  80. data/lib/grape/precompiled_json.rb +50 -0
  81. data/lib/grape/request.rb +57 -20
  82. data/lib/grape/router/base_route.rb +53 -7
  83. data/lib/grape/router/greedy_route.rb +16 -6
  84. data/lib/grape/router/mustermann_pattern.rb +44 -0
  85. data/lib/grape/router/pattern/path.rb +78 -0
  86. data/lib/grape/router/pattern.rb +65 -37
  87. data/lib/grape/router/route.rb +66 -18
  88. data/lib/grape/router.rb +126 -115
  89. data/lib/grape/serve_stream/file_body.rb +7 -0
  90. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  91. data/lib/grape/serve_stream/stream_response.rb +7 -0
  92. data/lib/grape/testing.rb +33 -0
  93. data/lib/grape/util/api_description.rb +67 -0
  94. data/lib/grape/util/cache.rb +22 -2
  95. data/lib/grape/util/deep_freeze.rb +34 -0
  96. data/lib/grape/util/freeze_on_new.rb +20 -0
  97. data/lib/grape/util/inheritable_setting.rb +777 -39
  98. data/lib/grape/util/lazy/base.rb +16 -0
  99. data/lib/grape/util/lazy/block.rb +2 -9
  100. data/lib/grape/util/lazy/value.rb +2 -9
  101. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  102. data/lib/grape/util/media_type.rb +13 -9
  103. data/lib/grape/util/path_normalizer.rb +37 -0
  104. data/lib/grape/util/registry.rb +13 -3
  105. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  106. data/lib/grape/util/stackable_values.rb +39 -10
  107. data/lib/grape/util/translation.rb +42 -0
  108. data/lib/grape/validations/attributes_iterator.rb +61 -29
  109. data/lib/grape/validations/coerce_options.rb +21 -0
  110. data/lib/grape/validations/contract_scope.rb +8 -13
  111. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  112. data/lib/grape/validations/oneof_collector.rb +35 -0
  113. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  114. data/lib/grape/validations/params_documentation.rb +52 -0
  115. data/lib/grape/validations/params_scope.rb +217 -304
  116. data/lib/grape/validations/shared_options.rb +19 -0
  117. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  118. data/lib/grape/validations/types/array_coercer.rb +7 -10
  119. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  120. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  121. data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
  122. data/lib/grape/validations/types/json.rb +1 -3
  123. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  124. data/lib/grape/validations/types/primitive_coercer.rb +14 -33
  125. data/lib/grape/validations/types/set_coercer.rb +1 -1
  126. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  127. data/lib/grape/validations/types.rb +29 -51
  128. data/lib/grape/validations/validations_spec.rb +164 -0
  129. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  130. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  131. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  132. data/lib/grape/validations/validators/base.rb +118 -34
  133. data/lib/grape/validations/validators/coerce_validator.rb +25 -37
  134. data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
  135. data/lib/grape/validations/validators/default_validator.rb +13 -20
  136. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  137. data/lib/grape/validations/validators/except_values_validator.rb +14 -4
  138. data/lib/grape/validations/validators/length_validator.rb +23 -22
  139. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  140. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  141. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  142. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  143. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  144. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  145. data/lib/grape/validations/validators/values_validator.rb +34 -26
  146. data/lib/grape/version.rb +1 -2
  147. data/lib/grape/xml.rb +8 -1
  148. data/lib/grape.rb +58 -31
  149. metadata +62 -39
  150. data/lib/grape/api/helpers.rb +0 -9
  151. data/lib/grape/dsl/api.rb +0 -17
  152. data/lib/grape/dsl/configuration.rb +0 -15
  153. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  154. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  155. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  156. data/lib/grape/exceptions/missing_option.rb +0 -11
  157. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  158. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  159. data/lib/grape/exceptions/unknown_options.rb +0 -11
  160. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  161. data/lib/grape/extensions/hash.rb +0 -27
  162. data/lib/grape/extensions/hashie/mash.rb +0 -24
  163. data/lib/grape/middleware/globals.rb +0 -14
  164. data/lib/grape/types/invalid_value.rb +0 -8
  165. data/lib/grape/util/base_inheritable.rb +0 -43
  166. data/lib/grape/util/inheritable_values.rb +0 -29
  167. data/lib/grape/util/reverse_stackable_values.rb +0 -15
  168. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  169. data/lib/grape/validations/attributes_doc.rb +0 -60
  170. data/lib/grape/validations/validator_factory.rb +0 -15
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module DSL
5
+ # Immutable value object holding the response-shaping booleans accepted
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
10
+ # +backtrace+/+original_exception+ to the formatter as
11
+ # +include_backtrace+/+include_original_exception+).
12
+ #
13
+ # Defaults are duplicated on +#initialize+ here and on +#rescue_from+'s
14
+ # signature on purpose: keeping them on both sides means each entry point
15
+ # is self-documenting without needing to import a shared constant — the
16
+ # DSL signature shows what a user sees in the IDE, and the Data object
17
+ # has working defaults when constructed directly (spec fixtures, a
18
+ # middleware built by hand). The two must stay in lockstep.
19
+ RescueOptions = Data.define(:backtrace, :original_exception) do
20
+ def initialize(backtrace: false, original_exception: false)
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -3,245 +3,370 @@
3
3
  module Grape
4
4
  module DSL
5
5
  module Routing
6
- extend ActiveSupport::Concern
7
- include Grape::DSL::Configuration
8
-
9
- module ClassMethods
10
- attr_reader :endpoints
11
-
12
- # Specify an API version.
13
- #
14
- # @example API with legacy support.
15
- # class MyAPI < Grape::API
16
- # version 'v2'
17
- #
18
- # get '/main' do
19
- # {some: 'data'}
20
- # end
21
- #
22
- # version 'v1' do
23
- # get '/main' do
24
- # {legacy: 'data'}
25
- # end
26
- # end
27
- # end
28
- #
29
- def version(*args, &block)
30
- if args.any?
31
- options = args.extract_options!
32
- options = options.reverse_merge(using: :path)
33
- requested_versions = args.flatten.map(&:to_s)
34
-
35
- raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
36
-
37
- @versions = versions | requested_versions
38
-
39
- if block
40
- within_namespace do
41
- namespace_inheritable(:version, requested_versions)
42
- namespace_inheritable(:version_options, options)
43
-
44
- instance_eval(&block)
45
- end
46
- else
47
- namespace_inheritable(:version, requested_versions)
48
- namespace_inheritable(:version_options, options)
49
- end
50
- end
6
+ attr_reader :endpoints
51
7
 
52
- @versions.last if instance_variable_defined?(:@versions) && @versions
53
- end
8
+ def given(conditional_option, &)
9
+ return unless conditional_option
54
10
 
55
- # Define a root URL prefix for your entire API.
56
- def prefix(prefix = nil)
57
- namespace_inheritable(:root_prefix, prefix&.to_s)
58
- end
11
+ mounted(&)
12
+ end
13
+
14
+ def mounted(&block)
15
+ evaluate_as_instance_with_configuration(block, lazy: true)
16
+ end
17
+
18
+ def cascade(value = nil)
19
+ return inheritable_setting.cascade_defined? ? inheritable_setting.cascade : true if value.nil?
20
+
21
+ inheritable_setting.cascade = value
22
+ end
59
23
 
60
- # Create a scope without affecting the URL.
61
- #
62
- # @param _name [Symbol] Purely placebo, just allows to name the scope to
63
- # make the code more readable.
64
- def scope(_name = nil, &block)
24
+ # Specify an API version.
25
+ #
26
+ # Called without arguments, returns the most recently declared version
27
+ # (or +nil+). Called with one or more version strings, registers them
28
+ # and stores a {Grape::DSL::VersionOptions} value object on the
29
+ # inheritable settings; when given a block, the registration applies
30
+ # within a nested namespace.
31
+ #
32
+ # @example API with legacy support.
33
+ # class MyAPI < Grape::API
34
+ # version 'v2'
35
+ #
36
+ # get '/main' do
37
+ # {some: 'data'}
38
+ # end
39
+ #
40
+ # version 'v1' do
41
+ # get '/main' do
42
+ # {legacy: 'data'}
43
+ # end
44
+ # end
45
+ # end
46
+ #
47
+ # @param args [Array<String, Symbol>] one or more version identifiers.
48
+ # @param using [Symbol] versioning strategy — one of +:path+ (default),
49
+ # +:header+, +:param+, or +:accept_version_header+.
50
+ # @param cascade [Boolean] forward to subsequent routes via the
51
+ # +X-Cascade+ header on version mismatch. Defaults to +true+.
52
+ # @param parameter [String] name of the query/body parameter that
53
+ # carries the version when +using: :param+. Defaults to +'apiver'+.
54
+ # @param strict [Boolean] reject requests that don't supply a usable
55
+ # version (header strategies). Defaults to +false+.
56
+ # @param vendor [String, nil] vendor segment for the +:header+
57
+ # strategy (+application/vnd.<vendor>-<version>+); required when
58
+ # +using: :header+.
59
+ # @yield optional block to scope routes under this version.
60
+ # @return [String, nil] the most recently declared version.
61
+ # @raise [Grape::Exceptions::MissingVendorOption] when +using: :header+
62
+ # is supplied without a +:vendor+.
63
+ def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil, &block)
64
+ return @versions&.last if args.empty?
65
+
66
+ raise Grape::Exceptions::MissingVendorOption.new if using == :header && vendor.nil?
67
+
68
+ requested_versions = args.flatten.map(&:to_s)
69
+ options = VersionOptions.new(using:, cascade:, parameter:, strict:, vendor:)
70
+
71
+ @versions = versions | requested_versions
72
+
73
+ if block
65
74
  within_namespace do
66
- nest(block)
75
+ inheritable_setting.version = requested_versions
76
+ inheritable_setting.version_options = options
77
+ instance_eval(&block)
67
78
  end
79
+ else
80
+ inheritable_setting.version = requested_versions
81
+ inheritable_setting.version_options = options
68
82
  end
69
83
 
70
- def build_with(build_with)
71
- namespace_inheritable(:build_params_with, build_with)
72
- end
84
+ @versions&.last
85
+ end
73
86
 
74
- # Do not route HEAD requests to GET requests automatically.
75
- def do_not_route_head!
76
- namespace_inheritable(:do_not_route_head, true)
77
- end
87
+ # Define a root URL prefix for your entire API.
88
+ def prefix(prefix = nil)
89
+ return inheritable_setting.root_prefix if prefix.nil?
78
90
 
79
- # Do not automatically route OPTIONS.
80
- def do_not_route_options!
81
- namespace_inheritable(:do_not_route_options, true)
82
- end
91
+ inheritable_setting.root_prefix = prefix.to_s
92
+ end
83
93
 
84
- def lint!
85
- namespace_inheritable(:lint, true)
94
+ # Create a scope without affecting the URL.
95
+ #
96
+ # @param _name [Symbol] Purely placebo, just allows to name the scope to
97
+ # make the code more readable.
98
+ def scope(_name = nil, &block)
99
+ within_namespace do
100
+ nest(block)
86
101
  end
102
+ end
87
103
 
88
- def do_not_document!
89
- namespace_inheritable(:do_not_document, true)
90
- end
104
+ def build_with(build_with)
105
+ inheritable_setting.build_params_with = build_with
106
+ end
91
107
 
92
- def mount(mounts, *opts)
93
- mounts = { mounts => '/' } unless mounts.respond_to?(:each_pair)
94
- mounts.each_pair do |app, path|
95
- if app.respond_to?(:mount_instance)
96
- opts_with = opts.any? ? opts.first[:with] : {}
97
- mount({ app.mount_instance(configuration: opts_with) => path }, *opts)
98
- next
99
- end
100
- in_setting = inheritable_setting
101
-
102
- if app.respond_to?(:inheritable_setting, true)
103
- mount_path = Grape::Router.normalize_path(path)
104
- app.top_level_setting.namespace_stackable[:mount_path] = mount_path
105
-
106
- app.inherit_settings(inheritable_setting)
107
-
108
- in_setting = app.top_level_setting
109
-
110
- app.change!
111
- change!
112
- end
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
-
123
- endpoints << Grape::Endpoint.new(
124
- in_setting,
125
- method: :any,
126
- path: path,
127
- app: app,
128
- route_options: { anchor: false },
129
- forward_match: !app.respond_to?(:inheritable_setting),
130
- for: self
131
- )
132
- end
133
- end
108
+ # Do not route HEAD requests to GET requests automatically.
109
+ def do_not_route_head!
110
+ inheritable_setting.do_not_route_head!
111
+ end
112
+
113
+ # Do not automatically route OPTIONS.
114
+ def do_not_route_options!
115
+ inheritable_setting.do_not_route_options!
116
+ end
117
+
118
+ def lint!
119
+ inheritable_setting.lint!
120
+ end
121
+
122
+ def do_not_document!
123
+ inheritable_setting.do_not_document!
124
+ end
134
125
 
135
- # Defines a route that will be recognized
136
- # by the Grape API.
137
- #
138
- # @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.
139
- # @param paths [String] One or more strings representing the URL segment(s) for this route.
140
- #
141
- # @example Defining a basic route.
142
- # class MyAPI < Grape::API
143
- # route(:any, '/hello') do
144
- # {hello: 'world'}
145
- # end
146
- # end
147
- def route(methods, paths = ['/'], route_options = {}, &block)
148
- methods = '*' if methods == :any
149
- endpoint_options = {
150
- method: methods,
151
- path: paths,
152
- for: self,
153
- route_options: {
154
- params: namespace_stackable_with_hash(:params) || {}
155
- }.deep_merge(route_setting(:description) || {}).deep_merge(route_options || {})
156
- }
157
-
158
- new_endpoint = Grape::Endpoint.new(inheritable_setting, endpoint_options, &block)
159
- endpoints << new_endpoint unless endpoints.any? { |e| e.equals?(new_endpoint) }
160
-
161
- route_end
162
- reset_validations!
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)
163
134
  end
164
135
 
165
- Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
166
- define_method supported_method.downcase do |*args, &block|
167
- options = args.extract_options!
168
- paths = args.first || ['/']
169
- route(supported_method, paths, options, &block)
136
+ normalize_mounts(mounts).each_pair do |app, path|
137
+ if app.respond_to?(:mount_instance)
138
+ mount({ app.mount_instance(configuration: mount_opts[:with] || {}) => path }, mount_opts)
139
+ next
170
140
  end
171
- end
141
+ in_setting = inheritable_setting
172
142
 
173
- # Declare a "namespace", which prefixes all subordinate routes with its
174
- # name. Any endpoints within a namespace, group, resource or segment,
175
- # etc., will share their parent context as well as any configuration
176
- # done in the namespace context.
177
- #
178
- # @example
179
- #
180
- # namespace :foo do
181
- # get 'bar' do
182
- # # defines the endpoint: GET /foo/bar
183
- # end
184
- # end
185
- def namespace(space = nil, options = {}, &block)
186
- return Namespace.joined_space_path(namespace_stackable(:namespace)) unless space || block
143
+ # Past the mount_instance branch above, a Grape app here is an already
144
+ # instantiated Grape::API::Instance (vs. a bare Rack app).
145
+ if app.is_a?(Grape::Mountable)
146
+ mount_path = Grape::Util::PathNormalizer.call(path)
147
+ app.top_level_setting.add_mount_path(mount_path)
187
148
 
188
- within_namespace do
189
- nest(block) do
190
- namespace_stackable(:namespace, Namespace.new(space, options)) if space
191
- end
149
+ app.inherit_settings(inheritable_setting)
150
+
151
+ in_setting = app.top_level_setting
152
+
153
+ app.change!
154
+ change!
192
155
  end
156
+
157
+ endpoints << Grape::Endpoint.new(
158
+ in_setting,
159
+ http_methods: :any,
160
+ path:,
161
+ app:,
162
+ anchor: false,
163
+ api: self
164
+ )
193
165
  end
166
+ end
194
167
 
195
- alias group namespace
196
- alias resource namespace
197
- alias resources namespace
198
- alias segment namespace
168
+ # Defines a route that will be recognized
169
+ # by the Grape API.
170
+ #
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.
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
+ #
177
+ # @example Defining a basic route.
178
+ # class MyAPI < Grape::API
179
+ # route(:any, '/hello') do
180
+ # {hello: 'world'}
181
+ # end
182
+ # end
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)
194
+ all_route_options.deep_merge!(route_options) if route_options.present?
195
+
196
+ new_endpoint = Grape::Endpoint.new(
197
+ inheritable_setting,
198
+ http_methods:,
199
+ path: paths,
200
+ api: self,
201
+ params:,
202
+ requirements:,
203
+ anchor:,
204
+ route_options: all_route_options,
205
+ &
206
+ )
207
+ endpoints << new_endpoint unless endpoints.include?(new_endpoint)
208
+
209
+ inheritable_setting.route_end
210
+ reset_validations!
211
+ end
199
212
 
200
- # An array of API routes.
201
- def routes
202
- @routes ||= prepare_routes
213
+ Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
214
+ define_method supported_method.downcase do |path = '/', **options, &block|
215
+ route(supported_method, path, **options, &block)
203
216
  end
217
+ end
204
218
 
205
- # Remove all defined routes.
206
- def reset_routes!
207
- endpoints.each(&:reset_routes!)
208
- @routes = nil
219
+ # Declare a "namespace", which prefixes all subordinate routes with its
220
+ # name. Any endpoints within a namespace, group, resource or segment,
221
+ # etc., will share their parent context as well as any configuration
222
+ # done in the namespace context.
223
+ #
224
+ # @example
225
+ #
226
+ # namespace :foo do
227
+ # get 'bar' do
228
+ # # defines the endpoint: GET /foo/bar
229
+ # end
230
+ # end
231
+ def namespace(space = nil, requirements: nil, **options, &block)
232
+ return inheritable_setting.namespace_path unless space || block
233
+
234
+ validate_requirements!(requirements)
235
+
236
+ within_namespace do
237
+ nest(block) do
238
+ inheritable_setting.add_namespace(Grape::Namespace.new(space, requirements:, **options)) if space
239
+ end
209
240
  end
241
+ end
210
242
 
211
- def reset_endpoints!
212
- @endpoints = []
213
- end
243
+ alias group namespace
244
+ alias resource namespace
245
+ alias resources namespace
246
+ alias segment namespace
214
247
 
215
- # This method allows you to quickly define a parameter route segment
216
- # in your API.
217
- #
218
- # @param param [Symbol] The name of the parameter you wish to declare.
219
- # @option options [Regexp] You may supply a regular expression that the declared parameter must meet.
220
- def route_param(param, options = {}, &block)
221
- options = options.dup
248
+ # An array of API routes.
249
+ def routes
250
+ @routes ||= endpoints.map(&:routes).flatten
251
+ end
222
252
 
223
- options[:requirements] = {
224
- param.to_sym => options[:requirements]
225
- } if options[:requirements].is_a?(Regexp)
253
+ # This method allows you to quickly define a parameter route segment
254
+ # in your API.
255
+ #
256
+ # @param param [Symbol] The name of the parameter you wish to declare.
257
+ # @option options [Regexp, Class, Symbol] The constraint the declared parameter must meet — a Regexp, or a capture type such as +Integer+.
258
+ def route_param(param, requirements: nil, type: nil, **, &)
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)
226
263
 
227
- Grape::Validations::ParamsScope.new(api: self) do
228
- requires param, type: options[:type]
229
- end if options.key?(:type)
264
+ param_requirements = requirements ? { param.to_sym => requirements } : requirements
230
265
 
231
- namespace(":#{param}", options, &block)
232
- end
266
+ Grape::Validations::ParamsScope.new(api: self) do
267
+ requires param, type: type
268
+ end if type
269
+
270
+ namespace(":#{param}", requirements: param_requirements, **, &)
271
+ end
272
+
273
+ # @return array of defined versions
274
+ def versions
275
+ @versions ||= []
276
+ end
277
+
278
+ private
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
+
298
+ # Remove all defined routes.
299
+ def reset_routes!
300
+ endpoints.each(&:reset_routes!)
301
+ @routes = nil
302
+ end
303
+
304
+ def reset_endpoints!
305
+ @endpoints = []
306
+ end
307
+
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
233
316
 
234
- # @return array of defined versions
235
- def versions
236
- @versions ||= []
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) }
237
323
  end
324
+ end
238
325
 
239
- private
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)
345
+ end
240
346
 
241
- def refresh_mounted_api(mounts, *opts)
242
- opts << { refresh_already_mounted: true }
243
- mount(mounts, *opts)
347
+ # Execute first the provided block, then each of the
348
+ # block passed in. Allows for simple 'before' setups
349
+ # of settings stack pushes.
350
+ def nest(*blocks, &block)
351
+ blocks.compact!
352
+ return instance_eval(&block) if blocks.empty?
353
+
354
+ evaluate_as_instance_with_configuration(block) if block
355
+ blocks.each { |b| evaluate_as_instance_with_configuration(b) }
356
+ reset_validations!
357
+ end
358
+
359
+ def evaluate_as_instance_with_configuration(block, lazy: false)
360
+ lazy_block = Grape::Util::Lazy::Block.new do |configuration|
361
+ value_for_configuration = configuration
362
+ self.configuration = value_for_configuration.evaluate if value_for_configuration.is_a?(Grape::Util::Lazy::Base)
363
+ response = instance_eval(&block)
364
+ self.configuration = value_for_configuration
365
+ response
244
366
  end
367
+ return lazy_block if @base && base_instance? && lazy
368
+
369
+ lazy_block.evaluate_from(configuration)
245
370
  end
246
371
  end
247
372
  end