grape 0.12.0 → 0.14.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Appraisals +9 -4
- data/CHANGELOG.md +265 -215
- data/CONTRIBUTING.md +4 -4
- data/Gemfile +0 -1
- data/Gemfile.lock +166 -0
- data/README.md +426 -161
- data/RELEASING.md +14 -6
- data/Rakefile +30 -33
- data/UPGRADING.md +54 -23
- data/benchmark/simple.rb +27 -0
- data/gemfiles/rack_1.5.2.gemfile +13 -0
- data/gemfiles/rails_3.gemfile +2 -2
- data/gemfiles/rails_4.gemfile +1 -2
- data/grape.gemspec +6 -7
- data/lib/grape/api.rb +24 -4
- data/lib/grape/dsl/callbacks.rb +20 -0
- data/lib/grape/dsl/configuration.rb +59 -2
- data/lib/grape/dsl/helpers.rb +8 -3
- data/lib/grape/dsl/inside_route.rb +100 -45
- data/lib/grape/dsl/parameters.rb +96 -7
- data/lib/grape/dsl/request_response.rb +1 -1
- data/lib/grape/dsl/routing.rb +17 -4
- data/lib/grape/dsl/settings.rb +36 -1
- data/lib/grape/dsl/validations.rb +7 -5
- data/lib/grape/endpoint.rb +102 -57
- data/lib/grape/error_formatter/base.rb +6 -6
- data/lib/grape/exceptions/base.rb +5 -5
- data/lib/grape/exceptions/invalid_version_header.rb +10 -0
- data/lib/grape/exceptions/unknown_parameter.rb +10 -0
- data/lib/grape/exceptions/validation_errors.rb +4 -3
- data/lib/grape/formatter/serializable_hash.rb +3 -2
- data/lib/grape/http/headers.rb +0 -1
- data/lib/grape/locale/en.yml +5 -1
- data/lib/grape/middleware/auth/base.rb +2 -2
- data/lib/grape/middleware/auth/dsl.rb +1 -1
- data/lib/grape/middleware/auth/strategies.rb +1 -1
- data/lib/grape/middleware/base.rb +8 -4
- data/lib/grape/middleware/error.rb +3 -2
- data/lib/grape/middleware/filter.rb +1 -1
- data/lib/grape/middleware/formatter.rb +64 -45
- data/lib/grape/middleware/globals.rb +3 -3
- data/lib/grape/middleware/versioner/accept_version_header.rb +5 -7
- data/lib/grape/middleware/versioner/header.rb +113 -50
- data/lib/grape/middleware/versioner/param.rb +5 -8
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +20 -0
- data/lib/grape/middleware/versioner/path.rb +3 -6
- data/lib/grape/namespace.rb +13 -2
- data/lib/grape/path.rb +4 -3
- data/lib/grape/request.rb +40 -0
- data/lib/grape/route.rb +5 -0
- data/lib/grape/util/content_types.rb +9 -9
- data/lib/grape/util/env.rb +22 -0
- data/lib/grape/util/file_response.rb +21 -0
- data/lib/grape/util/inheritable_setting.rb +23 -2
- data/lib/grape/util/inheritable_values.rb +1 -1
- data/lib/grape/util/stackable_values.rb +5 -2
- data/lib/grape/util/strict_hash_configuration.rb +2 -1
- data/lib/grape/validations/attributes_iterator.rb +8 -3
- data/lib/grape/validations/params_scope.rb +164 -22
- data/lib/grape/validations/types/build_coercer.rb +53 -0
- data/lib/grape/validations/types/custom_type_coercer.rb +183 -0
- data/lib/grape/validations/types/file.rb +28 -0
- data/lib/grape/validations/types/json.rb +65 -0
- data/lib/grape/validations/types/multiple_type_coercer.rb +76 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +59 -0
- data/lib/grape/validations/types/virtus_collection_patch.rb +16 -0
- data/lib/grape/validations/types.rb +144 -0
- data/lib/grape/validations/validators/all_or_none.rb +1 -1
- data/lib/grape/validations/validators/allow_blank.rb +3 -3
- data/lib/grape/validations/validators/base.rb +7 -0
- data/lib/grape/validations/validators/coerce.rb +32 -34
- data/lib/grape/validations/validators/presence.rb +2 -3
- data/lib/grape/validations/validators/regexp.rb +2 -4
- data/lib/grape/validations/validators/values.rb +3 -3
- data/lib/grape/validations.rb +5 -0
- data/lib/grape/version.rb +2 -1
- data/lib/grape.rb +15 -12
- data/pkg/grape-0.13.0.gem +0 -0
- data/spec/grape/api/custom_validations_spec.rb +5 -4
- data/spec/grape/api/deeply_included_options_spec.rb +7 -7
- data/spec/grape/api/nested_helpers_spec.rb +4 -2
- data/spec/grape/api/shared_helpers_spec.rb +8 -8
- data/spec/grape/api_spec.rb +151 -54
- data/spec/grape/dsl/configuration_spec.rb +13 -0
- data/spec/grape/dsl/helpers_spec.rb +16 -2
- data/spec/grape/dsl/inside_route_spec.rb +40 -4
- data/spec/grape/dsl/parameters_spec.rb +0 -6
- data/spec/grape/dsl/routing_spec.rb +1 -1
- data/spec/grape/dsl/validations_spec.rb +18 -0
- data/spec/grape/endpoint_spec.rb +130 -6
- data/spec/grape/entity_spec.rb +10 -8
- data/spec/grape/exceptions/invalid_accept_header_spec.rb +1 -15
- data/spec/grape/exceptions/validation_errors_spec.rb +28 -0
- data/spec/grape/integration/rack_spec.rb +3 -2
- data/spec/grape/middleware/base_spec.rb +40 -16
- data/spec/grape/middleware/error_spec.rb +16 -15
- data/spec/grape/middleware/exception_spec.rb +45 -43
- data/spec/grape/middleware/formatter_spec.rb +34 -5
- data/spec/grape/middleware/versioner/header_spec.rb +79 -47
- data/spec/grape/path_spec.rb +10 -10
- data/spec/grape/presenters/presenter_spec.rb +2 -2
- data/spec/grape/request_spec.rb +100 -0
- data/spec/grape/util/inheritable_values_spec.rb +14 -0
- data/spec/grape/util/stackable_values_spec.rb +10 -0
- data/spec/grape/validations/params_scope_spec.rb +86 -0
- data/spec/grape/validations/types_spec.rb +95 -0
- data/spec/grape/validations/validators/coerce_spec.rb +364 -10
- data/spec/grape/validations/validators/values_spec.rb +27 -15
- data/spec/grape/validations_spec.rb +53 -24
- data/spec/shared/versioning_examples.rb +2 -2
- data/spec/spec_helper.rb +0 -1
- data/spec/support/versioned_helpers.rb +2 -2
- metadata +55 -14
- data/.gitignore +0 -46
- data/.rspec +0 -2
- data/.rubocop.yml +0 -7
- data/.rubocop_todo.yml +0 -84
- data/.travis.yml +0 -20
- data/.yardopts +0 -2
- data/lib/backports/active_support/deep_dup.rb +0 -49
- data/lib/backports/active_support/duplicable.rb +0 -88
- data/lib/grape/http/request.rb +0 -27
@@ -3,6 +3,8 @@ require 'grape/middleware/base'
|
|
3
3
|
module Grape
|
4
4
|
module Middleware
|
5
5
|
class Formatter < Base
|
6
|
+
CHUNKED = 'chunked'.freeze
|
7
|
+
|
6
8
|
def default_options
|
7
9
|
{
|
8
10
|
default_format: :txt,
|
@@ -11,13 +13,6 @@ module Grape
|
|
11
13
|
}
|
12
14
|
end
|
13
15
|
|
14
|
-
def headers
|
15
|
-
env.dup.inject({}) do |h, (k, v)|
|
16
|
-
h[k.to_s.downcase[5..-1]] = v if k.to_s.downcase.start_with?('http_')
|
17
|
-
h
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
16
|
def before
|
22
17
|
negotiate_content_type
|
23
18
|
read_body_input
|
@@ -25,46 +20,70 @@ module Grape
|
|
25
20
|
|
26
21
|
def after
|
27
22
|
status, headers, bodies = *@app_response
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
bodies.collect do |body|
|
34
|
-
formatter.call body, env
|
35
|
-
end
|
36
|
-
else
|
37
|
-
bodies
|
38
|
-
end
|
39
|
-
rescue Grape::Exceptions::InvalidFormatter => e
|
40
|
-
throw :error, status: 500, message: e.message
|
23
|
+
|
24
|
+
if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include?(status)
|
25
|
+
@app_response
|
26
|
+
else
|
27
|
+
build_formatted_response(status, headers, bodies)
|
41
28
|
end
|
42
|
-
headers[Grape::Http::Headers::CONTENT_TYPE] = content_type_for(env['api.format']) unless headers[Grape::Http::Headers::CONTENT_TYPE]
|
43
|
-
Rack::Response.new(bodymap, status, headers)
|
44
29
|
end
|
45
30
|
|
46
31
|
private
|
47
32
|
|
33
|
+
def build_formatted_response(status, headers, bodies)
|
34
|
+
headers = ensure_content_type(headers)
|
35
|
+
|
36
|
+
if bodies.is_a?(Grape::Util::FileResponse)
|
37
|
+
Rack::Response.new([], status, headers) do |resp|
|
38
|
+
resp.body = bodies.file
|
39
|
+
end
|
40
|
+
else
|
41
|
+
# Allow content-type to be explicitly overwritten
|
42
|
+
formatter = fetch_formatter(headers, options)
|
43
|
+
bodymap = bodies.collect { |body| formatter.call(body, env) }
|
44
|
+
Rack::Response.new(bodymap, status, headers)
|
45
|
+
end
|
46
|
+
rescue Grape::Exceptions::InvalidFormatter => e
|
47
|
+
throw :error, status: 500, message: e.message
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_formatter(headers, options)
|
51
|
+
api_format = mime_types[headers[Grape::Http::Headers::CONTENT_TYPE]] || env[Grape::Env::API_FORMAT]
|
52
|
+
Grape::Formatter::Base.formatter_for(api_format, options)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Set the content type header for the API format if it is not already present.
|
56
|
+
#
|
57
|
+
# @param headers [Hash]
|
58
|
+
# @return [Hash]
|
59
|
+
def ensure_content_type(headers)
|
60
|
+
if headers[Grape::Http::Headers::CONTENT_TYPE]
|
61
|
+
headers
|
62
|
+
else
|
63
|
+
headers.merge(Grape::Http::Headers::CONTENT_TYPE => content_type_for(env[Grape::Env::API_FORMAT]))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
48
67
|
def request
|
49
68
|
@request ||= Rack::Request.new(env)
|
50
69
|
end
|
51
70
|
|
52
71
|
# store read input in env['api.request.input']
|
53
72
|
def read_body_input
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
73
|
+
return unless
|
74
|
+
(request.post? || request.put? || request.patch? || request.delete?) &&
|
75
|
+
(!request.form_data? || !request.media_type) &&
|
76
|
+
(!request.parseable_data?) &&
|
77
|
+
(request.content_length.to_i > 0 || request.env[Grape::Http::Headers::HTTP_TRANSFER_ENCODING] == CHUNKED)
|
78
|
+
|
79
|
+
return unless (input = env[Grape::Env::RACK_INPUT])
|
80
|
+
|
81
|
+
input.rewind
|
82
|
+
body = env[Grape::Env::API_REQUEST_INPUT] = input.read
|
83
|
+
begin
|
84
|
+
read_rack_input(body) if body && body.length > 0
|
85
|
+
ensure
|
86
|
+
input.rewind
|
68
87
|
end
|
69
88
|
end
|
70
89
|
|
@@ -76,14 +95,14 @@ module Grape
|
|
76
95
|
parser = Grape::Parser::Base.parser_for fmt, options
|
77
96
|
if parser
|
78
97
|
begin
|
79
|
-
body = (env[
|
98
|
+
body = (env[Grape::Env::API_REQUEST_BODY] = parser.call(body, env))
|
80
99
|
if body.is_a?(Hash)
|
81
|
-
if env[
|
82
|
-
env[
|
100
|
+
if env[Grape::Env::RACK_REQUEST_FORM_HASH]
|
101
|
+
env[Grape::Env::RACK_REQUEST_FORM_HASH] = env[Grape::Env::RACK_REQUEST_FORM_HASH].merge(body)
|
83
102
|
else
|
84
|
-
env[
|
103
|
+
env[Grape::Env::RACK_REQUEST_FORM_HASH] = body
|
85
104
|
end
|
86
|
-
env[
|
105
|
+
env[Grape::Env::RACK_REQUEST_FORM_INPUT] = env[Grape::Env::RACK_INPUT]
|
87
106
|
end
|
88
107
|
rescue Grape::Exceptions::Base => e
|
89
108
|
raise e
|
@@ -91,7 +110,7 @@ module Grape
|
|
91
110
|
throw :error, status: 400, message: e.message
|
92
111
|
end
|
93
112
|
else
|
94
|
-
env[
|
113
|
+
env[Grape::Env::API_REQUEST_BODY] = body
|
95
114
|
end
|
96
115
|
else
|
97
116
|
throw :error, status: 406, message: "The requested content-type '#{request.media_type}' is not supported."
|
@@ -101,7 +120,7 @@ module Grape
|
|
101
120
|
def negotiate_content_type
|
102
121
|
fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
|
103
122
|
if content_type_for(fmt)
|
104
|
-
env[
|
123
|
+
env[Grape::Env::API_FORMAT] = fmt
|
105
124
|
else
|
106
125
|
throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
|
107
126
|
end
|
@@ -133,7 +152,7 @@ module Grape
|
|
133
152
|
end
|
134
153
|
|
135
154
|
def mime_array
|
136
|
-
accept =
|
155
|
+
accept = env[Grape::Http::Headers::HTTP_ACCEPT]
|
137
156
|
return [] unless accept
|
138
157
|
|
139
158
|
accept_into_mime_and_quality = %r{
|
@@ -149,7 +168,7 @@ module Grape
|
|
149
168
|
|
150
169
|
accept.scan(accept_into_mime_and_quality)
|
151
170
|
.sort_by { |_, quality_preference| -quality_preference.to_f }
|
152
|
-
.
|
171
|
+
.flat_map { |mime, _| [mime, mime.sub(vendor_prefix_pattern, '')] }
|
153
172
|
end
|
154
173
|
end
|
155
174
|
end
|
@@ -5,9 +5,9 @@ module Grape
|
|
5
5
|
class Globals < Base
|
6
6
|
def before
|
7
7
|
request = Grape::Request.new(@env)
|
8
|
-
@env[
|
9
|
-
@env[
|
10
|
-
@env[
|
8
|
+
@env[Grape::Env::GRAPE_REQUEST] = request
|
9
|
+
@env[Grape::Env::GRAPE_REQUEST_HEADERS] = request.headers
|
10
|
+
@env[Grape::Env::GRAPE_REQUEST_PARAMS] = request.params if @env[Grape::Env::RACK_INPUT]
|
11
11
|
end
|
12
12
|
end
|
13
13
|
end
|
@@ -27,14 +27,12 @@ module Grape
|
|
27
27
|
end
|
28
28
|
end
|
29
29
|
|
30
|
-
|
31
|
-
# If the requested version is not supported:
|
32
|
-
unless versions.any? { |v| v.to_s == potential_version }
|
33
|
-
throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.'
|
34
|
-
end
|
30
|
+
return if potential_version.empty?
|
35
31
|
|
36
|
-
|
37
|
-
|
32
|
+
# If the requested version is not supported:
|
33
|
+
throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.' unless versions.any? { |v| v.to_s == potential_version }
|
34
|
+
|
35
|
+
env[Grape::Env::API_VERSION] = potential_version
|
38
36
|
end
|
39
37
|
|
40
38
|
private
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'grape/middleware/base'
|
2
|
+
require 'grape/middleware/versioner/parse_media_type_patch'
|
2
3
|
|
3
4
|
module Grape
|
4
5
|
module Middleware
|
@@ -8,13 +9,13 @@ module Grape
|
|
8
9
|
# application/vnd.:vendor-:version+:format
|
9
10
|
#
|
10
11
|
# Example: For request header
|
11
|
-
# Accept: application/vnd.mycompany-v1+json
|
12
|
+
# Accept: application/vnd.mycompany.a-cool-resource-v1+json
|
12
13
|
#
|
13
14
|
# The following rack env variables are set:
|
14
15
|
#
|
15
16
|
# env['api.type'] => 'application'
|
16
|
-
# env['api.subtype'] => 'vnd.mycompany-v1+json'
|
17
|
-
# env['api.vendor] => 'mycompany'
|
17
|
+
# env['api.subtype'] => 'vnd.mycompany.a-cool-resource-v1+json'
|
18
|
+
# env['api.vendor] => 'mycompany.a-cool-resource'
|
18
19
|
# env['api.version] => 'v1'
|
19
20
|
# env['api.format] => 'json'
|
20
21
|
#
|
@@ -22,54 +23,88 @@ module Grape
|
|
22
23
|
# X-Cascade header to alert Rack::Mount to attempt the next matched
|
23
24
|
# route.
|
24
25
|
class Header < Base
|
25
|
-
|
26
|
-
|
26
|
+
VENDOR_VERSION_HEADER_REGEX =
|
27
|
+
/\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))?(?:\+([a-z0-9*\-.]+))?\z/
|
27
28
|
|
28
|
-
|
29
|
-
|
30
|
-
if header.qvalues.empty?
|
31
|
-
fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must be set.', error_headers)
|
32
|
-
end
|
33
|
-
# Remove any acceptable content types with ranges.
|
34
|
-
header.qvalues.reject! do |media_type, _|
|
35
|
-
Rack::Accept::Header.parse_media_type(media_type).find { |s| s == '*' }
|
36
|
-
end
|
37
|
-
# If all Accept headers included a range:
|
38
|
-
if header.qvalues.empty?
|
39
|
-
fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must not contain ranges ("*").',
|
40
|
-
error_headers)
|
41
|
-
end
|
42
|
-
end
|
29
|
+
HAS_VENDOR_REGEX = /\Avnd\.[a-z0-9.\-_!#\$&\^]+/
|
30
|
+
HAS_VERSION_REGEX = /\Avnd\.([a-z0-9.\-_!#\$&\^]+?)(?:-([a-z0-9*.]+))+/
|
43
31
|
|
44
|
-
|
32
|
+
def before
|
33
|
+
strict_header_checks if strict?
|
45
34
|
|
46
35
|
if media_type
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
env['api.vendor'] = Regexp.last_match[1]
|
53
|
-
env['api.version'] = Regexp.last_match[2]
|
54
|
-
env['api.format'] = Regexp.last_match[3] # weird that Grape::Middleware::Formatter also does this
|
55
|
-
end
|
56
|
-
# If none of the available content types are acceptable:
|
57
|
-
elsif strict?
|
58
|
-
fail Grape::Exceptions::InvalidAcceptHeader.new('406 Not Acceptable', error_headers)
|
59
|
-
# If all acceptable content types specify a vendor or version that doesn't exist:
|
60
|
-
elsif header.values.all? { |header_value| has_vendor?(header_value) || version?(header_value) }
|
61
|
-
fail Grape::Exceptions::InvalidAcceptHeader.new('API vendor or version not found.', error_headers)
|
36
|
+
media_type_header_handler
|
37
|
+
elsif headers_contain_wrong_vendor?
|
38
|
+
fail_with_invalid_accept_header!('API vendor not found.')
|
39
|
+
elsif headers_contain_wrong_version?
|
40
|
+
fail_with_invalid_version_header!('API version not found.')
|
62
41
|
end
|
63
42
|
end
|
64
43
|
|
65
44
|
private
|
66
45
|
|
46
|
+
def strict_header_checks
|
47
|
+
strict_accept_header_presence_check
|
48
|
+
strict_version_vendor_accept_header_presence_check
|
49
|
+
end
|
50
|
+
|
51
|
+
def strict_accept_header_presence_check
|
52
|
+
return unless header.qvalues.empty?
|
53
|
+
fail_with_invalid_accept_header!('Accept header must be set.')
|
54
|
+
end
|
55
|
+
|
56
|
+
def strict_version_vendor_accept_header_presence_check
|
57
|
+
return unless versions.present?
|
58
|
+
return if an_accept_header_with_version_and_vendor_is_present?
|
59
|
+
fail_with_invalid_accept_header!('API vendor or version not found.')
|
60
|
+
end
|
61
|
+
|
62
|
+
def an_accept_header_with_version_and_vendor_is_present?
|
63
|
+
header.qvalues.keys.any? do |h|
|
64
|
+
VENDOR_VERSION_HEADER_REGEX =~ h.sub('application/', '')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def header
|
69
|
+
@header ||= rack_accept_header
|
70
|
+
end
|
71
|
+
|
72
|
+
def media_type
|
73
|
+
@media_type ||= header.best_of(available_media_types)
|
74
|
+
end
|
75
|
+
|
76
|
+
def media_type_header_handler
|
77
|
+
type, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
78
|
+
env[Grape::Env::API_TYPE] = type
|
79
|
+
env[Grape::Env::API_SUBTYPE] = subtype
|
80
|
+
|
81
|
+
return unless VENDOR_VERSION_HEADER_REGEX =~ subtype
|
82
|
+
|
83
|
+
env[Grape::Env::API_VENDOR] = Regexp.last_match[1]
|
84
|
+
env[Grape::Env::API_VERSION] = Regexp.last_match[2]
|
85
|
+
# weird that Grape::Middleware::Formatter also does this
|
86
|
+
env[Grape::Env::API_FORMAT] = Regexp.last_match[3]
|
87
|
+
end
|
88
|
+
|
89
|
+
def fail_with_invalid_accept_header!(message)
|
90
|
+
fail Grape::Exceptions::InvalidAcceptHeader
|
91
|
+
.new(message, error_headers)
|
92
|
+
end
|
93
|
+
|
94
|
+
def fail_with_invalid_version_header!(message)
|
95
|
+
fail Grape::Exceptions::InvalidVersionHeader
|
96
|
+
.new(message, error_headers)
|
97
|
+
end
|
98
|
+
|
67
99
|
def available_media_types
|
68
100
|
available_media_types = []
|
69
101
|
|
70
102
|
content_types.each do |extension, _media_type|
|
71
103
|
versions.reverse_each do |version|
|
72
|
-
available_media_types += [
|
104
|
+
available_media_types += [
|
105
|
+
"application/vnd.#{vendor}-#{version}+#{extension}",
|
106
|
+
"application/vnd.#{vendor}-#{version}"
|
107
|
+
]
|
73
108
|
end
|
74
109
|
available_media_types << "application/vnd.#{vendor}+#{extension}"
|
75
110
|
end
|
@@ -83,10 +118,22 @@ module Grape
|
|
83
118
|
available_media_types.flatten
|
84
119
|
end
|
85
120
|
|
121
|
+
def headers_contain_wrong_vendor?
|
122
|
+
header.values.all? do |header_value|
|
123
|
+
vendor?(header_value) && request_vendor(header_value) != vendor
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def headers_contain_wrong_version?
|
128
|
+
header.values.all? do |header_value|
|
129
|
+
version?(header_value) && !versions.include?(request_version(header_value))
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
86
133
|
def rack_accept_header
|
87
134
|
Rack::Accept::MediaType.new env[Grape::Http::Headers::HTTP_ACCEPT]
|
88
135
|
rescue RuntimeError => e
|
89
|
-
|
136
|
+
fail_with_invalid_accept_header!(e.message)
|
90
137
|
end
|
91
138
|
|
92
139
|
def versions
|
@@ -94,19 +141,25 @@ module Grape
|
|
94
141
|
end
|
95
142
|
|
96
143
|
def vendor
|
97
|
-
|
144
|
+
version_options && version_options[:vendor]
|
98
145
|
end
|
99
146
|
|
100
147
|
def strict?
|
101
|
-
|
148
|
+
version_options && version_options[:strict]
|
149
|
+
end
|
150
|
+
|
151
|
+
def version_options
|
152
|
+
options[:version_options]
|
102
153
|
end
|
103
154
|
|
104
|
-
# By default those errors contain an `X-Cascade` header set to `pass`,
|
105
|
-
#
|
106
|
-
#
|
155
|
+
# By default those errors contain an `X-Cascade` header set to `pass`,
|
156
|
+
# which allows nesting and stacking of routes
|
157
|
+
# (see [Rack::Mount](https://github.com/josh/rack-mount) for more
|
158
|
+
# information). To prevent # this behavior, and not add the `X-Cascade`
|
159
|
+
# header, one can set the `:cascade` option to `false`.
|
107
160
|
def cascade?
|
108
|
-
if
|
109
|
-
!!
|
161
|
+
if version_options && version_options.key?(:cascade)
|
162
|
+
!!version_options[:cascade]
|
110
163
|
else
|
111
164
|
true
|
112
165
|
end
|
@@ -118,16 +171,26 @@ module Grape
|
|
118
171
|
|
119
172
|
# @param [String] media_type a content type
|
120
173
|
# @return [Boolean] whether the content type sets a vendor
|
121
|
-
def
|
122
|
-
_, subtype = Rack::Accept::Header.parse_media_type
|
123
|
-
subtype[
|
174
|
+
def vendor?(media_type)
|
175
|
+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
176
|
+
subtype[HAS_VENDOR_REGEX]
|
177
|
+
end
|
178
|
+
|
179
|
+
def request_vendor(media_type)
|
180
|
+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
181
|
+
subtype.match(VENDOR_VERSION_HEADER_REGEX)[1]
|
182
|
+
end
|
183
|
+
|
184
|
+
def request_version(media_type)
|
185
|
+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
186
|
+
subtype.match(VENDOR_VERSION_HEADER_REGEX)[2]
|
124
187
|
end
|
125
188
|
|
126
189
|
# @param [String] media_type a content type
|
127
190
|
# @return [Boolean] whether the content type sets an API version
|
128
191
|
def version?(media_type)
|
129
|
-
_, subtype = Rack::Accept::Header.parse_media_type
|
130
|
-
subtype[
|
192
|
+
_, subtype = Rack::Accept::Header.parse_media_type(media_type)
|
193
|
+
subtype[HAS_VERSION_REGEX]
|
131
194
|
end
|
132
195
|
end
|
133
196
|
end
|
@@ -21,20 +21,17 @@ module Grape
|
|
21
21
|
class Param < Base
|
22
22
|
def default_options
|
23
23
|
{
|
24
|
-
parameter: 'apiver'
|
24
|
+
parameter: 'apiver'.freeze
|
25
25
|
}
|
26
26
|
end
|
27
27
|
|
28
28
|
def before
|
29
29
|
paramkey = options[:parameter]
|
30
30
|
potential_version = Rack::Utils.parse_nested_query(env[Grape::Http::Headers::QUERY_STRING])[paramkey]
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
env['api.version'] = potential_version
|
36
|
-
env['rack.request.query_hash'].delete(paramkey) if env.key? 'rack.request.query_hash'
|
37
|
-
end
|
31
|
+
return if potential_version.nil?
|
32
|
+
throw :error, status: 404, message: '404 API Version Not Found', headers: { Grape::Http::Headers::X_CASCADE => 'pass' } if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
|
33
|
+
env[Grape::Env::API_VERSION] = potential_version
|
34
|
+
env[Grape::Env::RACK_REQUEST_QUERY_HASH].delete(paramkey) if env.key? Grape::Env::RACK_REQUEST_QUERY_HASH
|
38
35
|
end
|
39
36
|
end
|
40
37
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Rack
|
2
|
+
module Accept
|
3
|
+
module Header
|
4
|
+
class << self
|
5
|
+
# Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44
|
6
|
+
def parse_media_type(media_type)
|
7
|
+
# see http://tools.ietf.org/html/rfc6838#section-4.2 for allowed characters in media type names
|
8
|
+
m = media_type.to_s.match(%r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$})
|
9
|
+
m ? [m[1], m[2], m[3] || ''] : []
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class MediaType
|
15
|
+
def parse_media_type(media_type)
|
16
|
+
Header.parse_media_type(media_type)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -33,12 +33,9 @@ module Grape
|
|
33
33
|
|
34
34
|
pieces = path.split('/')
|
35
35
|
potential_version = pieces[1]
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
40
|
-
env['api.version'] = potential_version
|
41
|
-
end
|
36
|
+
return unless potential_version =~ options[:pattern]
|
37
|
+
throw :error, status: 404, message: '404 API Version Not Found' if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
|
38
|
+
env[Grape::Env::API_VERSION] = potential_version
|
42
39
|
end
|
43
40
|
|
44
41
|
private
|
data/lib/grape/namespace.rb
CHANGED
@@ -1,22 +1,33 @@
|
|
1
1
|
module Grape
|
2
|
+
# A container for endpoints or other namespaces, which allows for both
|
3
|
+
# logical grouping of endpoints as well as sharing commonconfiguration.
|
4
|
+
# May also be referred to as group, segment, or resource.
|
2
5
|
class Namespace
|
3
6
|
attr_reader :space, :options
|
4
7
|
|
5
|
-
#
|
6
|
-
#
|
8
|
+
# @param space [String] the name of this namespace
|
9
|
+
# @param options [Hash] options hash
|
10
|
+
# @option options :requirements [Hash] param-regex pairs, all of which must
|
11
|
+
# be met by a request's params for all endpoints in this namespace, or
|
12
|
+
# validation will fail and return a 422.
|
7
13
|
def initialize(space, options = {})
|
8
14
|
@space = space.to_s
|
9
15
|
@options = options
|
10
16
|
end
|
11
17
|
|
18
|
+
# Retrieves the requirements from the options hash, if given.
|
19
|
+
# @return [Hash]
|
12
20
|
def requirements
|
13
21
|
options[:requirements] || {}
|
14
22
|
end
|
15
23
|
|
24
|
+
# (see ::joined_space_path)
|
16
25
|
def self.joined_space(settings)
|
17
26
|
(settings || []).map(&:space).join('/')
|
18
27
|
end
|
19
28
|
|
29
|
+
# Join the namespaces from a list of settings to create a path prefix.
|
30
|
+
# @param settings [Array] list of Grape::Util::InheritableSettings.
|
20
31
|
def self.joined_space_path(settings)
|
21
32
|
Rack::Mount::Utils.normalize_path(joined_space(settings))
|
22
33
|
end
|
data/lib/grape/path.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module Grape
|
2
|
+
# Represents a path to an endpoint.
|
2
3
|
class Path
|
3
4
|
def self.prepare(raw_path, namespace, settings)
|
4
5
|
Path.new(raw_path, namespace, settings).path_with_suffix
|
@@ -28,18 +29,18 @@ module Grape
|
|
28
29
|
!!(settings[:version] && settings[:version_options][:using] == :path)
|
29
30
|
end
|
30
31
|
|
31
|
-
def
|
32
|
+
def namespace?
|
32
33
|
namespace && namespace.to_s =~ /^\S/ && namespace != '/'
|
33
34
|
end
|
34
35
|
|
35
|
-
def
|
36
|
+
def path?
|
36
37
|
raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/'
|
37
38
|
end
|
38
39
|
|
39
40
|
def suffix
|
40
41
|
if uses_specific_format?
|
41
42
|
"(.#{settings[:format]})"
|
42
|
-
elsif !uses_path_versioning? || (
|
43
|
+
elsif !uses_path_versioning? || (namespace? || path?)
|
43
44
|
'(.:format)'
|
44
45
|
else
|
45
46
|
'(/.:format)'
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Grape
|
2
|
+
class Request < Rack::Request
|
3
|
+
HTTP_PREFIX = 'HTTP_'.freeze
|
4
|
+
|
5
|
+
alias_method :rack_params, :params
|
6
|
+
|
7
|
+
def params
|
8
|
+
@params ||= build_params
|
9
|
+
end
|
10
|
+
|
11
|
+
def headers
|
12
|
+
@headers ||= build_headers
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def build_params
|
18
|
+
params = Hashie::Mash.new(rack_params)
|
19
|
+
if env[Grape::Env::RACK_ROUTING_ARGS]
|
20
|
+
args = env[Grape::Env::RACK_ROUTING_ARGS].dup
|
21
|
+
# preserve version from query string parameters
|
22
|
+
args.delete(:version)
|
23
|
+
args.delete(:route_info)
|
24
|
+
params.deep_merge!(args)
|
25
|
+
end
|
26
|
+
params
|
27
|
+
end
|
28
|
+
|
29
|
+
def build_headers
|
30
|
+
headers = {}
|
31
|
+
env.each_pair do |k, v|
|
32
|
+
next unless k.to_s.start_with? HTTP_PREFIX
|
33
|
+
|
34
|
+
k = k[5..-1].split('_').each(&:capitalize!).join('-')
|
35
|
+
headers[k] = v
|
36
|
+
end
|
37
|
+
headers
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/grape/route.rb
CHANGED
@@ -1,10 +1,12 @@
|
|
1
1
|
module Grape
|
2
2
|
# A compiled route for inspection.
|
3
3
|
class Route
|
4
|
+
# @api private
|
4
5
|
def initialize(options = {})
|
5
6
|
@options = options || {}
|
6
7
|
end
|
7
8
|
|
9
|
+
# @api private
|
8
10
|
def method_missing(method_id, *arguments)
|
9
11
|
match = /route_([_a-zA-Z]\w*)/.match(method_id.to_s)
|
10
12
|
if match
|
@@ -14,12 +16,15 @@ module Grape
|
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
19
|
+
# Generate a short, human-readable representation of this route.
|
17
20
|
def to_s
|
18
21
|
"version=#{route_version}, method=#{route_method}, path=#{route_path}"
|
19
22
|
end
|
20
23
|
|
21
24
|
private
|
22
25
|
|
26
|
+
# This is defined so that certain Ruby methods which attempt to call #to_ary
|
27
|
+
# on objects, e.g. Array#join, will not hit #method_missing.
|
23
28
|
def to_ary
|
24
29
|
nil
|
25
30
|
end
|
@@ -1,18 +1,18 @@
|
|
1
1
|
module Grape
|
2
2
|
module ContentTypes
|
3
3
|
# Content types are listed in order of preference.
|
4
|
-
CONTENT_TYPES =
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
CONTENT_TYPES = {
|
5
|
+
xml: 'application/xml',
|
6
|
+
serializable_hash: 'application/json',
|
7
|
+
json: 'application/json',
|
8
|
+
binary: 'application/octet-stream',
|
9
|
+
txt: 'text/plain'
|
10
|
+
}
|
11
11
|
|
12
12
|
def self.content_types_for_settings(settings)
|
13
|
-
return
|
13
|
+
return if settings.blank?
|
14
14
|
|
15
|
-
settings.each_with_object(
|
15
|
+
settings.each_with_object({}) { |value, result| result.merge!(value) }
|
16
16
|
end
|
17
17
|
|
18
18
|
def self.content_types_for(from_settings)
|