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
@@ -8,138 +8,92 @@ module Grape
8
8
  class Endpoint
9
9
  extend Forwardable
10
10
  include Grape::DSL::Settings
11
+ include Grape::DSL::Headers
11
12
  include Grape::DSL::InsideRoute
12
13
 
13
- attr_accessor :block, :source, :options
14
- attr_reader :env, :request
14
+ attr_reader :env, :request, :source, :options, :endpoints
15
+ attr_accessor :options_route_enabled
15
16
 
16
17
  def_delegators :request, :params, :headers, :cookies
17
18
  def_delegator :cookies, :response_cookies
18
19
 
19
- class << self
20
- def new(...)
21
- self == Endpoint ? Class.new(Endpoint).new(...) : super
22
- end
20
+ # The API (a +Grape::API+ instance) this endpoint belongs to.
21
+ def_delegator :@config, :api
23
22
 
24
- def before_each(new_setup = false, &block)
25
- @before_each ||= []
26
- if new_setup == false
27
- return @before_each unless block
23
+ # The logger configured on the API this endpoint belongs to. Available
24
+ # inside route handlers, +before+/+after+/+after_validation+/+finally+
25
+ # filters, and +rescue_from+ blocks.
26
+ def_delegator :api, :logger
28
27
 
29
- @before_each << block
30
- else
31
- @before_each = [new_setup]
32
- end
33
- end
28
+ # The Rack app or Grape API mounted at this endpoint, or +nil+ for a plain
29
+ # block endpoint. Prefer this over +options[:app]+, which is retained only
30
+ # for backwards compatibility.
31
+ def mounted_app
32
+ config.app
33
+ end
34
34
 
35
- def run_before_each(endpoint)
36
- superclass.run_before_each(endpoint) unless self == Endpoint
37
- before_each.each { |blk| blk.try(:call, endpoint) }
38
- end
35
+ class << self
36
+ def block_to_unbound_method(block)
37
+ return unless block
39
38
 
40
- # @api private
41
- #
42
- # Create an UnboundMethod that is appropriate for executing an endpoint
43
- # route.
44
- #
45
- # The unbound method allows explicit calls to +return+ without raising a
46
- # +LocalJumpError+. The method will be removed, but a +Proc+ reference to
47
- # it will be returned. The returned +Proc+ expects a single argument: the
48
- # instance of +Endpoint+ to bind to the method during the call.
49
- #
50
- # @param [String, Symbol] method_name
51
- # @return [Proc]
52
- # @raise [NameError] an instance method with the same name already exists
53
- def generate_api_method(method_name, &block)
54
- raise NameError.new("method #{method_name.inspect} already exists and cannot be used as an unbound method name") if method_defined?(method_name)
55
-
56
- define_method(method_name, &block)
57
- method = instance_method(method_name)
58
- remove_method(method_name)
59
-
60
- proc do |endpoint_instance|
61
- ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: endpoint_instance) do
62
- method.bind_call(endpoint_instance)
63
- end
64
- end
39
+ define_method :temp_unbound_method, block
40
+ method = instance_method(:temp_unbound_method)
41
+ remove_method :temp_unbound_method
42
+ method
65
43
  end
66
44
  end
67
45
 
68
46
  # Create a new endpoint.
69
47
  # @param new_settings [InheritableSetting] settings to determine the params,
70
48
  # validations, and other properties from.
71
- # @param options [Hash] attributes of this endpoint
72
- # @option options path [String or Array] the path to this endpoint, within
73
- # the current scope.
74
- # @option options method [String or Array] which HTTP method(s) can be used
75
- # to reach this endpoint.
49
+ # @param http_methods [String or Array] which HTTP method(s) can be used to
50
+ # reach this endpoint.
51
+ # @param path [String or Array] the path to this endpoint, within the
52
+ # current scope.
53
+ # @param api [Grape::API] the API this endpoint belongs to. Exposed as
54
+ # {#api}.
55
+ # @param app [#call, nil] the Rack app or Grape API mounted at this
56
+ # endpoint; +nil+ for a plain block endpoint. Exposed as {#mounted_app}.
57
+ # @param params [Hash] the declared params for this endpoint, keyed by name.
58
+ # Kept out of +route_options+ and read via +config.params+.
59
+ # @param requirements [Hash, nil] regular-expression constraints for named
60
+ # path params. Read via +config.requirements+.
61
+ # @param anchor [Boolean] whether the route anchors to the whole path
62
+ # (default +true+). Read via +config.anchor+.
63
+ # @param options [Hash] attributes of this endpoint, normalized into a
64
+ # +Grape::Endpoint::Options+ value object.
76
65
  # @option options route_options [Hash]
