grape 4.0.1 → 4.1.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -1
- data/README.md +195 -95
- data/UPGRADING.md +364 -0
- data/grape.gemspec +1 -1
- data/lib/grape/api/instance.rb +57 -30
- data/lib/grape/api.rb +50 -20
- data/lib/grape/content_types.rb +26 -9
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/declared_params_handler.rb +35 -7
- data/lib/grape/dsl/declared.rb +5 -4
- data/lib/grape/dsl/desc.rb +1 -2
- data/lib/grape/dsl/entity.rb +35 -19
- data/lib/grape/dsl/headers.rb +3 -3
- data/lib/grape/dsl/helpers.rb +5 -8
- data/lib/grape/dsl/inside_route.rb +33 -27
- data/lib/grape/dsl/logger.rb +10 -4
- data/lib/grape/dsl/middleware.rb +4 -9
- data/lib/grape/dsl/mounting.rb +149 -0
- data/lib/grape/dsl/parameters.rb +78 -104
- data/lib/grape/dsl/request_response.rb +35 -22
- data/lib/grape/dsl/routing.rb +17 -191
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/version_options.rb +2 -2
- data/lib/grape/dsl/versioning.rb +98 -0
- data/lib/grape/endpoint/state.rb +15 -0
- data/lib/grape/endpoint.rb +229 -137
- data/lib/grape/error_formatter/base.rb +28 -13
- data/lib/grape/error_formatter/json.rb +3 -0
- data/lib/grape/error_formatter/jsonl.rb +15 -0
- data/lib/grape/error_formatter/xml.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +23 -7
- data/lib/grape/exceptions/halt.rb +39 -0
- data/lib/grape/exceptions/validation.rb +9 -9
- data/lib/grape/exceptions/validation_errors.rb +12 -2
- data/lib/grape/formatter/jsonl.rb +34 -0
- data/lib/grape/formatter/markdown.rb +17 -0
- data/lib/grape/formatter/md.rb +11 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/base.rb +16 -1
- data/lib/grape/middleware/auth/strategy_info.rb +2 -2
- data/lib/grape/middleware/base.rb +19 -13
- data/lib/grape/middleware/error.rb +133 -35
- data/lib/grape/middleware/formatter.rb +235 -94
- data/lib/grape/middleware/precomputed_content_types.rb +14 -2
- data/lib/grape/middleware/stack.rb +7 -8
- data/lib/grape/middleware/versioner/base.rb +26 -10
- data/lib/grape/middleware/versioner/header.rb +70 -15
- data/lib/grape/middleware/versioner/param.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +20 -11
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/mountable.rb +2 -4
- data/lib/grape/namespace.rb +2 -3
- data/lib/grape/parser/jsonl.rb +19 -0
- data/lib/grape/request.rb +46 -8
- data/lib/grape/router/base_route.rb +41 -0
- data/lib/grape/router/greedy_route.rb +1 -1
- data/lib/grape/router/mustermann_pattern.rb +1 -1
- data/lib/grape/router/pattern/path.rb +1 -1
- data/lib/grape/router/pattern.rb +11 -5
- data/lib/grape/router/route.rb +42 -11
- data/lib/grape/router/route_buckets.rb +246 -0
- data/lib/grape/router.rb +262 -32
- data/lib/grape/util/endpoint_configuration.rb +7 -1
- data/lib/grape/util/inheritable_setting.rb +80 -161
- data/lib/grape/util/lazy/value.rb +52 -11
- data/lib/grape/util/media_type.rb +38 -15
- data/lib/grape/util/path_normalizer.rb +7 -2
- data/lib/grape/util/registry.rb +9 -2
- data/lib/grape/util/scrub.rb +33 -0
- data/lib/grape/util/translation.rb +6 -6
- data/lib/grape/validations/attributes_iterator.rb +73 -37
- data/lib/grape/validations/coerce_options.rb +1 -1
- data/lib/grape/validations/params_documentation.rb +15 -7
- data/lib/grape/validations/params_scope.rb +176 -43
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
- data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
- data/lib/grape/validations/types/primitive_coercer.rb +17 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
- data/lib/grape/validations/types.rb +12 -18
- data/lib/grape/validations/validations_spec.rb +53 -22
- data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
- data/lib/grape/validations/validators/as_validator.rb +2 -3
- data/lib/grape/validations/validators/base.rb +124 -25
- data/lib/grape/validations/validators/coerce_validator.rb +2 -3
- data/lib/grape/validations/validators/default_validator.rb +18 -2
- data/lib/grape/validations/validators/except_values_validator.rb +16 -3
- data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
- data/lib/grape/validations/validators/length_validator.rb +24 -16
- data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
- data/lib/grape/validations/validators/values_validator.rb +18 -24
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +3 -0
- metadata +18 -11
- data/lib/grape/util/lazy/value_array.rb +0 -21
- data/lib/grape/util/lazy/value_enumerable.rb +0 -31
- data/lib/grape/util/lazy/value_hash.rb +0 -21
- data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
- data/lib/grape/validations/single_attribute_iterator.rb +0 -28
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module DSL
|
|
5
5
|
# Immutable value object holding the resolved options from
|
|
6
|
-
# +Grape::DSL::
|
|
6
|
+
# +Grape::DSL::Versioning#version+. Stored on the inheritable settings as
|
|
7
7
|
# +Grape::Util::InheritableSetting#version_options+ and read by internal call
|
|
8
|
-
# sites (`Path`, `Endpoint`, `API::Instance
|
|
8
|
+
# sites (`Path`, `Endpoint`, `API::Instance`,
|
|
9
9
|
# `Middleware::Versioner::Base`) via accessors.
|
|
10
10
|
#
|
|
11
11
|
# Defaults are duplicated on +#initialize+ here and on +#version+'s
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module DSL
|
|
5
|
+
module Versioning
|
|
6
|
+
# Called without a value, answers whether the API cascades: the value
|
|
7
|
+
# given here, else the +cascade:+ of +version+, else true. The compiled
|
|
8
|
+
# API reads it here too; this reader used to skip +version+, and so
|
|
9
|
+
# answered true for an API that did not cascade.
|
|
10
|
+
def cascade(value = nil)
|
|
11
|
+
return inheritable_setting.cascade = value unless value.nil?
|
|
12
|
+
|
|
13
|
+
setting = inheritable_setting
|
|
14
|
+
return setting.cascade if setting.cascade_defined?
|
|
15
|
+
return setting.version_options.cascade if setting.version_options
|
|
16
|
+
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Specify an API version.
|
|
21
|
+
#
|
|
22
|
+
# Called without arguments, returns the most recently declared version
|
|
23
|
+
# (or +nil+). Called with one or more version strings, registers them
|
|
24
|
+
# and stores a {Grape::DSL::VersionOptions} value object on the
|
|
25
|
+
# inheritable settings; when given a block, the registration applies
|
|
26
|
+
# within a nested namespace.
|
|
27
|
+
#
|
|
28
|
+
# @example API with legacy support.
|
|
29
|
+
# class MyAPI < Grape::API
|
|
30
|
+
# version 'v2'
|
|
31
|
+
#
|
|
32
|
+
# get '/main' do
|
|
33
|
+
# {some: 'data'}
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# version 'v1' do
|
|
37
|
+
# get '/main' do
|
|
38
|
+
# {legacy: 'data'}
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# @param args [Array<String, Symbol>] one or more version identifiers.
|
|
44
|
+
# @param using [Symbol] versioning strategy — one of +:path+ (default),
|
|
45
|
+
# +:header+, +:param+, or +:accept_version_header+.
|
|
46
|
+
# @param cascade [Boolean] forward to subsequent routes via the
|
|
47
|
+
# +X-Cascade+ header on version mismatch. Defaults to +true+.
|
|
48
|
+
# @param parameter [String] name of the query/body parameter that
|
|
49
|
+
# carries the version when +using: :param+. Defaults to +'apiver'+.
|
|
50
|
+
# @param strict [Boolean] reject requests that don't supply a usable
|
|
51
|
+
# version (header strategies). Defaults to +false+.
|
|
52
|
+
# @param vendor [String, nil] vendor segment for the +:header+
|
|
53
|
+
# strategy (+application/vnd.<vendor>-<version>+); required when
|
|
54
|
+
# +using: :header+.
|
|
55
|
+
# @yield optional block to scope routes under this version.
|
|
56
|
+
# @return [String, nil] the most recently declared version.
|
|
57
|
+
# @raise [Grape::Exceptions::MissingVendorOption] when +using: :header+
|
|
58
|
+
# is supplied without a +:vendor+.
|
|
59
|
+
def version(*args, using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil, &block)
|
|
60
|
+
return @versions&.last if args.empty?
|
|
61
|
+
|
|
62
|
+
options = build_version_options(using:, cascade:, parameter:, strict:, vendor:)
|
|
63
|
+
requested_versions = args.flatten.map(&:to_s)
|
|
64
|
+
|
|
65
|
+
@versions = versions | requested_versions
|
|
66
|
+
|
|
67
|
+
if block
|
|
68
|
+
within_namespace do
|
|
69
|
+
apply_version(requested_versions, options)
|
|
70
|
+
instance_eval(&block)
|
|
71
|
+
end
|
|
72
|
+
else
|
|
73
|
+
apply_version(requested_versions, options)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@versions.last
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return array of defined versions
|
|
80
|
+
def versions
|
|
81
|
+
@versions ||= []
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def build_version_options(using:, vendor:, **)
|
|
87
|
+
raise Grape::Exceptions::MissingVendorOption.new if using == :header && vendor.nil?
|
|
88
|
+
|
|
89
|
+
VersionOptions.new(using:, vendor:, **)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def apply_version(versions, options)
|
|
93
|
+
inheritable_setting.version = versions
|
|
94
|
+
inheritable_setting.version_options = options
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
class Endpoint
|
|
5
|
+
# Everything an endpoint's requests read from it and none of them changes.
|
|
6
|
+
# Each request's copy of the endpoint holds this and its own state, and
|
|
7
|
+
# nothing else of the endpoint. +app+, +callbacks+ and +build_params_with+
|
|
8
|
+
# are filled in by Endpoint#compile!.
|
|
9
|
+
State = Data.define(:inheritable_setting, :options, :config, :source, :options_route_enabled, :app, :callbacks, :build_params_with) do
|
|
10
|
+
def initialize(inheritable_setting:, options:, config:, source:, options_route_enabled: false, app: nil, callbacks: nil, build_params_with: nil)
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/grape/endpoint.rb
CHANGED
|
@@ -11,14 +11,41 @@ module Grape
|
|
|
11
11
|
include Grape::DSL::Headers
|
|
12
12
|
include Grape::DSL::InsideRoute
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# The innermost app of every endpoint's Rack stack. The endpoint to run
|
|
15
|
+
# comes out of the env, so the lambda holds no state and one of them serves
|
|
16
|
+
# every stack, rather than one being allocated per endpoint.
|
|
17
|
+
RUN_ENDPOINT = ->(env) { env[Grape::Env::API_ENDPOINT].run }
|
|
18
|
+
private_constant :RUN_ENDPOINT
|
|
19
|
+
|
|
20
|
+
attr_reader :env, :request, :endpoints
|
|
21
|
+
|
|
22
|
+
# Written out rather than delegated: the request path reads them, and a
|
|
23
|
+
# +def_delegator+ method checks +defined?+ on every call.
|
|
24
|
+
def inheritable_setting
|
|
25
|
+
@state.inheritable_setting
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def options
|
|
29
|
+
@state.options
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def source
|
|
33
|
+
@state.source
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def options_route_enabled
|
|
37
|
+
@state.options_route_enabled
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def options_route_enabled=(enabled)
|
|
41
|
+
@state = @state.with(options_route_enabled: enabled)
|
|
42
|
+
end
|
|
16
43
|
|
|
17
44
|
def_delegators :request, :params, :headers, :cookies
|
|
18
45
|
def_delegator :cookies, :response_cookies
|
|
19
46
|
|
|
20
47
|
# The API (a +Grape::API+ instance) this endpoint belongs to.
|
|
21
|
-
def_delegator
|
|
48
|
+
def_delegator :config, :api
|
|
22
49
|
|
|
23
50
|
# The logger configured on the API this endpoint belongs to. Available
|
|
24
51
|
# inside route handlers, +before+/+after+/+after_validation+/+finally+
|
|
@@ -32,6 +59,13 @@ module Grape
|
|
|
32
59
|
config.app
|
|
33
60
|
end
|
|
34
61
|
|
|
62
|
+
# The paths this endpoint was declared at, relative to its {#namespace},
|
|
63
|
+
# as an Array: the paths given to +route+, or for a mount the path
|
|
64
|
+
# {#mounted_app} was mounted at.
|
|
65
|
+
def path
|
|
66
|
+
config.path
|
|
67
|
+
end
|
|
68
|
+
|
|
35
69
|
class << self
|
|
36
70
|
def block_to_unbound_method(block)
|
|
37
71
|
return unless block
|
|
@@ -67,21 +101,18 @@ module Grape
|
|
|
67
101
|
# endpoint does not know if it will be mounted under a different endpoint.
|
|
68
102
|
# @yield a block defining what your API should do when this endpoint is hit
|
|
69
103
|
def initialize(new_settings, http_methods:, path:, api:, app: nil, params: {}, requirements: nil, anchor: true, **options, &block)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@options = options
|
|
73
|
-
@config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options)
|
|
104
|
+
config = Options.new(http_methods:, path:, api:, app:, params:, requirements:, anchor:, **options)
|
|
74
105
|
# +:app+ is still surfaced on the public options Hash for backwards
|
|
75
106
|
# compatibility (e.g. grape-swagger); prefer the +mounted_app+ reader.
|
|
76
|
-
|
|
107
|
+
options[:app] = app if app
|
|
77
108
|
|
|
78
|
-
@
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@endpoints =
|
|
109
|
+
@state = State.new(
|
|
110
|
+
inheritable_setting: new_settings.point_in_time_copy_for_endpoint,
|
|
111
|
+
options:,
|
|
112
|
+
config:,
|
|
113
|
+
source: self.class.block_to_unbound_method(block)
|
|
114
|
+
)
|
|
115
|
+
@endpoints = app.endpoints if app.respond_to?(:endpoints)
|
|
85
116
|
end
|
|
86
117
|
|
|
87
118
|
# Update our settings from a given parent settings instance. Used when
|
|
@@ -110,12 +141,7 @@ module Grape
|
|
|
110
141
|
|
|
111
142
|
reset_routes!
|
|
112
143
|
compile!
|
|
113
|
-
|
|
114
|
-
router.append(route.apply(self))
|
|
115
|
-
next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET
|
|
116
|
-
|
|
117
|
-
router.append(route.to_head.apply(self))
|
|
118
|
-
end
|
|
144
|
+
append_routes_to(router)
|
|
119
145
|
end
|
|
120
146
|
|
|
121
147
|
def namespace
|
|
@@ -129,7 +155,7 @@ module Grape
|
|
|
129
155
|
def call!(env)
|
|
130
156
|
env[Grape::Env::API_ENDPOINT] = self
|
|
131
157
|
@env = env
|
|
132
|
-
@app.call(env)
|
|
158
|
+
@state.app.call(env)
|
|
133
159
|
end
|
|
134
160
|
|
|
135
161
|
def ==(other)
|
|
@@ -156,45 +182,32 @@ module Grape
|
|
|
156
182
|
"#{self.class} in '#{route.origin}' endpoint"
|
|
157
183
|
end
|
|
158
184
|
|
|
159
|
-
|
|
160
|
-
|
|
185
|
+
# Serve the request the endpoint's Rack stack was entered with: its
|
|
186
|
+
# +before+ filters, its validations, the route block, its +after+ and
|
|
187
|
+
# +finally+ filters, answered as a Rack triplet. The innermost app of that
|
|
188
|
+
# stack calls it, so it expects #call! to have put the env in place first.
|
|
161
189
|
def run
|
|
162
190
|
instrument_run do
|
|
163
|
-
@request = Grape::Request.new(env, build_params_with: @build_params_with)
|
|
191
|
+
@request = Grape::Request.new(env, build_params_with: @state.build_params_with)
|
|
192
|
+
callbacks = @state.callbacks
|
|
164
193
|
begin
|
|
165
|
-
run_filters
|
|
194
|
+
run_filters callbacks, :before if callbacks
|
|
166
195
|
@before_filter_passed = true
|
|
167
196
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
raise Grape::Exceptions::MethodNotAllowed.new(header) unless options?
|
|
171
|
-
|
|
172
|
-
response_object = ''
|
|
173
|
-
status 204
|
|
174
|
-
else
|
|
175
|
-
run_filters before_validations, :before_validation
|
|
176
|
-
run_validators(request:)
|
|
177
|
-
run_filters after_validations, :after_validation
|
|
178
|
-
response_object = execute
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
run_filters afters, :after
|
|
197
|
+
response_object = env.key?(Grape::Env::GRAPE_ALLOWED_METHODS) ? answer_allowed_methods : validate_and_execute(callbacks)
|
|
198
|
+
run_filters callbacks, :after if callbacks
|
|
182
199
|
build_response_cookies
|
|
183
|
-
|
|
184
|
-
# status verifies body presence when DELETE
|
|
185
|
-
@body ||= response_object
|
|
186
|
-
|
|
187
|
-
# The body commonly is an Array of Strings, the application instance itself, or a Stream-like object
|
|
188
|
-
response_object = stream || [body]
|
|
189
|
-
|
|
190
|
-
[status, header, response_object]
|
|
200
|
+
rack_triplet(response_object)
|
|
191
201
|
ensure
|
|
192
|
-
run_filters
|
|
202
|
+
run_filters callbacks, :finally if callbacks
|
|
193
203
|
end
|
|
194
204
|
end
|
|
195
205
|
end
|
|
196
206
|
|
|
207
|
+
protected
|
|
208
|
+
|
|
197
209
|
def execute
|
|
210
|
+
source = @state.source
|
|
198
211
|
return unless source
|
|
199
212
|
|
|
200
213
|
instrument_render do
|
|
@@ -203,8 +216,8 @@ module Grape
|
|
|
203
216
|
end
|
|
204
217
|
|
|
205
218
|
def run_validators(request:)
|
|
206
|
-
validators = inheritable_setting.route_validations
|
|
207
|
-
return if validators.
|
|
219
|
+
validators = @state.inheritable_setting.route_validations
|
|
220
|
+
return if validators.empty?
|
|
208
221
|
|
|
209
222
|
validation_exceptions = nil
|
|
210
223
|
|
|
@@ -222,24 +235,73 @@ module Grape
|
|
|
222
235
|
raise Grape::Exceptions::ValidationErrors.new(exceptions: validation_exceptions, headers: header) if validation_exceptions
|
|
223
236
|
end
|
|
224
237
|
|
|
225
|
-
|
|
226
|
-
|
|
238
|
+
# An endpoint with filters still has none in most of its five phases, so
|
|
239
|
+
# an empty phase returns before it is instrumented.
|
|
240
|
+
def run_filters(callbacks, type)
|
|
241
|
+
filters = callbacks[type]
|
|
242
|
+
return if filters.empty?
|
|
227
243
|
|
|
228
244
|
instrument_run_filters(filters, type) do
|
|
229
245
|
filters.each { |filter| instance_eval(&filter) }
|
|
230
246
|
end
|
|
231
247
|
end
|
|
232
248
|
|
|
233
|
-
|
|
249
|
+
def config
|
|
250
|
+
@state.config
|
|
251
|
+
end
|
|
234
252
|
|
|
235
253
|
def options?
|
|
236
254
|
options_route_enabled && env[Rack::REQUEST_METHOD] == Rack::OPTIONS
|
|
237
255
|
end
|
|
238
256
|
|
|
257
|
+
# Makes an +allocate+d endpoint the prototype #build_prototype answers:
|
|
258
|
+
# +state+, and the ivars a request sets, in place so that every copy
|
|
259
|
+
# starts with the same shape.
|
|
260
|
+
def become_prototype(state)
|
|
261
|
+
@state = state
|
|
262
|
+
@status = nil
|
|
263
|
+
@stream = nil
|
|
264
|
+
@body = nil
|
|
265
|
+
@before_filter_passed = false
|
|
266
|
+
self
|
|
267
|
+
end
|
|
268
|
+
|
|
239
269
|
private
|
|
240
270
|
|
|
241
271
|
attr_reader :before_filter_passed
|
|
242
272
|
|
|
273
|
+
# The answer of a greedy route, which the router hands a request for a
|
|
274
|
+
# method its path has no route for: the Allow header, with a 204 to an
|
|
275
|
+
# auto-OPTIONS and a 405 to anything else.
|
|
276
|
+
def answer_allowed_methods
|
|
277
|
+
header['Allow'] = env[Grape::Env::GRAPE_ALLOWED_METHODS].join(', ')
|
|
278
|
+
raise Grape::Exceptions::MethodNotAllowed.new(header) unless options?
|
|
279
|
+
|
|
280
|
+
status 204
|
|
281
|
+
''
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# The route block's value, run once the params are validated between the
|
|
285
|
+
# +before_validation+ and +after_validation+ filters.
|
|
286
|
+
def validate_and_execute(callbacks)
|
|
287
|
+
run_filters callbacks, :before_validation if callbacks
|
|
288
|
+
run_validators(request:)
|
|
289
|
+
run_filters callbacks, :after_validation if callbacks
|
|
290
|
+
execute
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# The response #run answers with, its body +route_value+ unless the
|
|
294
|
+
# endpoint set a body or a stream itself.
|
|
295
|
+
def rack_triplet(route_value)
|
|
296
|
+
# status verifies body presence when DELETE
|
|
297
|
+
@body ||= route_value
|
|
298
|
+
|
|
299
|
+
# The body commonly is an Array of Strings, the application instance itself, or a Stream-like object
|
|
300
|
+
response_body = stream || [body]
|
|
301
|
+
|
|
302
|
+
[status, header, response_body]
|
|
303
|
+
end
|
|
304
|
+
|
|
243
305
|
# Instrument helpers. Each guards on +listening?+ so that with no subscriber
|
|
244
306
|
# the payload Hash and notification machinery are skipped and the block runs
|
|
245
307
|
# directly (no added allocations); the block is forwarded anonymously so
|
|
@@ -269,45 +331,49 @@ module Grape
|
|
|
269
331
|
end
|
|
270
332
|
|
|
271
333
|
def compile!
|
|
272
|
-
|
|
334
|
+
app = config.app || build_stack
|
|
273
335
|
warn_unauthenticated_mounted_app
|
|
274
|
-
|
|
336
|
+
# nil when no phase has a filter -- most endpoints -- so that #run skips
|
|
337
|
+
# all five on one read.
|
|
275
338
|
callbacks = inheritable_setting.callbacks
|
|
276
|
-
@
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
@
|
|
282
|
-
@prototype = build_prototype
|
|
339
|
+
@state = @state.with(
|
|
340
|
+
app:,
|
|
341
|
+
callbacks: callbacks.each_value.all?(&:empty?) ? nil : callbacks.freeze,
|
|
342
|
+
build_params_with: inheritable_setting.build_params_with
|
|
343
|
+
)
|
|
344
|
+
@prototype = build_prototype(build_helpers)
|
|
283
345
|
end
|
|
284
346
|
|
|
285
|
-
# The object each request is copied from
|
|
347
|
+
# The object each request is copied from: this endpoint's State, and none
|
|
348
|
+
# of the route-building state (+@prototype+, +@endpoints+, +@namespace+,
|
|
349
|
+
# +@routes+) the request path never reads.
|
|
350
|
+
#
|
|
351
|
+
# Ruby sizes a new object by the most instance variables any object of its
|
|
352
|
+
# class has held. A copy holds the State and the seven ivars a request
|
|
353
|
+
# sets, which fits an 80-byte slot until a route block or a helper sets one
|
|
354
|
+
# of its own; carrying the route-building state as well, it took 320. The
|
|
355
|
+
# endpoint itself counts too when there are no helpers, both being a
|
|
356
|
+
# Grape::Endpoint, and it holds at most five.
|
|
286
357
|
#
|
|
287
358
|
# Helpers reach an endpoint by module inclusion, and +Object#dup+ does not
|
|
288
359
|
# carry a singleton class over — so including them on the per-request copy
|
|
289
360
|
# meant giving every request a brand-new singleton class, whose method
|
|
290
361
|
# cache then started cold for the helpers, the route block and every DSL
|
|
291
362
|
# method the endpoint answers. Include them once into a subclass owned by
|
|
292
|
-
# this endpoint instead, and
|
|
293
|
-
#
|
|
294
|
-
#
|
|
295
|
-
#
|
|
296
|
-
#
|
|
297
|
-
# Endpoints with no helpers are their own prototype, exactly as before.
|
|
363
|
+
# this endpoint instead, and hand its State to an instance of it: the
|
|
364
|
+
# per-request copy already answers to the helpers, against a class that
|
|
365
|
+
# has been warm since boot. It also keeps request handling free of class
|
|
366
|
+
# mutation.
|
|
298
367
|
#
|
|
299
|
-
# +allocate+ rather than +new+ because the constructor
|
|
300
|
-
# the DSL's arguments — a fresh settings copy, a fresh
|
|
301
|
-
# +block_to_unbound_method+ — and would still not hold what
|
|
302
|
-
# just computed
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
|
368
|
+
# +allocate+ rather than +new+ because the constructor builds an endpoint
|
|
369
|
+
# from the DSL's arguments — a fresh settings copy, a fresh +config+,
|
|
370
|
+
# another +block_to_unbound_method+ — and would still not hold what
|
|
371
|
+
# +compile!+ has just computed, while the State already holds all of it.
|
|
372
|
+
def build_prototype(helpers)
|
|
373
|
+
(helpers ? helpers_class(helpers) : self.class).allocate.become_prototype(@state)
|
|
374
|
+
end
|
|
310
375
|
|
|
376
|
+
def helpers_class(helpers)
|
|
311
377
|
klass = Class.new(self.class)
|
|
312
378
|
# Anonymous classes answer nil to +name+ and render as a bare address,
|
|
313
379
|
# which would reach anything that reports on the endpoint — +inspect+
|
|
@@ -315,41 +381,59 @@ module Grape
|
|
|
315
381
|
# +set_temporary_name+ is for; a plain constant path is rejected, which
|
|
316
382
|
# is right, since this class is not reachable under that name.
|
|
317
383
|
klass.set_temporary_name("#{self.class}(helpers)")
|
|
318
|
-
klass.include(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
384
|
+
klass.include(helpers)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Each route, and after a GET route the HEAD route it also answers unless
|
|
388
|
+
# the API declared +do_not_route_head!+.
|
|
389
|
+
def append_routes_to(router)
|
|
390
|
+
routes.each do |route|
|
|
391
|
+
router.append(route.apply(self))
|
|
392
|
+
next if inheritable_setting.do_not_route_head? || route.request_method != Rack::GET
|
|
393
|
+
|
|
394
|
+
router.append(route.to_head.apply(self))
|
|
395
|
+
end
|
|
324
396
|
end
|
|
325
397
|
|
|
326
398
|
def to_routes
|
|
399
|
+
patterns = route_patterns
|
|
327
400
|
route_options = config.route_options
|
|
328
|
-
|
|
401
|
+
attributes = route_attributes
|
|
402
|
+
|
|
403
|
+
config.http_methods.flat_map do |method|
|
|
404
|
+
patterns.map { |pattern| Grape::Router::Route.new(self, method, pattern, route_options, **attributes) }
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# The pattern of each of the endpoint's paths, shared by the routes of
|
|
409
|
+
# every method it answers, as a HEAD route shares its GET route's.
|
|
410
|
+
def route_patterns
|
|
329
411
|
path_settings = inheritable_setting.path_settings
|
|
330
|
-
|
|
331
|
-
version = prepare_version
|
|
332
|
-
|
|
333
|
-
requirements = prepare_routes_requirements(config.requirements)
|
|
412
|
+
params = config.params
|
|
413
|
+
version = prepare_version
|
|
414
|
+
requirements = prepare_routes_requirements
|
|
334
415
|
anchor = config.anchor
|
|
335
|
-
settings = inheritable_setting.route_settings
|
|
336
416
|
|
|
337
|
-
config.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
)
|
|
348
|
-
Grape::Router::Route.new(self, method, pattern, route_options, forward_match:, params:, namespace:, prefix:, settings:)
|
|
349
|
-
end
|
|
417
|
+
config.path.map do |path|
|
|
418
|
+
Grape::Router::Pattern.build(
|
|
419
|
+
path:,
|
|
420
|
+
namespace:,
|
|
421
|
+
settings: path_settings,
|
|
422
|
+
anchor:,
|
|
423
|
+
params:,
|
|
424
|
+
version:,
|
|
425
|
+
requirements:
|
|
426
|
+
)
|
|
350
427
|
end
|
|
351
428
|
end
|
|
352
429
|
|
|
430
|
+
# What every route of the endpoint is built with besides its method,
|
|
431
|
+
# pattern and options, read once for all of them.
|
|
432
|
+
def route_attributes
|
|
433
|
+
setting = inheritable_setting
|
|
434
|
+
{ forward_match: bare_rack_app?, params: config.params, namespace:, prefix: setting.root_prefix, settings: setting.route_settings }
|
|
435
|
+
end
|
|
436
|
+
|
|
353
437
|
# True when a bare Rack app (anything that isn't a Grape app) is mounted at
|
|
354
438
|
# this endpoint. Such an app is called directly and matched by path prefix
|
|
355
439
|
# rather than an anchored route.
|
|
@@ -357,14 +441,16 @@ module Grape
|
|
|
357
441
|
config.app && !config.app.is_a?(Grape::Mountable)
|
|
358
442
|
end
|
|
359
443
|
|
|
360
|
-
def prepare_routes_requirements
|
|
444
|
+
def prepare_routes_requirements
|
|
361
445
|
namespace_requirements = inheritable_setting.namespace_requirements
|
|
362
|
-
|
|
363
|
-
namespace_requirements
|
|
446
|
+
route_requirements = config.requirements
|
|
447
|
+
namespace_requirements << route_requirements if route_requirements
|
|
448
|
+
{}.merge!(*namespace_requirements)
|
|
364
449
|
end
|
|
365
450
|
|
|
366
|
-
def prepare_version
|
|
367
|
-
|
|
451
|
+
def prepare_version
|
|
452
|
+
namespace_inheritable_version = inheritable_setting.version
|
|
453
|
+
return if namespace_inheritable_version.nil? || namespace_inheritable_version.empty?
|
|
368
454
|
|
|
369
455
|
namespace_inheritable_version.length == 1 ? namespace_inheritable_version.first : namespace_inheritable_version
|
|
370
456
|
end
|
|
@@ -372,36 +458,46 @@ module Grape
|
|
|
372
458
|
def build_stack
|
|
373
459
|
stack = Grape::Middleware::Stack.new
|
|
374
460
|
|
|
375
|
-
|
|
376
|
-
|
|
461
|
+
setting = inheritable_setting
|
|
462
|
+
content_types = setting.content_types
|
|
463
|
+
format = setting.format
|
|
377
464
|
|
|
378
|
-
stack.use Rack::Head
|
|
379
|
-
stack.use Rack::Lint if lint?
|
|
380
465
|
stack.use Grape::Middleware::Error, **error_middleware_options(format, content_types)
|
|
381
466
|
|
|
382
|
-
stack.concat
|
|
383
|
-
|
|
384
|
-
|
|
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
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
stack.use Grape::Middleware::Formatter,
|
|
394
|
-
format:,
|
|
395
|
-
default_format: inheritable_setting.default_format || :txt,
|
|
396
|
-
content_types:,
|
|
397
|
-
formatters: inheritable_setting.formatters,
|
|
398
|
-
parsers: inheritable_setting.parsers
|
|
467
|
+
stack.concat setting.middleware
|
|
468
|
+
use_versioner(stack)
|
|
469
|
+
stack.use Grape::Middleware::Formatter, **formatter_middleware_options(format, content_types)
|
|
399
470
|
|
|
400
471
|
builder = stack.build
|
|
401
|
-
builder.run
|
|
472
|
+
builder.run RUN_ENDPOINT
|
|
402
473
|
builder.to_app
|
|
403
474
|
end
|
|
404
475
|
|
|
476
|
+
# The versioner for the strategy +version+ declared, when it declared one.
|
|
477
|
+
def use_versioner(stack)
|
|
478
|
+
setting = inheritable_setting
|
|
479
|
+
versions = setting.version
|
|
480
|
+
return if versions.nil? || versions.empty?
|
|
481
|
+
|
|
482
|
+
version_options = setting.version_options
|
|
483
|
+
stack.use Grape::Middleware::Versioner.using(version_options.using),
|
|
484
|
+
versions: versions.flatten,
|
|
485
|
+
version_options:,
|
|
486
|
+
prefix: setting.root_prefix,
|
|
487
|
+
mount_path: setting.mount_path
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def formatter_middleware_options(format, content_types)
|
|
491
|
+
setting = inheritable_setting
|
|
492
|
+
{
|
|
493
|
+
format:,
|
|
494
|
+
default_format: setting.default_format || :txt,
|
|
495
|
+
content_types:,
|
|
496
|
+
formatters: setting.formatters,
|
|
497
|
+
parsers: setting.parsers
|
|
498
|
+
}
|
|
499
|
+
end
|
|
500
|
+
|
|
405
501
|
def error_middleware_options(format, content_types)
|
|
406
502
|
setting = inheritable_setting
|
|
407
503
|
{
|
|
@@ -449,9 +545,5 @@ module Grape
|
|
|
449
545
|
Rack::Utils.set_cookie_header! header, name, cookie_value
|
|
450
546
|
end
|
|
451
547
|
end
|
|
452
|
-
|
|
453
|
-
def lint?
|
|
454
|
-
inheritable_setting.lint? || Grape.config.lint
|
|
455
|
-
end
|
|
456
548
|
end
|
|
457
549
|
end
|