grape 2.3.0 → 3.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 +69 -0
- data/CONTRIBUTING.md +2 -10
- data/README.md +106 -43
- data/UPGRADING.md +90 -1
- data/grape.gemspec +4 -4
- data/lib/grape/api/instance.rb +51 -73
- data/lib/grape/api.rb +56 -89
- data/lib/grape/cookies.rb +31 -25
- data/lib/grape/dry_types.rb +48 -4
- data/lib/grape/dsl/callbacks.rb +8 -58
- data/lib/grape/dsl/desc.rb +8 -67
- data/lib/grape/dsl/headers.rb +1 -1
- data/lib/grape/dsl/helpers.rb +60 -65
- data/lib/grape/dsl/inside_route.rb +26 -61
- data/lib/grape/dsl/logger.rb +3 -6
- data/lib/grape/dsl/middleware.rb +22 -40
- data/lib/grape/dsl/parameters.rb +10 -19
- data/lib/grape/dsl/request_response.rb +136 -139
- data/lib/grape/dsl/routing.rb +230 -194
- data/lib/grape/dsl/settings.rb +22 -134
- data/lib/grape/dsl/validations.rb +37 -45
- data/lib/grape/endpoint.rb +91 -126
- data/lib/grape/error_formatter/base.rb +2 -0
- data/lib/grape/exceptions/base.rb +1 -1
- data/lib/grape/exceptions/conflicting_types.rb +11 -0
- data/lib/grape/exceptions/invalid_parameters.rb +11 -0
- data/lib/grape/exceptions/missing_group_type.rb +0 -2
- data/lib/grape/exceptions/too_deep_parameters.rb +11 -0
- data/lib/grape/exceptions/unknown_auth_strategy.rb +11 -0
- data/lib/grape/exceptions/unknown_params_builder.rb +11 -0
- data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
- data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +2 -5
- data/lib/grape/extensions/hash.rb +2 -1
- data/lib/grape/extensions/hashie/mash.rb +3 -5
- data/lib/grape/locale/en.yml +44 -44
- data/lib/grape/middleware/auth/base.rb +11 -32
- data/lib/grape/middleware/auth/dsl.rb +22 -29
- data/lib/grape/middleware/base.rb +30 -11
- data/lib/grape/middleware/error.rb +14 -32
- data/lib/grape/middleware/formatter.rb +40 -72
- data/lib/grape/middleware/stack.rb +28 -38
- data/lib/grape/middleware/versioner/accept_version_header.rb +2 -4
- data/lib/grape/middleware/versioner/base.rb +30 -56
- data/lib/grape/middleware/versioner/header.rb +2 -2
- data/lib/grape/middleware/versioner/param.rb +2 -3
- data/lib/grape/middleware/versioner/path.rb +1 -1
- data/lib/grape/namespace.rb +11 -0
- data/lib/grape/params_builder/base.rb +20 -0
- data/lib/grape/params_builder/hash.rb +11 -0
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +11 -0
- data/lib/grape/params_builder/hashie_mash.rb +11 -0
- data/lib/grape/params_builder.rb +32 -0
- data/lib/grape/request.rb +161 -22
- data/lib/grape/router/route.rb +1 -1
- data/lib/grape/router.rb +27 -8
- data/lib/grape/util/api_description.rb +56 -0
- data/lib/grape/util/base_inheritable.rb +5 -2
- data/lib/grape/util/inheritable_setting.rb +7 -0
- data/lib/grape/util/media_type.rb +1 -1
- data/lib/grape/util/registry.rb +1 -1
- data/lib/grape/validations/contract_scope.rb +2 -2
- data/lib/grape/validations/params_documentation.rb +50 -0
- data/lib/grape/validations/params_scope.rb +46 -56
- data/lib/grape/validations/types/array_coercer.rb +2 -3
- data/lib/grape/validations/types/dry_type_coercer.rb +4 -11
- data/lib/grape/validations/types/primitive_coercer.rb +1 -28
- data/lib/grape/validations/types.rb +10 -25
- data/lib/grape/validations/validators/base.rb +2 -9
- data/lib/grape/validations/validators/except_values_validator.rb +1 -1
- data/lib/grape/validations/validators/presence_validator.rb +1 -1
- data/lib/grape/validations/validators/regexp_validator.rb +1 -1
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +18 -9
- metadata +35 -20
- data/lib/grape/api/helpers.rb +0 -9
- data/lib/grape/dsl/api.rb +0 -19
- data/lib/grape/dsl/configuration.rb +0 -15
- data/lib/grape/error_formatter/jsonapi.rb +0 -7
- data/lib/grape/http/headers.rb +0 -56
- data/lib/grape/middleware/helpers.rb +0 -12
- data/lib/grape/parser/jsonapi.rb +0 -7
- data/lib/grape/types/invalid_value.rb +0 -8
- data/lib/grape/util/lazy/object.rb +0 -45
- data/lib/grape/util/strict_hash_configuration.rb +0 -108
- data/lib/grape/validations/attributes_doc.rb +0 -60
data/lib/grape/locale/en.yml
CHANGED
|
@@ -1,59 +1,59 @@
|
|
|
1
|
+
---
|
|
1
2
|
en:
|
|
2
3
|
grape:
|
|
3
4
|
errors:
|
|
4
|
-
format:
|
|
5
|
+
format: '%{attributes} %{message}'
|
|
5
6
|
messages:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
regexp: 'is invalid'
|
|
7
|
+
all_or_none: 'provide all or none of parameters'
|
|
8
|
+
at_least_one: 'are missing, at least one parameter must be provided'
|
|
9
9
|
blank: 'is empty'
|
|
10
|
-
|
|
10
|
+
coerce: 'is invalid'
|
|
11
|
+
conflicting_types: 'query params contains conflicting types'
|
|
12
|
+
empty_message_body: 'empty message body supplied with %{body_format} content-type'
|
|
13
|
+
exactly_one: 'are missing, exactly one parameter must be provided'
|
|
11
14
|
except_values: 'has a value not allowed'
|
|
12
|
-
|
|
15
|
+
incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
|
|
16
|
+
invalid_accept_header:
|
|
17
|
+
problem: 'invalid accept header'
|
|
18
|
+
resolution: '%{message}'
|
|
19
|
+
invalid_formatter: 'cannot convert %{klass} to %{to_format}'
|
|
20
|
+
invalid_message_body:
|
|
21
|
+
problem: 'message body does not match declared format'
|
|
22
|
+
resolution: 'when specifying %{body_format} as content-type, you must pass valid %{body_format} in the request''s ''body'' '
|
|
23
|
+
invalid_parameters: 'query params contains invalid format or byte sequence'
|
|
24
|
+
invalid_response: 'Invalid response'
|
|
25
|
+
invalid_version_header:
|
|
26
|
+
problem: 'invalid version header'
|
|
27
|
+
resolution: '%{message}'
|
|
28
|
+
invalid_versioner_option:
|
|
29
|
+
problem: 'unknown :using for versioner: %{strategy}'
|
|
30
|
+
resolution: 'available strategy for :using is :path, :header, :accept_version_header, :param'
|
|
31
|
+
invalid_with_option_for_represent:
|
|
32
|
+
problem: 'you must specify an entity class in the :with option'
|
|
33
|
+
resolution: 'eg: represent User, :with => Entity::User'
|
|
13
34
|
length: 'is expected to have length within %{min} and %{max}'
|
|
14
35
|
length_is: 'is expected to have length exactly equal to %{is}'
|
|
15
|
-
length_min: 'is expected to have length greater than or equal to %{min}'
|
|
16
36
|
length_max: 'is expected to have length less than or equal to %{max}'
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
summary: 'when version using header, you must specify :vendor option'
|
|
20
|
-
resolution: "eg: version 'v1', using: :header, vendor: 'twitter'"
|
|
37
|
+
length_min: 'is expected to have length greater than or equal to %{min}'
|
|
38
|
+
missing_group_type: 'group type is required'
|
|
21
39
|
missing_mime_type:
|
|
22
40
|
problem: 'missing mime type for %{new_format}'
|
|
23
|
-
resolution:
|
|
24
|
-
"you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES
|
|
25
|
-
or add your own with content_type :%{new_format}, 'application/%{new_format}'
|
|
26
|
-
"
|
|
27
|
-
invalid_with_option_for_represent:
|
|
28
|
-
problem: 'you must specify an entity class in the :with option'
|
|
29
|
-
resolution: 'eg: represent User, :with => Entity::User'
|
|
41
|
+
resolution: 'you can choose existing mime type from Grape::ContentTypes::CONTENT_TYPES or add your own with content_type :%{new_format}, ''application/%{new_format}'' '
|
|
30
42
|
missing_option: 'you must specify :%{option} options'
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
missing_vendor_option:
|
|
44
|
+
problem: 'missing :vendor option'
|
|
45
|
+
resolution: 'eg: version ''v1'', using: :header, vendor: ''twitter'''
|
|
46
|
+
summary: 'when version using header, you must specify :vendor option'
|
|
47
|
+
mutual_exclusion: 'are mutually exclusive'
|
|
48
|
+
presence: 'is missing'
|
|
49
|
+
regexp: 'is invalid'
|
|
50
|
+
same_as: 'is not the same as %{parameter}'
|
|
51
|
+
too_deep_parameters: 'query params are recursively nested over the specified limit (%{limit})'
|
|
52
|
+
too_many_multipart_files: 'the number of uploaded files exceeded the system''s configured limit (%{limit})'
|
|
53
|
+
unknown_auth_strategy: 'unknown auth strategy: %{strategy}'
|
|
36
54
|
unknown_options: 'unknown options: %{options}'
|
|
37
55
|
unknown_parameter: 'unknown parameter: %{param}'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
at_least_one: 'are missing, at least one parameter must be provided'
|
|
41
|
-
exactly_one: 'are missing, exactly one parameter must be provided'
|
|
42
|
-
all_or_none: 'provide all or none of parameters'
|
|
43
|
-
missing_group_type: 'group type is required'
|
|
56
|
+
unknown_params_builder: 'unknown params_builder: %{params_builder_type}'
|
|
57
|
+
unknown_validator: 'unknown validator: %{validator_type}'
|
|
44
58
|
unsupported_group_type: 'group type must be Array, Hash, JSON or Array[JSON]'
|
|
45
|
-
|
|
46
|
-
problem: "message body does not match declared format"
|
|
47
|
-
resolution:
|
|
48
|
-
"when specifying %{body_format} as content-type, you must pass valid
|
|
49
|
-
%{body_format} in the request's 'body'
|
|
50
|
-
"
|
|
51
|
-
empty_message_body: 'empty message body supplied with %{body_format} content-type'
|
|
52
|
-
too_many_multipart_files: "the number of uploaded files exceeded the system's configured limit (%{limit})"
|
|
53
|
-
invalid_accept_header:
|
|
54
|
-
problem: 'invalid accept header'
|
|
55
|
-
resolution: '%{message}'
|
|
56
|
-
invalid_version_header:
|
|
57
|
-
problem: 'invalid version header'
|
|
58
|
-
resolution: '%{message}'
|
|
59
|
-
invalid_response: 'Invalid response'
|
|
59
|
+
values: 'does not have a valid value'
|
|
@@ -3,40 +3,19 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Middleware
|
|
5
5
|
module Auth
|
|
6
|
-
class Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
@app = app
|
|
13
|
-
@options = options.shift
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def call(env)
|
|
17
|
-
dup._call(env)
|
|
6
|
+
class Base < Grape::Middleware::Base
|
|
7
|
+
def initialize(app, **options)
|
|
8
|
+
super
|
|
9
|
+
@auth_strategy = Grape::Middleware::Auth::Strategies[options[:type]].tap do |auth_strategy|
|
|
10
|
+
raise Grape::Exceptions::UnknownAuthStrategy.new(strategy: options[:type]) unless auth_strategy
|
|
11
|
+
end
|
|
18
12
|
end
|
|
19
13
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
auth_proc_context = context
|
|
26
|
-
|
|
27
|
-
strategy_info = Grape::Middleware::Auth::Strategies[options[:type]]
|
|
28
|
-
|
|
29
|
-
throw(:error, status: 401, message: 'API Authorization Failed.') if strategy_info.blank?
|
|
30
|
-
|
|
31
|
-
strategy = strategy_info.create(@app, options) do |*args|
|
|
32
|
-
auth_proc_context.instance_exec(*args, &auth_proc)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
strategy.call(env)
|
|
36
|
-
|
|
37
|
-
else
|
|
38
|
-
app.call(env)
|
|
39
|
-
end
|
|
14
|
+
def call!(env)
|
|
15
|
+
@env = env
|
|
16
|
+
@auth_strategy.create(app, options) do |*args|
|
|
17
|
+
context.instance_exec(*args, &options[:proc])
|
|
18
|
+
end.call(env)
|
|
40
19
|
end
|
|
41
20
|
end
|
|
42
21
|
end
|
|
@@ -4,40 +4,33 @@ module Grape
|
|
|
4
4
|
module Middleware
|
|
5
5
|
module Auth
|
|
6
6
|
module DSL
|
|
7
|
-
|
|
7
|
+
def auth(type = nil, options = {}, &block)
|
|
8
|
+
namespace_inheritable = inheritable_setting.namespace_inheritable
|
|
9
|
+
return namespace_inheritable[:auth] unless type
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def auth(type = nil, options = {}, &block)
|
|
13
|
-
if type
|
|
14
|
-
namespace_inheritable(:auth, options.reverse_merge(type: type.to_sym, proc: block))
|
|
15
|
-
use Grape::Middleware::Auth::Base, namespace_inheritable(:auth)
|
|
16
|
-
else
|
|
17
|
-
namespace_inheritable(:auth)
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Add HTTP Basic authorization to the API.
|
|
22
|
-
#
|
|
23
|
-
# @param [Hash] options A hash of options.
|
|
24
|
-
# @option options [String] :realm "API Authorization" The HTTP Basic realm.
|
|
25
|
-
def http_basic(options = {}, &block)
|
|
26
|
-
options[:realm] ||= 'API Authorization'
|
|
27
|
-
auth :http_basic, options, &block
|
|
28
|
-
end
|
|
11
|
+
namespace_inheritable[:auth] = options.reverse_merge(type: type.to_sym, proc: block)
|
|
12
|
+
use Grape::Middleware::Auth::Base, namespace_inheritable[:auth]
|
|
13
|
+
end
|
|
29
14
|
|
|
30
|
-
|
|
31
|
-
|
|
15
|
+
# Add HTTP Basic authorization to the API.
|
|
16
|
+
#
|
|
17
|
+
# @param [Hash] options A hash of options.
|
|
18
|
+
# @option options [String] :realm "API Authorization" The HTTP Basic realm.
|
|
19
|
+
def http_basic(options = {}, &block)
|
|
20
|
+
options[:realm] ||= 'API Authorization'
|
|
21
|
+
auth :http_basic, options, &block
|
|
22
|
+
end
|
|
32
23
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
else
|
|
36
|
-
options[:opaque] ||= 'secret'
|
|
37
|
-
end
|
|
24
|
+
def http_digest(options = {}, &block)
|
|
25
|
+
options[:realm] ||= 'API Authorization'
|
|
38
26
|
|
|
39
|
-
|
|
27
|
+
if options[:realm].respond_to?(:values_at)
|
|
28
|
+
options[:realm][:opaque] ||= 'secret'
|
|
29
|
+
else
|
|
30
|
+
options[:opaque] ||= 'secret'
|
|
40
31
|
end
|
|
32
|
+
|
|
33
|
+
auth :http_digest, options, &block
|
|
41
34
|
end
|
|
42
35
|
end
|
|
43
36
|
end
|
|
@@ -3,25 +3,18 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Middleware
|
|
5
5
|
class Base
|
|
6
|
-
include Helpers
|
|
7
6
|
include Grape::DSL::Headers
|
|
8
7
|
|
|
9
8
|
attr_reader :app, :env, :options
|
|
10
9
|
|
|
11
|
-
TEXT_HTML = 'text/html'
|
|
12
|
-
|
|
13
10
|
# @param [Rack Application] app The standard argument for a Rack middleware.
|
|
14
11
|
# @param [Hash] options A hash of options, simply stored for use by subclasses.
|
|
15
|
-
def initialize(app,
|
|
12
|
+
def initialize(app, **options)
|
|
16
13
|
@app = app
|
|
17
|
-
@options =
|
|
14
|
+
@options = merge_default_options(options)
|
|
18
15
|
@app_response = nil
|
|
19
16
|
end
|
|
20
17
|
|
|
21
|
-
def default_options
|
|
22
|
-
{}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
18
|
def call(env)
|
|
26
19
|
dup.call!(env).to_a
|
|
27
20
|
end
|
|
@@ -54,10 +47,18 @@ module Grape
|
|
|
54
47
|
# @return [Response, nil] a Rack SPEC response or nil to call the application afterwards.
|
|
55
48
|
def after; end
|
|
56
49
|
|
|
50
|
+
def rack_request
|
|
51
|
+
@rack_request ||= Rack::Request.new(env)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def context
|
|
55
|
+
env[Grape::Env::API_ENDPOINT]
|
|
56
|
+
end
|
|
57
|
+
|
|
57
58
|
def response
|
|
58
59
|
return @app_response if @app_response.is_a?(Rack::Response)
|
|
59
60
|
|
|
60
|
-
@app_response = Rack::Response
|
|
61
|
+
@app_response = Rack::Response[*@app_response]
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
def content_types
|
|
@@ -73,7 +74,15 @@ module Grape
|
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
def content_type
|
|
76
|
-
content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) ||
|
|
77
|
+
content_type_for(env[Grape::Env::API_FORMAT] || options[:format]) || 'text/html'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def query_params
|
|
81
|
+
rack_request.GET
|
|
82
|
+
rescue Rack::QueryParser::ParamsTooDeepError
|
|
83
|
+
raise Grape::Exceptions::TooDeepParameters.new(Rack::Utils.param_depth_limit)
|
|
84
|
+
rescue Rack::Utils::ParameterTypeError
|
|
85
|
+
raise Grape::Exceptions::ConflictingTypes
|
|
77
86
|
end
|
|
78
87
|
|
|
79
88
|
private
|
|
@@ -90,6 +99,16 @@ module Grape
|
|
|
90
99
|
def content_types_indifferent_access
|
|
91
100
|
@content_types_indifferent_access ||= content_types.with_indifferent_access
|
|
92
101
|
end
|
|
102
|
+
|
|
103
|
+
def merge_default_options(options)
|
|
104
|
+
if respond_to?(:default_options)
|
|
105
|
+
default_options.deep_merge(options)
|
|
106
|
+
elsif self.class.const_defined?(:DEFAULT_OPTIONS)
|
|
107
|
+
self.class::DEFAULT_OPTIONS.deep_merge(options)
|
|
108
|
+
else
|
|
109
|
+
options
|
|
110
|
+
end
|
|
111
|
+
end
|
|
93
112
|
end
|
|
94
113
|
end
|
|
95
114
|
end
|
|
@@ -3,31 +3,18 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Middleware
|
|
5
5
|
class Error < Base
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
backtrace: false, # true to display backtrace, true to let Grape handle Grape::Exceptions
|
|
19
|
-
original_exception: false # true to display exception
|
|
20
|
-
},
|
|
21
|
-
rescue_handlers: {}, # rescue handler blocks
|
|
22
|
-
base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
|
|
23
|
-
all_rescue_handler: nil # rescue handler block to rescue from all exceptions
|
|
24
|
-
}
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def initialize(app, *options)
|
|
28
|
-
super
|
|
29
|
-
self.class.include(@options[:helpers]) if @options[:helpers]
|
|
30
|
-
end
|
|
6
|
+
DEFAULT_OPTIONS = {
|
|
7
|
+
default_status: 500,
|
|
8
|
+
default_message: '',
|
|
9
|
+
format: :txt,
|
|
10
|
+
rescue_all: false,
|
|
11
|
+
rescue_grape_exceptions: false,
|
|
12
|
+
rescue_subclasses: true,
|
|
13
|
+
rescue_options: {
|
|
14
|
+
backtrace: false,
|
|
15
|
+
original_exception: false
|
|
16
|
+
}.freeze
|
|
17
|
+
}.freeze
|
|
31
18
|
|
|
32
19
|
def call!(env)
|
|
33
20
|
@env = env
|
|
@@ -39,7 +26,7 @@ module Grape
|
|
|
39
26
|
private
|
|
40
27
|
|
|
41
28
|
def rack_response(status, headers, message)
|
|
42
|
-
message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] ==
|
|
29
|
+
message = Rack::Utils.escape_html(message) if headers[Rack::CONTENT_TYPE] == 'text/html'
|
|
43
30
|
Rack::Response.new(Array.wrap(message), Rack::Utils.status_code(status), Grape::Util::Header.new.merge(headers))
|
|
44
31
|
end
|
|
45
32
|
|
|
@@ -111,12 +98,7 @@ module Grape
|
|
|
111
98
|
end
|
|
112
99
|
|
|
113
100
|
def run_rescue_handler(handler, error, endpoint)
|
|
114
|
-
if handler.instance_of?(Symbol)
|
|
115
|
-
raise NoMethodError, "undefined method '#{handler}'" unless respond_to?(handler)
|
|
116
|
-
|
|
117
|
-
handler = public_method(handler)
|
|
118
|
-
end
|
|
119
|
-
|
|
101
|
+
handler = endpoint.public_method(handler) if handler.instance_of?(Symbol)
|
|
120
102
|
response = catch(:error) do
|
|
121
103
|
handler.arity.zero? ? endpoint.instance_exec(&handler) : endpoint.instance_exec(error, &handler)
|
|
122
104
|
end
|
|
@@ -3,16 +3,11 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Middleware
|
|
5
5
|
class Formatter < Base
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
default_format: :txt,
|
|
12
|
-
formatters: {},
|
|
13
|
-
parsers: {}
|
|
14
|
-
}
|
|
15
|
-
end
|
|
6
|
+
DEFAULT_OPTIONS = {
|
|
7
|
+
default_format: :txt
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
ALL_MEDIA_TYPES = '*/*'
|
|
16
11
|
|
|
17
12
|
def before
|
|
18
13
|
negotiate_content_type
|
|
@@ -69,34 +64,27 @@ module Grape
|
|
|
69
64
|
end
|
|
70
65
|
end
|
|
71
66
|
|
|
72
|
-
def request
|
|
73
|
-
@request ||= Rack::Request.new(env)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# store read input in env['api.request.input']
|
|
77
67
|
def read_body_input
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
!request.parseable_data? &&
|
|
82
|
-
(request.content_length.to_i.positive? || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
|
|
83
|
-
|
|
84
|
-
return unless (input = env[Rack::RACK_INPUT])
|
|
68
|
+
input = rack_request.body # reads RACK_INPUT
|
|
69
|
+
return if input.nil?
|
|
70
|
+
return unless read_body_input?
|
|
85
71
|
|
|
86
|
-
|
|
72
|
+
input.try(:rewind)
|
|
87
73
|
body = env[Grape::Env::API_REQUEST_INPUT] = input.read
|
|
88
74
|
begin
|
|
89
|
-
read_rack_input(body)
|
|
75
|
+
read_rack_input(body)
|
|
90
76
|
ensure
|
|
91
|
-
|
|
77
|
+
input.try(:rewind)
|
|
92
78
|
end
|
|
93
79
|
end
|
|
94
80
|
|
|
95
|
-
# store parsed input in env['api.request.body']
|
|
96
81
|
def read_rack_input(body)
|
|
97
|
-
|
|
82
|
+
return if body.empty?
|
|
98
83
|
|
|
99
|
-
|
|
84
|
+
media_type = rack_request.media_type
|
|
85
|
+
fmt = media_type ? mime_types[media_type] : options[:default_format]
|
|
86
|
+
|
|
87
|
+
throw :error, status: 415, message: "The provided content-type '#{media_type}' is not supported." unless content_type_for(fmt)
|
|
100
88
|
parser = Grape::Parser.parser_for fmt, options[:parsers]
|
|
101
89
|
if parser
|
|
102
90
|
begin
|
|
@@ -119,63 +107,43 @@ module Grape
|
|
|
119
107
|
end
|
|
120
108
|
end
|
|
121
109
|
|
|
110
|
+
# this middleware will not try to format the following content-types since Rack already handles them
|
|
111
|
+
# when calling Rack's `params` function
|
|
112
|
+
# - application/x-www-form-urlencoded
|
|
113
|
+
# - multipart/form-data
|
|
114
|
+
# - multipart/related
|
|
115
|
+
# - multipart/mixed
|
|
116
|
+
def read_body_input?
|
|
117
|
+
(rack_request.post? || rack_request.put? || rack_request.patch? || rack_request.delete?) &&
|
|
118
|
+
!(rack_request.form_data? && rack_request.content_type) &&
|
|
119
|
+
!rack_request.parseable_data? &&
|
|
120
|
+
(rack_request.content_length.to_i.positive? || rack_request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
|
|
121
|
+
end
|
|
122
|
+
|
|
122
123
|
def negotiate_content_type
|
|
123
|
-
fmt = format_from_extension ||
|
|
124
|
+
fmt = format_from_extension || query_params['format'] || options[:format] || format_from_header || options[:default_format]
|
|
124
125
|
if content_type_for(fmt)
|
|
125
|
-
env[Grape::Env::API_FORMAT] = fmt
|
|
126
|
+
env[Grape::Env::API_FORMAT] = fmt.to_sym
|
|
126
127
|
else
|
|
127
128
|
throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
|
|
128
129
|
end
|
|
129
130
|
end
|
|
130
131
|
|
|
131
132
|
def format_from_extension
|
|
132
|
-
|
|
133
|
+
request_path = rack_request.path.try(:scrub)
|
|
134
|
+
dot_pos = request_path.rindex('.')
|
|
135
|
+
return unless dot_pos
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
# avoid symbol memory leak on an unknown format
|
|
137
|
-
return extension.to_sym if content_type_for(extension)
|
|
138
|
-
end
|
|
139
|
-
nil
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def format_from_params
|
|
143
|
-
fmt = Rack::Utils.parse_nested_query(env[Rack::QUERY_STRING])[FORMAT]
|
|
144
|
-
# avoid symbol memory leak on an unknown format
|
|
145
|
-
return fmt.to_sym if content_type_for(fmt)
|
|
146
|
-
|
|
147
|
-
fmt
|
|
137
|
+
extension = request_path[(dot_pos + 1)..]
|
|
138
|
+
extension if content_type_for(extension)
|
|
148
139
|
end
|
|
149
140
|
|
|
150
141
|
def format_from_header
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
end
|
|
154
|
-
nil
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def mime_array
|
|
158
|
-
accept = env[Grape::Http::Headers::HTTP_ACCEPT]
|
|
159
|
-
return [] unless accept
|
|
160
|
-
|
|
161
|
-
accept_into_mime_and_quality = %r{
|
|
162
|
-
(
|
|
163
|
-
\w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
|
|
164
|
-
(?:
|
|
165
|
-
(?:;[^,]*?)? # optionally multiple formats in a row
|
|
166
|
-
;\s*q=([\w.]+) # optional "quality" preference (eg q=0.5)
|
|
167
|
-
)?
|
|
168
|
-
}x
|
|
169
|
-
|
|
170
|
-
vendor_prefix_pattern = /vnd\.[^+]+\+/
|
|
171
|
-
|
|
172
|
-
accept.scan(accept_into_mime_and_quality)
|
|
173
|
-
.sort_by { |_, quality_preference| -(quality_preference ? quality_preference.to_f : 1.0) }
|
|
174
|
-
.flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
|
|
175
|
-
end
|
|
142
|
+
accept_header = env['HTTP_ACCEPT'].try(:scrub)
|
|
143
|
+
return if accept_header.blank? || accept_header == ALL_MEDIA_TYPES
|
|
176
144
|
|
|
177
|
-
|
|
178
|
-
|
|
145
|
+
media_type = Rack::Utils.best_q_match(accept_header, mime_types.keys)
|
|
146
|
+
mime_types[media_type] if media_type
|
|
179
147
|
end
|
|
180
148
|
end
|
|
181
149
|
end
|
|
@@ -5,10 +5,11 @@ module Grape
|
|
|
5
5
|
# Class to handle the stack of middlewares based on ActionDispatch::MiddlewareStack
|
|
6
6
|
# It allows to insert and insert after
|
|
7
7
|
class Stack
|
|
8
|
+
extend Forwardable
|
|
8
9
|
class Middleware
|
|
9
10
|
attr_reader :args, :block, :klass
|
|
10
11
|
|
|
11
|
-
def initialize(klass,
|
|
12
|
+
def initialize(klass, args, block)
|
|
12
13
|
@klass = klass
|
|
13
14
|
@args = args
|
|
14
15
|
@block = block
|
|
@@ -31,8 +32,12 @@ module Grape
|
|
|
31
32
|
klass.to_s
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def
|
|
35
|
-
|
|
35
|
+
def build(builder)
|
|
36
|
+
# we need to force the ruby2_keywords_hash for middlewares that initialize contains keywords
|
|
37
|
+
# like ActionDispatch::RequestId since middleware arguments are serialized
|
|
38
|
+
# https://rubyapi.org/3.4/o/hash#method-c-ruby2_keywords_hash
|
|
39
|
+
args[-1] = Hash.ruby2_keywords_hash(args[-1]) if args.last.is_a?(Hash) && Hash.respond_to?(:ruby2_keywords_hash)
|
|
40
|
+
builder.use(klass, *args, &block)
|
|
36
41
|
end
|
|
37
42
|
end
|
|
38
43
|
|
|
@@ -40,72 +45,57 @@ module Grape
|
|
|
40
45
|
|
|
41
46
|
attr_accessor :middlewares, :others
|
|
42
47
|
|
|
48
|
+
def_delegators :middlewares, :each, :size, :last, :[]
|
|
49
|
+
|
|
43
50
|
def initialize
|
|
44
51
|
@middlewares = []
|
|
45
52
|
@others = []
|
|
46
53
|
end
|
|
47
54
|
|
|
48
|
-
def
|
|
49
|
-
@middlewares.each(&block)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def size
|
|
53
|
-
middlewares.size
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def last
|
|
57
|
-
middlewares.last
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def [](index)
|
|
61
|
-
middlewares[index]
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def insert(index, *args, &block)
|
|
55
|
+
def insert(index, klass, *args, &block)
|
|
65
56
|
index = assert_index(index, :before)
|
|
66
|
-
|
|
67
|
-
middlewares.insert(index, middleware)
|
|
57
|
+
middlewares.insert(index, self.class::Middleware.new(klass, args, block))
|
|
68
58
|
end
|
|
69
|
-
ruby2_keywords :insert if respond_to?(:ruby2_keywords, true)
|
|
70
59
|
|
|
71
60
|
alias insert_before insert
|
|
72
61
|
|
|
73
|
-
def insert_after(index,
|
|
62
|
+
def insert_after(index, ...)
|
|
74
63
|
index = assert_index(index, :after)
|
|
75
|
-
insert(index + 1,
|
|
64
|
+
insert(index + 1, ...)
|
|
76
65
|
end
|
|
77
|
-
ruby2_keywords :insert_after if respond_to?(:ruby2_keywords, true)
|
|
78
66
|
|
|
79
|
-
def use(
|
|
80
|
-
middleware = self.class::Middleware.new(
|
|
67
|
+
def use(klass, *args, &block)
|
|
68
|
+
middleware = self.class::Middleware.new(klass, args, block)
|
|
81
69
|
middlewares.push(middleware)
|
|
82
70
|
end
|
|
83
71
|
|
|
84
72
|
def merge_with(middleware_specs)
|
|
85
|
-
middleware_specs.each do |operation, *args|
|
|
73
|
+
middleware_specs.each do |operation, klass, *args|
|
|
86
74
|
if args.last.is_a?(Proc)
|
|
87
75
|
last_proc = args.pop
|
|
88
|
-
public_send(operation, *args, &last_proc)
|
|
76
|
+
public_send(operation, klass, *args, &last_proc)
|
|
89
77
|
else
|
|
90
|
-
public_send(operation, *args)
|
|
78
|
+
public_send(operation, klass, *args)
|
|
91
79
|
end
|
|
92
80
|
end
|
|
93
81
|
end
|
|
94
82
|
|
|
95
83
|
# @return [Rack::Builder] the builder object with our middlewares applied
|
|
96
|
-
def build
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
m
|
|
84
|
+
def build
|
|
85
|
+
Rack::Builder.new.tap do |builder|
|
|
86
|
+
others.shift(others.size).each { |m| merge_with(m) }
|
|
87
|
+
middlewares.each do |m|
|
|
88
|
+
m.build(builder)
|
|
89
|
+
end
|
|
100
90
|
end
|
|
101
|
-
builder
|
|
102
91
|
end
|
|
103
92
|
|
|
104
93
|
# @description Add middlewares with :use operation to the stack. Store others with :insert_* operation for later
|
|
105
94
|
# @param [Array] other_specs An array of middleware specifications (e.g. [[:use, klass], [:insert_before, *args]])
|
|
106
95
|
def concat(other_specs)
|
|
107
|
-
|
|
108
|
-
|
|
96
|
+
use, not_use = other_specs.partition { |o| o.first == :use }
|
|
97
|
+
others << not_use
|
|
98
|
+
merge_with(use)
|
|
109
99
|
end
|
|
110
100
|
|
|
111
101
|
protected
|
|
@@ -18,10 +18,8 @@ module Grape
|
|
|
18
18
|
# route.
|
|
19
19
|
class AcceptVersionHeader < Base
|
|
20
20
|
def before
|
|
21
|
-
potential_version = env[
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
not_acceptable!('Accept-Version header must be set.') if strict? && potential_version.blank?
|
|
21
|
+
potential_version = env['HTTP_ACCEPT_VERSION'].try(:scrub)
|
|
22
|
+
not_acceptable!('Accept-Version header must be set.') if strict && potential_version.blank?
|
|
25
23
|
|
|
26
24
|
return if potential_version.blank?
|
|
27
25
|
|