77
66
  # @note This happens at the time of API definition, so in this context the
78
67
  # endpoint does not know if it will be mounted under a different endpoint.
79
68
  # @yield a block defining what your API should do when this endpoint is hit
80
- def initialize(new_settings, options = {}, &block)
81
- require_option(options, :path)
82
- require_option(options, :method)
83
-
84
- self.inheritable_setting = new_settings.point_in_time_copy
85
-
86
- # now +namespace_stackable(:declared_params)+ contains all params defined for
87
- # this endpoint and its parents, but later it will be cleaned up,
88
- # see +reset_validations!+ in lib/grape/dsl/validations.rb
89
- route_setting(:declared_params, namespace_stackable(:declared_params).flatten)
90
- route_setting(:saved_validations, namespace_stackable(:validations))
91
-
92
- namespace_stackable(:representations, []) unless namespace_stackable(:representations)
93
- namespace_inheritable(:default_error_status, 500) unless namespace_inheritable(:default_error_status)
69
+ def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, requirements: nil, anchor: true, **options, &block)
70
+ self.inheritable_setting = new_settings.point_in_time_copy_for_endpoint
94
71
 
95
72
  @options = options
96
-
97
- @options[:path] = Array(options[:path])
98
- @options[:path] << '/' if options[:path].empty?
99
-
100
- @options[:method] = Array(options[:method])
101
- @options[:route_options] ||= {}
102
-
103
- @lazy_initialize_lock = Mutex.new
104
- @lazy_initialized = nil
105
- @block = nil
73
+ @config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options)
74
+ # +:app+ is still surfaced on the public options Hash for backwards
75
+ # compatibility (e.g. grape-swagger); prefer the +mounted_app+ reader.
76
+ @options[:app] = app if app
106
77
 
107
78
  @status = nil
108
79
  @stream = nil
109
80
  @body = nil
110
- @proc = nil
111
-
112
- return unless block
113
-
114
- @source = block
115
- @block = self.class.generate_api_method(method_name, &block)
81
+ @source = self.class.block_to_unbound_method(block)
82
+ @before_filter_passed = false
83
+ @options_route_enabled = false
84
+ @endpoints = @config.app.endpoints if @config.app.respond_to?(:endpoints)
116
85
  end
117
86
 
118
- # Update our settings from a given set of stackable parameters. Used when
87
+ # Update our settings from a given parent settings instance. Used when
119
88
  # the endpoint's API is mounted under another one.
120
- def inherit_settings(namespace_stackable)
121
- parent_validations = namespace_stackable[:validations]
122
- inheritable_setting.route[:saved_validations].concat(parent_validations) if parent_validations.any?
123
- parent_declared_params = namespace_stackable[:declared_params]
124
- inheritable_setting.route[:declared_params].concat(parent_declared_params.flatten) if parent_declared_params.any?
125
-
126
- endpoints&.each { |e| e.inherit_settings(namespace_stackable) }
127
- end
128
-
129
- def require_option(options, key)
130
- raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
131
- end
132
-
133
- def method_name
134
- [options[:method],
135
- Namespace.joined_space(namespace_stackable(:namespace)),
136
- (namespace_stackable(:mount_path) || []).join('/'),
137
- options[:path].join('/')]
138
- .join(' ')
89
+ # @param settings [Grape::Util::InheritableSetting]
90
+ def inherit_settings(settings)
91
+ inheritable_setting.inherit_route_params(settings)
92
+ endpoints&.each { |e| e.inherit_settings(settings) }
139
93
  end
140
94
 
141
95
  def routes
