grape 0.3.0 → 0.7.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.
Potentially problematic release.
This version of grape might be problematic. Click here for more details.
- checksums.yaml +15 -0
- data/.gitignore +8 -0
- data/.rubocop.yml +70 -0
- data/.travis.yml +7 -6
- data/CHANGELOG.md +134 -4
- data/CONTRIBUTING.md +118 -0
- data/Gemfile +5 -2
- data/README.md +551 -116
- data/RELEASING.md +105 -0
- data/Rakefile +29 -8
- data/UPGRADING.md +124 -0
- data/grape.gemspec +3 -3
- data/lib/grape/api.rb +207 -88
- data/lib/grape/cookies.rb +4 -8
- data/lib/grape/endpoint.rb +198 -144
- data/lib/grape/error_formatter/base.rb +5 -7
- data/lib/grape/error_formatter/json.rb +3 -5
- data/lib/grape/error_formatter/txt.rb +1 -3
- data/lib/grape/error_formatter/xml.rb +4 -6
- data/lib/grape/exceptions/base.rb +9 -9
- data/lib/grape/exceptions/incompatible_option_values.rb +10 -0
- data/lib/grape/exceptions/invalid_formatter.rb +1 -4
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -5
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +1 -6
- data/lib/grape/exceptions/missing_mime_type.rb +1 -5
- data/lib/grape/exceptions/missing_option.rb +1 -4
- data/lib/grape/exceptions/missing_vendor_option.rb +1 -4
- data/lib/grape/exceptions/unknown_options.rb +1 -5
- data/lib/grape/exceptions/unknown_validator.rb +1 -3
- data/lib/grape/exceptions/validation.rb +13 -3
- data/lib/grape/exceptions/validation_errors.rb +43 -0
- data/lib/grape/formatter/base.rb +5 -7
- data/lib/grape/formatter/json.rb +0 -3
- data/lib/grape/formatter/serializable_hash.rb +15 -15
- data/lib/grape/formatter/txt.rb +0 -2
- data/lib/grape/formatter/xml.rb +0 -2
- data/lib/grape/http/request.rb +26 -0
- data/lib/grape/locale/en.yml +8 -5
- data/lib/grape/middleware/auth/base.rb +30 -0
- data/lib/grape/middleware/auth/basic.rb +3 -20
- data/lib/grape/middleware/auth/digest.rb +2 -19
- data/lib/grape/middleware/auth/oauth2.rb +31 -24
- data/lib/grape/middleware/base.rb +7 -7
- data/lib/grape/middleware/error.rb +36 -22
- data/lib/grape/middleware/filter.rb +3 -3
- data/lib/grape/middleware/formatter.rb +99 -61
- data/lib/grape/middleware/globals.rb +13 -0
- data/lib/grape/middleware/versioner/accept_version_header.rb +67 -0
- data/lib/grape/middleware/versioner/header.rb +22 -16
- data/lib/grape/middleware/versioner/param.rb +9 -11
- data/lib/grape/middleware/versioner/path.rb +10 -13
- data/lib/grape/middleware/versioner.rb +3 -1
- data/lib/grape/namespace.rb +23 -0
- data/lib/grape/parser/base.rb +3 -5
- data/lib/grape/parser/json.rb +0 -2
- data/lib/grape/parser/xml.rb +0 -2
- data/lib/grape/path.rb +70 -0
- data/lib/grape/route.rb +10 -6
- data/lib/grape/util/content_types.rb +2 -1
- data/lib/grape/util/deep_merge.rb +5 -5
- data/lib/grape/util/hash_stack.rb +13 -2
- data/lib/grape/validations/coerce.rb +11 -10
- data/lib/grape/validations/default.rb +25 -0
- data/lib/grape/validations/presence.rb +7 -3
- data/lib/grape/validations/regexp.rb +2 -5
- data/lib/grape/validations/values.rb +17 -0
- data/lib/grape/validations.rb +161 -54
- data/lib/grape/version.rb +1 -1
- data/lib/grape.rb +19 -4
- data/spec/grape/api_spec.rb +897 -268
- data/spec/grape/endpoint_spec.rb +283 -66
- data/spec/grape/entity_spec.rb +132 -29
- data/spec/grape/exceptions/missing_mime_type_spec.rb +3 -9
- data/spec/grape/exceptions/validation_errors_spec.rb +19 -0
- data/spec/grape/middleware/auth/basic_spec.rb +8 -8
- data/spec/grape/middleware/auth/digest_spec.rb +5 -5
- data/spec/grape/middleware/auth/oauth2_spec.rb +81 -36
- data/spec/grape/middleware/base_spec.rb +8 -13
- data/spec/grape/middleware/error_spec.rb +13 -17
- data/spec/grape/middleware/exception_spec.rb +47 -27
- data/spec/grape/middleware/formatter_spec.rb +103 -41
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +121 -0
- data/spec/grape/middleware/versioner/header_spec.rb +76 -51
- data/spec/grape/middleware/versioner/param_spec.rb +18 -18
- data/spec/grape/middleware/versioner/path_spec.rb +6 -6
- data/spec/grape/middleware/versioner_spec.rb +5 -2
- data/spec/grape/path_spec.rb +229 -0
- data/spec/grape/util/hash_stack_spec.rb +31 -32
- data/spec/grape/validations/coerce_spec.rb +116 -51
- data/spec/grape/validations/default_spec.rb +123 -0
- data/spec/grape/validations/presence_spec.rb +42 -44
- data/spec/grape/validations/regexp_spec.rb +9 -9
- data/spec/grape/validations/values_spec.rb +138 -0
- data/spec/grape/validations/zh-CN.yml +4 -3
- data/spec/grape/validations_spec.rb +681 -48
- data/spec/shared/versioning_examples.rb +22 -6
- data/spec/spec_helper.rb +3 -2
- data/spec/support/basic_auth_encode_helpers.rb +0 -1
- data/spec/support/content_type_helpers.rb +11 -0
- data/spec/support/versioned_helpers.rb +13 -5
- metadata +34 -84
@@ -3,18 +3,18 @@ require 'grape/middleware/base'
|
|
3
3
|
module Grape
|
4
4
|
module Middleware
|
5
5
|
class Error < Base
|
6
|
-
|
7
6
|
def default_options
|
8
7
|
{
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
12
|
-
:
|
13
|
-
:
|
14
|
-
:
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
8
|
+
default_status: 500, # default status returned on error
|
9
|
+
default_message: "",
|
10
|
+
format: :txt,
|
11
|
+
formatters: {},
|
12
|
+
error_formatters: {},
|
13
|
+
rescue_all: false, # true to rescue all exceptions
|
14
|
+
rescue_subclasses: true, # rescue subclasses of exceptions listed
|
15
|
+
rescue_options: { backtrace: false }, # true to display backtrace
|
16
|
+
rescue_handlers: {}, # rescue handler blocks
|
17
|
+
base_only_rescue_handlers: {} # rescue handler blocks rescuing only the base class
|
18
18
|
}
|
19
19
|
end
|
20
20
|
|
@@ -22,49 +22,63 @@ module Grape
|
|
22
22
|
@env = env
|
23
23
|
|
24
24
|
begin
|
25
|
-
error_response(catch(:error)
|
25
|
+
error_response(catch(:error) do
|
26
26
|
return @app.call(@env)
|
27
|
-
|
28
|
-
rescue
|
27
|
+
end)
|
28
|
+
rescue StandardError => e
|
29
29
|
is_rescuable = rescuable?(e.class)
|
30
30
|
if e.is_a?(Grape::Exceptions::Base) && !is_rescuable
|
31
|
-
handler = lambda {|
|
31
|
+
handler = lambda { |arg| error_response(arg) }
|
32
32
|
else
|
33
33
|
raise unless is_rescuable
|
34
|
-
handler =
|
34
|
+
handler = find_handler(e.class)
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
|
+
handler.nil? ? handle_error(e) : exec_handler(e, &handler)
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
41
|
+
def find_handler(klass)
|
42
|
+
handler = options[:rescue_handlers].find(-> { [] }) { |error, _| klass <= error }[1]
|
43
|
+
handler = options[:base_only_rescue_handlers][klass] || options[:base_only_rescue_handlers][:all] unless handler
|
44
|
+
handler
|
45
|
+
end
|
46
|
+
|
40
47
|
def rescuable?(klass)
|
41
|
-
options[:rescue_all] || (options[:
|
48
|
+
options[:rescue_all] || (options[:rescue_handlers] || []).any? { |error, handler| klass <= error } || (options[:base_only_rescue_handlers] || []).include?(klass)
|
49
|
+
end
|
50
|
+
|
51
|
+
def exec_handler(e, &handler)
|
52
|
+
if handler.lambda? && handler.arity == 0
|
53
|
+
instance_exec(&handler)
|
54
|
+
else
|
55
|
+
instance_exec(e, &handler)
|
56
|
+
end
|
42
57
|
end
|
43
58
|
|
44
59
|
def handle_error(e)
|
45
|
-
error_response(
|
60
|
+
error_response(message: e.message, backtrace: e.backtrace)
|
46
61
|
end
|
47
62
|
|
48
63
|
def error_response(error = {})
|
49
64
|
status = error[:status] || options[:default_status]
|
50
65
|
message = error[:message] || options[:default_message]
|
51
|
-
headers = {'Content-Type' => content_type}
|
66
|
+
headers = { 'Content-Type' => content_type }
|
52
67
|
headers.merge!(error[:headers]) if error[:headers].is_a?(Hash)
|
53
68
|
backtrace = error[:backtrace] || []
|
54
69
|
rack_response(format_message(message, backtrace), status, headers)
|
55
70
|
end
|
56
71
|
|
57
72
|
def rack_response(message, status = options[:default_status], headers = { 'Content-Type' => content_type })
|
58
|
-
Rack::Response.new([
|
73
|
+
Rack::Response.new([message], status, headers).finish
|
59
74
|
end
|
60
75
|
|
61
76
|
def format_message(message, backtrace)
|
62
77
|
format = env['api.format'] || options[:format]
|
63
78
|
formatter = Grape::ErrorFormatter::Base.formatter_for(format, options)
|
64
|
-
throw :error, :
|
79
|
+
throw :error, status: 406, message: "The requested format '#{format}' is not supported." unless formatter
|
65
80
|
formatter.call(message, backtrace, options, env)
|
66
81
|
end
|
67
|
-
|
68
82
|
end
|
69
83
|
end
|
70
84
|
end
|
@@ -3,14 +3,14 @@ module Grape
|
|
3
3
|
# This is a simple middleware for adding before and after filters
|
4
4
|
# to Grape APIs. It is used like so:
|
5
5
|
#
|
6
|
-
# use Grape::Middleware::Filter, :
|
6
|
+
# use Grape::Middleware::Filter, before: lambda { do_something }, after: lambda { do_something }
|
7
7
|
class Filter < Base
|
8
8
|
def before
|
9
|
-
app.instance_eval
|
9
|
+
app.instance_eval(&options[:before]) if options[:before]
|
10
10
|
end
|
11
11
|
|
12
12
|
def after
|
13
|
-
app.instance_eval
|
13
|
+
app.instance_eval(&options[:after]) if options[:after]
|
14
14
|
end
|
15
15
|
end
|
16
16
|
end
|
@@ -3,17 +3,19 @@ require 'grape/middleware/base'
|
|
3
3
|
module Grape
|
4
4
|
module Middleware
|
5
5
|
class Formatter < Base
|
6
|
-
|
7
6
|
def default_options
|
8
7
|
{
|
9
|
-
:
|
10
|
-
:
|
11
|
-
:
|
8
|
+
default_format: :txt,
|
9
|
+
formatters: {},
|
10
|
+
parsers: {}
|
12
11
|
}
|
13
12
|
end
|
14
13
|
|
15
14
|
def headers
|
16
|
-
env.dup.inject({})
|
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
|
17
19
|
end
|
18
20
|
|
19
21
|
def before
|
@@ -23,13 +25,15 @@ module Grape
|
|
23
25
|
|
24
26
|
def after
|
25
27
|
status, headers, bodies = *@app_response
|
26
|
-
|
28
|
+
# allow content-type to be explicitly overwritten
|
29
|
+
api_format = mime_types[headers["Content-Type"]] || env['api.format']
|
30
|
+
formatter = Grape::Formatter::Base.formatter_for api_format, options
|
27
31
|
begin
|
28
32
|
bodymap = bodies.collect do |body|
|
29
33
|
formatter.call body, env
|
30
34
|
end
|
31
|
-
rescue
|
32
|
-
throw :error, :
|
35
|
+
rescue Grape::Exceptions::InvalidFormatter => e
|
36
|
+
throw :error, status: 500, message: e.message
|
33
37
|
end
|
34
38
|
headers['Content-Type'] = content_type_for(env['api.format']) unless headers['Content-Type']
|
35
39
|
Rack::Response.new(bodymap, status, headers).to_a
|
@@ -37,76 +41,110 @@ module Grape
|
|
37
41
|
|
38
42
|
private
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
end
|
59
|
-
ensure
|
60
|
-
env['rack.input'].rewind
|
61
|
-
end
|
44
|
+
def request
|
45
|
+
@request ||= Rack::Request.new(env)
|
46
|
+
end
|
47
|
+
|
48
|
+
# store read input in env['api.request.input']
|
49
|
+
def read_body_input
|
50
|
+
if (request.post? || request.put? || request.patch? || request.delete?) &&
|
51
|
+
(!request.form_data? || !request.media_type) &&
|
52
|
+
(!request.parseable_data?) &&
|
53
|
+
(request.content_length.to_i > 0 || request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
|
54
|
+
|
55
|
+
if (input = env['rack.input'])
|
56
|
+
input.rewind
|
57
|
+
body = env['api.request.input'] = input.read
|
58
|
+
begin
|
59
|
+
read_rack_input(body) if body && body.length > 0
|
60
|
+
ensure
|
61
|
+
input.rewind
|
62
62
|
end
|
63
63
|
end
|
64
64
|
end
|
65
|
+
end
|
65
66
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
67
|
+
# store parsed input in env['api.request.body']
|
68
|
+
def read_rack_input(body)
|
69
|
+
fmt = mime_types[request.media_type] if request.media_type
|
70
|
+
fmt ||= options[:default_format]
|
71
|
+
if content_type_for(fmt)
|
72
|
+
parser = Grape::Parser::Base.parser_for fmt, options
|
73
|
+
if parser
|
74
|
+
begin
|
75
|
+
body = (env['api.request.body'] = parser.call(body, env))
|
76
|
+
if body.is_a?(Hash)
|
77
|
+
if env['rack.request.form_hash']
|
78
|
+
env['rack.request.form_hash'] = env['rack.request.form_hash'].merge(body)
|
79
|
+
else
|
80
|
+
env['rack.request.form_hash'] = body
|
81
|
+
end
|
82
|
+
env['rack.request.form_input'] = env['rack.input']
|
83
|
+
end
|
84
|
+
rescue StandardError => e
|
85
|
+
throw :error, status: 400, message: e.message
|
86
|
+
end
|
70
87
|
else
|
71
|
-
|
88
|
+
env['api.request.body'] = body
|
72
89
|
end
|
90
|
+
else
|
91
|
+
throw :error, status: 406, message: "The requested content-type '#{request.media_type}' is not supported."
|
73
92
|
end
|
93
|
+
end
|
74
94
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
return extension.to_sym if content_type_for(extension)
|
82
|
-
end
|
83
|
-
nil
|
95
|
+
def negotiate_content_type
|
96
|
+
fmt = format_from_extension || format_from_params || options[:format] || format_from_header || options[:default_format]
|
97
|
+
if content_type_for(fmt)
|
98
|
+
env['api.format'] = fmt
|
99
|
+
else
|
100
|
+
throw :error, status: 406, message: "The requested format '#{fmt}' is not supported."
|
84
101
|
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def format_from_extension
|
105
|
+
parts = request.path.split('.')
|
85
106
|
|
86
|
-
|
87
|
-
|
107
|
+
if parts.size > 1
|
108
|
+
extension = parts.last
|
88
109
|
# avoid symbol memory leak on an unknown format
|
89
|
-
return
|
90
|
-
fmt
|
110
|
+
return extension.to_sym if content_type_for(extension)
|
91
111
|
end
|
112
|
+
nil
|
113
|
+
end
|
92
114
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
115
|
+
def format_from_params
|
116
|
+
fmt = Rack::Utils.parse_nested_query(env['QUERY_STRING'])["format"]
|
117
|
+
# avoid symbol memory leak on an unknown format
|
118
|
+
return fmt.to_sym if content_type_for(fmt)
|
119
|
+
fmt
|
120
|
+
end
|
121
|
+
|
122
|
+
def format_from_header
|
123
|
+
mime_array.each do |t|
|
124
|
+
return mime_types[t] if mime_types.key?(t)
|
100
125
|
end
|
126
|
+
nil
|
127
|
+
end
|
101
128
|
|
102
|
-
|
103
|
-
|
129
|
+
def mime_array
|
130
|
+
accept = headers['accept']
|
131
|
+
return [] unless accept
|
104
132
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
133
|
+
accept_into_mime_and_quality = %r(
|
134
|
+
(
|
135
|
+
\w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
|
136
|
+
(?:
|
137
|
+
(?:;[^,]*?)? # optionally multiple formats in a row
|
138
|
+
;\s*q=([\d.]+) # optional "quality" preference (eg q=0.5)
|
139
|
+
)?
|
140
|
+
)x
|
141
|
+
|
142
|
+
vendor_prefix_pattern = /vnd\.[^+]+\+/
|
109
143
|
|
144
|
+
accept.scan(accept_into_mime_and_quality)
|
145
|
+
.sort_by { |_, quality_preference| -quality_preference.to_f }
|
146
|
+
.map { |mime, _| mime.sub(vendor_prefix_pattern, '') }
|
147
|
+
end
|
110
148
|
end
|
111
149
|
end
|
112
150
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'grape/middleware/base'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Middleware
|
5
|
+
class Globals < Base
|
6
|
+
def before
|
7
|
+
@env['grape.request'] = Grape::Request.new(@env)
|
8
|
+
@env['grape.request.headers'] = request.headers
|
9
|
+
@env['grape.request.params'] = request.params if @env['rack.input']
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'grape/middleware/base'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Middleware
|
5
|
+
module Versioner
|
6
|
+
# This middleware sets various version related rack environment variables
|
7
|
+
# based on the HTTP Accept-Version header
|
8
|
+
#
|
9
|
+
# Example: For request header
|
10
|
+
# Accept-Version: v1
|
11
|
+
#
|
12
|
+
# The following rack env variables are set:
|
13
|
+
#
|
14
|
+
# env['api.version'] => 'v1'
|
15
|
+
#
|
16
|
+
# If version does not match this route, then a 406 is raised with
|
17
|
+
# X-Cascade header to alert Rack::Mount to attempt the next matched
|
18
|
+
# route.
|
19
|
+
class AcceptVersionHeader < Base
|
20
|
+
def before
|
21
|
+
potential_version = (env['HTTP_ACCEPT_VERSION'] || '').strip
|
22
|
+
|
23
|
+
if strict?
|
24
|
+
# If no Accept-Version header:
|
25
|
+
if potential_version.empty?
|
26
|
+
throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
unless potential_version.empty?
|
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
|
35
|
+
|
36
|
+
env['api.version'] = potential_version
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def versions
|
43
|
+
options[:versions] || []
|
44
|
+
end
|
45
|
+
|
46
|
+
def strict?
|
47
|
+
options[:version_options] && options[:version_options][:strict]
|
48
|
+
end
|
49
|
+
|
50
|
+
# By default those errors contain an `X-Cascade` header set to `pass`, which allows nesting and stacking
|
51
|
+
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
|
52
|
+
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
53
|
+
def cascade?
|
54
|
+
if options[:version_options] && options[:version_options].has_key?(:cascade)
|
55
|
+
!!options[:version_options][:cascade]
|
56
|
+
else
|
57
|
+
true
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def error_headers
|
62
|
+
cascade? ? { 'X-Cascade' => 'pass' } : {}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -22,22 +22,25 @@ module Grape
|
|
22
22
|
# X-Cascade header to alert Rack::Mount to attempt the next matched
|
23
23
|
# route.
|
24
24
|
class Header < Base
|
25
|
-
|
26
25
|
def before
|
27
|
-
|
26
|
+
begin
|
27
|
+
header = Rack::Accept::MediaType.new env['HTTP_ACCEPT']
|
28
|
+
rescue RuntimeError => e
|
29
|
+
throw :error, status: 406, headers: error_headers, message: e.message
|
30
|
+
end
|
28
31
|
|
29
32
|
if strict?
|
30
33
|
# If no Accept header:
|
31
34
|
if header.qvalues.empty?
|
32
|
-
throw :error, :
|
35
|
+
throw :error, status: 406, headers: error_headers, message: 'Accept header must be set.'
|
33
36
|
end
|
34
37
|
# Remove any acceptable content types with ranges.
|
35
|
-
header.qvalues.reject! do |media_type,_|
|
36
|
-
Rack::Accept::Header.parse_media_type(media_type).find{|s| s == '*'}
|
38
|
+
header.qvalues.reject! do |media_type, _|
|
39
|
+
Rack::Accept::Header.parse_media_type(media_type).find { |s| s == '*' }
|
37
40
|
end
|
38
41
|
# If all Accept headers included a range:
|
39
42
|
if header.qvalues.empty?
|
40
|
-
throw :error, :
|
43
|
+
throw :error, status: 406, headers: error_headers, message: 'Accept header must not contain ranges ("*").'
|
41
44
|
end
|
42
45
|
end
|
43
46
|
|
@@ -55,19 +58,19 @@ module Grape
|
|
55
58
|
end
|
56
59
|
# If none of the available content types are acceptable:
|
57
60
|
elsif strict?
|
58
|
-
throw :error, :
|
61
|
+
throw :error, status: 406, headers: error_headers, message: '406 Not Acceptable'
|
59
62
|
# If all acceptable content types specify a vendor or version that doesn't exist:
|
60
|
-
elsif header.values.all?{ |
|
61
|
-
throw :error, :
|
63
|
+
elsif header.values.all? { |header_value| has_vendor?(header_value) || has_version?(header_value) }
|
64
|
+
throw :error, status: 406, headers: error_headers, message: 'API vendor or version not found.'
|
62
65
|
end
|
63
66
|
end
|
64
67
|
|
65
|
-
|
68
|
+
private
|
66
69
|
|
67
70
|
def available_media_types
|
68
71
|
available_media_types = []
|
69
72
|
|
70
|
-
content_types.each do |extension,media_type|
|
73
|
+
content_types.each do |extension, media_type|
|
71
74
|
versions.reverse.each do |version|
|
72
75
|
available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
|
73
76
|
end
|
@@ -76,7 +79,7 @@ module Grape
|
|
76
79
|
|
77
80
|
available_media_types << "application/vnd.#{vendor}"
|
78
81
|
|
79
|
-
content_types.each do |_,media_type|
|
82
|
+
content_types.each do |_, media_type|
|
80
83
|
available_media_types << media_type
|
81
84
|
end
|
82
85
|
|
@@ -99,7 +102,11 @@ module Grape
|
|
99
102
|
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
|
100
103
|
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
101
104
|
def cascade?
|
102
|
-
options[:version_options] &&
|
105
|
+
if options[:version_options] && options[:version_options].has_key?(:cascade)
|
106
|
+
!!options[:version_options][:cascade]
|
107
|
+
else
|
108
|
+
true
|
109
|
+
end
|
103
110
|
end
|
104
111
|
|
105
112
|
def error_headers
|
@@ -109,17 +116,16 @@ module Grape
|
|
109
116
|
# @param [String] media_type a content type
|
110
117
|
# @return [Boolean] whether the content type sets a vendor
|
111
118
|
def has_vendor?(media_type)
|
112
|
-
|
119
|
+
_, subtype = Rack::Accept::Header.parse_media_type media_type
|
113
120
|
subtype[/\Avnd\.[a-z0-9*.]+/]
|
114
121
|
end
|
115
122
|
|
116
123
|
# @param [String] media_type a content type
|
117
124
|
# @return [Boolean] whether the content type sets an API version
|
118
125
|
def has_version?(media_type)
|
119
|
-
|
126
|
+
_, subtype = Rack::Accept::Header.parse_media_type media_type
|
120
127
|
subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
|
121
128
|
end
|
122
|
-
|
123
129
|
end
|
124
130
|
end
|
125
131
|
end
|
@@ -4,40 +4,38 @@ module Grape
|
|
4
4
|
module Middleware
|
5
5
|
module Versioner
|
6
6
|
# This middleware sets various version related rack environment variables
|
7
|
-
# based on the request parameters and removes that parameter from the
|
7
|
+
# based on the request parameters and removes that parameter from the
|
8
8
|
# request parameters for subsequent middleware and API.
|
9
9
|
# If the version substring does not match any potential initialized
|
10
10
|
# versions, a 404 error is thrown.
|
11
11
|
# If the version substring is not passed the version (highest mounted)
|
12
12
|
# version will be used.
|
13
|
-
#
|
13
|
+
#
|
14
14
|
# Example: For a uri path
|
15
15
|
# /resource?apiver=v1
|
16
|
-
#
|
16
|
+
#
|
17
17
|
# The following rack env variables are set and path is rewritten to
|
18
18
|
# '/resource':
|
19
|
-
#
|
19
|
+
#
|
20
20
|
# env['api.version'] => 'v1'
|
21
21
|
class Param < Base
|
22
22
|
def default_options
|
23
23
|
{
|
24
|
-
:
|
24
|
+
parameter: "apiver"
|
25
25
|
}
|
26
26
|
end
|
27
27
|
|
28
28
|
def before
|
29
29
|
paramkey = options[:parameter]
|
30
|
-
potential_version =
|
31
|
-
|
30
|
+
potential_version = Rack::Utils.parse_nested_query(env['QUERY_STRING'])[paramkey]
|
32
31
|
unless potential_version.nil?
|
33
|
-
if options[:versions] && !
|
34
|
-
throw :error, :
|
32
|
+
if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
|
33
|
+
throw :error, status: 404, message: "404 API Version Not Found", headers: { 'X-Cascade' => 'pass' }
|
35
34
|
end
|
36
35
|
env['api.version'] = potential_version
|
37
|
-
env['rack.request.query_hash'].delete(paramkey)
|
36
|
+
env['rack.request.query_hash'].delete(paramkey) if env.key? 'rack.request.query_hash'
|
38
37
|
end
|
39
38
|
end
|
40
|
-
|
41
39
|
end
|
42
40
|
end
|
43
41
|
end
|
@@ -7,19 +7,19 @@ module Grape
|
|
7
7
|
# based on the uri path and removes the version substring from the uri
|
8
8
|
# path. If the version substring does not match any potential initialized
|
9
9
|
# versions, a 404 error is thrown.
|
10
|
-
#
|
10
|
+
#
|
11
11
|
# Example: For a uri path
|
12
12
|
# /v1/resource
|
13
|
-
#
|
13
|
+
#
|
14
14
|
# The following rack env variables are set and path is rewritten to
|
15
15
|
# '/resource':
|
16
|
-
#
|
16
|
+
#
|
17
17
|
# env['api.version'] => 'v1'
|
18
|
-
#
|
18
|
+
#
|
19
19
|
class Path < Base
|
20
20
|
def default_options
|
21
21
|
{
|
22
|
-
:
|
22
|
+
pattern: /.*/i
|
23
23
|
}
|
24
24
|
end
|
25
25
|
|
@@ -34,21 +34,18 @@ module Grape
|
|
34
34
|
pieces = path.split('/')
|
35
35
|
potential_version = pieces[1]
|
36
36
|
if potential_version =~ options[:pattern]
|
37
|
-
if options[:versions] && !
|
38
|
-
throw :error, :
|
37
|
+
if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
|
38
|
+
throw :error, status: 404, message: "404 API Version Not Found"
|
39
39
|
end
|
40
|
-
|
41
|
-
truncated_path = "/#{pieces[2..-1].join('/')}"
|
42
40
|
env['api.version'] = potential_version
|
43
41
|
end
|
44
42
|
end
|
45
43
|
|
46
44
|
private
|
47
45
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
46
|
+
def prefix
|
47
|
+
Rack::Mount::Utils.normalize_path(options[:prefix].to_s) if options[:prefix]
|
48
|
+
end
|
52
49
|
end
|
53
50
|
end
|
54
51
|
end
|
@@ -9,7 +9,7 @@
|
|
9
9
|
module Grape
|
10
10
|
module Middleware
|
11
11
|
module Versioner
|
12
|
-
|
12
|
+
module_function
|
13
13
|
|
14
14
|
# @param strategy [Symbol] :path, :header or :param
|
15
15
|
# @return a middleware class based on strategy
|
@@ -21,6 +21,8 @@ module Grape
|
|
21
21
|
Header
|
22
22
|
when :param
|
23
23
|
Param
|
24
|
+
when :accept_version_header
|
25
|
+
AcceptVersionHeader
|
24
26
|
else
|
25
27
|
raise Grape::Exceptions::InvalidVersionerOption.new(strategy)
|
26
28
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Grape
|
2
|
+
class Namespace
|
3
|
+
attr_reader :space, :options
|
4
|
+
|
5
|
+
# options:
|
6
|
+
# requirements: a hash
|
7
|
+
def initialize(space, options = {})
|
8
|
+
@space, @options = space.to_s, options
|
9
|
+
end
|
10
|
+
|
11
|
+
def requirements
|
12
|
+
options[:requirements] || {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.joined_space(settings)
|
16
|
+
settings.gather(:namespace).map(&:space).join("/")
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.joined_space_path(settings)
|
20
|
+
Rack::Mount::Utils.normalize_path(joined_space(settings))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|