grape 0.5.0 → 0.6.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 +15 -0
- data/.travis.yml +2 -6
- data/CHANGELOG.md +20 -0
- data/README.md +43 -11
- data/lib/grape.rb +6 -0
- data/lib/grape/api.rb +27 -14
- data/lib/grape/endpoint.rb +33 -34
- data/lib/grape/exceptions/base.rb +4 -2
- data/lib/grape/exceptions/validation.rb +13 -3
- data/lib/grape/exceptions/validation_errors.rb +42 -0
- data/lib/grape/http/request.rb +1 -1
- data/lib/grape/locale/en.yml +4 -3
- data/lib/grape/middleware/auth/base.rb +30 -0
- data/lib/grape/middleware/auth/basic.rb +2 -19
- data/lib/grape/middleware/auth/digest.rb +2 -19
- data/lib/grape/middleware/error.rb +10 -1
- data/lib/grape/middleware/formatter.rb +1 -1
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/header.rb +2 -2
- data/lib/grape/path.rb +72 -0
- data/lib/grape/route.rb +6 -1
- data/lib/grape/validations.rb +25 -8
- data/lib/grape/validations/coerce.rb +1 -2
- data/lib/grape/validations/presence.rb +6 -2
- data/lib/grape/validations/regexp.rb +1 -2
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api_spec.rb +71 -6
- data/spec/grape/entity_spec.rb +5 -5
- data/spec/grape/middleware/base_spec.rb +1 -1
- data/spec/grape/middleware/formatter_spec.rb +3 -3
- data/spec/grape/middleware/versioner/header_spec.rb +25 -0
- data/spec/grape/path_spec.rb +219 -0
- data/spec/grape/validations/coerce_spec.rb +31 -13
- data/spec/grape/validations/presence_spec.rb +12 -12
- data/spec/grape/validations/zh-CN.yml +4 -3
- data/spec/grape/validations_spec.rb +154 -10
- data/spec/support/versioned_helpers.rb +5 -2
- metadata +10 -45
data/lib/grape/http/request.rb
CHANGED
data/lib/grape/locale/en.yml
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
en:
|
2
2
|
grape:
|
3
3
|
errors:
|
4
|
+
format: ! '%{attribute} %{message}'
|
4
5
|
messages:
|
5
|
-
coerce: 'invalid
|
6
|
-
presence: 'missing
|
7
|
-
regexp: 'invalid
|
6
|
+
coerce: 'is invalid'
|
7
|
+
presence: 'is missing'
|
8
|
+
regexp: 'is invalid'
|
8
9
|
missing_vendor_option:
|
9
10
|
problem: 'missing :vendor option.'
|
10
11
|
summary: 'when version using header, you must specify :vendor option. '
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rack/auth/basic'
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Middleware
|
5
|
+
module Auth
|
6
|
+
class Base < Grape::Middleware::Base
|
7
|
+
attr_reader :authenticator
|
8
|
+
|
9
|
+
def initialize(app, options = {}, &authenticator)
|
10
|
+
super(app, options)
|
11
|
+
@authenticator = authenticator
|
12
|
+
end
|
13
|
+
|
14
|
+
def base_request
|
15
|
+
raise NotImplementedError.new("You must implement base_request.")
|
16
|
+
end
|
17
|
+
|
18
|
+
def credentials
|
19
|
+
base_request.provided?? base_request.credentials : [nil, nil]
|
20
|
+
end
|
21
|
+
|
22
|
+
def before
|
23
|
+
unless authenticator.call(*credentials)
|
24
|
+
throw :error, :status => 401, :message => "API Authorization Failed."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -3,27 +3,10 @@ require 'rack/auth/basic'
|
|
3
3
|
module Grape
|
4
4
|
module Middleware
|
5
5
|
module Auth
|
6
|
-
class Basic < Grape::Middleware::Base
|
7
|
-
|
8
|
-
|
9
|
-
def initialize(app, options = {}, &authenticator)
|
10
|
-
super(app, options)
|
11
|
-
@authenticator = authenticator
|
12
|
-
end
|
13
|
-
|
14
|
-
def basic_request
|
6
|
+
class Basic < Grape::Middleware::Auth::Base
|
7
|
+
def base_request
|
15
8
|
Rack::Auth::Basic::Request.new(env)
|
16
9
|
end
|
17
|
-
|
18
|
-
def credentials
|
19
|
-
basic_request.provided?? basic_request.credentials : [nil, nil]
|
20
|
-
end
|
21
|
-
|
22
|
-
def before
|
23
|
-
unless authenticator.call(*credentials)
|
24
|
-
throw :error, :status => 401, :message => "API Authorization Failed."
|
25
|
-
end
|
26
|
-
end
|
27
10
|
end
|
28
11
|
end
|
29
12
|
end
|
@@ -3,27 +3,10 @@ require 'rack/auth/digest/md5'
|
|
3
3
|
module Grape
|
4
4
|
module Middleware
|
5
5
|
module Auth
|
6
|
-
class Digest < Grape::Middleware::Base
|
7
|
-
|
8
|
-
|
9
|
-
def initialize(app, options = {}, &authenticator)
|
10
|
-
super(app, options)
|
11
|
-
@authenticator = authenticator
|
12
|
-
end
|
13
|
-
|
14
|
-
def digest_request
|
6
|
+
class Digest < Grape::Middleware::Auth::Base
|
7
|
+
def base_request
|
15
8
|
Rack::Auth::Digest::Request.new(env)
|
16
9
|
end
|
17
|
-
|
18
|
-
def credentials
|
19
|
-
digest_request.provided?? digest_request.credentials : [nil, nil]
|
20
|
-
end
|
21
|
-
|
22
|
-
def before
|
23
|
-
unless authenticator.call(*credentials)
|
24
|
-
throw :error, :status => 401, :message => "API Authorization Failed."
|
25
|
-
end
|
26
|
-
end
|
27
10
|
end
|
28
11
|
end
|
29
12
|
end
|
@@ -33,7 +33,8 @@ module Grape
|
|
33
33
|
raise unless is_rescuable
|
34
34
|
handler = options[:rescue_handlers][e.class] || options[:rescue_handlers][:all]
|
35
35
|
end
|
36
|
-
|
36
|
+
|
37
|
+
handler.nil? ? handle_error(e) : exec_handler(e, &handler)
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
@@ -41,6 +42,14 @@ module Grape
|
|
41
42
|
options[:rescue_all] || (options[:rescued_errors] || []).include?(klass)
|
42
43
|
end
|
43
44
|
|
45
|
+
def exec_handler(e, &handler)
|
46
|
+
if handler.lambda? && handler.arity == 0
|
47
|
+
self.instance_exec(&handler)
|
48
|
+
else
|
49
|
+
self.instance_exec(e, &handler)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
44
53
|
def handle_error(e)
|
45
54
|
error_response({ :message => e.message, :backtrace => e.backtrace })
|
46
55
|
end
|
@@ -41,7 +41,7 @@ module Grape
|
|
41
41
|
|
42
42
|
# store read input in env['api.request.input']
|
43
43
|
def read_body_input
|
44
|
-
if (request.post? || request.put? || request.patch?) &&
|
44
|
+
if (request.post? || request.put? || request.patch? || request.delete?) &&
|
45
45
|
(! request.form_data? || ! request.media_type) &&
|
46
46
|
(! request.parseable_data?) &&
|
47
47
|
(request.content_length.to_i > 0 || request.env['HTTP_TRANSFER_ENCODING'] == 'chunked')
|
@@ -11,7 +11,7 @@ module Grape
|
|
11
11
|
#
|
12
12
|
# The following rack env variables are set:
|
13
13
|
#
|
14
|
-
# env['api.version] => 'v1'
|
14
|
+
# env['api.version'] => 'v1'
|
15
15
|
#
|
16
16
|
# If version does not match this route, then a 406 is raised with
|
17
17
|
# X-Cascade header to alert Rack::Mount to attempt the next matched
|
@@ -99,8 +99,8 @@ 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
|
-
!! options[:version_options][:cascade] :
|
102
|
+
options[:version_options] && options[:version_options].has_key?(:cascade) ?
|
103
|
+
!! options[:version_options][:cascade] :
|
104
104
|
true
|
105
105
|
end
|
106
106
|
|
data/lib/grape/path.rb
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
module Grape
|
2
|
+
class Path
|
3
|
+
|
4
|
+
def self.prepare(raw_path, namespace, settings)
|
5
|
+
Path.new(raw_path, namespace, settings).path_with_suffix
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :raw_path, :namespace, :settings
|
9
|
+
|
10
|
+
def initialize(raw_path, namespace, settings)
|
11
|
+
@raw_path = raw_path
|
12
|
+
@namespace = namespace
|
13
|
+
@settings = settings
|
14
|
+
end
|
15
|
+
|
16
|
+
def mount_path
|
17
|
+
split_setting(:mount_path, '/')
|
18
|
+
end
|
19
|
+
|
20
|
+
def root_prefix
|
21
|
+
split_setting(:root_prefix, '/')
|
22
|
+
end
|
23
|
+
|
24
|
+
def uses_path_versioning?
|
25
|
+
settings[:version] && settings[:version_options][:using] == :path
|
26
|
+
end
|
27
|
+
|
28
|
+
def has_namespace?
|
29
|
+
namespace && namespace.to_s =~ /^\S/ && namespace != '/'
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_path?
|
33
|
+
raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/'
|
34
|
+
end
|
35
|
+
|
36
|
+
def suffix
|
37
|
+
if !uses_path_versioning? || (has_namespace? || has_path?)
|
38
|
+
'(.:format)'
|
39
|
+
else
|
40
|
+
'(/.:format)'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def path
|
45
|
+
Rack::Mount::Utils.normalize_path(parts.join('/'))
|
46
|
+
end
|
47
|
+
|
48
|
+
def path_with_suffix
|
49
|
+
"#{path}#{suffix}"
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
path_with_suffix
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def parts
|
59
|
+
parts = [mount_path, root_prefix].compact
|
60
|
+
parts << ':version' if uses_path_versioning?
|
61
|
+
parts << namespace.to_s
|
62
|
+
parts << raw_path.to_s
|
63
|
+
parts.flatten.reject { |part| part == '/' }
|
64
|
+
end
|
65
|
+
|
66
|
+
def split_setting(key, delimiter)
|
67
|
+
return if settings[key].nil?
|
68
|
+
settings[key].to_s.split("/")
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
data/lib/grape/route.rb
CHANGED
data/lib/grape/validations.rb
CHANGED
@@ -89,10 +89,11 @@ module Grape
|
|
89
89
|
class ParamsScope
|
90
90
|
attr_accessor :element, :parent
|
91
91
|
|
92
|
-
def initialize(
|
93
|
-
@element
|
94
|
-
@parent
|
95
|
-
@api
|
92
|
+
def initialize(opts, &block)
|
93
|
+
@element = opts[:element]
|
94
|
+
@parent = opts[:parent]
|
95
|
+
@api = opts[:api]
|
96
|
+
@optional = opts[:optional] || false
|
96
97
|
@declared_params = []
|
97
98
|
|
98
99
|
instance_eval(&block)
|
@@ -100,7 +101,15 @@ module Grape
|
|
100
101
|
configure_declared_params
|
101
102
|
end
|
102
103
|
|
103
|
-
def
|
104
|
+
def should_validate?(parameters)
|
105
|
+
return false if @optional && params(parameters).blank?
|
106
|
+
return true if parent.nil?
|
107
|
+
parent.should_validate?(parameters)
|
108
|
+
end
|
109
|
+
|
110
|
+
def requires(*attrs, &block)
|
111
|
+
return new_scope(attrs, &block) if block_given?
|
112
|
+
|
104
113
|
validations = {:presence => true}
|
105
114
|
if attrs.last.is_a?(Hash)
|
106
115
|
validations.merge!(attrs.pop)
|
@@ -110,7 +119,9 @@ module Grape
|
|
110
119
|
validates(attrs, validations)
|
111
120
|
end
|
112
121
|
|
113
|
-
def optional(*attrs)
|
122
|
+
def optional(*attrs, &block)
|
123
|
+
return new_scope(attrs, true, &block) if block_given?
|
124
|
+
|
114
125
|
validations = {}
|
115
126
|
if attrs.last.is_a?(Hash)
|
116
127
|
validations.merge!(attrs.pop)
|
@@ -121,7 +132,7 @@ module Grape
|
|
121
132
|
end
|
122
133
|
|
123
134
|
def group(element, &block)
|
124
|
-
|
135
|
+
requires(element, &block)
|
125
136
|
end
|
126
137
|
|
127
138
|
def params(params)
|
@@ -143,6 +154,11 @@ module Grape
|
|
143
154
|
|
144
155
|
private
|
145
156
|
|
157
|
+
def new_scope(attrs, optional=false, &block)
|
158
|
+
raise ArgumentError unless attrs.size == 1
|
159
|
+
ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, &block)
|
160
|
+
end
|
161
|
+
|
146
162
|
# Pushes declared params to parent or settings
|
147
163
|
def configure_declared_params
|
148
164
|
if @parent
|
@@ -179,6 +195,7 @@ module Grape
|
|
179
195
|
# Validate for presence before any other validators
|
180
196
|
if validations.has_key?(:presence) && validations[:presence]
|
181
197
|
validate('presence', validations[:presence], attrs, doc_attrs)
|
198
|
+
validations.delete(:presence)
|
182
199
|
end
|
183
200
|
|
184
201
|
# Before we run the rest of the validators, lets handle
|
@@ -214,7 +231,7 @@ module Grape
|
|
214
231
|
end
|
215
232
|
|
216
233
|
def params(&block)
|
217
|
-
ParamsScope.new(self,
|
234
|
+
ParamsScope.new(api: self, &block)
|
218
235
|
end
|
219
236
|
|
220
237
|
def document_attribute(names, opts)
|
@@ -12,8 +12,7 @@ module Grape
|
|
12
12
|
if valid_type?(new_value)
|
13
13
|
params[attr_name] = new_value
|
14
14
|
else
|
15
|
-
raise Grape::Exceptions::Validation, :
|
16
|
-
:param => @scope.full_name(attr_name), :message_key => :coerce
|
15
|
+
raise Grape::Exceptions::Validation, :param => @scope.full_name(attr_name), :message_key => :coerce
|
17
16
|
end
|
18
17
|
end
|
19
18
|
|
@@ -1,10 +1,14 @@
|
|
1
1
|
module Grape
|
2
2
|
module Validations
|
3
3
|
class PresenceValidator < Validator
|
4
|
+
def validate!(params)
|
5
|
+
return unless @scope.should_validate?(params)
|
6
|
+
super
|
7
|
+
end
|
8
|
+
|
4
9
|
def validate_param!(attr_name, params)
|
5
10
|
unless params.has_key?(attr_name)
|
6
|
-
raise Grape::Exceptions::Validation, :
|
7
|
-
:param => @scope.full_name(attr_name), :message_key => :presence
|
11
|
+
raise Grape::Exceptions::Validation, :param => @scope.full_name(attr_name), :message_key => :presence
|
8
12
|
end
|
9
13
|
end
|
10
14
|
end
|
@@ -4,8 +4,7 @@ module Grape
|
|
4
4
|
class RegexpValidator < SingleOptionValidator
|
5
5
|
def validate_param!(attr_name, params)
|
6
6
|
if params[attr_name] && !( params[attr_name].to_s =~ @option )
|
7
|
-
raise Grape::Exceptions::Validation, :
|
8
|
-
:param => @scope.full_name(attr_name), :message_key => :regexp
|
7
|
+
raise Grape::Exceptions::Validation, :param => @scope.full_name(attr_name), :message_key => :regexp
|
9
8
|
end
|
10
9
|
end
|
11
10
|
end
|
data/lib/grape/version.rb
CHANGED
data/spec/grape/api_spec.rb
CHANGED
@@ -275,7 +275,15 @@ describe Grape::API do
|
|
275
275
|
subject.version 'v1', :using => :header, :vendor => 'test'
|
276
276
|
subject.enable_root_route!
|
277
277
|
|
278
|
-
versioned_get "/", "v1", :using => :header
|
278
|
+
versioned_get "/", "v1", :using => :header, :vendor => 'test'
|
279
|
+
end
|
280
|
+
|
281
|
+
it 'header versioned APIs with multiple headers' do
|
282
|
+
subject.version ['v1', 'v2'], :using => :header, :vendor => 'test'
|
283
|
+
subject.enable_root_route!
|
284
|
+
|
285
|
+
versioned_get "/", "v1", :using => :header, :vendor => 'test'
|
286
|
+
versioned_get "/", "v2", :using => :header, :vendor => 'test'
|
279
287
|
end
|
280
288
|
|
281
289
|
it 'param versioned APIs' do
|
@@ -654,18 +662,18 @@ describe Grape::API do
|
|
654
662
|
describe '.middleware' do
|
655
663
|
it 'includes middleware arguments from settings' do
|
656
664
|
settings = Grape::Util::HashStack.new
|
657
|
-
settings.stub
|
658
|
-
subject.stub
|
665
|
+
settings.stub(:stack).and_return([{:middleware => [[ApiSpec::PhonyMiddleware, 'abc', 123]]}])
|
666
|
+
subject.stub(:settings).and_return(settings)
|
659
667
|
subject.middleware.should eql [[ApiSpec::PhonyMiddleware, 'abc', 123]]
|
660
668
|
end
|
661
669
|
|
662
670
|
it 'includes all middleware from stacked settings' do
|
663
671
|
settings = Grape::Util::HashStack.new
|
664
|
-
settings.stub
|
672
|
+
settings.stub(:stack).and_return [
|
665
673
|
{:middleware => [[ApiSpec::PhonyMiddleware, 123],[ApiSpec::PhonyMiddleware, 'abc']]},
|
666
674
|
{:middleware => [[ApiSpec::PhonyMiddleware, 'foo']]}
|
667
675
|
]
|
668
|
-
subject.stub
|
676
|
+
subject.stub(:settings).and_return(settings)
|
669
677
|
|
670
678
|
subject.middleware.should eql [
|
671
679
|
[ApiSpec::PhonyMiddleware, 123],
|
@@ -989,7 +997,7 @@ describe Grape::API do
|
|
989
997
|
end
|
990
998
|
|
991
999
|
it 'can rescue exceptions raised in the formatter' do
|
992
|
-
formatter =
|
1000
|
+
formatter = double(:formatter)
|
993
1001
|
formatter.stub(:call) { raise StandardError }
|
994
1002
|
Grape::Formatter::Base.stub(:formatter_for) { formatter }
|
995
1003
|
|
@@ -1074,6 +1082,42 @@ describe Grape::API do
|
|
1074
1082
|
end
|
1075
1083
|
end
|
1076
1084
|
|
1085
|
+
describe '.rescue_from klass, lambda' do
|
1086
|
+
it 'rescues an error with the lambda' do
|
1087
|
+
subject.rescue_from ArgumentError, lambda {
|
1088
|
+
rack_response("rescued with a lambda", 400)
|
1089
|
+
}
|
1090
|
+
subject.get('/rescue_lambda') { raise ArgumentError }
|
1091
|
+
|
1092
|
+
get '/rescue_lambda'
|
1093
|
+
last_response.status.should == 400
|
1094
|
+
last_response.body.should == "rescued with a lambda"
|
1095
|
+
end
|
1096
|
+
|
1097
|
+
it 'can execute the lambda with an argument' do
|
1098
|
+
subject.rescue_from ArgumentError, lambda {|e|
|
1099
|
+
rack_response(e.message, 400)
|
1100
|
+
}
|
1101
|
+
subject.get('/rescue_lambda') { raise ArgumentError, 'lambda takes an argument' }
|
1102
|
+
|
1103
|
+
get '/rescue_lambda'
|
1104
|
+
last_response.status.should == 400
|
1105
|
+
last_response.body.should == 'lambda takes an argument'
|
1106
|
+
end
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
describe '.rescue_from klass, with: method' do
|
1110
|
+
it 'rescues an error with the specified message' do
|
1111
|
+
def rescue_arg_error; Rack::Response.new('rescued with a method', 400); end
|
1112
|
+
subject.rescue_from ArgumentError, with: rescue_arg_error
|
1113
|
+
subject.get('/rescue_method') { raise ArgumentError }
|
1114
|
+
|
1115
|
+
get '/rescue_method'
|
1116
|
+
last_response.status.should == 400
|
1117
|
+
last_response.body.should == 'rescued with a method'
|
1118
|
+
end
|
1119
|
+
end
|
1120
|
+
|
1077
1121
|
describe '.error_format' do
|
1078
1122
|
it 'rescues all errors and return :txt' do
|
1079
1123
|
subject.rescue_from :all
|
@@ -1139,6 +1183,27 @@ describe Grape::API do
|
|
1139
1183
|
end
|
1140
1184
|
end
|
1141
1185
|
|
1186
|
+
describe 'with' do
|
1187
|
+
context 'class' do
|
1188
|
+
before :each do
|
1189
|
+
class CustomErrorFormatter
|
1190
|
+
def self.call(message, backtrace, option, env)
|
1191
|
+
"message: #{message} @backtrace"
|
1192
|
+
end
|
1193
|
+
end
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
it 'returns a custom error format' do
|
1197
|
+
subject.rescue_from :all, backtrace: true
|
1198
|
+
subject.error_formatter :txt, with: CustomErrorFormatter
|
1199
|
+
subject.get('/exception') { raise "rain!" }
|
1200
|
+
|
1201
|
+
get '/exception'
|
1202
|
+
last_response.body.should == 'message: rain! @backtrace'
|
1203
|
+
end
|
1204
|
+
end
|
1205
|
+
end
|
1206
|
+
|
1142
1207
|
it 'rescues all errors and return :json' do
|
1143
1208
|
subject.rescue_from :all
|
1144
1209
|
subject.format :json
|