grape 2.4.0 → 4.0.1
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 +351 -0
- data/CONTRIBUTING.md +18 -10
- data/README.md +457 -228
- data/UPGRADING.md +1240 -1
- data/grape.gemspec +7 -7
- data/lib/grape/api/instance.rb +65 -116
- data/lib/grape/api.rb +53 -62
- data/lib/grape/content_types.rb +58 -3
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +116 -0
- data/lib/grape/dry_types.rb +48 -4
- data/lib/grape/dsl/callbacks.rb +8 -58
- data/lib/grape/dsl/declared.rb +35 -0
- data/lib/grape/dsl/desc.rb +24 -67
- data/lib/grape/dsl/entity.rb +106 -0
- data/lib/grape/dsl/helpers.rb +80 -65
- data/lib/grape/dsl/inside_route.rb +56 -296
- data/lib/grape/dsl/logger.rb +4 -9
- data/lib/grape/dsl/middleware.rb +22 -40
- data/lib/grape/dsl/parameters.rb +86 -93
- data/lib/grape/dsl/request_response.rb +139 -139
- data/lib/grape/dsl/rescue_options.rb +25 -0
- data/lib/grape/dsl/routing.rb +328 -203
- data/lib/grape/dsl/settings.rb +29 -135
- data/lib/grape/dsl/validations.rb +38 -44
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +25 -0
- data/lib/grape/endpoint.rb +285 -245
- data/lib/grape/env.rb +8 -3
- data/lib/grape/error_formatter/base.rb +27 -21
- data/lib/grape/error_formatter/json.rb +6 -0
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/base.rb +20 -44
- data/lib/grape/exceptions/error_response.rb +48 -0
- data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
- data/lib/grape/exceptions/invalid_formatter.rb +1 -1
- data/lib/grape/exceptions/invalid_message_body.rb +1 -1
- data/lib/grape/exceptions/invalid_version_header.rb +1 -1
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
- data/lib/grape/exceptions/method_not_allowed.rb +1 -1
- data/lib/grape/exceptions/missing_group_type.rb +0 -2
- data/lib/grape/exceptions/missing_mime_type.rb +1 -1
- data/lib/grape/exceptions/request_error.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/unknown_parameter.rb +1 -1
- data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
- data/lib/grape/exceptions/unknown_validator.rb +1 -1
- data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
- data/lib/grape/exceptions/validation.rb +25 -7
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +21 -23
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +6 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +3 -5
- data/lib/grape/middleware/auth/base.rb +4 -3
- data/lib/grape/middleware/auth/dsl.rb +9 -23
- data/lib/grape/middleware/base.rb +41 -36
- data/lib/grape/middleware/error.rb +271 -73
- data/lib/grape/middleware/formatter.rb +152 -63
- data/lib/grape/middleware/precomputed_content_types.rb +51 -0
- data/lib/grape/middleware/stack.rb +20 -11
- data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
- data/lib/grape/middleware/versioner/base.rb +40 -48
- data/lib/grape/middleware/versioner/header.rb +5 -23
- data/lib/grape/middleware/versioner/param.rb +2 -2
- data/lib/grape/middleware/versioner/path.rb +54 -7
- data/lib/grape/mountable.rb +22 -0
- data/lib/grape/namespace.rb +19 -11
- data/lib/grape/params_builder/base.rb +2 -0
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/params_builder.rb +2 -19
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +8 -72
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +57 -20
- data/lib/grape/router/base_route.rb +53 -7
- data/lib/grape/router/greedy_route.rb +16 -6
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +65 -37
- data/lib/grape/router/route.rb +66 -18
- data/lib/grape/router.rb +126 -115
- data/lib/grape/serve_stream/file_body.rb +7 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +7 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/api_description.rb +67 -0
- data/lib/grape/util/cache.rb +22 -2
- data/lib/grape/util/deep_freeze.rb +34 -0
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +777 -39
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +2 -9
- data/lib/grape/util/lazy/value.rb +2 -9
- data/lib/grape/util/lazy/value_enumerable.rb +13 -16
- data/lib/grape/util/media_type.rb +13 -9
- data/lib/grape/util/path_normalizer.rb +37 -0
- data/lib/grape/util/registry.rb +13 -3
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +39 -10
- data/lib/grape/util/translation.rb +42 -0
- data/lib/grape/validations/attributes_iterator.rb +61 -29
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/contract_scope.rb +8 -13
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/oneof_collector.rb +35 -0
- data/lib/grape/validations/param_scope_tracker.rb +62 -0
- data/lib/grape/validations/params_documentation.rb +52 -0
- data/lib/grape/validations/params_scope.rb +217 -304
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/single_attribute_iterator.rb +6 -4
- data/lib/grape/validations/types/array_coercer.rb +7 -10
- data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
- 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 +14 -33
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
- data/lib/grape/validations/types.rb +29 -51
- data/lib/grape/validations/validations_spec.rb +164 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
- data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
- data/lib/grape/validations/validators/base.rb +118 -34
- data/lib/grape/validations/validators/coerce_validator.rb +25 -37
- data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
- data/lib/grape/validations/validators/default_validator.rb +13 -20
- data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
- data/lib/grape/validations/validators/except_values_validator.rb +14 -4
- data/lib/grape/validations/validators/length_validator.rb +23 -22
- data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
- data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +51 -0
- data/lib/grape/validations/validators/presence_validator.rb +4 -2
- data/lib/grape/validations/validators/regexp_validator.rb +11 -3
- data/lib/grape/validations/validators/same_as_validator.rb +7 -15
- data/lib/grape/validations/validators/values_validator.rb +34 -26
- data/lib/grape/version.rb +1 -2
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +58 -31
- metadata +62 -39
- data/lib/grape/api/helpers.rb +0 -9
- data/lib/grape/dsl/api.rb +0 -17
- data/lib/grape/dsl/configuration.rb +0 -15
- data/lib/grape/exceptions/conflicting_types.rb +0 -11
- data/lib/grape/exceptions/empty_message_body.rb +0 -11
- data/lib/grape/exceptions/invalid_parameters.rb +0 -11
- data/lib/grape/exceptions/missing_option.rb +0 -11
- data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
- data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
- data/lib/grape/exceptions/unknown_options.rb +0 -11
- data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
- data/lib/grape/extensions/hash.rb +0 -27
- data/lib/grape/extensions/hashie/mash.rb +0 -24
- data/lib/grape/middleware/globals.rb +0 -14
- data/lib/grape/types/invalid_value.rb +0 -8
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -29
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
- data/lib/grape/util/strict_hash_configuration.rb +0 -108
- data/lib/grape/validations/attributes_doc.rb +0 -60
- data/lib/grape/validations/validator_factory.rb +0 -15
|
@@ -17,23 +17,70 @@ module Grape
|
|
|
17
17
|
# env['api.version'] => 'v1'
|
|
18
18
|
#
|
|
19
19
|
class Path < Base
|
|
20
|
+
def initialize(app, **options)
|
|
21
|
+
super
|
|
22
|
+
@prefixes = [mount_path, Grape::Util::PathNormalizer.call(prefix)].select { |p| p.present? && p != '/' }.freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
20
25
|
def before
|
|
21
|
-
|
|
26
|
+
routed = routed_version
|
|
27
|
+
return env[Grape::Env::API_VERSION] = routed if routed
|
|
28
|
+
|
|
29
|
+
path_info = env[Grape::Env::GRAPE_NORMALIZED_PATH] || Grape::Util::PathNormalizer.call(env[Rack::PATH_INFO])
|
|
22
30
|
return if path_info == '/'
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
# `each` rather than `reduce`: Array does not override Enumerable's,
|
|
33
|
+
# so the generic accumulator path costs more than the work it drives
|
|
34
|
+
# on a list that holds at most a mount path and a prefix — and most
|
|
35
|
+
# often nothing at all.
|
|
36
|
+
@prefixes.each { |prefix| path_info = path_info.delete_prefix(prefix) if path_info.start_with?(prefix) }
|
|
27
37
|
|
|
28
38
|
slash_position = path_info.index('/', 1) # omit the first one
|
|
29
|
-
return
|
|
39
|
+
return version_from_first_segment(path_info, slash_position) if slash_position
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
version_from_only_segment(path_info)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
33
45
|
|
|
46
|
+
# Under path versioning every route pattern carries the version as a
|
|
47
|
+
# named capture (Pattern::Path#build_parts inserts it), constrained to
|
|
48
|
+
# the declared versions -- so the router has already sliced the segment
|
|
49
|
+
# out and validated it, and re-deriving it from PATH_INFO would repeat
|
|
50
|
+
# the normalize, prefix-strip and slice below for the same answer.
|
|
51
|
+
#
|
|
52
|
+
# Nil, and the path parsed as before, when there are no routing args at
|
|
53
|
+
# all (the middleware used outside a Grape router) and for the greedy
|
|
54
|
+
# routes behind auto-OPTIONS and 405, which capture nothing. An Array
|
|
55
|
+
# means the route declared a +:version+ segment of its own on top of the
|
|
56
|
+
# versioning one: Mustermann then reports the capture as every position
|
|
57
|
+
# it matched rather than the single segment recorded here, so that too
|
|
58
|
+
# is left to the path parse.
|
|
59
|
+
def routed_version
|
|
60
|
+
version = env[Grape::Env::GRAPE_ROUTING_ARGS]&.[](:version)
|
|
61
|
+
version if version.is_a?(String)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def version_from_first_segment(path_info, slash_position)
|
|
65
|
+
potential_version = path_info[1..(slash_position - 1)]
|
|
34
66
|
version_not_found! unless potential_version_match?(potential_version)
|
|
35
67
|
env[Grape::Env::API_VERSION] = potential_version
|
|
36
68
|
end
|
|
69
|
+
|
|
70
|
+
# The path is a single segment (e.g. `GET /v1` — the root route of a
|
|
71
|
+
# path-versioned API). Nothing follows to disambiguate a version from a
|
|
72
|
+
# plain path or from a `.format` suffix (`/v1.json`), so the version is
|
|
73
|
+
# only recorded on an exact match against the declared versions, and an
|
|
74
|
+
# unmatched segment is left for the router to resolve — never a 404.
|
|
75
|
+
def version_from_only_segment(path_info)
|
|
76
|
+
candidate = path_info[1..]
|
|
77
|
+
candidate = candidate[0...candidate.rindex('.')] while candidate.include?('.') && !declared_version?(candidate)
|
|
78
|
+
env[Grape::Env::API_VERSION] = candidate if declared_version?(candidate)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def declared_version?(candidate)
|
|
82
|
+
versions.present? && versions.include?(candidate)
|
|
83
|
+
end
|
|
37
84
|
end
|
|
38
85
|
end
|
|
39
86
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
# Marker module for a mountable Grape application. Both {Grape::API} (the
|
|
5
|
+
# remountable user-facing class) and {Grape::API::Instance} (the compiled
|
|
6
|
+
# engine) extend it, so a mounted Grape app can be told apart from a bare
|
|
7
|
+
# Rack app with `is_a?(Grape::Mountable)` — a single, explicit predicate
|
|
8
|
+
# rather than duck-typing on an incidental internal method such as
|
|
9
|
+
# `inheritable_setting`.
|
|
10
|
+
#
|
|
11
|
+
# `Grape::API` and `Grape::API::Instance` are not related by inheritance and
|
|
12
|
+
# do not even respond to the same methods (the former to `mount_instance`,
|
|
13
|
+
# the latter to `inheritable_setting`/`endpoints`), so there is no common
|
|
14
|
+
# ancestor to key an `is_a?` check on without this marker.
|
|
15
|
+
#
|
|
16
|
+
# It answers identity only ("is this a Grape app?"). Capability checks that
|
|
17
|
+
# go on to call a stage-specific method — e.g. `respond_to?(:endpoints)`
|
|
18
|
+
# before reading `endpoints` — must stay as they are, since a `Mountable`
|
|
19
|
+
# does not necessarily respond to every such method.
|
|
20
|
+
module Mountable
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/grape/namespace.rb
CHANGED
|
@@ -5,31 +5,39 @@ module Grape
|
|
|
5
5
|
# logical grouping of endpoints as well as sharing common configuration.
|
|
6
6
|
# May also be referred to as group, segment, or resource.
|
|
7
7
|
class Namespace
|
|
8
|
-
attr_reader :space, :options
|
|
8
|
+
attr_reader :space, :requirements, :options
|
|
9
9
|
|
|
10
10
|
# @param space [String] the name of this namespace
|
|
11
11
|
# @param options [Hash] options hash
|
|
12
12
|
# @option options :requirements [Hash] param-regex pairs, all of which must
|
|
13
13
|
# be met by a request's params for all endpoints in this namespace, or
|
|
14
14
|
# validation will fail and return a 422.
|
|
15
|
-
def initialize(space, options)
|
|
15
|
+
def initialize(space, requirements: nil, **options)
|
|
16
16
|
@space = space.to_s
|
|
17
|
+
@requirements = requirements
|
|
17
18
|
@options = options
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
# Retrieves the requirements from the options hash, if given.
|
|
21
|
-
# @return [Hash]
|
|
22
|
-
def requirements
|
|
23
|
-
options[:requirements] || {}
|
|
24
|
-
end
|
|
25
|
-
|
|
26
21
|
# (see ::joined_space_path)
|
|
27
22
|
def self.joined_space(settings)
|
|
28
23
|
settings&.map(&:space)
|
|
29
24
|
end
|
|
30
25
|
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
def ==(other)
|
|
27
|
+
other.class == self.class &&
|
|
28
|
+
other.space == space &&
|
|
29
|
+
other.requirements == requirements &&
|
|
30
|
+
other.options == options
|
|
31
|
+
end
|
|
32
|
+
alias eql? ==
|
|
33
|
+
|
|
34
|
+
def hash
|
|
35
|
+
[self.class, space, requirements, options].hash
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Join the namespaces from a list of Namespace objects to create a path
|
|
39
|
+
# prefix.
|
|
40
|
+
# @param settings [Array] list of Grape::Namespace objects.
|
|
33
41
|
def self.joined_space_path(settings)
|
|
34
42
|
JoinedSpaceCache[joined_space(settings)]
|
|
35
43
|
end
|
|
@@ -38,7 +46,7 @@ module Grape
|
|
|
38
46
|
def initialize
|
|
39
47
|
super
|
|
40
48
|
@cache = Hash.new do |h, joined_space|
|
|
41
|
-
h[joined_space] = Grape::
|
|
49
|
+
h[joined_space] = Grape::Util::PathNormalizer.call(joined_space.join('/'))
|
|
42
50
|
end
|
|
43
51
|
end
|
|
44
52
|
end
|
data/lib/grape/params_builder.rb
CHANGED
|
@@ -4,29 +4,12 @@ module Grape
|
|
|
4
4
|
module ParamsBuilder
|
|
5
5
|
extend Grape::Util::Registry
|
|
6
6
|
|
|
7
|
-
SHORT_NAME_LOOKUP = {
|
|
8
|
-
'Grape::Extensions::Hash::ParamBuilder' => :hash,
|
|
9
|
-
'Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder' => :hash_with_indifferent_access,
|
|
10
|
-
'Grape::Extensions::Hashie::Mash::ParamBuilder' => :hashie_mash
|
|
11
|
-
}.freeze
|
|
12
|
-
|
|
13
7
|
module_function
|
|
14
8
|
|
|
15
9
|
def params_builder_for(short_name)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
raise Grape::Exceptions::UnknownParamsBuilder, verified_short_name unless registry.key?(verified_short_name)
|
|
19
|
-
|
|
20
|
-
registry[verified_short_name]
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def verify_short_name!(short_name)
|
|
24
|
-
return short_name if short_name.is_a?(Symbol)
|
|
10
|
+
raise Grape::Exceptions::UnknownParamsBuilder, short_name unless registry.key?(short_name)
|
|
25
11
|
|
|
26
|
-
|
|
27
|
-
SHORT_NAME_LOOKUP[class_name].tap do |real_short_name|
|
|
28
|
-
Grape.deprecator.warn "#{class_name} has been deprecated. Use short name :#{real_short_name} instead."
|
|
29
|
-
end
|
|
12
|
+
registry[short_name]
|
|
30
13
|
end
|
|
31
14
|
end
|
|
32
15
|
end
|
data/lib/grape/parser/json.rb
CHANGED
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Parser
|
|
5
5
|
class Json < Base
|
|
6
6
|
def self.call(object, _env)
|
|
7
|
-
::Grape::Json.
|
|
7
|
+
::Grape::Json.parse(object)
|
|
8
8
|
rescue ::Grape::Json::ParseError
|
|
9
9
|
# handle JSON parsing errors via the rescue handlers or provide error message
|
|
10
10
|
raise Grape::Exceptions::InvalidMessageBody.new('application/json')
|
data/lib/grape/path.rb
CHANGED
|
@@ -1,76 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def initialize(raw_path, raw_namespace, settings)
|
|
13
|
-
@origin = PartsCache[build_parts(raw_path, raw_namespace, settings)]
|
|
14
|
-
@suffix = build_suffix(raw_path, raw_namespace, settings)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def to_s
|
|
18
|
-
"#{origin}#{suffix}"
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
def build_suffix(raw_path, raw_namespace, settings)
|
|
24
|
-
if uses_specific_format?(settings)
|
|
25
|
-
"(.#{settings[:format]})"
|
|
26
|
-
elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path))
|
|
27
|
-
NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT
|
|
28
|
-
else
|
|
29
|
-
DEFAULT_FORMAT_SEGMENT
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def build_parts(raw_path, raw_namespace, settings)
|
|
34
|
-
[].tap do |parts|
|
|
35
|
-
add_part(parts, settings[:mount_path])
|
|
36
|
-
add_part(parts, settings[:root_prefix])
|
|
37
|
-
parts << VERSION_SEGMENT if uses_path_versioning?(settings)
|
|
38
|
-
add_part(parts, raw_namespace)
|
|
39
|
-
add_part(parts, raw_path)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def add_part(parts, value)
|
|
44
|
-
parts << value if value && not_slash?(value)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def not_slash?(value)
|
|
48
|
-
value != '/'
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def uses_specific_format?(settings)
|
|
52
|
-
return false unless settings.key?(:format) && settings.key?(:content_types)
|
|
53
|
-
|
|
54
|
-
settings[:format] && Array(settings[:content_types]).size == 1
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def uses_path_versioning?(settings)
|
|
58
|
-
return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)
|
|
59
|
-
|
|
60
|
-
settings[:version] && settings[:version_options][:using] == :path
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def valid_part?(part)
|
|
64
|
-
part&.match?(/^\S/) && not_slash?(part)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
class PartsCache < Grape::Util::Cache
|
|
68
|
-
def initialize
|
|
69
|
-
super
|
|
70
|
-
@cache = Hash.new do |h, parts|
|
|
71
|
-
h[parts] = Grape::Router.normalize_path(parts.join('/'))
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
4
|
+
# @deprecated +Grape::Path+ moved to {Grape::Router::Pattern::Path}, since it
|
|
5
|
+
# is a router-internal detail that only exists to build a
|
|
6
|
+
# {Grape::Router::Pattern}. Reference the new constant instead.
|
|
7
|
+
Path = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
|
|
8
|
+
'Grape::Path',
|
|
9
|
+
'Grape::Router::Pattern::Path',
|
|
10
|
+
Grape.deprecator
|
|
11
|
+
)
|
|
76
12
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
# Marks a body as JSON that has already been rendered, so the JSON formatters
|
|
5
|
+
# serve it verbatim instead of encoding it a second time.
|
|
6
|
+
#
|
|
7
|
+
# +Grape::Formatter::Json+ calls +to_json+ on whatever the endpoint returned, so a
|
|
8
|
+
# String holding pre-rendered JSON — a blob cached in Redis, a +json_agg+ column read
|
|
9
|
+
# straight from Postgres, output from another serializer — comes back encoded twice:
|
|
10
|
+
#
|
|
11
|
+
# '{"a":1}' => "\"{\\\"a\\\":1}\""
|
|
12
|
+
#
|
|
13
|
+
# Wrapping the body opts that one response out of encoding:
|
|
14
|
+
#
|
|
15
|
+
# get '/cached' do
|
|
16
|
+
# body Grape::PrecompiledJson.new(Rails.cache.read('payload'))
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# An Array is joined into a JSON array without its members being parsed, which is
|
|
20
|
+
# what makes a cached collection cheap — N rendered blobs are spliced together
|
|
21
|
+
# rather than round-tripped:
|
|
22
|
+
#
|
|
23
|
+
# Grape::PrecompiledJson.new(['{"id":1}', '{"id":2}']).to_s # => '[{"id":1},{"id":2}]'
|
|
24
|
+
#
|
|
25
|
+
# +Array#join+ calls +to_s+ on each member, so members may themselves be
|
|
26
|
+
# +PrecompiledJson+ instances.
|
|
27
|
+
#
|
|
28
|
+
# Wrap only the whole body. A wrapper nested inside a Hash or Array that is then
|
|
29
|
+
# handed to an encoder is serialized as an ordinary object — +ActiveSupport::JSON+
|
|
30
|
+
# renders it through +as_json+ as +{"value":"{\"a\":1}"}+ — because no encoder knows
|
|
31
|
+
# to unwrap it. Nothing checks that the String actually holds JSON; that is the
|
|
32
|
+
# caller's responsibility.
|
|
33
|
+
class PrecompiledJson
|
|
34
|
+
# @param value [String, Array<String>] pre-rendered JSON
|
|
35
|
+
def initialize(value)
|
|
36
|
+
@value = value
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @return [String] the JSON to serve
|
|
40
|
+
# @raise [Grape::Exceptions::InvalidFormatter] if the value is neither a String
|
|
41
|
+
# nor an Array; the formatter middleware turns this into a 500 rather than
|
|
42
|
+
# letting a body Rack cannot serve reach the SPEC check.
|
|
43
|
+
def to_s
|
|
44
|
+
return @value if @value.is_a?(String)
|
|
45
|
+
return "[#{@value.join(',')}]" if @value.is_a?(Array)
|
|
46
|
+
|
|
47
|
+
raise Grape::Exceptions::InvalidFormatter.new(@value.class, 'json')
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/grape/request.rb
CHANGED
|
@@ -132,8 +132,8 @@ module Grape
|
|
|
132
132
|
X-UA-Compatible
|
|
133
133
|
X-WebKit-CS
|
|
134
134
|
X-XSS-Protection
|
|
135
|
-
].
|
|
136
|
-
|
|
135
|
+
].to_h do |header|
|
|
136
|
+
["HTTP_#{header.upcase.tr('-', '_')}", header]
|
|
137
137
|
end.freeze
|
|
138
138
|
|
|
139
139
|
alias rack_params params
|
|
@@ -141,7 +141,7 @@ module Grape
|
|
|
141
141
|
|
|
142
142
|
def initialize(env, build_params_with: nil)
|
|
143
143
|
super(env)
|
|
144
|
-
@
|
|
144
|
+
@build_params_with = build_params_with
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
def params
|
|
@@ -153,38 +153,75 @@ module Grape
|
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
def cookies
|
|
156
|
-
@cookies ||= Grape::Cookies.new(
|
|
156
|
+
@cookies ||= Grape::Cookies.new(rack_cookies)
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
-
#
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
159
|
+
# True once the cookie jar has been materialized (a cookie was read or
|
|
160
|
+
# written this request). Lets the endpoint skip response-cookie flushing,
|
|
161
|
+
# and the Grape::Cookies allocation it triggers, when no cookie was
|
|
162
|
+
# touched on the request.
|
|
163
|
+
def cookies?
|
|
164
|
+
!@cookies.nil?
|
|
163
165
|
end
|
|
164
166
|
|
|
165
167
|
private
|
|
166
168
|
|
|
169
|
+
# Resolved on first use rather than in the constructor: a request that never
|
|
170
|
+
# reads +params+ -- an endpoint with no validations whose block does not ask
|
|
171
|
+
# for them -- pays neither the registry lookup nor the global config read,
|
|
172
|
+
# and every request builds a Grape::Request.
|
|
173
|
+
def params_builder
|
|
174
|
+
@params_builder ||= Grape::ParamsBuilder.params_builder_for(@build_params_with || Grape.config[:param_builder])
|
|
175
|
+
end
|
|
176
|
+
|
|
167
177
|
def make_params
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
rescue
|
|
174
|
-
raise Grape::Exceptions::
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
178
|
+
params = params_builder.call(rack_params)
|
|
179
|
+
filtered = routing_args_as_params(env[Grape::Env::GRAPE_ROUTING_ARGS])
|
|
180
|
+
return params if filtered.blank?
|
|
181
|
+
|
|
182
|
+
params.deep_merge!(filtered)
|
|
183
|
+
rescue *Grape::RACK_ERRORS
|
|
184
|
+
raise Grape::Exceptions::RequestError
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The routing args carry two things that are not request params:
|
|
188
|
+
# +:route_info+, which is always Grape's own, and +:version+, which is only
|
|
189
|
+
# Grape's own when the API declared a version — that is captured as a path
|
|
190
|
+
# segment and exposed through +env['api.version']+ instead.
|
|
191
|
+
#
|
|
192
|
+
# An API that declares no version can legitimately name a param +:version+
|
|
193
|
+
# (`route_param :version`, `get '/:version'`), and that capture belongs to
|
|
194
|
+
# the application. Dropping it unconditionally left `params[:version]` nil
|
|
195
|
+
# on a route that had matched, losing the segment silently.
|
|
196
|
+
def routing_args_as_params(routing_args)
|
|
197
|
+
return if routing_args.nil?
|
|
198
|
+
return routing_args.except(:version, :route_info) if grape_owns_version?(routing_args)
|
|
199
|
+
|
|
200
|
+
routing_args.except(:route_info)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# A route reports a +version+ only when the API declared one, which is the
|
|
204
|
+
# case where the captured segment is Grape's rather than the application's.
|
|
205
|
+
def grape_owns_version?(routing_args)
|
|
206
|
+
return false unless routing_args.key?(:version)
|
|
207
|
+
|
|
208
|
+
route = routing_args[:route_info]
|
|
209
|
+
route.respond_to?(:version) && !route.version.nil?
|
|
179
210
|
end
|
|
180
211
|
|
|
212
|
+
# Uses a plain `each_header` block instead of `each_header.with_object`:
|
|
213
|
+
# `with_object` can only pass the block one value plus the memo, so the
|
|
214
|
+
# `k, v` pair would be boxed into a throwaway Array on every header. A
|
|
215
|
+
# two-arg block receives `k`/`v` directly and allocates nothing extra.
|
|
181
216
|
def build_headers
|
|
182
|
-
|
|
217
|
+
headers = Grape::Util::Header.new
|
|
218
|
+
each_header do |k, v|
|
|
183
219
|
next unless k.start_with? 'HTTP_'
|
|
184
220
|
|
|
185
221
|
transformed_header = KNOWN_HEADERS.fetch(k) { -k[5..].tr('_', '-').downcase }
|
|
186
222
|
headers[transformed_header] = v
|
|
187
223
|
end
|
|
224
|
+
headers
|
|
188
225
|
end
|
|
189
226
|
end
|
|
190
227
|
end
|
|
@@ -3,29 +3,75 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
class Router
|
|
5
5
|
class BaseRoute
|
|
6
|
+
extend Forwardable
|
|
7
|
+
|
|
6
8
|
delegate_missing_to :@options
|
|
7
9
|
|
|
8
|
-
attr_reader :
|
|
10
|
+
attr_reader :options, :pattern, :prefix, :settings, :namespace
|
|
11
|
+
|
|
12
|
+
# +version+, +anchor+ and +requirements+ shape the matcher, so they are
|
|
13
|
+
# read from the pattern rather than stored again on the route.
|
|
14
|
+
def_delegators :@pattern, :path, :origin, :version, :anchor, :requirements
|
|
15
|
+
def_delegators :@options, :description, *Grape::Util::ApiDescription::DSL_METHODS
|
|
9
16
|
|
|
10
|
-
def initialize(options)
|
|
17
|
+
def initialize(pattern, options = {}, namespace: nil, prefix: nil, settings: nil)
|
|
18
|
+
@pattern = pattern
|
|
11
19
|
@options = options.is_a?(ActiveSupport::OrderedOptions) ? options : ActiveSupport::OrderedOptions.new.update(options)
|
|
20
|
+
@namespace = namespace
|
|
21
|
+
@prefix = prefix
|
|
22
|
+
@settings = settings
|
|
12
23
|
end
|
|
13
24
|
|
|
14
|
-
|
|
25
|
+
# +success+ and +failure+ are the +desc+ DSL's names for +entity+ and
|
|
26
|
+
# +http_codes+: the block form writes the canonical key, a keyword option
|
|
27
|
+
# keeps the name it was written with, so both spellings are read here.
|
|
28
|
+
# Without this they resolve through +delegate_missing_to+, which answers
|
|
29
|
+
# nil for whichever of the two keys the description did not use.
|
|
30
|
+
def success
|
|
31
|
+
@options[:entity] || @options[:success]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def failure
|
|
35
|
+
@options[:http_codes] || @options[:failure]
|
|
36
|
+
end
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
|
|
38
|
+
# @deprecated Use {#default_response}, the name grape-swagger asks for.
|
|
39
|
+
# This one has to be written out rather than delegated like the rest:
|
|
40
|
+
# an ActiveSupport::OrderedOptions answers an unknown name with that
|
|
41
|
+
# key's value, but +default+ is not unknown to it — it is +Hash#default+,
|
|
42
|
+
# the Hash's own default value — so a delegator would report nil for
|
|
43
|
+
# every route.
|
|
44
|
+
def default
|
|
45
|
+
Grape.deprecator.warn('`Grape::Router::Route#default` is deprecated. Use `#default_response` instead.')
|
|
46
|
+
default_response
|
|
18
47
|
end
|
|
19
48
|
|
|
49
|
+
# Assigned eagerly in {#to_regexp} (router compilation) rather than
|
|
50
|
+
# memoized here: this reader is called from request-time route matching
|
|
51
|
+
# on instances shared across threads, so it must not write state.
|
|
52
|
+
attr_reader :regexp_capture_index
|
|
53
|
+
|
|
54
|
+
# The number of the group this route occupies in the union the router
|
|
55
|
+
# compiled it into. Only the union can say what it is, so it is written
|
|
56
|
+
# back by {Router#compile!} right after building one, and like
|
|
57
|
+
# +regexp_capture_index+ it is never assigned at request time.
|
|
58
|
+
attr_reader :regexp_capture_group
|
|
59
|
+
|
|
20
60
|
def pattern_regexp
|
|
21
|
-
pattern.to_regexp
|
|
61
|
+
@pattern.to_regexp
|
|
22
62
|
end
|
|
23
63
|
|
|
24
64
|
def to_regexp(index)
|
|
25
|
-
@
|
|
65
|
+
@regexp_capture_index = CaptureIndexCache[index]
|
|
26
66
|
Regexp.new("(?<#{regexp_capture_index}>#{pattern_regexp})")
|
|
27
67
|
end
|
|
28
68
|
|
|
69
|
+
# @api private
|
|
70
|
+
# @see #regexp_capture_group
|
|
71
|
+
def resolve_capture_group!(union_named_captures)
|
|
72
|
+
@regexp_capture_group = union_named_captures.fetch(regexp_capture_index).first
|
|
73
|
+
end
|
|
74
|
+
|
|
29
75
|
class CaptureIndexCache < Grape::Util::Cache
|
|
30
76
|
def initialize
|
|
31
77
|
super
|
|
@@ -6,14 +6,24 @@
|
|
|
6
6
|
module Grape
|
|
7
7
|
class Router
|
|
8
8
|
class GreedyRoute < BaseRoute
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
extend Forwardable
|
|
10
|
+
|
|
11
|
+
def_delegators :@endpoint, :call
|
|
12
|
+
|
|
13
|
+
attr_reader :endpoint, :allow_header
|
|
14
|
+
|
|
15
|
+
def initialize(pattern, endpoint:, allow_header:)
|
|
16
|
+
super(pattern)
|
|
17
|
+
@endpoint = endpoint
|
|
18
|
+
@allow_header = allow_header
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def params
|
|
22
|
+
nil
|
|
12
23
|
end
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
options[:params] || {}
|
|
25
|
+
def params_for(_input)
|
|
26
|
+
nil
|
|
17
27
|
end
|
|
18
28
|
end
|
|
19
29
|
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
class Router
|
|
5
|
+
# Grape-style path patterns for Mustermann: `:param`, `*splat`, `{name}` /
|
|
6
|
+
# `{+splat}`, `( )` optionals, `|`, and an Integer digit-only constraint
|
|
7
|
+
# (driven by Grape's `params` option).
|
|
8
|
+
#
|
|
9
|
+
# Inlined from the mustermann-grape gem (MIT) by namusyaka, Konstantin Haase
|
|
10
|
+
# and Daniel Doubrovkine. Grape instantiates this class directly (see
|
|
11
|
+
# {Grape::Router::Pattern}), so unlike the gem it is not registered as a
|
|
12
|
+
# Mustermann `type: :grape`.
|
|
13
|
+
class MustermannPattern < ::Mustermann::AST::Pattern
|
|
14
|
+
supported_options :params
|
|
15
|
+
|
|
16
|
+
on(nil, '?', ')') { |c| unexpected(c) }
|
|
17
|
+
|
|
18
|
+
on('*') { |_c| scan(/\w+/) ? node(:named_splat, buffer.matched) : node(:splat) }
|
|
19
|
+
on(':') do |_c|
|
|
20
|
+
param_name = scan(/\w+/)
|
|
21
|
+
# Integer params (declared via Grape's `params` option) match digits only;
|
|
22
|
+
# any other capture matches a single path segment (anything but / ? # .).
|
|
23
|
+
param_type = pattern&.options&.dig(:params, param_name, :type)
|
|
24
|
+
constraint = param_type == 'Integer' ? /\d/ : '[^/?#.]'
|
|
25
|
+
node(:capture, param_name, constraint:) { scan(/\w+/) }
|
|
26
|
+
end
|
|
27
|
+
on('\\') { |_c| node(:char, expect(/./)) }
|
|
28
|
+
on('(') { |_c| node(:optional, node(:group) { read unless scan(')') }) }
|
|
29
|
+
on('|') { |_c| node(:or) }
|
|
30
|
+
|
|
31
|
+
on('{') do |_c|
|
|
32
|
+
type = scan('+') ? :named_splat : :capture
|
|
33
|
+
name = expect(/[\w.]+/)
|
|
34
|
+
type = :splat if (type == :named_splat) && (name == 'splat')
|
|
35
|
+
expect('}')
|
|
36
|
+
node(type, name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
suffix('?') do |_c, element|
|
|
40
|
+
node(:optional, element)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|