grape 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +102 -1
- data/README.md +195 -95
- data/UPGRADING.md +364 -0
- data/grape.gemspec +1 -1
- data/lib/grape/api/instance.rb +57 -30
- data/lib/grape/api.rb +50 -20
- data/lib/grape/content_types.rb +26 -9
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/declared_params_handler.rb +35 -7
- data/lib/grape/dsl/declared.rb +5 -4
- data/lib/grape/dsl/desc.rb +1 -2
- data/lib/grape/dsl/entity.rb +35 -19
- data/lib/grape/dsl/headers.rb +3 -3
- data/lib/grape/dsl/helpers.rb +5 -8
- data/lib/grape/dsl/inside_route.rb +33 -27
- data/lib/grape/dsl/logger.rb +10 -4
- data/lib/grape/dsl/middleware.rb +4 -9
- data/lib/grape/dsl/mounting.rb +149 -0
- data/lib/grape/dsl/parameters.rb +78 -104
- data/lib/grape/dsl/request_response.rb +35 -22
- data/lib/grape/dsl/routing.rb +17 -191
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/version_options.rb +2 -2
- data/lib/grape/dsl/versioning.rb +98 -0
- data/lib/grape/endpoint/state.rb +15 -0
- data/lib/grape/endpoint.rb +229 -137
- data/lib/grape/error_formatter/base.rb +28 -13
- data/lib/grape/error_formatter/json.rb +3 -0
- data/lib/grape/error_formatter/jsonl.rb +15 -0
- data/lib/grape/error_formatter/xml.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +23 -7
- data/lib/grape/exceptions/halt.rb +39 -0
- data/lib/grape/exceptions/validation.rb +9 -9
- data/lib/grape/exceptions/validation_errors.rb +12 -2
- data/lib/grape/formatter/jsonl.rb +34 -0
- data/lib/grape/formatter/markdown.rb +17 -0
- data/lib/grape/formatter/md.rb +11 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/base.rb +16 -1
- data/lib/grape/middleware/auth/strategy_info.rb +2 -2
- data/lib/grape/middleware/base.rb +19 -13
- data/lib/grape/middleware/error.rb +133 -35
- data/lib/grape/middleware/formatter.rb +235 -94
- data/lib/grape/middleware/precomputed_content_types.rb +14 -2
- data/lib/grape/middleware/stack.rb +7 -8
- data/lib/grape/middleware/versioner/base.rb +26 -10
- data/lib/grape/middleware/versioner/header.rb +70 -15
- data/lib/grape/middleware/versioner/param.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +20 -11
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/mountable.rb +2 -4
- data/lib/grape/namespace.rb +2 -3
- data/lib/grape/parser/jsonl.rb +19 -0
- data/lib/grape/request.rb +46 -8
- data/lib/grape/router/base_route.rb +41 -0
- data/lib/grape/router/greedy_route.rb +1 -1
- data/lib/grape/router/mustermann_pattern.rb +1 -1
- data/lib/grape/router/pattern/path.rb +1 -1
- data/lib/grape/router/pattern.rb +11 -5
- data/lib/grape/router/route.rb +42 -11
- data/lib/grape/router/route_buckets.rb +246 -0
- data/lib/grape/router.rb +262 -32
- data/lib/grape/util/endpoint_configuration.rb +7 -1
- data/lib/grape/util/inheritable_setting.rb +80 -161
- data/lib/grape/util/lazy/value.rb +52 -11
- data/lib/grape/util/media_type.rb +38 -15
- data/lib/grape/util/path_normalizer.rb +7 -2
- data/lib/grape/util/registry.rb +9 -2
- data/lib/grape/util/scrub.rb +33 -0
- data/lib/grape/util/translation.rb +6 -6
- data/lib/grape/validations/attributes_iterator.rb +73 -37
- data/lib/grape/validations/coerce_options.rb +1 -1
- data/lib/grape/validations/params_documentation.rb +15 -7
- data/lib/grape/validations/params_scope.rb +176 -43
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
- data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
- data/lib/grape/validations/types/primitive_coercer.rb +17 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
- data/lib/grape/validations/types.rb +12 -18
- data/lib/grape/validations/validations_spec.rb +53 -22
- data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
- data/lib/grape/validations/validators/as_validator.rb +2 -3
- data/lib/grape/validations/validators/base.rb +124 -25
- data/lib/grape/validations/validators/coerce_validator.rb +2 -3
- data/lib/grape/validations/validators/default_validator.rb +18 -2
- data/lib/grape/validations/validators/except_values_validator.rb +16 -3
- data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
- data/lib/grape/validations/validators/length_validator.rb +24 -16
- data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
- data/lib/grape/validations/validators/values_validator.rb +18 -24
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +3 -0
- metadata +18 -11
- data/lib/grape/util/lazy/value_array.rb +0 -21
- data/lib/grape/util/lazy/value_enumerable.rb +0 -31
- data/lib/grape/util/lazy/value_hash.rb +0 -21
- data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
- data/lib/grape/validations/single_attribute_iterator.rb +0 -28
data/lib/grape/content_types.rb
CHANGED
|
@@ -15,14 +15,31 @@ module Grape
|
|
|
15
15
|
|
|
16
16
|
MIME_TYPES = Grape::ContentTypes::DEFAULTS.except(:serializable_hash).invert.freeze
|
|
17
17
|
|
|
18
|
+
# Formats known by more than one name. A request naming one of them -- by
|
|
19
|
+
# extension, +?format=+ or +default_format+ -- is served under whichever
|
|
20
|
+
# the API declared, so declaring +:md+ answers +.markdown+ too, through
|
|
21
|
+
# the formatter, parser and error formatter declared for +:md+. Both
|
|
22
|
+
# spellings are keys: an extension arrives as a String, +default_format+
|
|
23
|
+
# as a Symbol.
|
|
24
|
+
FORMAT_SYNONYMS = {
|
|
25
|
+
md: :markdown, 'md' => :markdown,
|
|
26
|
+
markdown: :md, 'markdown' => :md
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
18
29
|
def content_types_for(from_settings)
|
|
19
30
|
from_settings.presence || DEFAULTS
|
|
20
31
|
end
|
|
21
32
|
|
|
33
|
+
# Identity rather than +==+ for the defaults, and the pairs rather than
|
|
34
|
+
# the registry for the cache key, because Hash equality ignores order and
|
|
35
|
+
# this table's order is the API's preference: the +*/*+ every browser
|
|
36
|
+
# sends picks the first media type declared. Compared by value, the first
|
|
37
|
+
# registry built with a set of content types fixed that order for every
|
|
38
|
+
# later one declaring the same set in another order.
|
|
22
39
|
def mime_types_for(from_settings)
|
|
23
|
-
return MIME_TYPES if from_settings
|
|
40
|
+
return MIME_TYPES if from_settings.equal?(Grape::ContentTypes::DEFAULTS)
|
|
24
41
|
|
|
25
|
-
MimeTypesCache[from_settings]
|
|
42
|
+
MimeTypesCache[from_settings.to_a]
|
|
26
43
|
end
|
|
27
44
|
|
|
28
45
|
# Every format under both spellings in one plain Hash, so a lookup is a
|
|
@@ -51,11 +68,11 @@ module Grape
|
|
|
51
68
|
end
|
|
52
69
|
|
|
53
70
|
# Both tables below are derived from nothing but the content-type registry,
|
|
54
|
-
# and
|
|
55
|
-
# app
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
#
|
|
71
|
+
# and every endpoint builds its own content-type-aware middlewares — so an
|
|
72
|
+
# app held one copy per endpoint of tables it only ever reads. Keying the
|
|
73
|
+
# cache on the registry collapses them: keys compare by value, so every
|
|
74
|
+
# registry with the same content types shares one table -- declared in
|
|
75
|
+
# the same order, for MimeTypesCache (see #mime_types_for).
|
|
59
76
|
#
|
|
60
77
|
# Both the key and the table are frozen: the caller's registry stays
|
|
61
78
|
# reachable (through +middleware.options[:content_types]+, among others)
|
|
@@ -65,8 +82,8 @@ module Grape
|
|
|
65
82
|
class MimeTypesCache < Grape::Util::Cache
|
|
66
83
|
def initialize
|
|
67
84
|
super
|
|
68
|
-
@cache = Hash.new do |h,
|
|
69
|
-
h[
|
|
85
|
+
@cache = Hash.new do |h, content_types|
|
|
86
|
+
h[content_types.freeze] = content_types.to_h.invert.transform_keys! { |mime_type| Grape::ContentTypes.media_type(mime_type) }.freeze
|
|
70
87
|
end
|
|
71
88
|
end
|
|
72
89
|
end
|
data/lib/grape/cookies.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Grape
|
|
|
30
30
|
send_cookies << name
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
#
|
|
33
|
+
# Same attributes Rack::Utils uses to delete a cookie
|
|
34
34
|
def delete(name, **opts)
|
|
35
35
|
self.[]=(name, opts.merge(DELETED_COOKIES_ATTRS))
|
|
36
36
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
4
|
class DeclaredParamsHandler
|
|
5
|
-
def initialize(include_missing: true, evaluate_given: false, stringify: false, contract_key_map:
|
|
5
|
+
def initialize(include_missing: true, evaluate_given: false, stringify: false, contract_key_map: [])
|
|
6
6
|
@include_missing = include_missing
|
|
7
7
|
@evaluate_given = evaluate_given
|
|
8
8
|
@stringify = stringify
|
|
@@ -20,7 +20,16 @@ module Grape
|
|
|
20
20
|
|
|
21
21
|
private
|
|
22
22
|
|
|
23
|
+
# An element of an Array scope that is not a Hash has no keys to filter,
|
|
24
|
+
# so it is returned as it came in, the way a leaf value is. Validation
|
|
25
|
+
# lets one through wherever nothing in the scope is required of it: an
|
|
26
|
+
# optional scope passes over blank elements, and a scope whose params are
|
|
27
|
+
# all optional checks nothing on an element that is not a Hash. Filtering
|
|
28
|
+
# it anyway meant building an empty copy of it, which raised for +nil+,
|
|
29
|
+
# +false+ and numbers and indexed a String with a Symbol.
|
|
23
30
|
def recursive_declared(passed_params, declared_params:, route_params:, renamed_params:, params_nested_path: [])
|
|
31
|
+
return passed_params unless passed_params.is_a?(Array) || passed_params.respond_to?(:key?)
|
|
32
|
+
|
|
24
33
|
res = if passed_params.is_a?(Array)
|
|
25
34
|
passed_params.map do |passed_param|
|
|
26
35
|
recursive_declared(passed_param, declared_params:, params_nested_path:, renamed_params:, route_params:)
|
|
@@ -29,7 +38,7 @@ module Grape
|
|
|
29
38
|
declared_hash(passed_params, declared_params:, params_nested_path:, renamed_params:, route_params:)
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
@contract_key_map
|
|
41
|
+
@contract_key_map.each { |key_map| key_map.write(passed_params, res) }
|
|
33
42
|
|
|
34
43
|
res
|
|
35
44
|
end
|
|
@@ -63,8 +72,13 @@ module Grape
|
|
|
63
72
|
memo_key = build_memo_key(params_nested_path, parent, renamed_params)
|
|
64
73
|
passed_children = passed_params[parent] || passed_params.class.new
|
|
65
74
|
nested_path = nested_path_for(params_nested_path, parent)
|
|
75
|
+
# A value that is neither a Hash nor an Array where children are declared
|
|
76
|
+
# -- left unvalidated by a +given+ whose dependency is not met, say -- is
|
|
77
|
+
# returned as it came in, as #recursive_declared returns an element like
|
|
78
|
+
# it.
|
|
79
|
+
has_passed_children = !passed_children.respond_to?(:any?) || passed_children.any?
|
|
66
80
|
|
|
67
|
-
memo[memo_key] = handle_passed_param(nested_path, route_params:, has_passed_children:
|
|
81
|
+
memo[memo_key] = handle_passed_param(nested_path, route_params:, has_passed_children:) do
|
|
68
82
|
recursive_declared(
|
|
69
83
|
passed_children,
|
|
70
84
|
declared_params: children,
|
|
@@ -87,8 +101,11 @@ module Grape
|
|
|
87
101
|
end
|
|
88
102
|
end
|
|
89
103
|
|
|
104
|
+
# The lookup key is the param's whole path, built fresh -- two Arrays and a
|
|
105
|
+
# String per declared param -- and only an API using +as:+ has anything to
|
|
106
|
+
# find with it, so the common empty table is not asked.
|
|
90
107
|
def build_memo_key(params_nested_path, declared_param, renamed_params)
|
|
91
|
-
renamed_param_name = renamed_params[nested_path_for(params_nested_path, declared_param)]
|
|
108
|
+
renamed_param_name = renamed_params[nested_path_for(params_nested_path, declared_param)] unless renamed_params.empty?
|
|
92
109
|
param = renamed_param_name || declared_param
|
|
93
110
|
@stringify ? param.to_s : param.to_sym
|
|
94
111
|
end
|
|
@@ -106,11 +123,22 @@ module Grape
|
|
|
106
123
|
type = route_params.dig(key, :type)
|
|
107
124
|
return yield if type.nil?
|
|
108
125
|
|
|
109
|
-
|
|
126
|
+
empty_value_for(type, key, route_params) || yield
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# The empty value a param of +type+ declares when it was not passed, or
|
|
130
|
+
# nil when +type+ has none. A fresh one every time: the caller may fill it.
|
|
131
|
+
def empty_value_for(type, key, route_params)
|
|
132
|
+
return {} if type == 'Hash' && no_child_params?(route_params, "#{key}[")
|
|
110
133
|
return [] if type == 'Array' || (type.start_with?('[') && !type.include?(','))
|
|
111
|
-
return Set.new if type == 'Set' || type.start_with?('#<Set', 'Set')
|
|
112
134
|
|
|
113
|
-
|
|
135
|
+
Set.new if type.start_with?('#<Set', 'Set')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The prefix is built once by the caller: interpolated inside the block, it
|
|
139
|
+
# was a new String for every param on the route.
|
|
140
|
+
def no_child_params?(route_params, prefix)
|
|
141
|
+
route_params.keys.none? { |k| k.start_with?(prefix) }
|
|
114
142
|
end
|
|
115
143
|
end
|
|
116
144
|
end
|
data/lib/grape/dsl/declared.rb
CHANGED
|
@@ -15,10 +15,11 @@ module Grape
|
|
|
15
15
|
# consisting only of keys that have been declared by a
|
|
16
16
|
# `params` statement against the current/target endpoint or parent
|
|
17
17
|
# namespaces.
|
|
18
|
-
# @param
|
|
19
|
-
# @param
|
|
20
|
-
#
|
|
21
|
-
#
|
|
18
|
+
# @param passed_params [Hash] The initial hash to filter. Usually this will just be `params`
|
|
19
|
+
# @param include_parent_namespaces [Boolean] false to only return params declared against the current/target endpoint
|
|
20
|
+
# @param include_missing [Boolean] include declared params that were not passed
|
|
21
|
+
# @param evaluate_given [Boolean] drop params whose +given+ condition is not met
|
|
22
|
+
# @param stringify [Boolean] return String keys
|
|
22
23
|
def declared(passed_params, include_parent_namespaces: true, include_missing: true, evaluate_given: false, stringify: false)
|
|
23
24
|
raise MethodNotYetAvailable unless before_filter_passed
|
|
24
25
|
|
data/lib/grape/dsl/desc.rb
CHANGED
|
@@ -9,8 +9,7 @@ module Grape
|
|
|
9
9
|
# @param description [String] descriptive string for this endpoint
|
|
10
10
|
# or namespace
|
|
11
11
|
# @param options [Hash] other properties you can set to describe the
|
|
12
|
-
# endpoint or namespace
|
|
13
|
-
# passing a positional options Hash is deprecated.
|
|
12
|
+
# endpoint or namespace, as keyword arguments. Optional.
|
|
14
13
|
# @option options :detail [String] additional detail about this endpoint
|
|
15
14
|
# @option options :summary [String] summary for this endpoint
|
|
16
15
|
# @option options :params [Hash] param types and info. normally, you set
|
data/lib/grape/dsl/entity.rb
CHANGED
|
@@ -26,35 +26,41 @@ module Grape
|
|
|
26
26
|
# admin: current_user.admin?
|
|
27
27
|
# end
|
|
28
28
|
def present(*args, root: nil, with: nil, **options)
|
|
29
|
-
|
|
29
|
+
# No [nil, object] Array to destructure for the one-argument form.
|
|
30
|
+
object = args.first
|
|
31
|
+
key, object = args if args.count == 2 && object.is_a?(Symbol)
|
|
30
32
|
entity_class = with || entity_class_for_obj(object)
|
|
31
33
|
representation = entity_class ? entity_representation_for(entity_class, object, options) : object
|
|
32
34
|
representation = { root => representation } if root
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
representation = body&.merge(key => representation) || { key => representation }
|
|
36
|
-
elsif entity_class.present? && body
|
|
37
|
-
raise ArgumentError, "representation of type `#{representation.class}` cannot be merged" unless representation.respond_to?(:merge)
|
|
38
|
-
|
|
39
|
-
representation = body.merge(representation)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
body representation
|
|
36
|
+
body merged_with_body(representation, key, entity_class)
|
|
43
37
|
end
|
|
44
38
|
|
|
45
39
|
# Attempt to locate the Entity class for a given object, if not given
|
|
46
|
-
# explicitly
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# live on it, so a global ::Entity class is not a representer.
|
|
40
|
+
# explicitly: a +represent+ registration or Klass::Entity for the object's
|
|
41
|
+
# class or an ancestor, then the same for its element class. Object is
|
|
42
|
+
# excluded: a global ::Entity class is not a representer.
|
|
50
43
|
# @param object [Object] the object to locate the Entity class for
|
|
51
44
|
# @return [Class] the located Entity class, or nil if none is found
|
|
52
45
|
def entity_class_for_obj(object)
|
|
53
|
-
|
|
46
|
+
# Read once for both lookups: it walks every scope up to the API's root,
|
|
47
|
+
# and any object that answers #first (a Hash among them) needs both.
|
|
48
|
+
representations = inheritable_setting.representations
|
|
49
|
+
entity_for_class(object.class, representations) || entity_for_class(element_class(object), representations)
|
|
54
50
|
end
|
|
55
51
|
|
|
56
52
|
private
|
|
57
53
|
|
|
54
|
+
# +representation+ merged into the body an earlier +present+ set: under
|
|
55
|
+
# +key+ when there is one, and whole when it came from an entity.
|
|
56
|
+
def merged_with_body(representation, key, entity_class)
|
|
57
|
+
return body&.merge(key => representation) || { key => representation } if key
|
|
58
|
+
return representation unless entity_class && body
|
|
59
|
+
raise ArgumentError, "representation of type `#{representation.class}` cannot be merged" unless representation.respond_to?(:merge)
|
|
60
|
+
|
|
61
|
+
body.merge(representation)
|
|
62
|
+
end
|
|
63
|
+
|
|
58
64
|
# The class standing in for a collection or wrapper: ActiveRecord::Relation
|
|
59
65
|
# and the like expose #klass, anything else falls back to the class of its
|
|
60
66
|
# first element.
|
|
@@ -76,16 +82,22 @@ module Grape
|
|
|
76
82
|
end
|
|
77
83
|
|
|
78
84
|
# @param klass [Class, nil] the class to look an entity up for.
|
|
85
|
+
# @param representations [Hash, nil] the +represent+ registrations in scope.
|
|
79
86
|
# @return [Class, nil] the registered or conventionally named entity.
|
|
80
|
-
def entity_for_class(klass)
|
|
87
|
+
def entity_for_class(klass, representations)
|
|
81
88
|
return if klass.nil?
|
|
82
89
|
|
|
83
|
-
representations = inheritable_setting.representations
|
|
84
90
|
if representations
|
|
85
91
|
potential = klass.ancestors.detect { |ancestor| representations.key?(ancestor) }
|
|
86
92
|
return representations[potential] if potential && representations[potential]
|
|
87
93
|
end
|
|
88
94
|
|
|
95
|
+
conventional_entity_for(klass) if klass.const_defined?(:Entity)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# The +Entity+ constant of the nearest of +klass+'s ancestors defining
|
|
99
|
+
# one, when it can represent.
|
|
100
|
+
def conventional_entity_for(klass)
|
|
89
101
|
owner = klass.ancestors.detect { |ancestor| ancestor != Object && ancestor.const_defined?(:Entity, false) }
|
|
90
102
|
return unless owner
|
|
91
103
|
|
|
@@ -93,13 +105,17 @@ module Grape
|
|
|
93
105
|
entity if entity.respond_to?(:represent)
|
|
94
106
|
end
|
|
95
107
|
|
|
108
|
+
# The embeds go straight into the call: collected into a Hash of their
|
|
109
|
+
# own and splatted, they cost every +present+ a second Hash.
|
|
110
|
+
#
|
|
96
111
|
# @param entity_class [Class] the entity class to use for representation.
|
|
97
112
|
# @param object [Object] the object to represent.
|
|
98
113
|
# @param options [Hash] additional options forwarded to the entity's `represent` call.
|
|
99
114
|
# @return the representation of the given object as done through the given entity_class.
|
|
100
115
|
def entity_representation_for(entity_class, object, options)
|
|
101
|
-
|
|
102
|
-
|
|
116
|
+
return entity_class.represent(object, env:, **options) unless env.key?(Grape::Env::API_VERSION)
|
|
117
|
+
|
|
118
|
+
entity_class.represent(object, env:, version: env[Grape::Env::API_VERSION], **options)
|
|
103
119
|
end
|
|
104
120
|
end
|
|
105
121
|
end
|
data/lib/grape/dsl/headers.rb
CHANGED
|
@@ -4,10 +4,10 @@ module Grape
|
|
|
4
4
|
module DSL
|
|
5
5
|
module Headers
|
|
6
6
|
# This method has four responsibilities:
|
|
7
|
-
# 1. Set a
|
|
8
|
-
# 2. Retrieve a
|
|
7
|
+
# 1. Set a specific header value by key
|
|
8
|
+
# 2. Retrieve a specific header value by key
|
|
9
9
|
# 3. Retrieve all headers that have been set
|
|
10
|
-
# 4. Delete a
|
|
10
|
+
# 4. Delete a specific header key-value pair
|
|
11
11
|
def header(key = nil, val = nil)
|
|
12
12
|
if key
|
|
13
13
|
val ? header[key] = val : header.delete(key)
|
data/lib/grape/dsl/helpers.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Grape
|
|
|
6
6
|
# Add helper methods that will be accessible from any
|
|
7
7
|
# endpoint within this namespace (and child namespaces).
|
|
8
8
|
#
|
|
9
|
-
# When called
|
|
10
|
-
#
|
|
9
|
+
# When called with neither modules nor a block, returns a module
|
|
10
|
+
# including all known helpers within this scope.
|
|
11
11
|
#
|
|
12
12
|
# @param [Array] new_modules optional array of modules to include
|
|
13
13
|
# @param [Block] block optional block of methods to include
|
|
@@ -77,11 +77,8 @@ module Grape
|
|
|
77
77
|
|
|
78
78
|
# When +Grape.config.warn_on_helper_overrides+ is enabled, emit a
|
|
79
79
|
# warning to +$stderr+ for any helper method that masks an instance
|
|
80
|
-
# method on +Grape::Endpoint+. Helpers are
|
|
81
|
-
#
|
|
82
|
-
# instance methods — usually intentional, but a common source of
|
|
83
|
-
# surprise when the framework gains a method that already collides
|
|
84
|
-
# with an existing helper name.
|
|
80
|
+
# method on +Grape::Endpoint+. Helpers are included into a subclass of
|
|
81
|
+
# the endpoint (see Endpoint#build_prototype), so they take precedence.
|
|
85
82
|
def warn_on_endpoint_overrides(mod)
|
|
86
83
|
overridden = mod.instance_methods(false).select { |m| Grape::Endpoint.method_defined?(m) }
|
|
87
84
|
return if overridden.empty?
|
|
@@ -113,7 +110,7 @@ module Grape
|
|
|
113
110
|
protected
|
|
114
111
|
|
|
115
112
|
def process_named_params
|
|
116
|
-
return
|
|
113
|
+
return unless @named_params
|
|
117
114
|
|
|
118
115
|
api.inheritable_setting.add_named_params(@named_params)
|
|
119
116
|
end
|
|
@@ -9,7 +9,15 @@ module Grape
|
|
|
9
9
|
# Backward compatibility: alias exception class to previous location
|
|
10
10
|
MethodNotYetAvailable = Declared::MethodNotYetAvailable
|
|
11
11
|
|
|
12
|
-
# The
|
|
12
|
+
# The body #redirect renders when the caller gives none, by status.
|
|
13
|
+
REDIRECT_MESSAGES = {
|
|
14
|
+
301 => 'This resource has been moved permanently to %<url>s.',
|
|
15
|
+
302 => 'This resource has been moved temporarily to %<url>s.',
|
|
16
|
+
303 => 'An alternate resource is located at %<url>s.'
|
|
17
|
+
}.freeze
|
|
18
|
+
private_constant :REDIRECT_MESSAGES
|
|
19
|
+
|
|
20
|
+
# The API version negotiated for this request.
|
|
13
21
|
def version
|
|
14
22
|
env[Grape::Env::API_VERSION]
|
|
15
23
|
end
|
|
@@ -23,13 +31,14 @@ module Grape
|
|
|
23
31
|
#
|
|
24
32
|
# @param message [String] The message to display.
|
|
25
33
|
# @param status [Integer] The HTTP Status Code. Defaults to default_error_status, 500 if not set.
|
|
26
|
-
# @param additional_headers [Hash]
|
|
34
|
+
# @param additional_headers [Hash] Additional headers for the response.
|
|
27
35
|
# @param backtrace [Array<String>] The backtrace of the exception that caused the error.
|
|
28
36
|
# @param original_exception [Exception] The original exception that caused the error.
|
|
37
|
+
# @raise [Grape::Exceptions::Halt] always, carrying the response.
|
|
29
38
|
def error!(message, status = nil, additional_headers = nil, backtrace = nil, original_exception = nil)
|
|
30
39
|
resolved_status = self.status(status || inheritable_setting.default_error_status)
|
|
31
40
|
headers = additional_headers.present? ? header.merge(additional_headers) : header
|
|
32
|
-
|
|
41
|
+
raise Grape::Exceptions::Halt.new(
|
|
33
42
|
message:, status: resolved_status, headers:, backtrace:, original_exception:
|
|
34
43
|
)
|
|
35
44
|
end
|
|
@@ -40,26 +49,27 @@ module Grape
|
|
|
40
49
|
# @param permanent [Boolean] default false.
|
|
41
50
|
# @param body default a short message including the URL.
|
|
42
51
|
def redirect(url, permanent: false, body: nil)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
status 302
|
|
52
|
-
body_message ||= "This resource has been moved temporarily to #{url}."
|
|
53
|
-
end
|
|
52
|
+
code = if permanent
|
|
53
|
+
301
|
|
54
|
+
elsif http_version == 'HTTP/1.1' && !request.get?
|
|
55
|
+
303
|
|
56
|
+
else
|
|
57
|
+
302
|
|
58
|
+
end
|
|
59
|
+
status code
|
|
54
60
|
header 'Location', url
|
|
55
|
-
|
|
61
|
+
# A caller-supplied body keeps the API's format, and the content type
|
|
62
|
+
# that goes with it: it may be structured, and the txt formatter would
|
|
63
|
+
# render a Hash through `to_s`.
|
|
64
|
+
return body(body) if body
|
|
65
|
+
|
|
56
66
|
# Render the message Grape generated as the plain text it is. Setting
|
|
57
67
|
# only the header left it to the API's own formatter, which on a JSON
|
|
58
68
|
# API returned the sentence wrapped in quotes under a text/plain content
|
|
59
|
-
# type.
|
|
60
|
-
|
|
61
|
-
api_format :txt
|
|
62
|
-
body
|
|
69
|
+
# type.
|
|
70
|
+
content_type 'text/plain'
|
|
71
|
+
api_format :txt
|
|
72
|
+
body format(REDIRECT_MESSAGES.fetch(code), url:)
|
|
63
73
|
end
|
|
64
74
|
|
|
65
75
|
# Set or retrieve the HTTP status code.
|
|
@@ -67,13 +77,9 @@ module Grape
|
|
|
67
77
|
# @param status [Integer] The HTTP Status Code to return for this request.
|
|
68
78
|
def status(status = nil)
|
|
69
79
|
return @status || default_status if status.nil?
|
|
80
|
+
raise ArgumentError, 'status code must be Integer or Symbol' unless status.is_a?(Symbol) || status.is_a?(Integer)
|
|
70
81
|
|
|
71
|
-
|
|
72
|
-
when Symbol, Integer
|
|
73
|
-
@status = Rack::Utils.status_code(status)
|
|
74
|
-
else
|
|
75
|
-
raise ArgumentError, 'status code must be Integer or Symbol'
|
|
76
|
-
end
|
|
82
|
+
@status = Rack::Utils.status_code(status)
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
# Set response content-type
|
|
@@ -121,7 +127,7 @@ module Grape
|
|
|
121
127
|
#
|
|
122
128
|
# @example
|
|
123
129
|
# get '/file' do
|
|
124
|
-
# sendfile
|
|
130
|
+
# sendfile '/path/to/file'
|
|
125
131
|
# end
|
|
126
132
|
#
|
|
127
133
|
# GET /file # => "contents of file"
|
|
@@ -205,7 +211,7 @@ module Grape
|
|
|
205
211
|
def default_status
|
|
206
212
|
request_method = env[Rack::REQUEST_METHOD]
|
|
207
213
|
return 201 if request_method == Rack::POST
|
|
208
|
-
return 204 if request_method == Rack::DELETE &&
|
|
214
|
+
return 204 if request_method == Rack::DELETE && Grape::Util::Scrub.blank?(@body)
|
|
209
215
|
|
|
210
216
|
200
|
|
211
217
|
end
|
data/lib/grape/dsl/logger.rb
CHANGED
|
@@ -3,14 +3,20 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module DSL
|
|
5
5
|
module Logger
|
|
6
|
-
# Set or
|
|
6
|
+
# Set or retrieve the configured logger. If none was configured, this
|
|
7
7
|
# method will create a new one, logging to stdout.
|
|
8
|
+
#
|
|
9
|
+
# A logger belongs to the API it is set on, and to the APIs mounted
|
|
10
|
+
# under it that set none of their own. It used to be kept in the
|
|
11
|
+
# process-wide settings, so setting one on any API replaced it on every
|
|
12
|
+
# other. Only the default is still shared: an API that sets a level on
|
|
13
|
+
# it at class level reads it off its base instance, and has to find
|
|
14
|
+
# that level again on each instance it is mounted as.
|
|
8
15
|
# @param logger [Object] the new logger to use
|
|
9
16
|
def logger(logger = nil)
|
|
10
|
-
|
|
11
|
-
return global_settings[:logger] = logger if logger
|
|
17
|
+
return top_level_setting.logger = logger if logger
|
|
12
18
|
|
|
13
|
-
|
|
19
|
+
top_level_setting.logger || (inheritable_setting.global[:logger] ||= ::Logger.new($stdout))
|
|
14
20
|
end
|
|
15
21
|
end
|
|
16
22
|
end
|
data/lib/grape/dsl/middleware.rb
CHANGED
|
@@ -10,24 +10,19 @@ module Grape
|
|
|
10
10
|
# @param middleware_class [Class] The class of the middleware you'd like
|
|
11
11
|
# to inject.
|
|
12
12
|
def use(middleware_class, *args, &block)
|
|
13
|
-
|
|
14
|
-
arr << block if block
|
|
15
|
-
|
|
16
|
-
inheritable_setting.add_middleware(arr)
|
|
13
|
+
inheritable_setting.add_middleware([:use, middleware_class, *args, block])
|
|
17
14
|
end
|
|
18
15
|
|
|
19
16
|
%i[insert insert_before insert_after].each do |method_name|
|
|
20
17
|
define_method method_name do |*args, &block|
|
|
21
|
-
|
|
22
|
-
arr << block if block
|
|
23
|
-
|
|
24
|
-
inheritable_setting.add_middleware(arr)
|
|
18
|
+
inheritable_setting.add_middleware([method_name, *args, block])
|
|
25
19
|
end
|
|
26
20
|
end
|
|
27
21
|
|
|
28
22
|
# Retrieve an array of the middleware classes
|
|
29
23
|
# and arguments that are currently applied to the
|
|
30
|
-
# application
|
|
24
|
+
# application, each ending in the block it was
|
|
25
|
+
# given, +nil+ when none.
|
|
31
26
|
def middleware
|
|
32
27
|
inheritable_setting.middleware
|
|
33
28
|
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module DSL
|
|
5
|
+
module Mounting
|
|
6
|
+
def given(conditional_option, &)
|
|
7
|
+
return unless conditional_option
|
|
8
|
+
|
|
9
|
+
mounted(&)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def mounted(&block)
|
|
13
|
+
evaluate_as_instance_with_configuration(block, lazy: true)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def mount(mounts, opts = {})
|
|
17
|
+
mount_opts = opts
|
|
18
|
+
if opts[:refresh_already_mounted]
|
|
19
|
+
Grape.deprecator.warn('`refresh_already_mounted` is not a `mount` option and will be ignored in a future release.')
|
|
20
|
+
drop_endpoints_mounted_for(mounts)
|
|
21
|
+
# Dropped before the recursion below re-enters with the same options,
|
|
22
|
+
# so a Grape API does not warn once per mount and once per instance.
|
|
23
|
+
mount_opts = opts.except(:refresh_already_mounted)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
normalize_mounts(mounts).each_pair do |app, path|
|
|
27
|
+
if app.respond_to?(:mount_instance)
|
|
28
|
+
mount({ app.mount_instance(configuration: mount_opts[:with] || {}) => path }, mount_opts)
|
|
29
|
+
next
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
endpoints << Grape::Endpoint.new(
|
|
33
|
+
mounted_setting(app, path),
|
|
34
|
+
http_methods: '*',
|
|
35
|
+
path:,
|
|
36
|
+
app:,
|
|
37
|
+
anchor: false,
|
|
38
|
+
api: self
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# The settings the endpoint mounting +app+ at +path+ is built with. Past
|
|
46
|
+
# #mount's +mount_instance+ branch, a Grape app is an already instantiated
|
|
47
|
+
# Grape::API::Instance (vs. a bare Rack app): it inherits this API's
|
|
48
|
+
# settings and is built from its own.
|
|
49
|
+
def mounted_setting(app, path)
|
|
50
|
+
return inheritable_setting unless app.is_a?(Grape::Mountable)
|
|
51
|
+
|
|
52
|
+
app.top_level_setting.add_mount_path(Grape::Util::PathNormalizer.call(path))
|
|
53
|
+
app.inherit_settings(inheritable_setting)
|
|
54
|
+
app.change!
|
|
55
|
+
change!
|
|
56
|
+
app.top_level_setting
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Re-mount +mounts+, replacing any endpoint already mounted for the same
|
|
60
|
+
# base API rather than adding a second one. Called by
|
|
61
|
+
# {Grape::API.refresh_mount_step} when a class-level method runs after the
|
|
62
|
+
# API was mounted.
|
|
63
|
+
#
|
|
64
|
+
# A bare Rack app is put back where it was mounted. It is routed as an
|
|
65
|
+
# ANY route, and those are tried in the order they were declared, so
|
|
66
|
+
# re-mounting it last let a catch-all declared after it answer first. A
|
|
67
|
+
# mounted Grape API still moves to the end: its routes answer for
|
|
68
|
+
# specific methods, where moving them would change which of two
|
|
69
|
+
# routes on the same path answers.
|
|
70
|
+
def refresh_mounted_api(mounts, opts = {})
|
|
71
|
+
namespace = inheritable_setting.namespace_path
|
|
72
|
+
normalize_mounts(mounts).each_pair do |app, path|
|
|
73
|
+
position = endpoints.index { |endpoint| mounted_at?(endpoint, app, Array(path), namespace) } unless app.respond_to?(:mount_instance)
|
|
74
|
+
drop_endpoints_mounted_for(app => path)
|
|
75
|
+
mount({ app => path }, opts)
|
|
76
|
+
endpoints.insert(position, endpoints.pop) if position
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A mounted Grape API is stored as the throwaway instance +mount+ built
|
|
81
|
+
# for it, never as the class that was written, so endpoints are matched on
|
|
82
|
+
# the base API both of them share -- and on where it was mounted, the
|
|
83
|
+
# path and the current namespace: the same API mounted at two paths, or
|
|
84
|
+
# in two namespaces, is two mounts, and refreshing one must leave the
|
|
85
|
+
# other in place.
|
|
86
|
+
def drop_endpoints_mounted_for(mounts)
|
|
87
|
+
namespace = inheritable_setting.namespace_path
|
|
88
|
+
normalize_mounts(mounts).each_pair do |app, path|
|
|
89
|
+
paths = Array(path)
|
|
90
|
+
endpoints.delete_if do |endpoint|
|
|
91
|
+
dropped = mounted_at?(endpoint, app, paths, namespace)
|
|
92
|
+
release_mount_instance(endpoint.mounted_app) if dropped
|
|
93
|
+
dropped
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# A Grape API keeps every instance +mount+ builds for it, so that steps
|
|
99
|
+
# recorded later replay onto each (see Grape::API::Instance.base=). Once
|
|
100
|
+
# the endpoint serving one is dropped, nothing serves it: its API lets go
|
|
101
|
+
# of it, and of the instances built for the APIs mounted inside it.
|
|
102
|
+
# Otherwise every refresh left another copy of each mounted API behind.
|
|
103
|
+
def release_mount_instance(mounted)
|
|
104
|
+
return unless mounted.respond_to?(:base) && mounted.base.respond_to?(:instances)
|
|
105
|
+
|
|
106
|
+
mounted.base.instances.delete(mounted)
|
|
107
|
+
mounted.endpoints.each { |endpoint| release_mount_instance(endpoint.mounted_app) }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# A bare app mounts at the root. The test is +Hash+ rather than
|
|
111
|
+
# +respond_to?(:each_pair)+ because a Struct or an OpenStruct answers that
|
|
112
|
+
# too, and neither can express an app => path mapping — their keys are
|
|
113
|
+
# member names. Reading one as a mapping would silently mount nonsense
|
|
114
|
+
# instead of mounting the app itself.
|
|
115
|
+
def normalize_mounts(mounts)
|
|
116
|
+
mounts.is_a?(Hash) ? mounts : { mounts => '/' }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def mounted_at?(endpoint, app, paths, namespace)
|
|
120
|
+
same_mounted_app?(endpoint.mounted_app, app) && endpoint.path == paths && endpoint.namespace == namespace
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Two mounts refer to the same app when they share the same base Grape
|
|
124
|
+
# API. +mount+ turns every mounted Grape API into a throwaway
|
|
125
|
+
# +mount_instance+ (a fresh +Class.new+ per mount), so object identity
|
|
126
|
+
# never holds across mounts; comparing the base is the real signal.
|
|
127
|
+
# Plain Rack apps have no base and are mounted as-is, so they fall back
|
|
128
|
+
# to object identity.
|
|
129
|
+
def same_mounted_app?(mounted, app)
|
|
130
|
+
return mounted.base.equal?(app.base) if mounted.respond_to?(:base) && app.respond_to?(:base)
|
|
131
|
+
|
|
132
|
+
mounted.equal?(app)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def evaluate_as_instance_with_configuration(block, lazy: false)
|
|
136
|
+
lazy_block = Grape::Util::Lazy::Block.new do |configuration|
|
|
137
|
+
value_for_configuration = configuration
|
|
138
|
+
self.configuration = value_for_configuration.evaluate if value_for_configuration.is_a?(Grape::Util::Lazy::Base)
|
|
139
|
+
response = instance_eval(&block)
|
|
140
|
+
self.configuration = value_for_configuration
|
|
141
|
+
response
|
|
142
|
+
end
|
|
143
|
+
return lazy_block if @base && base_instance? && lazy
|
|
144
|
+
|
|
145
|
+
lazy_block.evaluate_from(configuration)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|