142
- @routes ||= endpoints&.collect(&:routes)&.flatten || to_routes
96
+ @routes ||= endpoints&.flat_map(&:routes) || to_routes
143
97
  end
144
98
 
145
99
  def reset_routes!
@@ -149,74 +103,27 @@ module Grape
149
103
  end
150
104
 
151
105
  def mount_in(router)
152
- return endpoints.each { |e| e.mount_in(router) } if endpoints
106
+ if endpoints
107
+ compile!
108
+ return endpoints.each { |e| e.mount_in(router) }
109
+ end
153
110
 
154
111
  reset_routes!
112
+ compile!
155
113
  routes.each do |route|
156
114
  router.append(route.apply(self))
157
- next unless !namespace_inheritable(:do_not_route_head) && route.request_method == Rack::GET
115
+ next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET
158
116
 
159
- route.dup.then do |head_route|
160
- head_route.convert_to_head_request!
161
- router.append(head_route.apply(self))
162
- end
117
+ router.append(route.to_head.apply(self))
163
118
  end
164
119
  end
165
120
 
166
- def to_routes
167
- default_route_options = prepare_default_route_attributes
168
-
169
- map_routes do |method, raw_path|
170
- prepared_path = Path.new(raw_path, namespace, prepare_default_path_settings)
171
- params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options
172
- route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params)
173
- route.apply(self)
174
- end.flatten
175
- end
176
-
177
- def prepare_routes_requirements
178
- {}.merge!(*namespace_stackable(:namespace).map(&:requirements)).tap do |requirements|
179
- endpoint_requirements = options.dig(:route_options, :requirements)
180
- requirements.merge!(endpoint_requirements) if endpoint_requirements
181
- end
182
- end
183
-
184
- def prepare_default_route_attributes
185
- {
186
- namespace: namespace,
187
- version: prepare_version,
188
- requirements: prepare_routes_requirements,
189
- prefix: namespace_inheritable(:root_prefix),
190
- anchor: options[:route_options].fetch(:anchor, true),
191
- settings: inheritable_setting.route.except(:declared_params, :saved_validations),
192
- forward_match: options[:forward_match]
193
- }
194
- end
195
-
196
- def prepare_version
197
- version = namespace_inheritable(:version)
198
- return if version.blank?
199
-
200
- version.length == 1 ? version.first : version
201
- end
202
-
203
- def map_routes
204
- options[:method].map { |method| options[:path].map { |path| yield method, path } }
205
- end
206
-
207
- def prepare_default_path_settings
208
- namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
209
- namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
210
- namespace_stackable_hash.merge!(namespace_inheritable_hash)
211
- end
212
-
213
121
  def namespace
214
- @namespace ||= Namespace.joined_space_path(namespace_stackable(:namespace))
122
+ @namespace ||= inheritable_setting.namespace_path
215
123
  end
216
124
 
217
125
  def call(env)
218
- lazy_initialize!
219
- dup.call!(env)
126
+ @prototype.dup.call!(env)
220
127
  end
221
128
 
222
129
  def call!(env)
@@ -225,14 +132,19 @@ module Grape
225
132
  @app.call(env)
226
133
  end
227
134
 
228
- # Return the collection of endpoints within this endpoint.
229
- # This is the case when an Grape::API mounts another Grape::API.
230
- def endpoints
231
- @endpoints ||= options[:app].try(:endpoints)
135
+ def ==(other)
136
+ other.is_a?(self.class) &&
137
+ config == other.config &&
138
+ inheritable_setting == other.inheritable_setting
232
139
  end
140
+ alias eql? ==
233
141
 
234
- def equals?(endpoint)
235
- (options == endpoint.options) && (inheritable_setting.to_hash == endpoint.inheritable_setting.to_hash)
142
+ # Mirrors #==. The class stays out: #== admits a subclass instance
143
+ # through is_a?, and such a pair must hash alike. The block (#source) is
144
+ # not part of either, matching the long-standing duplicate-route check in
145
+ # DSL::Routing#route.
146
+ def hash
147
+ [config, inheritable_setting].hash
236
148
  end
237
149
 
