grape 3.3.4 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +128 -0
- data/CONTRIBUTING.md +16 -0
- data/README.md +208 -8
- data/UPGRADING.md +756 -6
- data/lib/grape/api/instance.rb +40 -33
- data/lib/grape/content_types.rb +47 -1
- data/lib/grape/dry_types.rb +1 -1
- data/lib/grape/dsl/callbacks.rb +3 -9
- data/lib/grape/dsl/declared.rb +4 -4
- data/lib/grape/dsl/desc.rb +18 -8
- data/lib/grape/dsl/entity.rb +37 -19
- data/lib/grape/dsl/helpers.rb +3 -3
- data/lib/grape/dsl/inside_route.rb +20 -8
- data/lib/grape/dsl/middleware.rb +3 -3
- data/lib/grape/dsl/parameters.rb +54 -28
- data/lib/grape/dsl/request_response.rb +37 -39
- data/lib/grape/dsl/rescue_options.rb +6 -5
- data/lib/grape/dsl/routing.rb +116 -49
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/validations.rb +3 -3
- data/lib/grape/dsl/version_options.rb +3 -3
- data/lib/grape/endpoint/options.rb +13 -7
- data/lib/grape/endpoint.rb +141 -118
- data/lib/grape/env.rb +7 -0
- data/lib/grape/error_formatter/base.rb +6 -5
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/error_response.rb +4 -1
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/validation.rb +9 -8
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +1 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/dsl.rb +5 -33
- data/lib/grape/middleware/error.rb +153 -46
- data/lib/grape/middleware/formatter.rb +95 -26
- data/lib/grape/middleware/precomputed_content_types.rb +12 -7
- data/lib/grape/middleware/stack.rb +13 -4
- data/lib/grape/middleware/versioner/base.rb +3 -9
- data/lib/grape/middleware/versioner/path.rb +49 -7
- data/lib/grape/namespace.rb +3 -2
- data/lib/grape/path.rb +8 -69
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +36 -4
- data/lib/grape/router/base_route.rb +47 -10
- data/lib/grape/router/greedy_route.rb +5 -1
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +60 -14
- data/lib/grape/router/route.rb +62 -10
- data/lib/grape/router.rb +100 -57
- data/lib/grape/serve_stream/file_body.rb +6 -0
- data/lib/grape/serve_stream/stream_response.rb +6 -0
- data/lib/grape/util/api_description.rb +10 -1
- data/lib/grape/util/cache.rb +21 -2
- data/lib/grape/util/deep_freeze.rb +1 -2
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +751 -37
- data/lib/grape/util/media_type.rb +10 -3
- data/lib/grape/util/path_normalizer.rb +12 -9
- data/lib/grape/util/registry.rb +12 -2
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +32 -6
- data/lib/grape/validations/attributes_iterator.rb +21 -4
- data/lib/grape/validations/contract_scope.rb +8 -7
- data/lib/grape/validations/oneof_collector.rb +6 -10
- data/lib/grape/validations/params_documentation.rb +2 -2
- data/lib/grape/validations/params_scope.rb +66 -26
- data/lib/grape/validations/single_attribute_iterator.rb +5 -3
- data/lib/grape/validations/types/array_coercer.rb +4 -6
- data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
- data/lib/grape/validations/types/json.rb +1 -3
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +12 -9
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
- data/lib/grape/validations/types.rb +5 -5
- data/lib/grape/validations/validations_spec.rb +14 -4
- data/lib/grape/validations/validators/base.rb +4 -7
- data/lib/grape/validations/validators/coerce_validator.rb +8 -2
- data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
- data/lib/grape/validations/validators/length_validator.rb +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +2 -0
- data/lib/grape/validations/validators/same_as_validator.rb +1 -0
- data/lib/grape/version.rb +2 -1
- data/lib/grape.rb +23 -2
- metadata +10 -9
- data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -33
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
data/lib/grape/router.rb
CHANGED
|
@@ -2,14 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
4
|
class Router
|
|
5
|
-
# @deprecated Use {Grape::Util::PathNormalizer.call} instead.
|
|
6
|
-
def self.normalize_path(path)
|
|
7
|
-
Grape.deprecator.warn(
|
|
8
|
-
'`Grape::Router.normalize_path` is deprecated. Use `Grape::Util::PathNormalizer.call` instead.'
|
|
9
|
-
)
|
|
10
|
-
Grape::Util::PathNormalizer.call(path)
|
|
11
|
-
end
|
|
12
|
-
|
|
13
5
|
def initialize
|
|
14
6
|
@neutral_map = []
|
|
15
7
|
@neutral_regexes = []
|
|
@@ -23,14 +15,18 @@ module Grape
|
|
|
23
15
|
def compile!
|
|
24
16
|
return if @compiled
|
|
25
17
|
|
|
26
|
-
@union = Regexp.union(@neutral_regexes)
|
|
18
|
+
@union = resolve_capture_groups(Regexp.union(@neutral_regexes), @neutral_map)
|
|
27
19
|
@neutral_regexes = nil
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
20
|
+
# Compiled from the routes actually registered rather than from
|
|
21
|
+
# Grape::HTTP_SUPPORTED_METHODS. A route declared with any other verb
|
|
22
|
+
# (`route :purge, '/cache'`) is accepted at definition time and is
|
|
23
|
+
# already collected into the resource's Allow header, so skipping it here
|
|
24
|
+
# left it in @map but out of @optimized_map — the only map #match? reads.
|
|
25
|
+
# The resource then advertised a method that could never be matched and
|
|
26
|
+
# answered every request for it with 405.
|
|
27
|
+
@map.each do |method, routes|
|
|
32
28
|
optimized_map = routes.map.with_index { |route, index| route.to_regexp(index) }
|
|
33
|
-
@optimized_map[method] = Regexp.union(optimized_map)
|
|
29
|
+
@optimized_map[method] = resolve_capture_groups(Regexp.union(optimized_map), routes)
|
|
34
30
|
end
|
|
35
31
|
@map.freeze
|
|
36
32
|
@optimized_map.freeze
|
|
@@ -48,10 +44,11 @@ module Grape
|
|
|
48
44
|
|
|
49
45
|
def call(env)
|
|
50
46
|
with_optimization do
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
47
|
+
# Published on the env so the middleware the matched route runs -- the
|
|
48
|
+
# path versioner above all -- reads the path this routed on instead of
|
|
49
|
+
# normalizing PATH_INFO a second time.
|
|
50
|
+
input = env[Grape::Env::GRAPE_NORMALIZED_PATH] = Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
|
|
51
|
+
transaction(input, env[Rack::REQUEST_METHOD], env)
|
|
55
52
|
end
|
|
56
53
|
end
|
|
57
54
|
|
|
@@ -67,52 +64,69 @@ module Grape
|
|
|
67
64
|
|
|
68
65
|
private
|
|
69
66
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@map[method]&.each do |route|
|
|
82
|
-
next if exact_route == route
|
|
83
|
-
next unless route.match?(input)
|
|
84
|
-
|
|
85
|
-
response = process_route(route, input, env)
|
|
86
|
-
break unless cascade?(response)
|
|
87
|
-
end
|
|
88
|
-
response
|
|
89
|
-
end
|
|
90
|
-
|
|
67
|
+
# Resolve +input+ against the compiled routes, in priority order:
|
|
68
|
+
#
|
|
69
|
+
# 1. the routes registered for +method+ — the compiled-union match first,
|
|
70
|
+
# then, when that route cascades, its siblings (see #rotation);
|
|
71
|
+
# 2. the ANY (+'*'+) routes;
|
|
72
|
+
# 3. the greedy neighbour, which answers auto-OPTIONS and 405.
|
|
73
|
+
#
|
|
74
|
+
# Returns nil when nothing answered, leaving the caller to 404. A response
|
|
75
|
+
# that cascades is never final: it is returned only once every later
|
|
76
|
+
# candidate has declined too, so the caller (or a mounting app upstream)
|
|
77
|
+
# can keep looking.
|
|
91
78
|
def transaction(input, method, env)
|
|
92
|
-
|
|
79
|
+
exact_route = match?(input, method)
|
|
80
|
+
response = process_route(exact_route, input, env) if exact_route
|
|
93
81
|
return response if halt?(response)
|
|
94
82
|
|
|
95
|
-
|
|
83
|
+
# A cascading route has only declined this request. Its siblings — the
|
|
84
|
+
# routes sharing this path but differing in, say, version — must be
|
|
85
|
+
# tried before falling back to the ANY routes and the greedy neighbour.
|
|
86
|
+
# Skipped when nothing matched: the compiled union is the disjunction of
|
|
87
|
+
# the same patterns #rotation walks, so a miss there is a miss here.
|
|
88
|
+
cascaded = !response.nil?
|
|
89
|
+
if cascaded
|
|
90
|
+
response = rotation(input, method, env, exact_route)
|
|
91
|
+
return response if response && !cascade?(response)
|
|
92
|
+
end
|
|
93
|
+
|
|
96
94
|
last_neighbor_route = greedy_match?(input)
|
|
97
95
|
|
|
98
96
|
# If last_neighbor_route exists and request method is OPTIONS,
|
|
99
97
|
# return response by using #include_allow_header.
|
|
100
|
-
return process_route(last_neighbor_route, input, env, include_allow_header: true) if !
|
|
101
|
-
|
|
102
|
-
route = match?(input, '*')
|
|
98
|
+
return process_route(last_neighbor_route, input, env, include_allow_header: true) if !cascaded && method == Rack::OPTIONS && last_neighbor_route
|
|
103
99
|
|
|
104
|
-
|
|
100
|
+
star_route = match?(input, '*')
|
|
105
101
|
|
|
106
|
-
if
|
|
107
|
-
|
|
108
|
-
|
|
102
|
+
if star_route
|
|
103
|
+
close_body(response) if response # superseded by the ANY route
|
|
104
|
+
response = process_route(star_route, input, env)
|
|
105
|
+
return response if halt?(response)
|
|
109
106
|
|
|
110
|
-
|
|
107
|
+
cascaded ||= !response.nil?
|
|
111
108
|
end
|
|
112
109
|
|
|
113
|
-
return process_route(last_neighbor_route, input, env, include_allow_header: true) if !
|
|
110
|
+
return process_route(last_neighbor_route, input, env, include_allow_header: true) if !cascaded && last_neighbor_route
|
|
114
111
|
|
|
115
|
-
|
|
112
|
+
response
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# The routes registered for +method+ other than +exact_route+, tried in
|
|
116
|
+
# registration order until one answers without cascading. Returns the last
|
|
117
|
+
# response processed — a cascading one when every sibling declined, so the
|
|
118
|
+
# caller can hand it back — or nil when no sibling matched.
|
|
119
|
+
def rotation(input, method, env, exact_route)
|
|
120
|
+
response = nil
|
|
121
|
+
@map[method]&.each do |route|
|
|
122
|
+
next if exact_route == route
|
|
123
|
+
next unless route.match?(input)
|
|
124
|
+
|
|
125
|
+
close_body(response) if response # the previous sibling cascaded
|
|
126
|
+
response = process_route(route, input, env)
|
|
127
|
+
break unless cascade?(response)
|
|
128
|
+
end
|
|
129
|
+
response
|
|
116
130
|
end
|
|
117
131
|
|
|
118
132
|
# Returns true if `response` should be returned as-is from the enclosing
|
|
@@ -122,18 +136,42 @@ module Grape
|
|
|
122
136
|
return false unless response
|
|
123
137
|
|
|
124
138
|
cascade = cascade?(response)
|
|
125
|
-
response
|
|
139
|
+
close_body(response) if cascade
|
|
126
140
|
!cascade
|
|
127
141
|
end
|
|
128
142
|
|
|
143
|
+
# Releases a response the router has decided not to return. Rack requires
|
|
144
|
+
# every body it hands out to be closed, and a cascading candidate is
|
|
145
|
+
# discarded as soon as a later one answers.
|
|
146
|
+
def close_body(response)
|
|
147
|
+
body = response[2]
|
|
148
|
+
body.close if body.respond_to?(:close)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Routing args are rebuilt for every attempt: when a route cascades
|
|
152
|
+
# (X-Cascade pass), the next candidate must not observe the previous
|
|
153
|
+
# attempt's +route_info+ or path captures.
|
|
129
154
|
def process_route(route, input, env, include_allow_header: false)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
155
|
+
# The path captures are the hash: +route_info+ is written into them
|
|
156
|
+
# rather than merged in from a second one.
|
|
157
|
+
routing_args = route.params_for(input) || {}
|
|
158
|
+
routing_args[:route_info] = route
|
|
159
|
+
env[Grape::Env::GRAPE_ROUTING_ARGS] = routing_args
|
|
133
160
|
env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header
|
|
134
161
|
route.call(env)
|
|
135
162
|
end
|
|
136
163
|
|
|
164
|
+
# Tells each route the number of the group it ended up as in +union+. The
|
|
165
|
+
# numbering is a property of the union rather than of the route's own
|
|
166
|
+
# pattern -- every route ahead of it contributes however many groups its
|
|
167
|
+
# pattern declares -- so it can only be resolved once the union is built.
|
|
168
|
+
# Returns the union, so a caller can assign it in one expression.
|
|
169
|
+
def resolve_capture_groups(union, routes)
|
|
170
|
+
named_captures = union.named_captures
|
|
171
|
+
routes.each { |route| route.resolve_capture_group!(named_captures) }
|
|
172
|
+
union
|
|
173
|
+
end
|
|
174
|
+
|
|
137
175
|
def with_optimization
|
|
138
176
|
compile!
|
|
139
177
|
yield || default_response
|
|
@@ -143,12 +181,17 @@ module Grape
|
|
|
143
181
|
[404, DEFAULT_RESPONSE_HEADERS.dup, DEFAULT_RESPONSE_BODY.dup]
|
|
144
182
|
end
|
|
145
183
|
|
|
184
|
+
# Which alternative of the union matched is answered by scanning one group
|
|
185
|
+
# per registered route, so on an API with many of them that scan is what a
|
|
186
|
+
# request costs. The groups are indexed by number rather than by name: a
|
|
187
|
+
# name sends MatchData through the pattern's name table on every lookup,
|
|
188
|
+
# a number indexes the match region directly.
|
|
146
189
|
def match?(input, method)
|
|
147
|
-
@optimized_map[method]&.match(input) { |m| @map[method].detect { |route| m[route.
|
|
190
|
+
@optimized_map[method]&.match(input) { |m| @map[method].detect { |route| m[route.regexp_capture_group] } }
|
|
148
191
|
end
|
|
149
192
|
|
|
150
193
|
def greedy_match?(input)
|
|
151
|
-
@union.match(input) { |m| @neutral_map.detect { |route| m[route.
|
|
194
|
+
@union.match(input) { |m| @neutral_map.detect { |route| m[route.regexp_capture_group] } }
|
|
152
195
|
end
|
|
153
196
|
|
|
154
197
|
def cascade?(response)
|
|
@@ -6,7 +6,7 @@ module Grape
|
|
|
6
6
|
DSL_METHODS = %i[
|
|
7
7
|
body_name
|
|
8
8
|
consumes
|
|
9
|
-
|
|
9
|
+
default_response
|
|
10
10
|
deprecated
|
|
11
11
|
detail
|
|
12
12
|
entity
|
|
@@ -38,6 +38,15 @@ module Grape
|
|
|
38
38
|
alias success entity
|
|
39
39
|
alias failure http_codes
|
|
40
40
|
|
|
41
|
+
# @deprecated Use {#default_response}. The description is read back
|
|
42
|
+
# through an ActiveSupport::OrderedOptions, where +default+ is
|
|
43
|
+
# +Hash#default+ rather than a key lookup, and grape-swagger has always
|
|
44
|
+
# asked the route for +default_response+.
|
|
45
|
+
def default(value)
|
|
46
|
+
Grape.deprecator.warn('`default` in a `desc` block is deprecated. Use `default_response` instead.')
|
|
47
|
+
default_response(value)
|
|
48
|
+
end
|
|
49
|
+
|
|
41
50
|
def configuration
|
|
42
51
|
@configuration ||= eval_endpoint_config(@endpoint_configuration)
|
|
43
52
|
end
|
data/lib/grape/util/cache.rb
CHANGED
|
@@ -2,16 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Util
|
|
5
|
+
# Base class for the lazily-filled lookup caches (coercers, dry-types,
|
|
6
|
+
# path parts, ...). Subclasses assign a default-block +Hash+ to +@cache+
|
|
7
|
+
# in their +initialize+.
|
|
8
|
+
#
|
|
9
|
+
# Lookups are synchronized: caches are written at API *definition* time
|
|
10
|
+
# (a +params+ block runs when the class body is evaluated), which is not
|
|
11
|
+
# covered by the compile-time +Grape::API::Instance::LOCK+ and can happen
|
|
12
|
+
# concurrently — e.g. parallel eager loading, or two threads autoloading
|
|
13
|
+
# different API files. A +Monitor+ (reentrant) rather than a +Mutex+,
|
|
14
|
+
# because a cache miss may re-enter the same cache: building a
|
|
15
|
+
# multiple-type coercer through +Types::CoercerCache+ builds its member
|
|
16
|
+
# coercers through +Types.build_coercer+, which lands in the same cache.
|
|
5
17
|
class Cache
|
|
6
18
|
include Singleton
|
|
7
19
|
|
|
8
20
|
attr_reader :cache
|
|
9
21
|
|
|
22
|
+
def initialize
|
|
23
|
+
@monitor = Monitor.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def [](key)
|
|
27
|
+
@monitor.synchronize { @cache[key] }
|
|
28
|
+
end
|
|
29
|
+
|
|
10
30
|
class << self
|
|
11
31
|
extend Forwardable
|
|
12
32
|
|
|
13
|
-
def_delegators :
|
|
14
|
-
def_delegators :instance, :cache
|
|
33
|
+
def_delegators :instance, :[], :cache
|
|
15
34
|
end
|
|
16
35
|
end
|
|
17
36
|
end
|
|
@@ -10,9 +10,8 @@ module Grape
|
|
|
10
10
|
#
|
|
11
11
|
# Intentionally left unfrozen:
|
|
12
12
|
# - Procs / lambdas — may be deferred DB-backed callables
|
|
13
|
-
# - Coercers (e.g. ArrayCoercer) — use lazy ivar memoization at request time
|
|
14
13
|
# - Classes / Modules — shared constants that must remain open
|
|
15
|
-
# - ParamsScope — self-
|
|
14
|
+
# - Coercers and ParamsScope — self-freeze at construction
|
|
16
15
|
def deep_freeze(obj)
|
|
17
16
|
case obj
|
|
18
17
|
when Hash
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Util
|
|
5
|
+
# Extend into a class whose instances are shared across requests:
|
|
6
|
+
# +new+ returns a frozen instance, so any later ivar write — e.g.
|
|
7
|
+
# request-time memoization — raises FrozenError instead of being a
|
|
8
|
+
# latent data race.
|
|
9
|
+
#
|
|
10
|
+
# Must stay a +new+ wrapper (never an +initialize+ wrapper): the freeze
|
|
11
|
+
# runs after the entire initialize chain, so subclasses may assign ivars
|
|
12
|
+
# after +super+. Extending a hierarchy base covers its subclasses —
|
|
13
|
+
# singleton classes inherit along the class hierarchy.
|
|
14
|
+
module FreezeOnNew
|
|
15
|
+
def new(...)
|
|
16
|
+
super.freeze
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|