grape 0.7.0 → 0.8.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 +5 -13
- data/.rubocop.yml +6 -6
- data/.travis.yml +11 -2
- data/CHANGELOG.md +23 -1
- data/Gemfile +11 -10
- data/Guardfile +5 -6
- data/README.md +194 -13
- data/UPGRADING.md +3 -3
- data/grape.gemspec +1 -1
- data/grape.png +0 -0
- data/lib/grape/api.rb +21 -13
- data/lib/grape/endpoint.rb +31 -13
- data/lib/grape/exceptions/validation.rb +2 -2
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/oauth2.rb +69 -65
- data/lib/grape/middleware/error.rb +4 -2
- data/lib/grape/middleware/formatter.rb +2 -2
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/header.rb +3 -3
- data/lib/grape/util/hash_stack.rb +1 -1
- data/lib/grape/validations.rb +22 -8
- data/lib/grape/validations/default.rb +1 -1
- data/lib/grape/validations/exactly_one_of.rb +26 -0
- data/lib/grape/validations/mutual_exclusion.rb +25 -0
- data/lib/grape/validations/presence.rb +1 -1
- data/lib/grape/validations/regexp.rb +2 -1
- data/lib/grape/validations/values.rb +7 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +390 -333
- data/spec/grape/endpoint_spec.rb +129 -99
- data/spec/grape/entity_spec.rb +47 -27
- data/spec/grape/exceptions/invalid_formatter_spec.rb +1 -1
- data/spec/grape/exceptions/invalid_versioner_option_spec.rb +1 -1
- data/spec/grape/exceptions/missing_mime_type_spec.rb +2 -2
- data/spec/grape/exceptions/missing_option_spec.rb +1 -1
- data/spec/grape/exceptions/unknown_options_spec.rb +1 -1
- data/spec/grape/exceptions/unknown_validator_spec.rb +1 -1
- data/spec/grape/middleware/auth/basic_spec.rb +3 -3
- data/spec/grape/middleware/auth/digest_spec.rb +4 -4
- data/spec/grape/middleware/auth/oauth2_spec.rb +11 -11
- data/spec/grape/middleware/base_spec.rb +9 -9
- data/spec/grape/middleware/error_spec.rb +4 -4
- data/spec/grape/middleware/exception_spec.rb +17 -17
- data/spec/grape/middleware/formatter_spec.rb +38 -38
- data/spec/grape/middleware/versioner/accept_version_header_spec.rb +11 -11
- data/spec/grape/middleware/versioner/header_spec.rb +39 -39
- data/spec/grape/middleware/versioner/param_spec.rb +10 -10
- data/spec/grape/middleware/versioner/path_spec.rb +7 -7
- data/spec/grape/middleware/versioner_spec.rb +4 -4
- data/spec/grape/path_spec.rb +6 -6
- data/spec/grape/util/hash_stack_spec.rb +22 -22
- data/spec/grape/validations/coerce_spec.rb +34 -34
- data/spec/grape/validations/default_spec.rb +16 -16
- data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
- data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
- data/spec/grape/validations/presence_spec.rb +34 -34
- data/spec/grape/validations/regexp_spec.rb +11 -4
- data/spec/grape/validations/values_spec.rb +34 -20
- data/spec/grape/validations_spec.rb +300 -147
- data/spec/shared/versioning_examples.rb +18 -18
- data/spec/spec_helper.rb +2 -1
- metadata +81 -38
data/UPGRADING.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Upgrading Grape
|
2
2
|
===============
|
3
3
|
|
4
|
-
### Upgrading to 0.7.0
|
4
|
+
### Upgrading to >= 0.7.0
|
5
5
|
|
6
6
|
#### Changes in Exception Handling
|
7
7
|
|
@@ -78,9 +78,9 @@ end
|
|
78
78
|
|
79
79
|
This caused the ambiguity and unexpected errors described in [#543](https://github.com/intridea/Grape/issues/543).
|
80
80
|
|
81
|
-
In Grape 0.
|
81
|
+
In Grape 0.7.0, the `group`, `optional` and `requires` keywords take an additional `type` attribute which defaults to `Array`. This means that without a `type` attribute, these nested parameters will no longer accept a single hash, only an array (of hashes).
|
82
82
|
|
83
|
-
Whereas in 0.6.1 the API above accepted the following json, it no longer does in 0.
|
83
|
+
Whereas in 0.6.1 the API above accepted the following json, it no longer does in 0.7.0.
|
84
84
|
|
85
85
|
```json
|
86
86
|
{
|
data/grape.gemspec
CHANGED
@@ -20,7 +20,7 @@ Gem::Specification.new do |s|
|
|
20
20
|
s.add_runtime_dependency 'activesupport'
|
21
21
|
s.add_runtime_dependency 'multi_json', '>= 1.3.2'
|
22
22
|
s.add_runtime_dependency 'multi_xml', '>= 0.5.2'
|
23
|
-
s.add_runtime_dependency 'hashie', '>= 1.
|
23
|
+
s.add_runtime_dependency 'hashie', '>= 2.1.0'
|
24
24
|
s.add_runtime_dependency 'virtus', '>= 1.0.0'
|
25
25
|
s.add_runtime_dependency 'builder'
|
26
26
|
|
data/grape.png
ADDED
Binary file
|
data/lib/grape/api.rb
CHANGED
@@ -99,7 +99,7 @@ module Grape
|
|
99
99
|
options ||= {}
|
100
100
|
options = { using: :path }.merge(options)
|
101
101
|
|
102
|
-
raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.
|
102
|
+
raise Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
|
103
103
|
|
104
104
|
@versions = versions | args
|
105
105
|
nest(block) do
|
@@ -159,7 +159,7 @@ module Grape
|
|
159
159
|
end
|
160
160
|
|
161
161
|
def error_formatter(format, options)
|
162
|
-
if options.is_a?(Hash) && options.
|
162
|
+
if options.is_a?(Hash) && options.key?(:with)
|
163
163
|
formatter = options[:with]
|
164
164
|
else
|
165
165
|
formatter = options
|
@@ -212,14 +212,24 @@ module Grape
|
|
212
212
|
end
|
213
213
|
|
214
214
|
options = args.last.is_a?(Hash) ? args.pop : {}
|
215
|
-
handler ||= proc { options[:with] } if options.
|
215
|
+
handler ||= proc { options[:with] } if options.key?(:with)
|
216
216
|
|
217
|
-
|
218
|
-
|
217
|
+
if args.include?(:all)
|
218
|
+
set(:rescue_all, true)
|
219
|
+
imbue :all_rescue_handler, handler
|
220
|
+
else
|
221
|
+
handler_type =
|
222
|
+
case options[:rescue_subclasses]
|
223
|
+
when nil, true
|
224
|
+
:rescue_handlers
|
225
|
+
else
|
226
|
+
:base_only_rescue_handlers
|
227
|
+
end
|
219
228
|
|
220
|
-
|
229
|
+
imbue handler_type, Hash[args.map { |arg| [arg, handler] }]
|
230
|
+
end
|
221
231
|
|
222
|
-
|
232
|
+
imbue(:rescue_options, options)
|
223
233
|
end
|
224
234
|
|
225
235
|
# Allows you to specify a default representation entity for a
|
@@ -472,7 +482,7 @@ module Grape
|
|
472
482
|
|
473
483
|
def cascade(value = nil)
|
474
484
|
if value.nil?
|
475
|
-
settings.
|
485
|
+
settings.key?(:cascade) ? !!settings[:cascade] : true
|
476
486
|
else
|
477
487
|
set(:cascade, value)
|
478
488
|
end
|
@@ -548,8 +558,8 @@ module Grape
|
|
548
558
|
# errors from reaching upstream. This is effectivelly done by unsetting
|
549
559
|
# X-Cascade. Default :cascade is true.
|
550
560
|
def cascade?
|
551
|
-
return !!self.class.settings[:cascade] if self.class.settings.
|
552
|
-
return !!self.class.settings[:version_options][:cascade] if self.class.settings[:version_options] && self.class.settings[:version_options].
|
561
|
+
return !!self.class.settings[:cascade] if self.class.settings.key?(:cascade)
|
562
|
+
return !!self.class.settings[:version_options][:cascade] if self.class.settings[:version_options] && self.class.settings[:version_options].key?(:cascade)
|
553
563
|
true
|
554
564
|
end
|
555
565
|
|
@@ -579,9 +589,7 @@ module Grape
|
|
579
589
|
methods_per_path.each do |path, methods|
|
580
590
|
allowed_methods = methods.dup
|
581
591
|
unless self.class.settings[:do_not_route_head]
|
582
|
-
if allowed_methods.include?('GET')
|
583
|
-
allowed_methods = allowed_methods | ['HEAD']
|
584
|
-
end
|
592
|
+
allowed_methods |= ['HEAD'] if allowed_methods.include?('GET')
|
585
593
|
end
|
586
594
|
|
587
595
|
allow_header = (['OPTIONS'] | allowed_methods).join(', ')
|
data/lib/grape/endpoint.rb
CHANGED
@@ -8,6 +8,18 @@ module Grape
|
|
8
8
|
attr_reader :env, :request, :headers, :params
|
9
9
|
|
10
10
|
class << self
|
11
|
+
def before_each(new_setup = false, &block)
|
12
|
+
if new_setup == false
|
13
|
+
if block_given?
|
14
|
+
@before_each = block
|
15
|
+
else
|
16
|
+
return @before_each
|
17
|
+
end
|
18
|
+
else
|
19
|
+
@before_each = new_setup
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
11
23
|
# @api private
|
12
24
|
#
|
13
25
|
# Create an UnboundMethod that is appropriate for executing an endpoint
|
@@ -52,7 +64,7 @@ module Grape
|
|
52
64
|
end
|
53
65
|
|
54
66
|
def require_option(options, key)
|
55
|
-
raise Grape::Exceptions::MissingOption.new(key) unless options.
|
67
|
+
raise Grape::Exceptions::MissingOption.new(key) unless options.key?(key)
|
56
68
|
end
|
57
69
|
|
58
70
|
def method_name
|
@@ -78,8 +90,8 @@ module Grape
|
|
78
90
|
end
|
79
91
|
methods.each do |method|
|
80
92
|
route_set.add_route(self, {
|
81
|
-
|
82
|
-
|
93
|
+
path_info: route.route_compiled,
|
94
|
+
request_method: method
|
83
95
|
}, route_info: route)
|
84
96
|
end
|
85
97
|
end
|
@@ -105,7 +117,7 @@ module Grape
|
|
105
117
|
regex = Rack::Mount::RegexpWithNamedGroups.new(path)
|
106
118
|
path_params = {}
|
107
119
|
# named parameters in the api path
|
108
|
-
named_params = regex.named_captures.map { |nc| nc[0] } - %w
|
120
|
+
named_params = regex.named_captures.map { |nc| nc[0] } - %w(version format)
|
109
121
|
named_params.each { |named_param| path_params[named_param] = "" }
|
110
122
|
# route parameters declared via desc or appended to the api declaration
|
111
123
|
route_params = (options[:route_options][:params] || {})
|
@@ -118,9 +130,8 @@ module Grape
|
|
118
130
|
method: request_method,
|
119
131
|
path: prepared_path,
|
120
132
|
params: path_params,
|
121
|
-
compiled: path
|
122
|
-
)
|
123
|
-
)
|
133
|
+
compiled: path
|
134
|
+
))
|
124
135
|
end
|
125
136
|
end
|
126
137
|
routes
|
@@ -325,14 +336,18 @@ module Grape
|
|
325
336
|
entity_class = options.delete(:with)
|
326
337
|
|
327
338
|
if entity_class.nil?
|
328
|
-
# entity class not explicitely defined, auto-detect from first object in the collection
|
329
|
-
|
339
|
+
# entity class not explicitely defined, auto-detect from relation#klass or first object in the collection
|
340
|
+
object_class = if object.respond_to?(:klass)
|
341
|
+
object.klass
|
342
|
+
else
|
343
|
+
object.respond_to?(:first) ? object.first.class : object.class
|
344
|
+
end
|
330
345
|
|
331
|
-
|
346
|
+
object_class.ancestors.each do |potential|
|
332
347
|
entity_class ||= (settings[:representations] || {})[potential]
|
333
348
|
end
|
334
349
|
|
335
|
-
entity_class ||=
|
350
|
+
entity_class ||= object_class.const_get(:Entity) if object_class.const_defined?(:Entity) && object_class.const_get(:Entity).respond_to?(:represent)
|
336
351
|
end
|
337
352
|
|
338
353
|
root = options.delete(:root)
|
@@ -384,11 +399,13 @@ module Grape
|
|
384
399
|
|
385
400
|
cookies.read(@request)
|
386
401
|
|
402
|
+
self.class.before_each.call(self) if self.class.before_each
|
403
|
+
|
387
404
|
run_filters befores
|
388
405
|
|
389
406
|
run_filters before_validations
|
390
407
|
|
391
|
-
#
|
408
|
+
# Retrieve validations from this namespace and all parent namespaces.
|
392
409
|
validation_errors = []
|
393
410
|
settings.gather(:validations).each do |validator|
|
394
411
|
begin
|
@@ -424,7 +441,8 @@ module Grape
|
|
424
441
|
error_formatters: settings[:error_formatters],
|
425
442
|
rescue_options: settings[:rescue_options],
|
426
443
|
rescue_handlers: merged_setting(:rescue_handlers),
|
427
|
-
base_only_rescue_handlers: merged_setting(:base_only_rescue_handlers)
|
444
|
+
base_only_rescue_handlers: merged_setting(:base_only_rescue_handlers),
|
445
|
+
all_rescue_handler: settings[:all_rescue_handler]
|
428
446
|
|
429
447
|
aggregate_setting(:middleware).each do |m|
|
430
448
|
m = m.dup
|
@@ -6,9 +6,9 @@ module Grape
|
|
6
6
|
attr_accessor :param
|
7
7
|
|
8
8
|
def initialize(args = {})
|
9
|
-
raise "Param is missing:" unless args.
|
9
|
+
raise "Param is missing:" unless args.key? :param
|
10
10
|
@param = args[:param]
|
11
|
-
args[:message] = translate_message(args[:message_key]) if args.
|
11
|
+
args[:message] = translate_message(args[:message_key]) if args.key? :message_key
|
12
12
|
super
|
13
13
|
end
|
14
14
|
|
data/lib/grape/locale/en.yml
CHANGED
@@ -28,3 +28,5 @@ en:
|
|
28
28
|
unknown_validator: 'unknown validator: %{validator_type}'
|
29
29
|
unknown_options: 'unknown options: %{options}'
|
30
30
|
incompatible_option_values: '%{option1}: %{value1} is incompatible with %{option2}: %{value2}'
|
31
|
+
mutual_exclusion: 'are mutually exclusive'
|
32
|
+
exactly_one: "- exactly one parameter must be provided"
|
@@ -1,79 +1,83 @@
|
|
1
|
-
module Grape
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
1
|
+
module Grape
|
2
|
+
module Middleware
|
3
|
+
module Auth
|
4
|
+
# OAuth 2.0 authorization for Grape APIs.
|
5
|
+
class OAuth2 < Grape::Middleware::Base
|
6
|
+
def default_options
|
7
|
+
{
|
8
|
+
token_class: 'AccessToken',
|
9
|
+
realm: 'OAuth API',
|
10
|
+
parameter: %w(bearer_token oauth_token access_token),
|
11
|
+
accepted_headers: %w(HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION REDIRECT_X_HTTP_AUTHORIZATION),
|
12
|
+
header: [/Bearer (.*)/i, /OAuth (.*)/i],
|
13
|
+
required: true
|
14
|
+
}
|
15
|
+
end
|
14
16
|
|
15
|
-
|
16
|
-
|
17
|
-
|
17
|
+
def before
|
18
|
+
verify_token(token_parameter || token_header)
|
19
|
+
end
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
21
|
+
def request
|
22
|
+
@request ||= Grape::Request.new(env)
|
23
|
+
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
|
25
|
+
def params
|
26
|
+
@params ||= request.params
|
27
|
+
end
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
def token_parameter
|
30
|
+
Array(options[:parameter]).each do |p|
|
31
|
+
return params[p] if params[p]
|
32
|
+
end
|
33
|
+
nil
|
34
|
+
end
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
def token_header
|
37
|
+
return false unless authorization_header
|
38
|
+
Array(options[:header]).each do |regexp|
|
39
|
+
return $1 if authorization_header =~ regexp
|
40
|
+
end
|
41
|
+
nil
|
42
|
+
end
|
41
43
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
def authorization_header
|
45
|
+
options[:accepted_headers].each do |head|
|
46
|
+
return env[head] if env[head]
|
47
|
+
end
|
48
|
+
nil
|
49
|
+
end
|
48
50
|
|
49
|
-
|
50
|
-
|
51
|
-
|
51
|
+
def token_class
|
52
|
+
@klass ||= eval(options[:token_class]) # rubocop:disable Eval
|
53
|
+
end
|
52
54
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
55
|
+
def verify_token(token)
|
56
|
+
token = token_class.verify(token)
|
57
|
+
if token
|
58
|
+
if token.respond_to?(:expired?) && token.expired?
|
59
|
+
error_out(401, 'invalid_grant')
|
60
|
+
else
|
61
|
+
if !token.respond_to?(:permission_for?) || token.permission_for?(env)
|
62
|
+
env['api.token'] = token
|
63
|
+
else
|
64
|
+
error_out(403, 'insufficient_scope')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
elsif !!options[:required]
|
68
|
+
error_out(401, 'invalid_grant')
|
63
69
|
end
|
64
70
|
end
|
65
|
-
elsif !!options[:required]
|
66
|
-
error_out(401, 'invalid_grant')
|
67
|
-
end
|
68
|
-
end
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
72
|
+
def error_out(status, error)
|
73
|
+
throw :error,
|
74
|
+
message: error,
|
75
|
+
status: status,
|
76
|
+
headers: {
|
77
|
+
'WWW-Authenticate' => "OAuth realm='#{options[:realm]}', error='#{error}'"
|
78
|
+
}
|
79
|
+
end
|
80
|
+
end
|
77
81
|
end
|
78
82
|
end
|
79
83
|
end
|
@@ -14,7 +14,8 @@ module Grape
|
|
14
14
|
rescue_subclasses: true, # rescue subclasses of exceptions listed
|
15
15
|
rescue_options: { backtrace: false }, # true to display backtrace
|
16
16
|
rescue_handlers: {}, # rescue handler blocks
|
17
|
-
base_only_rescue_handlers: {} # rescue handler blocks rescuing only the base class
|
17
|
+
base_only_rescue_handlers: {}, # rescue handler blocks rescuing only the base class
|
18
|
+
all_rescue_handler: nil # rescue handler block to rescue from all exceptions
|
18
19
|
}
|
19
20
|
end
|
20
21
|
|
@@ -40,7 +41,8 @@ module Grape
|
|
40
41
|
|
41
42
|
def find_handler(klass)
|
42
43
|
handler = options[:rescue_handlers].find(-> { [] }) { |error, _| klass <= error }[1]
|
43
|
-
handler
|
44
|
+
handler ||= options[:base_only_rescue_handlers][klass]
|
45
|
+
handler ||= options[:all_rescue_handler]
|
44
46
|
handler
|
45
47
|
end
|
46
48
|
|
@@ -130,14 +130,14 @@ module Grape
|
|
130
130
|
accept = headers['accept']
|
131
131
|
return [] unless accept
|
132
132
|
|
133
|
-
accept_into_mime_and_quality = %r
|
133
|
+
accept_into_mime_and_quality = %r{
|
134
134
|
(
|
135
135
|
\w+/[\w+.-]+) # eg application/vnd.example.myformat+xml
|
136
136
|
(?:
|
137
137
|
(?:;[^,]*?)? # optionally multiple formats in a row
|
138
138
|
;\s*q=([\d.]+) # optional "quality" preference (eg q=0.5)
|
139
139
|
)?
|
140
|
-
|
140
|
+
}x
|
141
141
|
|
142
142
|
vendor_prefix_pattern = /vnd\.[^+]+\+/
|
143
143
|
|
@@ -51,7 +51,7 @@ module Grape
|
|
51
51
|
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
|
52
52
|
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
53
53
|
def cascade?
|
54
|
-
if options[:version_options] && options[:version_options].
|
54
|
+
if options[:version_options] && options[:version_options].key?(:cascade)
|
55
55
|
!!options[:version_options][:cascade]
|
56
56
|
else
|
57
57
|
true
|
@@ -60,7 +60,7 @@ module Grape
|
|
60
60
|
elsif strict?
|
61
61
|
throw :error, status: 406, headers: error_headers, message: '406 Not Acceptable'
|
62
62
|
# If all acceptable content types specify a vendor or version that doesn't exist:
|
63
|
-
elsif header.values.all? { |header_value| has_vendor?(header_value) ||
|
63
|
+
elsif header.values.all? { |header_value| has_vendor?(header_value) || version?(header_value) }
|
64
64
|
throw :error, status: 406, headers: error_headers, message: 'API vendor or version not found.'
|
65
65
|
end
|
66
66
|
end
|
@@ -102,7 +102,7 @@ module Grape
|
|
102
102
|
# of routes (see [Rack::Mount](https://github.com/josh/rack-mount) for more information). To prevent
|
103
103
|
# this behavior, and not add the `X-Cascade` header, one can set the `:cascade` option to `false`.
|
104
104
|
def cascade?
|
105
|
-
if options[:version_options] && options[:version_options].
|
105
|
+
if options[:version_options] && options[:version_options].key?(:cascade)
|
106
106
|
!!options[:version_options][:cascade]
|
107
107
|
else
|
108
108
|
true
|
@@ -122,7 +122,7 @@ module Grape
|
|
122
122
|
|
123
123
|
# @param [String] media_type a content type
|
124
124
|
# @return [Boolean] whether the content type sets an API version
|
125
|
-
def
|
125
|
+
def version?(media_type)
|
126
126
|
_, subtype = Rack::Accept::Header.parse_media_type media_type
|
127
127
|
subtype[/\Avnd\.[a-z0-9*.]+-[a-z0-9*\-.]+/]
|
128
128
|
end
|