238
150
  # The purpose of this override is solely for stripping internals when an error occurs while calling
@@ -247,22 +159,21 @@ module Grape
247
159
  protected
248
160
 
249
161
  def run
250
- ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env: env) do
251
- @request = Grape::Request.new(env, build_params_with: namespace_inheritable(:build_params_with))
162
+ instrument_run do
163
+ @request = Grape::Request.new(env, build_params_with: @build_params_with)
252
164
  begin
253
- self.class.run_before_each(self)
254
165
  run_filters befores, :before
166
+ @before_filter_passed = true
255
167
 
256
168
  if env.key?(Grape::Env::GRAPE_ALLOWED_METHODS)
257
169
  header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
258
170
  raise Grape::Exceptions::MethodNotAllowed.new(header) unless options?
259
171
 
260
- header 'Allow', header['Allow']
261
172
  response_object = ''
262
173
  status 204
263
174
  else
264
175
  run_filters before_validations, :before_validation
265
- run_validators validations, request
176
+ run_validators(request:)
266
177
  run_filters after_validations, :after_validation
267
178
  response_object = execute
268
179
  end
@@ -284,134 +195,263 @@ module Grape
284
195
  end
285
196
 
286
197
  def execute
287
- @block&.call(self)
288
- end
289
-
290
- def helpers
291
- lazy_initialize! && @helpers
292
- end
293
-
294
- def lazy_initialize!
295
- return true if @lazy_initialized
296
-
297
- @lazy_initialize_lock.synchronize do
298
- return true if @lazy_initialized
299
-
300
- @helpers = build_helpers&.tap { |mod| self.class.include mod }
301
- @app = options[:app] || build_stack(@helpers)
198
+ return unless source
302
199
 
303
- @lazy_initialized = true
200
+ instrument_render do
201
+ source.bind_call(self)
304
202
  end
305
203
  end
306
204
 
307
- def run_validators(validators, request)
308
- validation_errors = []
205
+ def run_validators(request:)
206
+ validators = inheritable_setting.route_validations
207
+ return if validators.blank?
309
208
 
310
- ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators: validators, request: request) do
311
- validators.each do |validator|
312
- validator.validate(request)
313
- rescue Grape::Exceptions::Validation => e
314
- validation_errors << e
315
- break if validator.fail_fast?
316
- rescue Grape::Exceptions::ValidationArrayErrors => e
317
- validation_errors.concat e.errors
318
- break if validator.fail_fast?
209
+ validation_exceptions = nil
210
+
211
+ Grape::Validations::ParamScopeTracker.track do
212
+ instrument_run_validators(validators, request) do
213
+ validators.each do |validator|
214
+ validator.validate(request)
215
+ rescue Grape::Exceptions::Validation, Grape::Exceptions::ValidationArrayErrors => e
216
+ (validation_exceptions ||= []) << e
217
+ break if validator.fail_fast?
218
+ end
319
219
  end
320
220
  end
321
221
 
322
- validation_errors.any? && raise(Grape::Exceptions::ValidationErrors.new(errors: validation_errors, headers: header))
222
+ raise Grape::Exceptions::ValidationErrors.new(exceptions: validation_exceptions, headers: header) if validation_exceptions
323
223
  end
324
224
 
325
225
  def run_filters(filters, type = :other)
326
- ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters: filters, type: type) do
327
- filters&.each { |filter| instance_eval(&filter) }
226
+ return if filters.blank?
227
+
228
+ instrument_run_filters(filters, type) do
229
+ filters.each { |filter| instance_eval(&filter) }
328
230
  end
329
- post_extension = DSL::InsideRoute.post_filter_methods(type)
330
- extend post_extension if post_extension
331
231
  end
332
232
 
333
- %i[befores before_validations after_validations afters finallies].each do |method|
334
- define_method method do
335
- namespace_stackable(method)
336
- end
233
+ attr_reader :befores, :before_validations, :after_validations, :afters, :finallies, :config
234
+
235
+ def options?
236
+ options_route_enabled && env[Rack::REQUEST_METHOD] == Rack::OPTIONS
337
237
  end
338
238
 
