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/api/instance.rb
CHANGED
|
@@ -21,11 +21,13 @@ module Grape
|
|
|
21
21
|
Boolean = Grape::API::Boolean
|
|
22
22
|
|
|
23
23
|
class << self
|
|
24
|
-
extend Forwardable
|
|
25
|
-
|
|
26
24
|
attr_accessor :configuration
|
|
27
25
|
|
|
28
|
-
|
|
26
|
+
def to_s
|
|
27
|
+
@base&.to_s || super
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :base
|
|
29
31
|
|
|
30
32
|
def base=(grape_api)
|
|
31
33
|
@base = grape_api
|
|
@@ -52,20 +54,19 @@ module Grape
|
|
|
52
54
|
# the headers, and the body. See [the rack specification]
|
|
53
55
|
# (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
|
|
54
56
|
def call(env)
|
|
55
|
-
compile
|
|
56
|
-
@instance.call(env)
|
|
57
|
+
compile!.call(env)
|
|
57
58
|
end
|
|
58
59
|
|
|
60
|
+
# Returns the compiled instance, so callers serve the one they
|
|
61
|
+
# compiled rather than re-reading @instance — +change!+ can nil it
|
|
62
|
+
# between the two reads (see #call / #recognize_path).
|
|
59
63
|
def compile!
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
LOCK.synchronize { @instance ||= new }
|
|
64
|
+
@instance || LOCK.synchronize { @instance ||= new }
|
|
63
65
|
end
|
|
64
66
|
|
|
65
67
|
# see Grape::Router#recognize_path
|
|
66
68
|
def recognize_path(path)
|
|
67
|
-
compile
|
|
68
|
-
@instance.router.recognize_path(path)
|
|
69
|
+
compile!.router.recognize_path(path)
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
# Wipe the compiled API so we can recompile after changes were made.
|
|
@@ -81,7 +82,7 @@ module Grape
|
|
|
81
82
|
# Propagate any inherited params down to our endpoints, and reset any
|
|
82
83
|
# compiled routes.
|
|
83
84
|
endpoints.each do |e|
|
|
84
|
-
e.inherit_settings(top_level_setting
|
|
85
|
+
e.inherit_settings(top_level_setting)
|
|
85
86
|
e.reset_routes!
|
|
86
87
|
end
|
|
87
88
|
|
|
@@ -110,13 +111,19 @@ module Grape
|
|
|
110
111
|
|
|
111
112
|
@router.compile!
|
|
112
113
|
@router.freeze
|
|
114
|
+
@cascade = resolve_cascade
|
|
113
115
|
end
|
|
114
116
|
|
|
115
117
|
# Handle a request. See Rack documentation for what `env` is.
|
|
116
118
|
def call(env)
|
|
117
119
|
status, headers, response = @router.call(env)
|
|
118
|
-
unless cascade
|
|
119
|
-
|
|
120
|
+
unless @cascade
|
|
121
|
+
# +merge!+, not +merge+: the latter is a `dup` plus a `merge!`, so the
|
|
122
|
+
# Header built on this line would be allocated only to be discarded.
|
|
123
|
+
# The copy stays because +headers+ can come from a mounted Rack app,
|
|
124
|
+
# which is free to hand back a frozen or shared Hash that the delete
|
|
125
|
+
# below must not reach into.
|
|
126
|
+
headers = Grape::Util::Header.new.merge!(headers)
|
|
120
127
|
headers.delete('X-Cascade')
|
|
121
128
|
end
|
|
122
129
|
|
|
@@ -131,12 +138,13 @@ module Grape
|
|
|
131
138
|
# In some applications (e.g. mounting grape on rails), one might need to trap
|
|
132
139
|
# errors from reaching upstream. This is effectivelly done by unsetting
|
|
133
140
|
# X-Cascade. Default :cascade is true.
|
|
141
|
+
#
|
|
142
|
+
# Resolved in the constructor rather than per request: answering it walks
|
|
143
|
+
# the whole scope chain twice, and it reads the same settings the routes
|
|
144
|
+
# were compiled and frozen from -- changing those has to go through
|
|
145
|
+
# +change!+, which discards this instance.
|
|
134
146
|
def cascade?
|
|
135
|
-
|
|
136
|
-
return namespace_inheritable[:cascade] if namespace_inheritable.key?(:cascade)
|
|
137
|
-
return namespace_inheritable[:version_options].cascade if namespace_inheritable[:version_options]
|
|
138
|
-
|
|
139
|
-
true
|
|
147
|
+
@cascade
|
|
140
148
|
end
|
|
141
149
|
|
|
142
150
|
reset!
|
|
@@ -152,18 +160,11 @@ module Grape
|
|
|
152
160
|
# contain already versioning information when using path versioning.
|
|
153
161
|
all_routes = self.class.endpoints.flat_map(&:routes)
|
|
154
162
|
|
|
155
|
-
|
|
156
|
-
# root-prefix/versioning keys stripped, so adding these routes won't
|
|
157
|
-
# prepend versioning information again. This used to delete those keys
|
|
158
|
-
# from the shared class-level settings and restore them in an ensure;
|
|
159
|
-
# a request served concurrently on another instance during a runtime
|
|
160
|
-
# recompile could observe the missing keys (e.g. via #cascade?). A
|
|
161
|
-
# local copy keeps that mutation off the shared object.
|
|
162
|
-
namespace_inheritable = self.class.inheritable_setting.namespace_inheritable.to_hash.except(*ROOT_PREFIX_VERSIONING_KEYS)
|
|
163
|
-
collect_route_config_per_pattern(all_routes, namespace_inheritable)
|
|
163
|
+
collect_route_config_per_pattern(all_routes)
|
|
164
164
|
end
|
|
165
165
|
|
|
166
|
-
def collect_route_config_per_pattern(all_routes
|
|
166
|
+
def collect_route_config_per_pattern(all_routes)
|
|
167
|
+
setting = self.class.inheritable_setting
|
|
167
168
|
routes_by_regexp = all_routes.group_by(&:pattern_regexp)
|
|
168
169
|
|
|
169
170
|
# Build the configuration based on the first endpoint and the collection of methods supported.
|
|
@@ -172,18 +173,24 @@ module Grape
|
|
|
172
173
|
|
|
173
174
|
last_route = routes.last # Most of the configuration is taken from the last endpoint
|
|
174
175
|
allowed_methods = routes.map(&:request_method)
|
|
175
|
-
allowed_methods |= [Rack::HEAD] if !
|
|
176
|
+
allowed_methods |= [Rack::HEAD] if !setting.do_not_route_head? && allowed_methods.include?(Rack::GET)
|
|
176
177
|
|
|
177
|
-
allow_header =
|
|
178
|
-
last_route.app.options_route_enabled = true unless
|
|
178
|
+
allow_header = setting.do_not_route_options? ? allowed_methods : [Rack::OPTIONS] | allowed_methods
|
|
179
|
+
last_route.app.options_route_enabled = true unless setting.do_not_route_options? || allowed_methods.include?(Rack::OPTIONS)
|
|
179
180
|
|
|
180
181
|
greedy_route = Grape::Router::GreedyRoute.new(last_route.pattern, endpoint: last_route.app, allow_header:)
|
|
181
182
|
@router.associate_routes(greedy_route)
|
|
182
183
|
end
|
|
183
184
|
end
|
|
184
185
|
|
|
185
|
-
|
|
186
|
-
|
|
186
|
+
# Backs {#cascade?}; called once, from the constructor.
|
|
187
|
+
def resolve_cascade
|
|
188
|
+
setting = self.class.inheritable_setting
|
|
189
|
+
return setting.cascade if setting.cascade_defined?
|
|
190
|
+
return setting.version_options.cascade if setting.version_options
|
|
191
|
+
|
|
192
|
+
true
|
|
193
|
+
end
|
|
187
194
|
end
|
|
188
195
|
end
|
|
189
196
|
end
|
data/lib/grape/content_types.rb
CHANGED
|
@@ -22,7 +22,20 @@ module Grape
|
|
|
22
22
|
def mime_types_for(from_settings)
|
|
23
23
|
return MIME_TYPES if from_settings == Grape::ContentTypes::DEFAULTS
|
|
24
24
|
|
|
25
|
-
from_settings
|
|
25
|
+
MimeTypesCache[from_settings]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Every format under both spellings in one plain Hash, so a lookup is a
|
|
29
|
+
# single +Hash#[]+. +HashWithIndifferentAccess+ converted the key on every
|
|
30
|
+
# read instead, and this is read two or three times per request — to
|
|
31
|
+
# negotiate the format, and again to set the response content type.
|
|
32
|
+
#
|
|
33
|
+
# Keys arrive as Symbols: the +content_type+ DSL symbolizes what it is
|
|
34
|
+
# given and the defaults are Symbols. The key is stored as it came too,
|
|
35
|
+
# so a middleware constructed directly with String keys still answers to
|
|
36
|
+
# either spelling, as the indifferent hash did.
|
|
37
|
+
def lookup_for(from_settings)
|
|
38
|
+
LookupCache[from_settings]
|
|
26
39
|
end
|
|
27
40
|
|
|
28
41
|
# The media type of a content-type header: the part before any `;`
|
|
@@ -36,5 +49,38 @@ module Grape
|
|
|
36
49
|
base = content_type.include?(';') ? content_type.split(';', 2).first : content_type
|
|
37
50
|
base.strip
|
|
38
51
|
end
|
|
52
|
+
|
|
53
|
+
# Both tables below are derived from nothing but the content-type registry,
|
|
54
|
+
# and one content-type-aware middleware is built per API instance — so an
|
|
55
|
+
# app mounting N APIs held N copies of tables it only ever reads. Keying
|
|
56
|
+
# the cache on the registry itself collapses them: Hash keys compare by
|
|
57
|
+
# value, so every API that registers the same content types shares one
|
|
58
|
+
# table.
|
|
59
|
+
#
|
|
60
|
+
# Both the key and the table are frozen: the caller's registry stays
|
|
61
|
+
# reachable (through +middleware.options[:content_types]+, among others)
|
|
62
|
+
# and mutating a live key would corrupt a cache that is now shared
|
|
63
|
+
# process-wide.
|
|
64
|
+
|
|
65
|
+
class MimeTypesCache < Grape::Util::Cache
|
|
66
|
+
def initialize
|
|
67
|
+
super
|
|
68
|
+
@cache = Hash.new do |h, from_settings|
|
|
69
|
+
h[from_settings.dup.freeze] = from_settings.invert.transform_keys! { |mime_type| Grape::ContentTypes.media_type(mime_type) }.freeze
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class LookupCache < Grape::Util::Cache
|
|
75
|
+
def initialize
|
|
76
|
+
super
|
|
77
|
+
@cache = Hash.new do |h, from_settings|
|
|
78
|
+
h[from_settings.dup.freeze] = from_settings.each_with_object({}) do |(format, media_type), lookup|
|
|
79
|
+
lookup[format] = media_type
|
|
80
|
+
lookup[format.is_a?(String) ? format.to_sym : format.to_s] = media_type
|
|
81
|
+
end.freeze
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
39
85
|
end
|
|
40
86
|
end
|
data/lib/grape/dry_types.rb
CHANGED
|
@@ -48,7 +48,7 @@ module Grape
|
|
|
48
48
|
def self.wrapped_dry_types_const_get(dry_type, type)
|
|
49
49
|
dry_type.const_get(type.name, false)
|
|
50
50
|
rescue NameError
|
|
51
|
-
raise ArgumentError, "type
|
|
51
|
+
raise ArgumentError, "type `#{type}` should support coercion via `[]`" unless type.respond_to?(:[])
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
end
|
data/lib/grape/dsl/callbacks.rb
CHANGED
|
@@ -9,15 +9,9 @@ module Grape
|
|
|
9
9
|
# after: execute the given block after the endpoint code has run except in unsuccessful
|
|
10
10
|
# finally: execute the given block after the endpoint code even if unsuccessful
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
after_validation: :after_validations,
|
|
16
|
-
after: :afters,
|
|
17
|
-
finally: :finallies
|
|
18
|
-
}.each do |method_name, plural_key|
|
|
19
|
-
define_method method_name do |&block|
|
|
20
|
-
inheritable_setting.namespace_stackable[plural_key] = block
|
|
12
|
+
%i[before before_validation after_validation after finally].each do |callback_name|
|
|
13
|
+
define_method callback_name do |&block|
|
|
14
|
+
inheritable_setting.add_callback(callback_name, block)
|
|
21
15
|
end
|
|
22
16
|
end
|
|
23
17
|
end
|
data/lib/grape/dsl/declared.rb
CHANGED
|
@@ -22,11 +22,11 @@ module Grape
|
|
|
22
22
|
def declared(passed_params, include_parent_namespaces: true, include_missing: true, evaluate_given: false, stringify: false)
|
|
23
23
|
raise MethodNotYetAvailable unless before_filter_passed
|
|
24
24
|
|
|
25
|
-
contract_key_map = inheritable_setting.
|
|
25
|
+
contract_key_map = inheritable_setting.contract_key_maps
|
|
26
26
|
handler = DeclaredParamsHandler.new(include_missing:, evaluate_given:, stringify:, contract_key_map:)
|
|
27
|
-
declared_params = include_parent_namespaces ? inheritable_setting.
|
|
28
|
-
renamed_params = inheritable_setting.
|
|
29
|
-
route_params =
|
|
27
|
+
declared_params = include_parent_namespaces ? inheritable_setting.route_declared_params : (inheritable_setting.declared_params.last || [])
|
|
28
|
+
renamed_params = inheritable_setting.route_renamed_params
|
|
29
|
+
route_params = config.params
|
|
30
30
|
|
|
31
31
|
handler.call(passed_params, declared_params, route_params, renamed_params)
|
|
32
32
|
end
|
data/lib/grape/dsl/desc.rb
CHANGED
|
@@ -19,6 +19,9 @@ module Grape
|
|
|
19
19
|
# successful call to this action
|
|
20
20
|
# @option options :http_codes [Array[Array]] possible HTTP codes this
|
|
21
21
|
# endpoint may return, with their meanings, in a 2d array
|
|
22
|
+
# @option options :default_response [Hash] the definition and entity used
|
|
23
|
+
# to present this endpoint's default response. Was `:default`, which is
|
|
24
|
+
# deprecated.
|
|
22
25
|
# @option options :named [String] a specific name to help find this route
|
|
23
26
|
# @option options :body_name [String] override the autogenerated body name param
|
|
24
27
|
# @option options :headers [Hash] HTTP headers this method can accept
|
|
@@ -50,21 +53,28 @@ module Grape
|
|
|
50
53
|
# # ...
|
|
51
54
|
# end
|
|
52
55
|
#
|
|
53
|
-
def desc(description,
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
def desc(description, **options, &config_block)
|
|
57
|
+
resolved_options =
|
|
58
|
+
if options.key?(:default)
|
|
59
|
+
Grape.deprecator.warn('The `default` option of `desc` is deprecated. Use `default_response` instead.')
|
|
60
|
+
# Rebuilt rather than mutated in place: an explicit +default_response+
|
|
61
|
+
# still wins, since the merged Hash is the one that keeps its key.
|
|
62
|
+
{ default_response: options[:default] }.merge(options.except(:default))
|
|
63
|
+
else
|
|
64
|
+
options
|
|
65
|
+
end
|
|
58
66
|
|
|
59
67
|
settings =
|
|
60
68
|
if config_block
|
|
61
69
|
endpoint_config = defined?(configuration) ? configuration : nil
|
|
62
70
|
Grape::Util::ApiDescription.new(description, endpoint_config, &config_block).settings
|
|
63
71
|
else
|
|
64
|
-
|
|
72
|
+
resolved_options.merge(description:)
|
|
65
73
|
end
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
# Only the route scope is consumed downstream (by +route+ and the
|
|
75
|
+
# route's readers, e.g. +http_codes+); the namespace scope was
|
|
76
|
+
# write-only, so it is no longer populated.
|
|
77
|
+
inheritable_setting.route_description = settings
|
|
68
78
|
end
|
|
69
79
|
end
|
|
70
80
|
end
|
data/lib/grape/dsl/entity.rb
CHANGED
|
@@ -34,7 +34,7 @@ module Grape
|
|
|
34
34
|
if key
|
|
35
35
|
representation = body&.merge(key => representation) || { key => representation }
|
|
36
36
|
elsif entity_class.present? && body
|
|
37
|
-
raise ArgumentError, "
|
|
37
|
+
raise ArgumentError, "representation of type `#{representation.class}` cannot be merged" unless representation.respond_to?(:merge)
|
|
38
38
|
|
|
39
39
|
representation = body.merge(representation)
|
|
40
40
|
end
|
|
@@ -45,34 +45,52 @@ module Grape
|
|
|
45
45
|
# Attempt to locate the Entity class for a given object, if not given
|
|
46
46
|
# explicitly. This is done by looking for the presence of Klass::Entity,
|
|
47
47
|
# where Klass is the class of the `object` parameter, or one of its
|
|
48
|
-
# ancestors.
|
|
48
|
+
# ancestors. Object is excluded from the search: top-level constants
|
|
49
|
+
# live on it, so a global ::Entity class is not a representer.
|
|
49
50
|
# @param object [Object] the object to locate the Entity class for
|
|
50
51
|
# @return [Class] the located Entity class, or nil if none is found
|
|
51
52
|
def entity_class_for_obj(object)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
representations = inheritable_setting.namespace_stackable_with_hash(:representations)
|
|
55
|
-
if representations
|
|
56
|
-
potential = klass.ancestors.detect { |potential| representations.key?(potential) }
|
|
57
|
-
return representations[potential] if potential && representations[potential]
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
return unless klass.const_defined?(:Entity)
|
|
61
|
-
|
|
62
|
-
entity = klass.const_get(:Entity)
|
|
63
|
-
entity if entity.respond_to?(:represent)
|
|
53
|
+
entity_for_class(object.class) || entity_for_class(element_class(object))
|
|
64
54
|
end
|
|
65
55
|
|
|
66
56
|
private
|
|
67
57
|
|
|
68
|
-
#
|
|
58
|
+
# The class standing in for a collection or wrapper: ActiveRecord::Relation
|
|
59
|
+
# and the like expose #klass, anything else falls back to the class of its
|
|
60
|
+
# first element.
|
|
61
|
+
#
|
|
62
|
+
# Consulted only once the object's own class has come up empty, because
|
|
63
|
+
# both tests are duck-typed and plenty of single objects answer them —
|
|
64
|
+
# a Struct is Enumerable, so it responds to #first, and so does any model
|
|
65
|
+
# that includes Enumerable. Asking this first meant `represent Model,
|
|
66
|
+
# with: Entity` was silently ignored for those, the entity for the
|
|
67
|
+
# *element* type being looked up instead. Deferring it also keeps #first
|
|
68
|
+
# from being called at all when the object resolves on its own class.
|
|
69
|
+
#
|
|
69
70
|
# @param object [Object] the object to represent.
|
|
70
|
-
# @return [Class
|
|
71
|
-
def
|
|
71
|
+
# @return [Class, nil]
|
|
72
|
+
def element_class(object)
|
|
72
73
|
return object.klass if object.respond_to?(:klass)
|
|
73
|
-
return object.first.class if object.respond_to?(:first)
|
|
74
74
|
|
|
75
|
-
object.class
|
|
75
|
+
object.first.class if object.respond_to?(:first)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @param klass [Class, nil] the class to look an entity up for.
|
|
79
|
+
# @return [Class, nil] the registered or conventionally named entity.
|
|
80
|
+
def entity_for_class(klass)
|
|
81
|
+
return if klass.nil?
|
|
82
|
+
|
|
83
|
+
representations = inheritable_setting.representations
|
|
84
|
+
if representations
|
|
85
|
+
potential = klass.ancestors.detect { |ancestor| representations.key?(ancestor) }
|
|
86
|
+
return representations[potential] if potential && representations[potential]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
owner = klass.ancestors.detect { |ancestor| ancestor != Object && ancestor.const_defined?(:Entity, false) }
|
|
90
|
+
return unless owner
|
|
91
|
+
|
|
92
|
+
entity = owner.const_get(:Entity, false)
|
|
93
|
+
entity if entity.respond_to?(:represent)
|
|
76
94
|
end
|
|
77
95
|
|
|
78
96
|
# @param entity_class [Class] the entity class to use for representation.
|
data/lib/grape/dsl/helpers.rb
CHANGED
|
@@ -52,12 +52,12 @@ module Grape
|
|
|
52
52
|
def make_inclusion(mod, &)
|
|
53
53
|
define_boolean_in_mod(mod)
|
|
54
54
|
inject_api_helpers_to_mod(mod, &)
|
|
55
|
-
inheritable_setting.
|
|
55
|
+
inheritable_setting.add_helper(mod)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
def include_all_in_scope
|
|
59
59
|
mod = Module.new
|
|
60
|
-
|
|
60
|
+
inheritable_setting.helpers.each { |mod_to_include| mod.include mod_to_include }
|
|
61
61
|
change!
|
|
62
62
|
mod
|
|
63
63
|
end
|
|
@@ -115,7 +115,7 @@ module Grape
|
|
|
115
115
|
def process_named_params
|
|
116
116
|
return if @named_params.blank?
|
|
117
117
|
|
|
118
|
-
api.inheritable_setting.
|
|
118
|
+
api.inheritable_setting.add_named_params(@named_params)
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
end
|
|
@@ -15,7 +15,7 @@ module Grape
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def configuration
|
|
18
|
-
config.
|
|
18
|
+
config.api.configuration.evaluate
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# End the request and display an error to the
|
|
@@ -27,10 +27,10 @@ module Grape
|
|
|
27
27
|
# @param backtrace [Array<String>] The backtrace of the exception that caused the error.
|
|
28
28
|
# @param original_exception [Exception] The original exception that caused the error.
|
|
29
29
|
def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil)
|
|
30
|
-
|
|
30
|
+
resolved_status = self.status(status || inheritable_setting.default_error_status)
|
|
31
31
|
headers = additional_headers.present? ? header.merge(additional_headers) : header
|
|
32
32
|
throw :error, Grape::Exceptions::ErrorResponse.new(
|
|
33
|
-
message:, status
|
|
33
|
+
message:, status: resolved_status, headers:, backtrace:, original_exception:
|
|
34
34
|
)
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -53,6 +53,12 @@ module Grape
|
|
|
53
53
|
end
|
|
54
54
|
header 'Location', url
|
|
55
55
|
content_type 'text/plain'
|
|
56
|
+
# Render the message Grape generated as the plain text it is. Setting
|
|
57
|
+
# only the header left it to the API's own formatter, which on a JSON
|
|
58
|
+
# API returned the sentence wrapped in quotes under a text/plain content
|
|
59
|
+
# type. A caller-supplied body keeps the API's format: it may be
|
|
60
|
+
# structured, and the txt formatter would render a Hash through `to_s`.
|
|
61
|
+
api_format :txt unless body
|
|
56
62
|
body body_message
|
|
57
63
|
end
|
|
58
64
|
|
|
@@ -66,7 +72,7 @@ module Grape
|
|
|
66
72
|
when Symbol, Integer
|
|
67
73
|
@status = Rack::Utils.status_code(status)
|
|
68
74
|
else
|
|
69
|
-
raise ArgumentError, '
|
|
75
|
+
raise ArgumentError, 'status code must be Integer or Symbol'
|
|
70
76
|
end
|
|
71
77
|
end
|
|
72
78
|
|
|
@@ -122,7 +128,7 @@ module Grape
|
|
|
122
128
|
def sendfile(value = nil)
|
|
123
129
|
return stream if value.nil?
|
|
124
130
|
|
|
125
|
-
raise ArgumentError, '
|
|
131
|
+
raise ArgumentError, 'argument must be a file path' unless value.is_a?(String)
|
|
126
132
|
|
|
127
133
|
file_body = Grape::ServeStream::FileBody.new(value)
|
|
128
134
|
@stream = Grape::ServeStream::StreamResponse.new(file_body)
|
|
@@ -185,15 +191,21 @@ module Grape
|
|
|
185
191
|
def stream_body(value)
|
|
186
192
|
return Grape::ServeStream::FileBody.new(value) if value.is_a?(String)
|
|
187
193
|
|
|
188
|
-
raise ArgumentError, '
|
|
194
|
+
raise ArgumentError, 'stream object must respond to :each' unless value.respond_to?(:each)
|
|
189
195
|
|
|
190
196
|
value
|
|
191
197
|
end
|
|
192
198
|
|
|
193
199
|
# The default HTTP status when none has been set explicitly.
|
|
200
|
+
# Reads the request method once instead of asking through +post?+ and
|
|
201
|
+
# +delete?+, each of which reads it again. Every response that did not set
|
|
202
|
+
# a status of its own comes through here, so it is read straight off the
|
|
203
|
+
# env: +Grape::Request+ wraps the very same Hash and answers
|
|
204
|
+
# +request_method+ with the same lookup, two method calls further down.
|
|
194
205
|
def default_status
|
|
195
|
-
|
|
196
|
-
return
|
|
206
|
+
request_method = env[Rack::REQUEST_METHOD]
|
|
207
|
+
return 201 if request_method == Rack::POST
|
|
208
|
+
return 204 if request_method == Rack::DELETE && @body.blank?
|
|
197
209
|
|
|
198
210
|
200
|
|
199
211
|
end
|
data/lib/grape/dsl/middleware.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Grape
|
|
|
13
13
|
arr = [:use, middleware_class, *args]
|
|
14
14
|
arr << block if block
|
|
15
15
|
|
|
16
|
-
inheritable_setting.
|
|
16
|
+
inheritable_setting.add_middleware(arr)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
%i[insert insert_before insert_after].each do |method_name|
|
|
@@ -21,7 +21,7 @@ module Grape
|
|
|
21
21
|
arr = [method_name, *args]
|
|
22
22
|
arr << block if block
|
|
23
23
|
|
|
24
|
-
inheritable_setting.
|
|
24
|
+
inheritable_setting.add_middleware(arr)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -29,7 +29,7 @@ module Grape
|
|
|
29
29
|
# and arguments that are currently applied to the
|
|
30
30
|
# application.
|
|
31
31
|
def middleware
|
|
32
|
-
inheritable_setting.
|
|
32
|
+
inheritable_setting.middleware
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
data/lib/grape/dsl/parameters.rb
CHANGED
|
@@ -29,7 +29,7 @@ module Grape
|
|
|
29
29
|
# end
|
|
30
30
|
# end
|
|
31
31
|
def build_with(build_with)
|
|
32
|
-
@api.inheritable_setting.
|
|
32
|
+
@api.inheritable_setting.build_params_with = build_with
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
# Include reusable params rules among current.
|
|
@@ -54,7 +54,9 @@ module Grape
|
|
|
54
54
|
# end
|
|
55
55
|
# end
|
|
56
56
|
def use(*names, **options)
|
|
57
|
-
|
|
57
|
+
return redispatch_legacy_options(:use, names, options) if legacy_options?(names)
|
|
58
|
+
|
|
59
|
+
named_params = @api.inheritable_setting.named_params || {}
|
|
58
60
|
names.each do |name|
|
|
59
61
|
params_block = named_params.fetch(name) do
|
|
60
62
|
raise "Params :#{name} not found!"
|
|
@@ -74,8 +76,8 @@ module Grape
|
|
|
74
76
|
#
|
|
75
77
|
# @param attrs list of parameters names, or, if :using is
|
|
76
78
|
# passed as an option, which keys to include (:all or :none) from
|
|
77
|
-
# the :using hash.
|
|
78
|
-
#
|
|
79
|
+
# the :using hash. Passing the options as a trailing positional Hash
|
|
80
|
+
# is deprecated; pass them as keyword arguments instead.
|
|
79
81
|
# @option attrs :type [Class] the type to coerce this parameter to before
|
|
80
82
|
# passing it to the endpoint. See {Grape::Validations::Types} for a list of
|
|
81
83
|
# types that are supported automatically. Custom classes may be used
|
|
@@ -122,34 +124,20 @@ module Grape
|
|
|
122
124
|
# requires :name, type: String
|
|
123
125
|
# end
|
|
124
126
|
# end
|
|
125
|
-
def requires(*attrs, using: nil, except: nil, **opts, &
|
|
126
|
-
|
|
127
|
-
opts = @group.deep_merge(opts) if @group
|
|
128
|
-
|
|
129
|
-
return require_required_and_optional_fields(attrs.first, using:, except:) if using
|
|
127
|
+
def requires(*attrs, using: nil, except: nil, as: nil, **opts, &)
|
|
128
|
+
return redispatch_legacy_options(:requires, attrs, { using:, except:, as: }.compact.merge(opts), &) if legacy_options?(attrs)
|
|
130
129
|
|
|
131
|
-
|
|
132
|
-
block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], &block) : push_declared_params(attrs, as: opts[:as])
|
|
130
|
+
declare(attrs, opts, required: true, using:, except:, as:, &)
|
|
133
131
|
end
|
|
134
132
|
|
|
135
133
|
# Allow, but don't require, one or more parameters for the current
|
|
136
134
|
# endpoint.
|
|
137
135
|
# @param (see #requires)
|
|
138
136
|
# @option (see #requires)
|
|
139
|
-
def optional(*attrs, using: nil, except: nil, **opts, &
|
|
140
|
-
|
|
141
|
-
opts = @group.deep_merge(opts) if @group
|
|
142
|
-
|
|
143
|
-
# check type for optional parameter group
|
|
144
|
-
if attrs && block
|
|
145
|
-
raise Grape::Exceptions::MissingGroupType if type.nil?
|
|
146
|
-
raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
return require_optional_fields(attrs.first, using:, except:) if using
|
|
137
|
+
def optional(*attrs, using: nil, except: nil, as: nil, **opts, &)
|
|
138
|
+
return redispatch_legacy_options(:optional, attrs, { using:, except:, as: }.compact.merge(opts), &) if legacy_options?(attrs)
|
|
150
139
|
|
|
151
|
-
|
|
152
|
-
block ? new_scope(attrs.first, type: opts[:type], as: opts[:as], optional: true, &block) : push_declared_params(attrs, as: opts[:as])
|
|
140
|
+
declare(attrs, opts, required: false, using:, except:, as:, &)
|
|
153
141
|
end
|
|
154
142
|
|
|
155
143
|
# Define common settings for one or more parameters
|
|
@@ -162,7 +150,7 @@ module Grape
|
|
|
162
150
|
|
|
163
151
|
%i[mutually_exclusive exactly_one_of at_least_one_of all_or_none_of].each do |validator|
|
|
164
152
|
define_method validator do |*attrs, message: nil|
|
|
165
|
-
validates(attrs, validator => { value: true, message: })
|
|
153
|
+
validates(attrs, { validator => { value: true, message: } })
|
|
166
154
|
end
|
|
167
155
|
end
|
|
168
156
|
|
|
@@ -203,13 +191,51 @@ module Grape
|
|
|
203
191
|
# @return hash of parameters relevant for the current scope
|
|
204
192
|
# @api private
|
|
205
193
|
def params(params)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
params
|
|
194
|
+
scoped = @parent ? (@parent.qualifying_params.presence || @parent.params(params)) : params
|
|
195
|
+
@element ? map_params(scoped, @element) : scoped
|
|
209
196
|
end
|
|
210
197
|
|
|
211
198
|
private
|
|
212
199
|
|
|
200
|
+
# @deprecated A trailing positional options Hash is deprecated; pass keyword
|
|
201
|
+
# arguments instead. Before Ruby 3 keyword separation this Hash was pulled
|
|
202
|
+
# off the argument list by `extract_options!`; now it lands in the splat and
|
|
203
|
+
# would silently be treated as a parameter name.
|
|
204
|
+
# +requires+ and +optional+ differ only in whether what they declare is
|
|
205
|
+
# required, so the declaration itself lives here.
|
|
206
|
+
#
|
|
207
|
+
# The two +using:+ helpers stay separate. They are not one computation
|
|
208
|
+
# with a flag flipped: +requires+ treats +except+ as "these go in the
|
|
209
|
+
# other bucket", while +optional+ has no other bucket — +:all+ ignores
|
|
210
|
+
# +except+ and +:none+ uses it to drop fields entirely.
|
|
211
|
+
def declare(attrs, opts, required:, using:, except:, as:, &block)
|
|
212
|
+
merged_opts = @group&.deep_merge(opts) || opts
|
|
213
|
+
declared_as = as || merged_opts[:as]
|
|
214
|
+
|
|
215
|
+
if using
|
|
216
|
+
return require_required_and_optional_fields(attrs.first, using:, except:) if required
|
|
217
|
+
|
|
218
|
+
return require_optional_fields(attrs.first, using:, except:)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
validates(attrs, merged_opts, required:)
|
|
222
|
+
return push_declared_params(attrs, as: declared_as) unless block
|
|
223
|
+
|
|
224
|
+
new_scope(attrs.first, type: merged_opts[:type], as: declared_as, optional: !required, &block)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def legacy_options?(args)
|
|
228
|
+
args.size > 1 && args.last.is_a?(Hash)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Re-invokes +method_name+ with the trailing Hash splatted as keyword
|
|
232
|
+
# arguments, so Ruby routes its keys to the same place they would have
|
|
233
|
+
# reached had the caller omitted the braces.
|
|
234
|
+
def redispatch_legacy_options(method_name, args, opts, &)
|
|
235
|
+
Grape.deprecator.warn("Passing a positional options Hash to `#{method_name}` is deprecated. Pass keyword arguments instead.")
|
|
236
|
+
__send__(method_name, *args[0..-2], **args.last.merge(opts), &)
|
|
237
|
+
end
|
|
238
|
+
|
|
213
239
|
def first_hash_key_or_param(parameter)
|
|
214
240
|
parameter.is_a?(Hash) ? parameter.keys.first : parameter
|
|
215
241
|
end
|