grape 3.2.1 → 3.3.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 +80 -0
- data/README.md +116 -43
- data/UPGRADING.md +336 -1
- data/grape.gemspec +5 -5
- data/lib/grape/api/instance.rb +7 -7
- data/lib/grape/api.rb +22 -25
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +48 -50
- data/lib/grape/dsl/callbacks.rb +9 -3
- data/lib/grape/dsl/desc.rb +8 -2
- data/lib/grape/dsl/entity.rb +88 -0
- data/lib/grape/dsl/helpers.rb +27 -7
- data/lib/grape/dsl/inside_route.rb +38 -129
- data/lib/grape/dsl/logger.rb +3 -5
- data/lib/grape/dsl/parameters.rb +32 -38
- data/lib/grape/dsl/request_response.rb +53 -48
- data/lib/grape/dsl/rescue_options.rb +24 -0
- data/lib/grape/dsl/routing.rb +51 -35
- data/lib/grape/dsl/settings.rb +14 -8
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +19 -0
- data/lib/grape/endpoint.rb +96 -68
- data/lib/grape/env.rb +1 -3
- data/lib/grape/error_formatter/base.rb +23 -20
- data/lib/grape/error_formatter/json.rb +8 -4
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/exceptions/base.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +45 -0
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/validation.rb +14 -0
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +12 -20
- data/lib/grape/formatter/serializable_hash.rb +5 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/base.rb +2 -3
- data/lib/grape/middleware/auth/dsl.rb +23 -8
- data/lib/grape/middleware/base.rb +22 -33
- data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
- data/lib/grape/middleware/error.rb +152 -62
- data/lib/grape/middleware/formatter.rb +66 -50
- data/lib/grape/middleware/precomputed_content_types.rb +46 -0
- data/lib/grape/middleware/stack.rb +5 -6
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/base.rb +34 -38
- data/lib/grape/middleware/versioner/header.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +8 -3
- data/lib/grape/namespace.rb +3 -3
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +14 -17
- data/lib/grape/request.rb +15 -8
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern.rb +6 -10
- data/lib/grape/router.rb +28 -42
- data/lib/grape/serve_stream/file_body.rb +1 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +1 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/base_inheritable.rb +13 -16
- data/lib/grape/util/inheritable_setting.rb +44 -27
- data/lib/grape/util/inheritable_values.rb +7 -3
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +2 -9
- data/lib/grape/util/lazy/value.rb +2 -9
- data/lib/grape/util/lazy/value_enumerable.rb +13 -16
- data/lib/grape/util/media_type.rb +1 -4
- data/lib/grape/util/path_normalizer.rb +34 -0
- data/lib/grape/util/registry.rb +1 -1
- data/lib/grape/util/stackable_values.rb +11 -8
- data/lib/grape/validations/attributes_iterator.rb +13 -13
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/oneof_collector.rb +39 -0
- data/lib/grape/validations/param_scope_tracker.rb +14 -9
- data/lib/grape/validations/params_documentation.rb +25 -23
- data/lib/grape/validations/params_scope.rb +54 -172
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/types/array_coercer.rb +2 -2
- data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
- data/lib/grape/validations/types/primitive_coercer.rb +10 -5
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
- data/lib/grape/validations/types.rb +23 -30
- data/lib/grape/validations/validations_spec.rb +149 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
- data/lib/grape/validations/validators/base.rb +39 -22
- data/lib/grape/validations/validators/coerce_validator.rb +5 -3
- data/lib/grape/validations/validators/default_validator.rb +7 -8
- data/lib/grape/validations/validators/except_values_validator.rb +3 -2
- data/lib/grape/validations/validators/length_validator.rb +1 -1
- data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
- data/lib/grape/validations/validators/oneof_validator.rb +49 -0
- data/lib/grape/validations/validators/values_validator.rb +5 -5
- data/lib/grape/version.rb +1 -1
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +6 -6
- metadata +34 -18
- data/lib/grape/middleware/globals.rb +0 -14
|
@@ -17,12 +17,17 @@ module Grape
|
|
|
17
17
|
# env['api.version'] => 'v1'
|
|
18
18
|
#
|
|
19
19
|
class Path < Base
|
|
20
|
+
def initialize(app, **options)
|
|
21
|
+
super
|
|
22
|
+
@prefixes = [mount_path, Grape::Util::PathNormalizer.call(prefix)].select { |p| p.present? && p != '/' }.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
20
25
|
def before
|
|
21
|
-
path_info = Grape::
|
|
26
|
+
path_info = Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
|
|
22
27
|
return if path_info == '/'
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
path_info = @prefixes.reduce(path_info) do |pi, path|
|
|
30
|
+
pi.start_with?(path) ? pi.delete_prefix(path) : pi
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
slash_position = path_info.index('/', 1) # omit the first one
|
data/lib/grape/namespace.rb
CHANGED
|
@@ -23,13 +23,13 @@ module Grape
|
|
|
23
23
|
settings&.map(&:space)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def ==(other)
|
|
27
27
|
other.class == self.class &&
|
|
28
28
|
other.space == space &&
|
|
29
29
|
other.requirements == requirements &&
|
|
30
30
|
other.options == options
|
|
31
31
|
end
|
|
32
|
-
alias
|
|
32
|
+
alias eql? ==
|
|
33
33
|
|
|
34
34
|
def hash
|
|
35
35
|
[self.class, space, requirements, options].hash
|
|
@@ -45,7 +45,7 @@ module Grape
|
|
|
45
45
|
def initialize
|
|
46
46
|
super
|
|
47
47
|
@cache = Hash.new do |h, joined_space|
|
|
48
|
-
h[joined_space] = Grape::
|
|
48
|
+
h[joined_space] = Grape::Util::PathNormalizer.call(joined_space.join('/'))
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
end
|
data/lib/grape/parser/json.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Parser
|
|
5
5
|
class Json < Base
|
|
6
6
|
def self.call(object, _env)
|
|
7
|
-
::Grape::Json.
|
|
7
|
+
::Grape::Json.parse(object)
|
|
8
8
|
rescue ::Grape::Json::ParseError
|
|
9
9
|
# handle JSON parsing errors via the rescue handlers or provide error message
|
|
10
10
|
raise Grape::Exceptions::InvalidMessageBody.new('application/json')
|
data/lib/grape/path.rb
CHANGED
|
@@ -21,23 +21,20 @@ module Grape
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
23
|
def build_suffix(raw_path, raw_namespace, settings)
|
|
24
|
-
if uses_specific_format?(settings)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
else
|
|
29
|
-
DEFAULT_FORMAT_SEGMENT
|
|
30
|
-
end
|
|
24
|
+
return "(.#{settings[:format]})" if uses_specific_format?(settings)
|
|
25
|
+
return NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT if !uses_path_versioning?(settings) || valid_part?(raw_namespace) || valid_part?(raw_path)
|
|
26
|
+
|
|
27
|
+
DEFAULT_FORMAT_SEGMENT
|
|
31
28
|
end
|
|
32
29
|
|
|
33
30
|
def build_parts(raw_path, raw_namespace, settings)
|
|
34
|
-
[]
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
parts = []
|
|
32
|
+
add_part(parts, settings[:mount_path])
|
|
33
|
+
add_part(parts, settings[:root_prefix])
|
|
34
|
+
parts << VERSION_SEGMENT if uses_path_versioning?(settings)
|
|
35
|
+
add_part(parts, raw_namespace)
|
|
36
|
+
add_part(parts, raw_path)
|
|
37
|
+
parts
|
|
41
38
|
end
|
|
42
39
|
|
|
43
40
|
def add_part(parts, value)
|
|
@@ -55,9 +52,9 @@ module Grape
|
|
|
55
52
|
end
|
|
56
53
|
|
|
57
54
|
def uses_path_versioning?(settings)
|
|
58
|
-
return false unless settings.key?(:version) && settings[:version_options]
|
|
55
|
+
return false unless settings.key?(:version) && settings[:version_options]
|
|
59
56
|
|
|
60
|
-
settings[:version] && settings[:version_options]
|
|
57
|
+
settings[:version] && settings[:version_options].using == :path
|
|
61
58
|
end
|
|
62
59
|
|
|
63
60
|
def valid_part?(part)
|
|
@@ -68,7 +65,7 @@ module Grape
|
|
|
68
65
|
def initialize
|
|
69
66
|
super
|
|
70
67
|
@cache = Hash.new do |h, parts|
|
|
71
|
-
h[parts] = Grape::
|
|
68
|
+
h[parts] = Grape::Util::PathNormalizer.call(parts.join('/'))
|
|
72
69
|
end
|
|
73
70
|
end
|
|
74
71
|
end
|
data/lib/grape/request.rb
CHANGED
|
@@ -132,8 +132,8 @@ module Grape
|
|
|
132
132
|
X-UA-Compatible
|
|
133
133
|
X-WebKit-CS
|
|
134
134
|
X-XSS-Protection
|
|
135
|
-
].
|
|
136
|
-
|
|
135
|
+
].to_h do |header|
|
|
136
|
+
["HTTP_#{header.upcase.tr('-', '_')}", header]
|
|
137
137
|
end.freeze
|
|
138
138
|
|
|
139
139
|
alias rack_params params
|
|
@@ -153,19 +153,26 @@ module Grape
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
def cookies
|
|
156
|
-
@cookies ||= Grape::Cookies.new(
|
|
156
|
+
@cookies ||= Grape::Cookies.new(rack_cookies)
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
# True once the cookie jar has been materialized (a cookie was read or
|
|
160
|
+
# written this request). Lets the endpoint skip response-cookie flushing,
|
|
161
|
+
# and the Grape::Cookies allocation it triggers, when no cookie was
|
|
162
|
+
# touched on the request.
|
|
163
|
+
def cookies?
|
|
164
|
+
!@cookies.nil?
|
|
163
165
|
end
|
|
164
166
|
|
|
165
167
|
private
|
|
166
168
|
|
|
167
169
|
def make_params
|
|
168
|
-
@params_builder.call(rack_params)
|
|
170
|
+
params = @params_builder.call(rack_params)
|
|
171
|
+
routing_args = env[Grape::Env::GRAPE_ROUTING_ARGS]
|
|
172
|
+
filtered = routing_args&.except(:version, :route_info)
|
|
173
|
+
return params if filtered.blank?
|
|
174
|
+
|
|
175
|
+
params.deep_merge!(filtered)
|
|
169
176
|
rescue *Grape::RACK_ERRORS
|
|
170
177
|
raise Grape::Exceptions::RequestError
|
|
171
178
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
class Router
|
|
5
|
+
# Grape-style path patterns for Mustermann: `:param`, `*splat`, `{name}` /
|
|
6
|
+
# `{+splat}`, `( )` optionals, `|`, and an Integer digit-only constraint
|
|
7
|
+
# (driven by Grape's `params` option).
|
|
8
|
+
#
|
|
9
|
+
# Inlined from the mustermann-grape gem (MIT) by namusyaka, Konstantin Haase
|
|
10
|
+
# and Daniel Doubrovkine. Grape instantiates this class directly (see
|
|
11
|
+
# {Grape::Router::Pattern}), so unlike the gem it is not registered as a
|
|
12
|
+
# Mustermann `type: :grape`.
|
|
13
|
+
class MustermannPattern < ::Mustermann::AST::Pattern
|
|
14
|
+
supported_options :params
|
|
15
|
+
|
|
16
|
+
on(nil, '?', ')') { |c| unexpected(c) }
|
|
17
|
+
|
|
18
|
+
on('*') { |_c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
|
|
19
|
+
on(':') do |_c|
|
|
20
|
+
param_name = scan(/\w+/)
|
|
21
|
+
# Integer params (declared via Grape's `params` option) match digits only;
|
|
22
|
+
# any other capture matches a single path segment (anything but / ? # .).
|
|
23
|
+
param_type = pattern&.options&.dig(:params, param_name, :type)
|
|
24
|
+
constraint = param_type == 'Integer' ? /\d/ : '[^/?#.]'
|
|
25
|
+
node(:capture, param_name, constraint:) { scan(/\w+/) }
|
|
26
|
+
end
|
|
27
|
+
on('\\') { |_c| node(:char, expect(/./)) }
|
|
28
|
+
on('(') { |_c| node(:optional, node(:group) { read unless scan(')') }) }
|
|
29
|
+
on('|') { |_c| node(:or) }
|
|
30
|
+
|
|
31
|
+
on('{') do |_c|
|
|
32
|
+
type = scan('+') ? :named_splat : :capture
|
|
33
|
+
name = expect(/[\w.]+/)
|
|
34
|
+
type = :splat if (type == :named_splat) && (name == 'splat')
|
|
35
|
+
expect('}')
|
|
36
|
+
node(type, name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
suffix('?') do |_c, element|
|
|
40
|
+
node(:optional, element)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/grape/router/pattern.rb
CHANGED
|
@@ -16,7 +16,7 @@ module Grape
|
|
|
16
16
|
def initialize(origin:, suffix:, anchor:, params:, format:, version:, requirements:)
|
|
17
17
|
@origin = origin
|
|
18
18
|
@path = PatternCache[[build_path_from_pattern(@origin, anchor), suffix]]
|
|
19
|
-
@pattern =
|
|
19
|
+
@pattern = MustermannPattern.new(@path, uri_decode: true, params:, capture: extract_capture(format, version, requirements))
|
|
20
20
|
@to_regexp = @pattern.to_regexp
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -39,15 +39,11 @@ module Grape
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def build_path_from_pattern(pattern, anchor)
|
|
42
|
-
if pattern.end_with?('*path')
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"#{pattern}?*path"
|
|
48
|
-
else
|
|
49
|
-
"#{pattern}/?*path"
|
|
50
|
-
end
|
|
42
|
+
return pattern.dup.insert(pattern.rindex('/') + 1, '?') if pattern.end_with?('*path')
|
|
43
|
+
return pattern if anchor
|
|
44
|
+
return "#{pattern}?*path" if pattern.end_with?('/')
|
|
45
|
+
|
|
46
|
+
"#{pattern}/?*path"
|
|
51
47
|
end
|
|
52
48
|
|
|
53
49
|
def map_str(value)
|
data/lib/grape/router.rb
CHANGED
|
@@ -2,31 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
4
|
class Router
|
|
5
|
-
#
|
|
6
|
-
# normalize_path("/foo") # => "/foo"
|
|
7
|
-
# normalize_path("/foo/") # => "/foo"
|
|
8
|
-
# normalize_path("foo") # => "/foo"
|
|
9
|
-
# normalize_path("") # => "/"
|
|
10
|
-
# normalize_path("/%ab") # => "/%AB"
|
|
11
|
-
# https://github.com/rails/rails/blob/00cc4ff0259c0185fe08baadaa40e63ea2534f6e/actionpack/lib/action_dispatch/journey/router/utils.rb#L19
|
|
5
|
+
# @deprecated Use {Grape::Util::PathNormalizer.call} instead.
|
|
12
6
|
def self.normalize_path(path)
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return path if path.start_with?('/') && !(path.end_with?('/') || path.match?(%r{%|//}))
|
|
18
|
-
|
|
19
|
-
# Slow path
|
|
20
|
-
encoding = path.encoding
|
|
21
|
-
path = "/#{path}"
|
|
22
|
-
path.squeeze!('/')
|
|
23
|
-
|
|
24
|
-
unless path == '/'
|
|
25
|
-
path.delete_suffix!('/')
|
|
26
|
-
path.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase }
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
path.force_encoding(encoding)
|
|
7
|
+
Grape.deprecator.warn(
|
|
8
|
+
'`Grape::Router.normalize_path` is deprecated. Use `Grape::Util::PathNormalizer.call` instead.'
|
|
9
|
+
)
|
|
10
|
+
Grape::Util::PathNormalizer.call(path)
|
|
30
11
|
end
|
|
31
12
|
|
|
32
13
|
def initialize
|
|
@@ -62,7 +43,7 @@ module Grape
|
|
|
62
43
|
|
|
63
44
|
def call(env)
|
|
64
45
|
with_optimization do
|
|
65
|
-
input =
|
|
46
|
+
input = Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
|
|
66
47
|
method = env[Rack::REQUEST_METHOD]
|
|
67
48
|
response, route = identity(input, method, env)
|
|
68
49
|
response || rotation(input, method, env, route)
|
|
@@ -103,20 +84,10 @@ module Grape
|
|
|
103
84
|
end
|
|
104
85
|
|
|
105
86
|
def transaction(input, method, env)
|
|
106
|
-
# using a Proc is important since `return` will exit the enclosing function
|
|
107
|
-
cascade_or_return_response = proc do |response|
|
|
108
|
-
if response
|
|
109
|
-
cascade?(response).tap do |cascade|
|
|
110
|
-
return response unless cascade
|
|
111
|
-
|
|
112
|
-
# we need to close the body if possible before dismissing
|
|
113
|
-
response[2].close if response[2].respond_to?(:close)
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
87
|
response = yield
|
|
119
|
-
|
|
88
|
+
return response if halt?(response)
|
|
89
|
+
|
|
90
|
+
last_response_cascade = !response.nil?
|
|
120
91
|
last_neighbor_route = greedy_match?(input)
|
|
121
92
|
|
|
122
93
|
# If last_neighbor_route exists and request method is OPTIONS,
|
|
@@ -127,18 +98,33 @@ module Grape
|
|
|
127
98
|
|
|
128
99
|
return last_neighbor_route.call(env) if last_neighbor_route && last_response_cascade && route
|
|
129
100
|
|
|
130
|
-
|
|
101
|
+
if route
|
|
102
|
+
route_response = process_route(route, input, env)
|
|
103
|
+
return route_response if halt?(route_response)
|
|
104
|
+
|
|
105
|
+
last_response_cascade = !route_response.nil?
|
|
106
|
+
end
|
|
131
107
|
|
|
132
108
|
return process_route(last_neighbor_route, input, env, include_allow_header: true) if !last_response_cascade && last_neighbor_route
|
|
133
109
|
|
|
134
110
|
nil
|
|
135
111
|
end
|
|
136
112
|
|
|
113
|
+
# Returns true if `response` should be returned as-is from the enclosing
|
|
114
|
+
# transaction. Closes the body as a side effect when the response is
|
|
115
|
+
# cascading so callers can safely try the next match.
|
|
116
|
+
def halt?(response)
|
|
117
|
+
return false unless response
|
|
118
|
+
|
|
119
|
+
cascade = cascade?(response)
|
|
120
|
+
response[2].close if cascade && response[2].respond_to?(:close)
|
|
121
|
+
!cascade
|
|
122
|
+
end
|
|
123
|
+
|
|
137
124
|
def process_route(route, input, env, include_allow_header: false)
|
|
138
|
-
args = env[Grape::Env::GRAPE_ROUTING_ARGS] || { route_info: route }
|
|
139
125
|
route_params = route.params(input)
|
|
140
|
-
|
|
141
|
-
env[Grape::Env::GRAPE_ROUTING_ARGS]
|
|
126
|
+
env[Grape::Env::GRAPE_ROUTING_ARGS] ||= { route_info: route }
|
|
127
|
+
env[Grape::Env::GRAPE_ROUTING_ARGS].merge!(route_params) if route_params.present?
|
|
142
128
|
env[Grape::Env::GRAPE_ALLOWED_METHODS] = route.allow_header if include_allow_header
|
|
143
129
|
route.call(env)
|
|
144
130
|
end
|
|
@@ -6,11 +6,9 @@ module Grape
|
|
|
6
6
|
# for using Rack::SendFile middleware
|
|
7
7
|
class SendfileResponse < Rack::Response
|
|
8
8
|
def respond_to?(method_name, include_all = false)
|
|
9
|
-
if method_name == :to_path
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
super
|
|
13
|
-
end
|
|
9
|
+
return @body.respond_to?(:to_path, include_all) if method_name == :to_path
|
|
10
|
+
|
|
11
|
+
super
|
|
14
12
|
end
|
|
15
13
|
|
|
16
14
|
def to_path
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Testing
|
|
5
|
+
module RunBeforeEach
|
|
6
|
+
def run
|
|
7
|
+
self.class.run_before_each(self)
|
|
8
|
+
super
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
module ClassMethods
|
|
13
|
+
def before_each(&block)
|
|
14
|
+
raise ArgumentError, 'a block is required' unless block
|
|
15
|
+
|
|
16
|
+
@before_each ||= []
|
|
17
|
+
@before_each << block
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset_before_each
|
|
21
|
+
@before_each&.clear
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def run_before_each(endpoint)
|
|
25
|
+
superclass.run_before_each(endpoint) unless self == Grape::Endpoint
|
|
26
|
+
@before_each&.each { |blk| blk.call(endpoint) }
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Grape::Endpoint.prepend(RunBeforeEach)
|
|
31
|
+
Grape::Endpoint.extend(ClassMethods)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -4,6 +4,9 @@ module Grape
|
|
|
4
4
|
module Util
|
|
5
5
|
# Base for classes which need to operate with own values kept
|
|
6
6
|
# in the hash and inherited values kept in a Hash-like object.
|
|
7
|
+
#
|
|
8
|
+
# +@new_values+ is lazily allocated on first write so settings layers
|
|
9
|
+
# that only inherit (never override) don't carry an empty Hash each.
|
|
7
10
|
class BaseInheritable
|
|
8
11
|
attr_accessor :inherited_values, :new_values
|
|
9
12
|
|
|
@@ -11,35 +14,29 @@ module Grape
|
|
|
11
14
|
# of the Hash class.
|
|
12
15
|
def initialize(inherited_values = nil)
|
|
13
16
|
@inherited_values = inherited_values || {}
|
|
14
|
-
@new_values
|
|
17
|
+
# @new_values stays nil until the first write.
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def delete(*keys)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
end
|
|
21
|
+
return [] unless @new_values
|
|
22
|
+
|
|
23
|
+
keys.map { |key| @new_values.delete(key) }
|
|
22
24
|
end
|
|
23
25
|
|
|
24
26
|
def initialize_copy(other)
|
|
25
27
|
super
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
@inherited_values = other.inherited_values
|
|
29
|
+
@new_values = other.new_values&.dup
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
def keys
|
|
31
|
-
if new_values.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
combined.uniq!
|
|
35
|
-
end
|
|
36
|
-
else
|
|
37
|
-
inherited_values.keys
|
|
38
|
-
end
|
|
33
|
+
return @inherited_values.keys if @new_values.nil? || @new_values.empty?
|
|
34
|
+
|
|
35
|
+
(@inherited_values.keys + @new_values.keys).uniq
|
|
39
36
|
end
|
|
40
37
|
|
|
41
38
|
def key?(name)
|
|
42
|
-
inherited_values.key?(name) || new_values
|
|
39
|
+
@inherited_values.key?(name) || @new_values&.key?(name) || false
|
|
43
40
|
end
|
|
44
41
|
end
|
|
45
42
|
end
|
|
@@ -5,7 +5,17 @@ module Grape
|
|
|
5
5
|
# A branchable, inheritable settings object which can store both stackable
|
|
6
6
|
# and inheritable values (see InheritableValues and StackableValues).
|
|
7
7
|
class InheritableSetting
|
|
8
|
-
|
|
8
|
+
attr_reader :route, :namespace, :namespace_inheritable, :namespace_stackable, :namespace_reverse_stackable, :parent
|
|
9
|
+
|
|
10
|
+
# Lazy-allocated; +api_class+ and +point_in_time_copies+ are rarely
|
|
11
|
+
# written on most settings layers, so don't pay for a Hash/Array each.
|
|
12
|
+
def api_class
|
|
13
|
+
@api_class ||= {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def point_in_time_copies
|
|
17
|
+
@point_in_time_copies ||= []
|
|
18
|
+
end
|
|
9
19
|
|
|
10
20
|
# Retrieve global settings.
|
|
11
21
|
def self.global
|
|
@@ -23,17 +33,14 @@ module Grape
|
|
|
23
33
|
# instance can then be set to inherit from an existing instance (see
|
|
24
34
|
# #inherit_from).
|
|
25
35
|
def initialize
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
self.namespace = InheritableValues.new # only inheritable from a parent when
|
|
36
|
+
@route = {}
|
|
37
|
+
@namespace = InheritableValues.new # only inheritable from a parent when
|
|
29
38
|
# used with a mount, or should every API::Class be a separate namespace by default?
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
self.parent = nil
|
|
39
|
+
@namespace_inheritable = InheritableValues.new
|
|
40
|
+
@namespace_stackable = StackableValues.new
|
|
41
|
+
@namespace_reverse_stackable = ReverseStackableValues.new
|
|
42
|
+
@parent = nil
|
|
43
|
+
# @api_class and @point_in_time_copies stay nil until first access.
|
|
37
44
|
end
|
|
38
45
|
|
|
39
46
|
# Return the class-level global properties.
|
|
@@ -48,14 +55,14 @@ module Grape
|
|
|
48
55
|
def inherit_from(parent)
|
|
49
56
|
return if parent.nil?
|
|
50
57
|
|
|
51
|
-
|
|
58
|
+
@parent = parent
|
|
52
59
|
|
|
53
60
|
namespace_inheritable.inherited_values = parent.namespace_inheritable
|
|
54
61
|
namespace_stackable.inherited_values = parent.namespace_stackable
|
|
55
62
|
namespace_reverse_stackable.inherited_values = parent.namespace_reverse_stackable
|
|
56
|
-
|
|
63
|
+
@route = parent.route.merge(route)
|
|
57
64
|
|
|
58
|
-
point_in_time_copies
|
|
65
|
+
@point_in_time_copies&.each { |cloned_one| cloned_one.inherit_from parent }
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
# Create a point-in-time copy of this settings instance, with clones of
|
|
@@ -63,19 +70,11 @@ module Grape
|
|
|
63
70
|
# changed via #inherit_from, it will copy that inheritence to any copies
|
|
64
71
|
# which were made.
|
|
65
72
|
def point_in_time_copy
|
|
66
|
-
self.class.new
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
new_setting.namespace_inheritable = namespace_inheritable.clone
|
|
72
|
-
new_setting.namespace_stackable = namespace_stackable.clone
|
|
73
|
-
new_setting.namespace_reverse_stackable = namespace_reverse_stackable.clone
|
|
74
|
-
new_setting.route = route.clone
|
|
75
|
-
new_setting.api_class = api_class
|
|
76
|
-
|
|
77
|
-
new_setting.inherit_from(parent)
|
|
78
|
-
end
|
|
73
|
+
new_setting = self.class.new
|
|
74
|
+
point_in_time_copies << new_setting
|
|
75
|
+
new_setting.copy_state_from(self)
|
|
76
|
+
new_setting.inherit_from(parent)
|
|
77
|
+
new_setting
|
|
79
78
|
end
|
|
80
79
|
|
|
81
80
|
# Resets the instance store of per-route settings.
|
|
@@ -96,12 +95,30 @@ module Grape
|
|
|
96
95
|
}
|
|
97
96
|
end
|
|
98
97
|
|
|
98
|
+
def ==(other)
|
|
99
|
+
other.is_a?(self.class) && to_hash == other.to_hash
|
|
100
|
+
end
|
|
101
|
+
alias eql? ==
|
|
102
|
+
|
|
99
103
|
def namespace_stackable_with_hash(key)
|
|
100
104
|
data = namespace_stackable[key]
|
|
101
105
|
return if data.blank?
|
|
102
106
|
|
|
103
107
|
data.each_with_object({}) { |value, result| result.deep_merge!(value) }
|
|
104
108
|
end
|
|
109
|
+
|
|
110
|
+
protected
|
|
111
|
+
|
|
112
|
+
# Used by +point_in_time_copy+ to populate a freshly-built instance
|
|
113
|
+
# with cloned state from another instance of the same class.
|
|
114
|
+
def copy_state_from(source)
|
|
115
|
+
@namespace = source.namespace.clone
|
|
116
|
+
@namespace_inheritable = source.namespace_inheritable.clone
|
|
117
|
+
@namespace_stackable = source.namespace_stackable.clone
|
|
118
|
+
@namespace_reverse_stackable = source.namespace_reverse_stackable.clone
|
|
119
|
+
@route = source.route.clone
|
|
120
|
+
@api_class = source.api_class
|
|
121
|
+
end
|
|
105
122
|
end
|
|
106
123
|
end
|
|
107
124
|
end
|
|
@@ -4,11 +4,13 @@ module Grape
|
|
|
4
4
|
module Util
|
|
5
5
|
class InheritableValues < BaseInheritable
|
|
6
6
|
def [](name)
|
|
7
|
-
|
|
7
|
+
return @inherited_values[name] unless @new_values
|
|
8
|
+
|
|
9
|
+
@new_values.fetch(name) { @inherited_values[name] }
|
|
8
10
|
end
|
|
9
11
|
|
|
10
12
|
def []=(name, value)
|
|
11
|
-
new_values[name] = value
|
|
13
|
+
(@new_values ||= {})[name] = value
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
def merge(new_hash)
|
|
@@ -22,7 +24,9 @@ module Grape
|
|
|
22
24
|
protected
|
|
23
25
|
|
|
24
26
|
def values
|
|
25
|
-
@inherited_values.merge(@new_values)
|
|
27
|
+
return @inherited_values.merge(@new_values) if @new_values && !@new_values.empty?
|
|
28
|
+
|
|
29
|
+
@inherited_values.is_a?(Hash) ? @inherited_values.dup : @inherited_values.to_hash
|
|
26
30
|
end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Util
|
|
5
|
+
module Lazy
|
|
6
|
+
# Abstract parent for lazy wrappers used by the remount/configuration
|
|
7
|
+
# machinery. Call sites can type-check with +is_a?(Grape::Util::Lazy::Base)+
|
|
8
|
+
# instead of enumerating the concrete subclasses.
|
|
9
|
+
class Base
|
|
10
|
+
def to_s
|
|
11
|
+
evaluate.to_s
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Util
|
|
5
5
|
module Lazy
|
|
6
|
-
class Block
|
|
6
|
+
class Block < Base
|
|
7
7
|
def initialize(&new_block)
|
|
8
|
+
super()
|
|
8
9
|
@block = new_block
|
|
9
10
|
end
|
|
10
11
|
|
|
@@ -15,14 +16,6 @@ module Grape
|
|
|
15
16
|
def evaluate
|
|
16
17
|
@block.call({})
|
|
17
18
|
end
|
|
18
|
-
|
|
19
|
-
def lazy?
|
|
20
|
-
true
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def to_s
|
|
24
|
-
evaluate.to_s
|
|
25
|
-
end
|
|
26
19
|
end
|
|
27
20
|
end
|
|
28
21
|
end
|