339
- def validations
340
- return enum_for(:validations) unless block_given?
239
+ private
240
+
241
+ attr_reader :before_filter_passed
242
+
243
+ # Instrument helpers. Each guards on +listening?+ so that with no subscriber
244
+ # the payload Hash and notification machinery are skipped and the block runs
245
+ # directly (no added allocations); the block is forwarded anonymously so
246
+ # nothing is allocated unless a subscriber is present.
247
+ def instrument_run(&)
248
+ return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run.grape')
249
+
250
+ ActiveSupport::Notifications.instrument('endpoint_run.grape', endpoint: self, env:, &)
251
+ end
252
+
253
+ def instrument_render(&)
254
+ return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_render.grape')
255
+
256
+ ActiveSupport::Notifications.instrument('endpoint_render.grape', endpoint: self, &)
257
+ end
258
+
259
+ def instrument_run_validators(validators, request, &)
260
+ return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run_validators.grape')
261
+
262
+ ActiveSupport::Notifications.instrument('endpoint_run_validators.grape', endpoint: self, validators:, request:, &)
263
+ end
264
+
265
+ def instrument_run_filters(filters, type, &)
266
+ return yield unless ActiveSupport::Notifications.notifier.listening?('endpoint_run_filters.grape')
267
+
268
+ ActiveSupport::Notifications.instrument('endpoint_run_filters.grape', endpoint: self, filters:, type:, &)
269
+ end
270
+
271
+ def compile!
272
+ @app = config.app || build_stack
273
+ warn_unauthenticated_mounted_app
274
+ @helpers = build_helpers
275
+ callbacks = inheritable_setting.callbacks
276
+ @befores = callbacks.fetch(:before)
277
+ @before_validations = callbacks.fetch(:before_validation)
278
+ @after_validations = callbacks.fetch(:after_validation)
279
+ @afters = callbacks.fetch(:after)
280
+ @finallies = callbacks.fetch(:finally)
281
+ @build_params_with = inheritable_setting.build_params_with
282
+ @prototype = build_prototype
283
+ end
284
+
285
+ # The object each request is copied from.
286
+ #
287
+ # Helpers reach an endpoint by module inclusion, and +Object#dup+ does not
288
+ # carry a singleton class over — so including them on the per-request copy
289
+ # meant giving every request a brand-new singleton class, whose method
290
+ # cache then started cold for the helpers, the route block and every DSL
291
+ # method the endpoint answers. Include them once into a subclass owned by
292
+ # this endpoint instead, and copy this endpoint's state onto an instance of
293
+ # it: the per-request copy already answers to the helpers, against a class
294
+ # that has been warm since boot. It also keeps request handling free of
295
+ # class mutation.
296
+ #
297
+ # Endpoints with no helpers are their own prototype, exactly as before.
298
+ #
299
+ # +allocate+ rather than +new+ because the constructor rebuilds state from
300
+ # the DSL's arguments — a fresh settings copy, a fresh +@config+, another
301
+ # +block_to_unbound_method+ — and would still not hold what +compile!+ has
302
+ # just computed. What is wanted is what +dup+ itself does (allocate, then
303
+ # copy the ivars over) across a class boundary, which neither +dup+ nor
304
+ # +initialize_copy+ will do: both insist on the receiver's own class. So
305
+ # the copy is written out here. Taking all of them is safe because
306
+ # everything set past this point (+routes+, +namespace+) is route-building
307
+ # memoization the request path never reads.
308
+ def build_prototype
309
+ return self unless @helpers
310
+
311
+ klass = Class.new(self.class)
312
+ # Anonymous classes answer nil to +name+ and render as a bare address,
313
+ # which would reach anything that reports on the endpoint — +inspect+
314
+ # here, a logger or an error tracker elsewhere. The annotation is what
315
+ # +set_temporary_name+ is for; a plain constant path is rejected, which
316
+ # is right, since this class is not reachable under that name.
317
+ klass.set_temporary_name("#{self.class}(helpers)")
318
+ klass.include(@helpers)
319
+ prototype = klass.allocate
320
+ # Everything but the prototype slot itself, so that recompiling (a
321
+ # remount) does not leave the new prototype holding the one it replaces.
322
+ (instance_variables - [:@prototype]).each { |name| prototype.instance_variable_set(name, instance_variable_get(name)) }
323
+ prototype
324
+ end
341
325
 
