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
|
@@ -6,66 +6,76 @@ module Grape
|
|
|
6
6
|
# Specify the default format for the API's serializers.
|
|
7
7
|
# May be `:json` or `:txt` (default).
|
|
8
8
|
def default_format(new_format = nil)
|
|
9
|
-
return inheritable_setting.
|
|
9
|
+
return inheritable_setting.default_format if new_format.nil?
|
|
10
10
|
|
|
11
|
-
inheritable_setting.
|
|
11
|
+
inheritable_setting.default_format = new_format.to_sym
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
# Specify the format for the API's serializers.
|
|
15
15
|
# May be `:json`, `:xml`, `:txt`, etc.
|
|
16
16
|
def format(new_format = nil)
|
|
17
|
-
return inheritable_setting.
|
|
17
|
+
return inheritable_setting.format if new_format.nil?
|
|
18
18
|
|
|
19
19
|
symbolic_new_format = new_format.to_sym
|
|
20
|
-
inheritable_setting.
|
|
21
|
-
inheritable_setting.
|
|
20
|
+
inheritable_setting.format = symbolic_new_format
|
|
21
|
+
inheritable_setting.default_error_formatter = Grape::ErrorFormatter.formatter_for(symbolic_new_format)
|
|
22
22
|
|
|
23
23
|
content_type = content_types[symbolic_new_format]
|
|
24
24
|
raise Grape::Exceptions::MissingMimeType.new(new_format) unless content_type
|
|
25
25
|
|
|
26
|
-
inheritable_setting.
|
|
26
|
+
inheritable_setting.add_content_type(symbolic_new_format, content_type)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Specify a custom formatter for a content-type.
|
|
30
30
|
def formatter(content_type, new_formatter)
|
|
31
|
-
inheritable_setting.
|
|
31
|
+
inheritable_setting.add_formatter(content_type.to_sym, new_formatter)
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Specify a custom parser for a content-type.
|
|
35
35
|
def parser(content_type, new_parser)
|
|
36
|
-
inheritable_setting.
|
|
36
|
+
inheritable_setting.add_parser(content_type.to_sym, new_parser)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
# Specify a default error formatter.
|
|
39
|
+
# Specify a default error formatter, by the name it is registered under.
|
|
40
|
+
# A name nothing is registered for used to be stored as
|
|
41
|
+
# +ErrorFormatter::Txt+ — the fallback the lookup applied — so a typo
|
|
42
|
+
# read back as a working setting and silently rendered errors as text.
|
|
40
43
|
def default_error_formatter(new_formatter_name = nil)
|
|
41
|
-
return inheritable_setting.
|
|
44
|
+
return inheritable_setting.default_error_formatter if new_formatter_name.nil?
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
formatter = Grape::ErrorFormatter.formatter_for(new_formatter_name)
|
|
47
|
+
raise Grape::Exceptions::UnknownErrorFormatter.new(new_formatter_name) if formatter.nil?
|
|
48
|
+
|
|
49
|
+
inheritable_setting.default_error_formatter = formatter
|
|
45
50
|
end
|
|
46
51
|
|
|
52
|
+
# Specify a custom error formatter for a format, passed positionally or
|
|
53
|
+
# as +with:+. A nil formatter used to be registered as-is, which is the
|
|
54
|
+
# one thing the registration cannot mean: the format then resolves as if
|
|
55
|
+
# the call had never been made. Reject it here, where the mistake is.
|
|
47
56
|
def error_formatter(format, options = nil, with: nil)
|
|
48
57
|
formatter = with || options
|
|
49
|
-
|
|
58
|
+
raise ArgumentError, "error_formatter `#{format.inspect}` requires a formatter, given positionally or as `with:`" if formatter.nil?
|
|
59
|
+
|
|
60
|
+
inheritable_setting.add_error_formatter(format.to_sym, formatter)
|
|
50
61
|
end
|
|
51
62
|
|
|
52
63
|
# Specify additional content-types, e.g.:
|
|
53
64
|
# content_type :xls, 'application/vnd.ms-excel'
|
|
54
65
|
def content_type(key, val)
|
|
55
|
-
inheritable_setting.
|
|
66
|
+
inheritable_setting.add_content_type(key.to_sym, val)
|
|
56
67
|
end
|
|
57
68
|
|
|
58
69
|
# All available content types.
|
|
59
70
|
def content_types
|
|
60
|
-
|
|
61
|
-
Grape::ContentTypes.content_types_for c_types
|
|
71
|
+
Grape::ContentTypes.content_types_for(inheritable_setting.content_types)
|
|
62
72
|
end
|
|
63
73
|
|
|
64
74
|
# Specify the default status code for errors.
|
|
65
75
|
def default_error_status(new_status = nil)
|
|
66
|
-
return inheritable_setting.
|
|
76
|
+
return inheritable_setting.default_error_status if new_status.nil?
|
|
67
77
|
|
|
68
|
-
inheritable_setting.
|
|
78
|
+
inheritable_setting.default_error_status = new_status
|
|
69
79
|
end
|
|
70
80
|
|
|
71
81
|
# Allows you to rescue certain exceptions that occur to return
|
|
@@ -99,32 +109,20 @@ module Grape
|
|
|
99
109
|
def rescue_from(*args, with: nil, rescue_subclasses: true, backtrace: false, original_exception: false, &block)
|
|
100
110
|
handler = extract_handler(args, with:, block:)
|
|
101
111
|
meta_selector = (args & META_RESCUE_SELECTORS).first
|
|
102
|
-
raise ArgumentError, "rescue_from
|
|
103
|
-
|
|
104
|
-
namespace_inheritable = nil
|
|
105
|
-
arg = nil
|
|
106
|
-
|
|
107
|
-
if args.one?
|
|
108
|
-
arg = args.first
|
|
109
|
-
namespace_inheritable = inheritable_setting.namespace_inheritable
|
|
110
|
-
end
|
|
112
|
+
raise ArgumentError, "rescue_from `#{meta_selector.inspect}` does not accept additional arguments" if meta_selector && args.size > 1
|
|
111
113
|
|
|
112
|
-
case
|
|
114
|
+
case meta_selector
|
|
113
115
|
when :all
|
|
114
|
-
|
|
115
|
-
namespace_inheritable[:all_rescue_handler] = handler
|
|
116
|
+
inheritable_setting.add_all_rescue_handler(handler)
|
|
116
117
|
when :grape_exceptions
|
|
117
|
-
|
|
118
|
-
namespace_inheritable[:rescue_grape_exceptions] = true
|
|
119
|
-
namespace_inheritable[:grape_exceptions_rescue_handler] = handler
|
|
118
|
+
inheritable_setting.add_grape_exceptions_rescue_handler(handler)
|
|
120
119
|
when :internal_grape_exceptions
|
|
121
|
-
|
|
120
|
+
inheritable_setting.add_internal_grape_exceptions_rescue_handler(handler)
|
|
122
121
|
else
|
|
123
|
-
|
|
124
|
-
inheritable_setting.namespace_reverse_stackable[handler_type] = args.to_h { |klass| [klass, handler] }
|
|
122
|
+
inheritable_setting.add_rescue_handlers(args.to_h { |klass| [klass, handler] }, subclasses: rescue_subclasses)
|
|
125
123
|
end
|
|
126
124
|
|
|
127
|
-
inheritable_setting.
|
|
125
|
+
inheritable_setting.add_rescue_options(RescueOptions.new(backtrace:, original_exception:))
|
|
128
126
|
end
|
|
129
127
|
|
|
130
128
|
# Allows you to specify a default representation entity for a
|
|
@@ -150,7 +148,7 @@ module Grape
|
|
|
150
148
|
def represent(model_class, with:)
|
|
151
149
|
raise Grape::Exceptions::InvalidWithOptionForRepresent.new unless with.is_a?(Class)
|
|
152
150
|
|
|
153
|
-
inheritable_setting.
|
|
151
|
+
inheritable_setting.add_representation(model_class, with)
|
|
154
152
|
end
|
|
155
153
|
|
|
156
154
|
private
|
|
@@ -165,7 +163,7 @@ module Grape
|
|
|
165
163
|
case with
|
|
166
164
|
when Proc, Symbol then with
|
|
167
165
|
when String then with.to_sym
|
|
168
|
-
else raise ArgumentError, "with:
|
|
166
|
+
else raise ArgumentError, "with: `#{with.class}`, expected Symbol, String or Proc"
|
|
169
167
|
end
|
|
170
168
|
end
|
|
171
169
|
end
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module DSL
|
|
5
5
|
# Immutable value object holding the response-shaping booleans accepted
|
|
6
|
-
# by +Grape::DSL::RequestResponse#rescue_from+.
|
|
7
|
-
# inheritable settings
|
|
8
|
-
#
|
|
6
|
+
# by +Grape::DSL::RequestResponse#rescue_from+. Recorded on the
|
|
7
|
+
# inheritable settings via +Grape::Util::InheritableSetting#add_rescue_options+
|
|
8
|
+
# (the nearest scope's latest registration wins on read, see
|
|
9
|
+
# +#rescue_options+) and delegated to by +Grape::Middleware::Error+ (which forwards
|
|
9
10
|
# +backtrace+/+original_exception+ to the formatter as
|
|
10
11
|
# +include_backtrace+/+include_original_exception+).
|
|
11
12
|
#
|
|
@@ -13,8 +14,8 @@ module Grape
|
|
|
13
14
|
# signature on purpose: keeping them on both sides means each entry point
|
|
14
15
|
# is self-documenting without needing to import a shared constant — the
|
|
15
16
|
# DSL signature shows what a user sees in the IDE, and the Data object
|
|
16
|
-
# has working defaults when constructed directly (
|
|
17
|
-
#
|
|
17
|
+
# has working defaults when constructed directly (spec fixtures, a
|
|
18
|
+
# middleware built by hand). The two must stay in lockstep.
|
|
18
19
|
RescueOptions = Data.define(:backtrace, :original_exception) do
|
|
19
20
|
def initialize(backtrace: false, original_exception: false)
|
|
20
21
|
super
|
data/lib/grape/dsl/routing.rb
CHANGED
|
@@ -16,9 +16,9 @@ module Grape
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def cascade(value = nil)
|
|
19
|
-
return inheritable_setting.
|
|
19
|
+
return inheritable_setting.cascade_defined? ? inheritable_setting.cascade : true if value.nil?
|
|
20
20
|
|
|
21
|
-
inheritable_setting.
|
|
21
|
+
inheritable_setting.cascade = value
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# Specify an API version.
|
|
@@ -72,13 +72,13 @@ module Grape
|
|
|
72
72
|
|
|
73
73
|
if block
|
|
74
74
|
within_namespace do
|
|
75
|
-
inheritable_setting.
|
|
76
|
-
inheritable_setting.
|
|
75
|
+
inheritable_setting.version = requested_versions
|
|
76
|
+
inheritable_setting.version_options = options
|
|
77
77
|
instance_eval(&block)
|
|
78
78
|
end
|
|
79
79
|
else
|
|
80
|
-
inheritable_setting.
|
|
81
|
-
inheritable_setting.
|
|
80
|
+
inheritable_setting.version = requested_versions
|
|
81
|
+
inheritable_setting.version_options = options
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
@versions&.last
|
|
@@ -86,9 +86,9 @@ module Grape
|
|
|
86
86
|
|
|
87
87
|
# Define a root URL prefix for your entire API.
|
|
88
88
|
def prefix(prefix = nil)
|
|
89
|
-
return inheritable_setting.
|
|
89
|
+
return inheritable_setting.root_prefix if prefix.nil?
|
|
90
90
|
|
|
91
|
-
inheritable_setting.
|
|
91
|
+
inheritable_setting.root_prefix = prefix.to_s
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
# Create a scope without affecting the URL.
|
|
@@ -102,33 +102,40 @@ module Grape
|
|
|
102
102
|
end
|
|
103
103
|
|
|
104
104
|
def build_with(build_with)
|
|
105
|
-
inheritable_setting.
|
|
105
|
+
inheritable_setting.build_params_with = build_with
|
|
106
106
|
end
|
|
107
107
|
|
|
108
108
|
# Do not route HEAD requests to GET requests automatically.
|
|
109
109
|
def do_not_route_head!
|
|
110
|
-
inheritable_setting.
|
|
110
|
+
inheritable_setting.do_not_route_head!
|
|
111
111
|
end
|
|
112
112
|
|
|
113
113
|
# Do not automatically route OPTIONS.
|
|
114
114
|
def do_not_route_options!
|
|
115
|
-
inheritable_setting.
|
|
115
|
+
inheritable_setting.do_not_route_options!
|
|
116
116
|
end
|
|
117
117
|
|
|
118
118
|
def lint!
|
|
119
|
-
inheritable_setting.
|
|
119
|
+
inheritable_setting.lint!
|
|
120
120
|
end
|
|
121
121
|
|
|
122
122
|
def do_not_document!
|
|
123
|
-
inheritable_setting.
|
|
123
|
+
inheritable_setting.do_not_document!
|
|
124
124
|
end
|
|
125
125
|
|
|
126
|
-
def mount(mounts,
|
|
127
|
-
|
|
128
|
-
|
|
126
|
+
def mount(mounts, opts = {})
|
|
127
|
+
mount_opts = opts
|
|
128
|
+
if opts[:refresh_already_mounted]
|
|
129
|
+
Grape.deprecator.warn('`refresh_already_mounted` is not a `mount` option and will be ignored in a future release.')
|
|
130
|
+
drop_endpoints_mounted_for(mounts)
|
|
131
|
+
# Dropped before the recursion below re-enters with the same options,
|
|
132
|
+
# so a Grape API does not warn once per mount and once per instance.
|
|
133
|
+
mount_opts = opts.except(:refresh_already_mounted)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
normalize_mounts(mounts).each_pair do |app, path|
|
|
129
137
|
if app.respond_to?(:mount_instance)
|
|
130
|
-
|
|
131
|
-
mount({ app.mount_instance(configuration: opts_with) => path }, *opts)
|
|
138
|
+
mount({ app.mount_instance(configuration: mount_opts[:with] || {}) => path }, mount_opts)
|
|
132
139
|
next
|
|
133
140
|
end
|
|
134
141
|
in_setting = inheritable_setting
|
|
@@ -137,7 +144,7 @@ module Grape
|
|
|
137
144
|
# instantiated Grape::API::Instance (vs. a bare Rack app).
|
|
138
145
|
if app.is_a?(Grape::Mountable)
|
|
139
146
|
mount_path = Grape::Util::PathNormalizer.call(path)
|
|
140
|
-
app.top_level_setting.
|
|
147
|
+
app.top_level_setting.add_mount_path(mount_path)
|
|
141
148
|
|
|
142
149
|
app.inherit_settings(inheritable_setting)
|
|
143
150
|
|
|
@@ -147,23 +154,13 @@ module Grape
|
|
|
147
154
|
change!
|
|
148
155
|
end
|
|
149
156
|
|
|
150
|
-
# When trying to mount multiple times the same endpoint, remove the previous ones
|
|
151
|
-
# from the list of endpoints if refresh_already_mounted parameter is true
|
|
152
|
-
refresh_already_mounted = opts.any? ? opts.first[:refresh_already_mounted] : false
|
|
153
|
-
if refresh_already_mounted && !endpoints.empty?
|
|
154
|
-
endpoints.delete_if do |endpoint|
|
|
155
|
-
endpoint.options[:app].to_s == app.to_s
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
157
|
endpoints << Grape::Endpoint.new(
|
|
160
158
|
in_setting,
|
|
161
|
-
|
|
159
|
+
http_methods: :any,
|
|
162
160
|
path:,
|
|
163
161
|
app:,
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for: self
|
|
162
|
+
anchor: false,
|
|
163
|
+
api: self
|
|
167
164
|
)
|
|
168
165
|
end
|
|
169
166
|
end
|
|
@@ -173,6 +170,9 @@ module Grape
|
|
|
173
170
|
#
|
|
174
171
|
# @param methods [HTTP Verb] One or more HTTP verbs that are accepted by this route. Set to `:any` if you want any verb to be accepted.
|
|
175
172
|
# @param paths [String] One or more strings representing the URL segment(s) for this route.
|
|
173
|
+
# @param requirements [Hash] Regular-expression constraints for named path params; the route matches only when every requirement is satisfied.
|
|
174
|
+
# @param anchor [Boolean] Whether the route is anchored to the whole path. Defaults to `true`; pass `false` for catch-all routes (e.g. `'/(*:path)'`).
|
|
175
|
+
# @param route_options [Hash] Any additional custom options, carried through to `route.options`.
|
|
176
176
|
#
|
|
177
177
|
# @example Defining a basic route.
|
|
178
178
|
# class MyAPI < Grape::API
|
|
@@ -180,19 +180,27 @@ module Grape
|
|
|
180
180
|
# {hello: 'world'}
|
|
181
181
|
# end
|
|
182
182
|
# end
|
|
183
|
-
def route(methods, paths = ['/'],
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
def route(methods, paths = ['/'], requirements: nil, anchor: true, **route_options, &)
|
|
184
|
+
validate_requirements!(requirements)
|
|
185
|
+
|
|
186
|
+
http_methods = methods == :any ? '*' : methods
|
|
187
|
+
endpoint_description = inheritable_setting.route_description
|
|
188
|
+
|
|
189
|
+
# +params+, +requirements+ and +anchor+ each travel as their own endpoint
|
|
190
|
+
# input; the route-options bag keeps the description's other keys
|
|
191
|
+
# (+success+, +tags+, …) plus any custom options.
|
|
192
|
+
params = prepare_params(endpoint_description[:params])
|
|
193
|
+
all_route_options = endpoint_description.except(:params)
|
|
189
194
|
all_route_options.deep_merge!(route_options) if route_options.present?
|
|
190
195
|
|
|
191
196
|
new_endpoint = Grape::Endpoint.new(
|
|
192
197
|
inheritable_setting,
|
|
193
|
-
|
|
198
|
+
http_methods:,
|
|
194
199
|
path: paths,
|
|
195
|
-
|
|
200
|
+
api: self,
|
|
201
|
+
params:,
|
|
202
|
+
requirements:,
|
|
203
|
+
anchor:,
|
|
196
204
|
route_options: all_route_options,
|
|
197
205
|
&
|
|
198
206
|
)
|
|
@@ -204,7 +212,7 @@ module Grape
|
|
|
204
212
|
|
|
205
213
|
Grape::HTTP_SUPPORTED_METHODS.each do |supported_method|
|
|
206
214
|
define_method supported_method.downcase do |path = '/', **options, &block|
|
|
207
|
-
route(supported_method, path, options, &block)
|
|
215
|
+
route(supported_method, path, **options, &block)
|
|
208
216
|
end
|
|
209
217
|
end
|
|
210
218
|
|
|
@@ -221,11 +229,13 @@ module Grape
|
|
|
221
229
|
# end
|
|
222
230
|
# end
|
|
223
231
|
def namespace(space = nil, requirements: nil, **options, &block)
|
|
224
|
-
return
|
|
232
|
+
return inheritable_setting.namespace_path unless space || block
|
|
233
|
+
|
|
234
|
+
validate_requirements!(requirements)
|
|
225
235
|
|
|
226
236
|
within_namespace do
|
|
227
237
|
nest(block) do
|
|
228
|
-
inheritable_setting.
|
|
238
|
+
inheritable_setting.add_namespace(Grape::Namespace.new(space, requirements:, **options)) if space
|
|
229
239
|
end
|
|
230
240
|
end
|
|
231
241
|
end
|
|
@@ -244,15 +254,20 @@ module Grape
|
|
|
244
254
|
# in your API.
|
|
245
255
|
#
|
|
246
256
|
# @param param [Symbol] The name of the parameter you wish to declare.
|
|
247
|
-
# @option options [Regexp]
|
|
257
|
+
# @option options [Regexp, Class, Symbol] The constraint the declared parameter must meet — a Regexp, or a capture type such as +Integer+.
|
|
248
258
|
def route_param(param, requirements: nil, type: nil, **, &)
|
|
249
|
-
|
|
259
|
+
# The param is named here, so the constraint is its own: nest whatever
|
|
260
|
+
# it is, not just a Regexp. A Hash would name the param twice, or key a
|
|
261
|
+
# capture this namespace does not introduce, and belongs on +namespace+.
|
|
262
|
+
raise ArgumentError, "route_param :`#{param}` constrains :`#{param}`; pass the constraint itself, or a Hash of requirements to the enclosing namespace" if requirements.respond_to?(:to_hash)
|
|
263
|
+
|
|
264
|
+
param_requirements = requirements ? { param.to_sym => requirements } : requirements
|
|
250
265
|
|
|
251
266
|
Grape::Validations::ParamsScope.new(api: self) do
|
|
252
267
|
requires param, type: type
|
|
253
268
|
end if type
|
|
254
269
|
|
|
255
|
-
namespace(":#{param}", requirements
|
|
270
|
+
namespace(":#{param}", requirements: param_requirements, **, &)
|
|
256
271
|
end
|
|
257
272
|
|
|
258
273
|
# @return array of defined versions
|
|
@@ -262,6 +277,24 @@ module Grape
|
|
|
262
277
|
|
|
263
278
|
private
|
|
264
279
|
|
|
280
|
+
# Requirements are keyed by param name and merged across namespaces, so
|
|
281
|
+
# anything else has nothing to attach to. Rejected here rather than at
|
|
282
|
+
# the merge, which runs on the first request that builds the routes.
|
|
283
|
+
def validate_requirements!(requirements)
|
|
284
|
+
return if requirements.nil? || requirements.respond_to?(:to_hash)
|
|
285
|
+
|
|
286
|
+
raise ArgumentError, "requirements must be a Hash of param name => constraint, got `#{requirements.class}`"
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Compose a route's params: the declared params (+params do … end+) deep-merged
|
|
290
|
+
# with any documented alongside +desc ..., params:+ (+description_params+).
|
|
291
|
+
def prepare_params(description_params)
|
|
292
|
+
endpoint_params = inheritable_setting.params_documentation || {}
|
|
293
|
+
return endpoint_params if description_params.blank?
|
|
294
|
+
|
|
295
|
+
endpoint_params.deep_merge(description_params)
|
|
296
|
+
end
|
|
297
|
+
|
|
265
298
|
# Remove all defined routes.
|
|
266
299
|
def reset_routes!
|
|
267
300
|
endpoints.each(&:reset_routes!)
|
|
@@ -272,9 +305,43 @@ module Grape
|
|
|
272
305
|
@endpoints = []
|
|
273
306
|
end
|
|
274
307
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
308
|
+
# Re-mount +mounts+, replacing any endpoint already mounted for the same
|
|
309
|
+
# base API rather than adding a second one. Called by
|
|
310
|
+
# {Grape::API.refresh_mount_step} when a class-level method runs after the
|
|
311
|
+
# API was mounted.
|
|
312
|
+
def refresh_mounted_api(mounts, opts = {})
|
|
313
|
+
drop_endpoints_mounted_for(mounts)
|
|
314
|
+
mount(mounts, opts)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# A mounted Grape API is stored as the throwaway instance +mount+ built
|
|
318
|
+
# for it, never as the class that was written, so endpoints are matched on
|
|
319
|
+
# the base API both of them share.
|
|
320
|
+
def drop_endpoints_mounted_for(mounts)
|
|
321
|
+
normalize_mounts(mounts).each_key do |app|
|
|
322
|
+
endpoints.delete_if { |endpoint| same_mounted_app?(endpoint.mounted_app, app) }
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# A bare app mounts at the root. The test is +Hash+ rather than
|
|
327
|
+
# +respond_to?(:each_pair)+ because a Struct or an OpenStruct answers that
|
|
328
|
+
# too, and neither can express an app => path mapping — their keys are
|
|
329
|
+
# member names. Reading one as a mapping would silently mount nonsense
|
|
330
|
+
# instead of mounting the app itself.
|
|
331
|
+
def normalize_mounts(mounts)
|
|
332
|
+
mounts.is_a?(Hash) ? mounts : { mounts => '/' }
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Two mounts refer to the same app when they share the same base Grape
|
|
336
|
+
# API. +mount+ turns every mounted Grape API into a throwaway
|
|
337
|
+
# +mount_instance+ (a fresh +Class.new+ per mount), so object identity
|
|
338
|
+
# never holds across mounts; comparing the base is the real signal.
|
|
339
|
+
# Plain Rack apps have no base and are mounted as-is, so they fall back
|
|
340
|
+
# to object identity.
|
|
341
|
+
def same_mounted_app?(mounted, app)
|
|
342
|
+
return mounted.base.equal?(app.base) if mounted.respond_to?(:base) && app.respond_to?(:base)
|
|
343
|
+
|
|
344
|
+
mounted.equal?(app)
|
|
278
345
|
end
|
|
279
346
|
|
|
280
347
|
# Execute first the provided block, then each of the
|
data/lib/grape/dsl/settings.rb
CHANGED
|
@@ -17,8 +17,8 @@ module Grape
|
|
|
17
17
|
# subclass, allowing to define the schema inline. When the
|
|
18
18
|
# +contract+ parameter is a schema, it will be used as a parent. Optional.
|
|
19
19
|
def contract(contract = nil, &block)
|
|
20
|
-
raise ArgumentError, '
|
|
21
|
-
raise ArgumentError, '
|
|
20
|
+
raise ArgumentError, 'either contract or block must be provided' unless contract || block
|
|
21
|
+
raise ArgumentError, 'cannot inherit from contract, only schema' if block && contract.respond_to?(:schema)
|
|
22
22
|
|
|
23
23
|
Grape::Validations::ContractScope.new(self, contract, &block)
|
|
24
24
|
end
|
|
@@ -44,7 +44,7 @@ module Grape
|
|
|
44
44
|
# # whatever
|
|
45
45
|
# end
|
|
46
46
|
def reset_validations!
|
|
47
|
-
inheritable_setting.
|
|
47
|
+
inheritable_setting.reset_validations!
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
end
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module DSL
|
|
5
5
|
# Immutable value object holding the resolved options from
|
|
6
6
|
# +Grape::DSL::Routing#version+. Stored on the inheritable settings as
|
|
7
|
-
# +
|
|
7
|
+
# +Grape::Util::InheritableSetting#version_options+ and read by internal call
|
|
8
8
|
# sites (`Path`, `Endpoint`, `API::Instance#cascade?`,
|
|
9
9
|
# `Middleware::Versioner::Base`) via accessors.
|
|
10
10
|
#
|
|
@@ -12,8 +12,8 @@ module Grape
|
|
|
12
12
|
# signature on purpose: keeping them on both sides means each entry point
|
|
13
13
|
# is self-documenting without needing to import a shared constant — the
|
|
14
14
|
# DSL signature shows what a user sees in the IDE, and the Data object
|
|
15
|
-
# has working defaults when constructed directly (
|
|
16
|
-
#
|
|
15
|
+
# has working defaults when constructed directly (spec fixtures, a
|
|
16
|
+
# middleware built by hand). The two must stay in lockstep.
|
|
17
17
|
VersionOptions = Data.define(:using, :cascade, :parameter, :strict, :vendor) do
|
|
18
18
|
def initialize(using: :path, cascade: true, parameter: 'apiver', strict: false, vendor: nil)
|
|
19
19
|
super
|
|
@@ -6,13 +6,19 @@ module Grape
|
|
|
6
6
|
# +Grape::Endpoint.new+. Internal to {Grape::Endpoint}, which builds it
|
|
7
7
|
# from the +**options+ Hash in #initialize so the public +options+ reader
|
|
8
8
|
# stays a plain Hash for downstream gems (e.g. grape-swagger).
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
Options = Data.define(:path, :http_methods, :api, :route_options, :app, :params, :requirements, :anchor) do
|
|
10
|
+
def initialize(path:, http_methods:, api:, route_options: {}, app: nil, params: {}, requirements: nil, anchor: true)
|
|
11
|
+
# +Array()+ hands back the very Array it was given, so defaulting an
|
|
12
|
+
# empty one by appending would append to the caller's Array — and raise
|
|
13
|
+
# FrozenError on a frozen one. Build a new Array instead of growing
|
|
14
|
+
# theirs: nothing about constructing an endpoint should be visible in
|
|
15
|
+
# the path the caller passed in.
|
|
16
|
+
paths = Array(path)
|
|
17
|
+
super(
|
|
18
|
+
path: paths.presence || ['/'],
|
|
19
|
+
http_methods: Array(http_methods),
|
|
20
|
+
api:, route_options:, app:, params:, requirements:, anchor:
|
|
21
|
+
)
|
|
16
22
|
end
|
|
17
23
|
end
|
|
18
24
|
end
|