grape 0.6.0 → 0.6.1
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 +8 -8
- data/.rubocop.yml +65 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +17 -1
- data/Gemfile +1 -0
- data/README.md +16 -8
- data/RELEASING.md +105 -0
- data/grape.gemspec +1 -1
- data/lib/grape.rb +1 -0
- data/lib/grape/api.rb +88 -54
- data/lib/grape/cookies.rb +4 -6
- data/lib/grape/endpoint.rb +81 -69
- data/lib/grape/error_formatter/base.rb +5 -4
- data/lib/grape/error_formatter/json.rb +3 -3
- data/lib/grape/error_formatter/txt.rb +1 -1
- data/lib/grape/error_formatter/xml.rb +4 -4
- data/lib/grape/exceptions/base.rb +7 -7
- data/lib/grape/exceptions/incompatible_option_values.rb +13 -0
- data/lib/grape/exceptions/invalid_formatter.rb +1 -1
- data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
- data/lib/grape/exceptions/invalid_with_option_for_represent.rb +1 -4
- data/lib/grape/exceptions/missing_mime_type.rb +1 -1
- data/lib/grape/exceptions/missing_option.rb +1 -1
- data/lib/grape/exceptions/missing_vendor_option.rb +1 -1
- data/lib/grape/exceptions/unknown_options.rb +1 -1
- data/lib/grape/exceptions/unknown_validator.rb +1 -1
- data/lib/grape/exceptions/validation.rb +2 -2
- data/lib/grape/exceptions/validation_errors.rb +1 -1
- data/lib/grape/formatter/base.rb +5 -4
- data/lib/grape/formatter/serializable_hash.rb +7 -6
- data/lib/grape/http/request.rb +2 -2
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/base.rb +3 -3
- data/lib/grape/middleware/auth/basic.rb +1 -1
- data/lib/grape/middleware/auth/oauth2.rb +18 -20
- data/lib/grape/middleware/base.rb +1 -1
- data/lib/grape/middleware/error.rb +19 -19
- data/lib/grape/middleware/filter.rb +3 -3
- data/lib/grape/middleware/formatter.rb +29 -23
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/middleware/versioner/accept_version_header.rb +8 -6
- data/lib/grape/middleware/versioner/header.rb +16 -14
- data/lib/grape/middleware/versioner/param.rb +7 -7
- data/lib/grape/middleware/versioner/path.rb +7 -9
- data/lib/grape/parser/base.rb +3 -2
- data/lib/grape/path.rb +1 -1
- data/lib/grape/route.rb +6 -4
- 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 +2 -2
- data/lib/grape/validations.rb +34 -30
- data/lib/grape/validations/coerce.rb +6 -5
- data/lib/grape/validations/default.rb +0 -1
- data/lib/grape/validations/presence.rb +1 -1
- data/lib/grape/validations/regexp.rb +2 -2
- data/lib/grape/validations/values.rb +16 -0
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +229 -210
- data/spec/grape/endpoint_spec.rb +56 -54
- data/spec/grape/entity_spec.rb +31 -33
- data/spec/grape/exceptions/missing_mime_type_spec.rb +3 -9
- 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 +23 -23
- data/spec/grape/middleware/base_spec.rb +6 -6
- data/spec/grape/middleware/error_spec.rb +11 -15
- data/spec/grape/middleware/exception_spec.rb +45 -25
- data/spec/grape/middleware/formatter_spec.rb +56 -45
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +25 -25
- data/spec/grape/middleware/versioner/header_spec.rb +54 -54
- data/spec/grape/middleware/versioner/param_spec.rb +17 -18
- data/spec/grape/middleware/versioner/path_spec.rb +6 -6
- data/spec/grape/middleware/versioner_spec.rb +1 -1
- data/spec/grape/util/hash_stack_spec.rb +26 -27
- data/spec/grape/validations/coerce_spec.rb +39 -34
- data/spec/grape/validations/default_spec.rb +12 -13
- data/spec/grape/validations/presence_spec.rb +18 -22
- data/spec/grape/validations/regexp_spec.rb +9 -9
- data/spec/grape/validations/values_spec.rb +64 -0
- data/spec/grape/validations_spec.rb +127 -70
- data/spec/shared/versioning_examples.rb +5 -5
- data/spec/support/basic_auth_encode_helpers.rb +0 -1
- data/spec/support/versioned_helpers.rb +5 -6
- metadata +10 -4
@@ -24,21 +24,21 @@ module Grape
|
|
24
24
|
if strict?
|
25
25
|
# If no Accept-Version header:
|
26
26
|
if potential_version.empty?
|
27
|
-
throw :error, :
|
27
|
+
throw :error, status: 406, headers: error_headers, message: 'Accept-Version header must be set.'
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
31
|
unless potential_version.empty?
|
32
32
|
# If the requested version is not supported:
|
33
|
-
|
34
|
-
throw :error, :
|
33
|
+
unless versions.any? { |v| v.to_s == potential_version }
|
34
|
+
throw :error, status: 406, headers: error_headers, message: 'The requested version is not supported.'
|
35
35
|
end
|
36
36
|
|
37
37
|
env['api.version'] = potential_version
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
|
41
|
+
private
|
42
42
|
|
43
43
|
def versions
|
44
44
|
options[:versions] || []
|
@@ -52,9 +52,11 @@ module Grape
|
|
52
52
|
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
|
53
53
|
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
54
54
|
def cascade?
|
55
|
-
options[:version_options] && options[:version_options].has_key?(:cascade)
|
56
|
-
!!
|
55
|
+
if options[:version_options] && options[:version_options].has_key?(:cascade)
|
56
|
+
!!options[:version_options][:cascade]
|
57
|
+
else
|
57
58
|
true
|
59
|
+
end
|
58
60
|
end
|
59
61
|
|
60
62
|
def error_headers
|
@@ -29,15 +29,15 @@ module Grape
|
|
29
29
|
if strict?
|
30
30
|
# If no Accept header:
|
31
31
|
if header.qvalues.empty?
|
32
|
-
throw :error, :
|
32
|
+
throw :error, status: 406, headers: error_headers, message: 'Accept header must be set.'
|
33
33
|
end
|
34
34
|
# 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 == '*'}
|
35
|
+
header.qvalues.reject! do |media_type, _|
|
36
|
+
Rack::Accept::Header.parse_media_type(media_type).find { |s| s == '*' }
|
37
37
|
end
|
38
38
|
# If all Accept headers included a range:
|
39
39
|
if header.qvalues.empty?
|
40
|
-
throw :error, :
|
40
|
+
throw :error, status: 406, headers: error_headers, message: 'Accept header must not contain ranges ("*").'
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
@@ -55,19 +55,19 @@ module Grape
|
|
55
55
|
end
|
56
56
|
# If none of the available content types are acceptable:
|
57
57
|
elsif strict?
|
58
|
-
throw :error, :
|
58
|
+
throw :error, status: 406, headers: error_headers, message: '406 Not Acceptable'
|
59
59
|
# If all acceptable content types specify a vendor or version that doesn't exist:
|
60
|
-
elsif header.values.all?{ |
|
61
|
-
throw :error, :
|
60
|
+
elsif header.values.all? { |header_value| has_vendor?(header_value) || has_version?(header_value) }
|
61
|
+
throw :error, status: 406, headers: error_headers, message: 'API vendor or version not found.'
|
62
62
|
end
|
63
63
|
end
|
64
64
|
|
65
|
-
|
65
|
+
private
|
66
66
|
|
67
67
|
def available_media_types
|
68
68
|
available_media_types = []
|
69
69
|
|
70
|
-
content_types.each do |extension,media_type|
|
70
|
+
content_types.each do |extension, media_type|
|
71
71
|
versions.reverse.each do |version|
|
72
72
|
available_media_types += ["application/vnd.#{vendor}-#{version}+#{extension}", "application/vnd.#{vendor}-#{version}"]
|
73
73
|
end
|
@@ -76,7 +76,7 @@ module Grape
|
|
76
76
|
|
77
77
|
available_media_types << "application/vnd.#{vendor}"
|
78
78
|
|
79
|
-
content_types.each do |_,media_type|
|
79
|
+
content_types.each do |_, media_type|
|
80
80
|
available_media_types << media_type
|
81
81
|
end
|
82
82
|
|
@@ -99,9 +99,11 @@ module Grape
|
|
99
99
|
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
|
100
100
|
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
101
101
|
def cascade?
|
102
|
-
options[:version_options] && options[:version_options].has_key?(:cascade)
|
103
|
-
!!
|
102
|
+
if options[:version_options] && options[:version_options].has_key?(:cascade)
|
103
|
+
!!options[:version_options][:cascade]
|
104
|
+
else
|
104
105
|
true
|
106
|
+
end
|
105
107
|
end
|
106
108
|
|
107
109
|
def error_headers
|
@@ -111,14 +113,14 @@ module Grape
|
|
111
113
|
# @param [String] media_type a content type
|
112
114
|
# @return [Boolean] whether the content type sets a vendor
|
113
115
|
def has_vendor?(media_type)
|
114
|
-
|
116
|
+
_, subtype = Rack::Accept::Header.parse_media_type media_type
|
115
117
|
subtype[/\Avnd\.[a-z0-9*.]+/]
|
116
118
|
end
|
117
119
|
|
118
120
|
# @param [String] media_type a content type
|
119
121
|
# @return [Boolean] whether the content type sets an API version
|
120
122
|
def has_version?(media_type)
|
121
|
-
|
123
|
+
_, subtype = Rack::Accept::Header.parse_media_type media_type
|
122
124
|
subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
|
123
125
|
end
|
124
126
|
|
@@ -4,24 +4,24 @@ 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
|
|
@@ -30,8 +30,8 @@ module Grape
|
|
30
30
|
potential_version = request.params[paramkey]
|
31
31
|
|
32
32
|
unless potential_version.nil?
|
33
|
-
if options[:versions] && !
|
34
|
-
throw :error, :
|
33
|
+
if options[:versions] && !options[:versions].find { |v| v.to_s == potential_version }
|
34
|
+
throw :error, status: 404, message: "404 API Version Not Found", headers: { 'X-Cascade' => 'pass' }
|
35
35
|
end
|
36
36
|
env['api.version'] = potential_version
|
37
37
|
env['rack.request.query_hash'].delete(paramkey)
|
@@ -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,11 +34,9 @@ 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
|
data/lib/grape/parser/base.rb
CHANGED
data/lib/grape/path.rb
CHANGED
data/lib/grape/route.rb
CHANGED
@@ -2,24 +2,26 @@ module Grape
|
|
2
2
|
|
3
3
|
# A compiled route for inspection.
|
4
4
|
class Route
|
5
|
-
|
5
|
+
|
6
6
|
def initialize(options = {})
|
7
7
|
@options = options || {}
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
def method_missing(method_id, *arguments)
|
11
|
-
|
11
|
+
match = /route_([_a-zA-Z]\w*)/.match(method_id.to_s)
|
12
|
+
if match
|
12
13
|
@options[match.captures.last.to_sym]
|
13
14
|
else
|
14
15
|
super
|
15
16
|
end
|
16
17
|
end
|
17
|
-
|
18
|
+
|
18
19
|
def to_s
|
19
20
|
"version=#{route_version}, method=#{route_method}, path=#{route_path}"
|
20
21
|
end
|
21
22
|
|
22
23
|
private
|
24
|
+
|
23
25
|
def to_ary
|
24
26
|
nil
|
25
27
|
end
|
@@ -5,10 +5,11 @@ module Grape
|
|
5
5
|
:xml, 'application/xml',
|
6
6
|
:serializable_hash, 'application/json',
|
7
7
|
:json, 'application/json',
|
8
|
+
:jsonapi, 'application/vnd.api+json',
|
8
9
|
:atom, 'application/atom+xml',
|
9
10
|
:rss, 'application/rss+xml',
|
10
11
|
:txt, 'text/plain',
|
11
|
-
|
12
|
+
]
|
12
13
|
|
13
14
|
def self.content_types_for(from_settings)
|
14
15
|
from_settings || Grape::ContentTypes::CONTENT_TYPES
|
@@ -3,18 +3,18 @@ class Hash
|
|
3
3
|
# activesupport/lib/active_support/core_ext/hash/deep_merge.rb
|
4
4
|
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
5
5
|
#
|
6
|
-
# h1 = {:
|
7
|
-
# h2 = {:
|
6
|
+
# h1 = {x: {y: [4,5,6]}, z: [7,8,9]}
|
7
|
+
# h2 = {x: {y: [7,8,9]}, z: "xyz"}
|
8
8
|
#
|
9
|
-
# h1.deep_merge(h2) #=> { :
|
10
|
-
# h2.deep_merge(h1) #=> { :
|
9
|
+
# h1.deep_merge(h2) #=> { x: {y: [7, 8, 9]}, z: "xyz" }
|
10
|
+
# h2.deep_merge(h1) #=> { x: {y: [4, 5, 6]}, z: [7, 8, 9] }
|
11
11
|
def deep_merge(other_hash)
|
12
12
|
dup.deep_merge!(other_hash)
|
13
13
|
end
|
14
14
|
|
15
15
|
# Same as +deep_merge+, but modifies +self+.
|
16
16
|
def deep_merge!(other_hash)
|
17
|
-
other_hash.each_pair do |k,v|
|
17
|
+
other_hash.each_pair do |k, v|
|
18
18
|
tv = self[k]
|
19
19
|
self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v
|
20
20
|
end
|
@@ -84,7 +84,7 @@ module Grape
|
|
84
84
|
|
85
85
|
# Prepend another HashStack's to self
|
86
86
|
def prepend(hash_stack)
|
87
|
-
@stack.unshift
|
87
|
+
@stack.unshift(*hash_stack.stack)
|
88
88
|
self
|
89
89
|
end
|
90
90
|
|
@@ -100,7 +100,7 @@ module Grape
|
|
100
100
|
# @param key [Symbol] The key to gather
|
101
101
|
# @return [Array]
|
102
102
|
def gather(key)
|
103
|
-
stack.map{|s| s[key] }.flatten.compact.uniq
|
103
|
+
stack.map { |s| s[key] }.flatten.compact.uniq
|
104
104
|
end
|
105
105
|
|
106
106
|
def to_s
|
data/lib/grape/validations.rb
CHANGED
@@ -45,14 +45,14 @@ module Grape
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
48
|
-
|
48
|
+
private
|
49
49
|
|
50
50
|
def self.convert_to_short_name(klass)
|
51
|
-
ret = klass.name.gsub(/::/, '/')
|
52
|
-
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
|
53
|
-
gsub(/([a-z\d])([A-Z])/,'\1_\2')
|
54
|
-
tr("-", "_")
|
55
|
-
downcase
|
51
|
+
ret = klass.name.gsub(/::/, '/')
|
52
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
53
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
54
|
+
.tr("-", "_")
|
55
|
+
.downcase
|
56
56
|
File.basename(ret, '_validator')
|
57
57
|
end
|
58
58
|
end
|
@@ -72,7 +72,7 @@ module Grape
|
|
72
72
|
class Validator
|
73
73
|
def self.inherited(klass)
|
74
74
|
short_name = convert_to_short_name(klass)
|
75
|
-
Validations
|
75
|
+
Validations.register_validator(short_name, klass)
|
76
76
|
end
|
77
77
|
end
|
78
78
|
|
@@ -110,10 +110,8 @@ module Grape
|
|
110
110
|
def requires(*attrs, &block)
|
111
111
|
return new_scope(attrs, &block) if block_given?
|
112
112
|
|
113
|
-
validations = {:
|
114
|
-
if attrs.last.is_a?(Hash)
|
115
|
-
validations.merge!(attrs.pop)
|
116
|
-
end
|
113
|
+
validations = { presence: true }
|
114
|
+
validations.merge!(attrs.pop) if attrs.last.is_a?(Hash)
|
117
115
|
|
118
116
|
push_declared_params(attrs)
|
119
117
|
validates(attrs, validations)
|
@@ -123,9 +121,7 @@ module Grape
|
|
123
121
|
return new_scope(attrs, true, &block) if block_given?
|
124
122
|
|
125
123
|
validations = {}
|
126
|
-
if attrs.last.is_a?(Hash)
|
127
|
-
validations.merge!(attrs.pop)
|
128
|
-
end
|
124
|
+
validations.merge!(attrs.pop) if attrs.last.is_a?(Hash)
|
129
125
|
|
130
126
|
push_declared_params(attrs)
|
131
127
|
validates(attrs, validations)
|
@@ -146,15 +142,15 @@ module Grape
|
|
146
142
|
name.to_s
|
147
143
|
end
|
148
144
|
|
149
|
-
|
145
|
+
protected
|
150
146
|
|
151
147
|
def push_declared_params(attrs)
|
152
148
|
@declared_params.concat attrs
|
153
149
|
end
|
154
150
|
|
155
|
-
|
151
|
+
private
|
156
152
|
|
157
|
-
def new_scope(attrs, optional=false, &block)
|
153
|
+
def new_scope(attrs, optional = false, &block)
|
158
154
|
raise ArgumentError unless attrs.size == 1
|
159
155
|
ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, &block)
|
160
156
|
end
|
@@ -170,26 +166,34 @@ module Grape
|
|
170
166
|
end
|
171
167
|
|
172
168
|
def validates(attrs, validations)
|
173
|
-
doc_attrs = { :
|
169
|
+
doc_attrs = { required: validations.keys.include?(:presence) }
|
174
170
|
|
175
171
|
# special case (type = coerce)
|
176
|
-
if validations
|
177
|
-
validations[:coerce] = validations.delete(:type)
|
178
|
-
end
|
172
|
+
validations[:coerce] = validations.delete(:type) if validations.key?(:type)
|
179
173
|
|
180
|
-
|
181
|
-
|
182
|
-
|
174
|
+
coerce_type = validations[:coerce]
|
175
|
+
doc_attrs[:type] = coerce_type.to_s if coerce_type
|
176
|
+
|
177
|
+
desc = validations.delete(:desc)
|
178
|
+
doc_attrs[:desc] = desc if desc
|
179
|
+
|
180
|
+
default = validations[:default]
|
181
|
+
doc_attrs[:default] = default if default
|
182
|
+
|
183
|
+
values = validations[:values]
|
184
|
+
doc_attrs[:values] = values if values
|
183
185
|
|
184
|
-
if
|
185
|
-
|
186
|
+
# default value should be present in values array, if both exist
|
187
|
+
if default && values && !values.include?(default)
|
188
|
+
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values)
|
186
189
|
end
|
187
190
|
|
188
|
-
if
|
189
|
-
|
191
|
+
# type should be compatible with values array, if both exist
|
192
|
+
if coerce_type && values && values.any? { |v| !v.instance_of?(coerce_type) }
|
193
|
+
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
|
190
194
|
end
|
191
195
|
|
192
|
-
full_attrs = attrs.collect{ |name| { :
|
196
|
+
full_attrs = attrs.collect { |name| { name: name, full_name: full_name(name) } }
|
193
197
|
@api.document_attribute(full_attrs, doc_attrs)
|
194
198
|
|
195
199
|
# Validate for presence before any other validators
|
@@ -212,7 +216,7 @@ module Grape
|
|
212
216
|
end
|
213
217
|
|
214
218
|
def validate(type, options, attrs, doc_attrs)
|
215
|
-
validator_class = Validations
|
219
|
+
validator_class = Validations.validators[type.to_s]
|
216
220
|
|
217
221
|
if validator_class
|
218
222
|
(@api.settings.peek[:validations] ||= []) << validator_class.new(attrs, options, doc_attrs[:required], self)
|