342
- route_setting(:saved_validations)&.each do |saved_validation|
343
- yield Grape::Validations::ValidatorFactory.create_validator(saved_validation)
326
+ def to_routes
327
+ route_options = config.route_options
328
+ params = config.params
329
+ path_settings = inheritable_setting.path_settings
330
+ forward_match = bare_rack_app?
331
+ version = prepare_version(inheritable_setting.version)
332
+ prefix = inheritable_setting.root_prefix
333
+ requirements = prepare_routes_requirements(config.requirements)
334
+ anchor = config.anchor
335
+ settings = inheritable_setting.route_settings
336
+
337
+ config.http_methods.flat_map do |method|
338
+ config.path.map do |path|
339
+ pattern = Grape::Router::Pattern.build(
340
+ path:,
341
+ namespace:,
342
+ settings: path_settings,
343
+ anchor:,
344
+ params:,
345
+ version:,
346
+ requirements:
347
+ )
348
+ Grape::Router::Route.new(self, method, pattern, route_options, forward_match:, params:, namespace:, prefix:, settings:)
349
+ end
344
350
  end
345
351
  end
346
352
 
347
- def options?
348
- options[:options_route_enabled] &&
349
- env[Rack::REQUEST_METHOD] == Rack::OPTIONS
353
+ # True when a bare Rack app (anything that isn't a Grape app) is mounted at
354
+ # this endpoint. Such an app is called directly and matched by path prefix
355
+ # rather than an anchored route.
356
+ def bare_rack_app?
357
+ config.app && !config.app.is_a?(Grape::Mountable)
350
358
  end
351
359
 
352
- private
360
+ def prepare_routes_requirements(route_options_requirements)
361
+ namespace_requirements = inheritable_setting.namespace_requirements
362
+ namespace_requirements << route_options_requirements if route_options_requirements.present?
363
+ namespace_requirements.reduce({}, :merge)
364
+ end
353
365
 
354
- def build_stack(helpers)
366
+ def prepare_version(namespace_inheritable_version)
367
+ return if namespace_inheritable_version.blank?
368
+
369
+ namespace_inheritable_version.length == 1 ? namespace_inheritable_version.first : namespace_inheritable_version
370
+ end
371
+
372
+ def build_stack
355
373
  stack = Grape::Middleware::Stack.new
356
374
 
357
- content_types = namespace_stackable_with_hash(:content_types)
358
- format = namespace_inheritable(:format)
375
+ content_types = inheritable_setting.content_types
376
+ format = inheritable_setting.format
359
377
 
360
378
  stack.use Rack::Head
361
379
  stack.use Rack::Lint if lint?
362
- stack.use Class.new(Grape::Middleware::Error),
363
- helpers: helpers,
364
- format: format,
365
- content_types: content_types,
366
- default_status: namespace_inheritable(:default_error_status),
367
- rescue_all: namespace_inheritable(:rescue_all),
368
- rescue_grape_exceptions: namespace_inheritable(:rescue_grape_exceptions),
369
- default_error_formatter: namespace_inheritable(:default_error_formatter),
370
- error_formatters: namespace_stackable_with_hash(:error_formatters),
371
- rescue_options: namespace_stackable_with_hash(:rescue_options),
372
- rescue_handlers: namespace_reverse_stackable_with_hash(:rescue_handlers),
373
- base_only_rescue_handlers: namespace_stackable_with_hash(:base_only_rescue_handlers),
374
- all_rescue_handler: namespace_inheritable(:all_rescue_handler),
375
- grape_exceptions_rescue_handler: namespace_inheritable(:grape_exceptions_rescue_handler)
376
-
377
- stack.concat namespace_stackable(:middleware)
378
-
379
- if namespace_inheritable(:version).present?
380
- stack.use Grape::Middleware::Versioner.using(namespace_inheritable(:version_options)[:using]),
381
- versions: namespace_inheritable(:version).flatten,
382
- version_options: namespace_inheritable(:version_options),
383
- prefix: namespace_inheritable(:root_prefix),
384
- mount_path: namespace_stackable(:mount_path).first
380
+ stack.use Grape::Middleware::Error, **error_middleware_options(format, content_types)
381
+
382
+ stack.concat inheritable_setting.middleware
383
+
384
+ if inheritable_setting.version.present?
385
+ version_options = inheritable_setting.version_options
386
+ stack.use Grape::Middleware::Versioner.using(version_options.using),
387
+ versions: inheritable_setting.version.flatten,
388
+ version_options:,
389
+ prefix: inheritable_setting.root_prefix,
390
+ mount_path: inheritable_setting.mount_path
385
391
  end
386
392
 
387
393
  stack.use Grape::Middleware::Formatter,
388
- format: format,
389
- default_format: namespace_inheritable(:default_format) || :txt,
390
- content_types: content_types,
391
- formatters: namespace_stackable_with_hash(:formatters),
392
- parsers: namespace_stackable_with_hash(:parsers)
394
+ format:,
395
+ default_format: inheritable_setting.default_format || :txt,
396
+ content_types:,
397
+ formatters: inheritable_setting.formatters,
398
+ parsers: inheritable_setting.parsers
393
399
 
394
400
  builder = stack.build
395
401
  builder.run ->(env) { env[Grape::Env::API_ENDPOINT].run }
396
402
  builder.to_app
397
403
  end
398
404
 
405
+ def error_middleware_options(format, content_types)
406
+ setting = inheritable_setting
407
+ {
408
+ format:,
409
+ content_types:,
410
+ default_status: setting.default_error_status,
411
+ rescue_all: setting.rescue_all?,
412
+ rescue_grape_exceptions: setting.rescue_grape_exceptions?,
413
+ default_error_formatter: setting.default_error_formatter,
414
+ error_formatters: setting.error_formatters,
415
+ rescue_options: setting.rescue_options,
416
+ rescue_handlers: setting.rescue_handlers,
417
+ base_only_rescue_handlers: setting.base_only_rescue_handlers,
418
+ all_rescue_handler: setting.all_rescue_handler,
419
+ grape_exceptions_rescue_handler: setting.grape_exceptions_rescue_handler,
420
+ internal_grape_exceptions_rescue_handler: setting.internal_grape_exceptions_rescue_handler
421
+ }
422
+ end
423
+
399
424
  def build_helpers
400
- helpers = namespace_stackable(:helpers)
425
+ helpers = inheritable_setting.helpers
401
426
  return if helpers.empty?
402
427
 
403
428
  Module.new { helpers.each { |mod_to_include| include mod_to_include } }
404
429
  end
405
430
 
431
+ # A bare Rack app mounted with +mount+ is called directly (see +compile!+):
432
+ # it does not go through +build_stack+, so the API's authentication
433
+ # middleware never runs and the mount is reachable unauthenticated. Mounted
434
+ # Grape APIs are unaffected because they rebuild their own stack from the
435
+ # inherited settings. Warn so this bypass isn't silent.
436
+ def warn_unauthenticated_mounted_app
437
+ return unless bare_rack_app?
438
+ return unless inheritable_setting.auth
439
+
440
+ warn "Grape: #{config.app} is mounted under an API that declares authentication, but authentication " \
441
+ 'middleware does not wrap mounted Rack applications. Requests to this mount are not authenticated by Grape.'
442
+ end
443
+
406
444
  def build_response_cookies
445
+ return unless request.cookies?
446
+
407
447
  response_cookies do |name, value|
408
- cookie_value = value.is_a?(Hash) ? value : { value: value }
448
+ cookie_value = value.is_a?(Hash) ? value : { value: }
409
449
  Rack::Utils.set_cookie_header! header, name, cookie_value
410
450
  end
411
451
  end
412
452
 
413
453
  def lint?
414
- namespace_inheritable(:lint) || Grape.config.lint
454
+ inheritable_setting.lint? || Grape.config.lint
415
455
  end
416
456
  end
417
457
  end