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.

Files changed (38) hide show
  1. checksums.yaml +15 -0
  2. data/.travis.yml +2 -6
  3. data/CHANGELOG.md +20 -0
  4. data/README.md +43 -11
  5. data/lib/grape.rb +6 -0
  6. data/lib/grape/api.rb +27 -14
  7. data/lib/grape/endpoint.rb +33 -34
  8. data/lib/grape/exceptions/base.rb +4 -2
  9. data/lib/grape/exceptions/validation.rb +13 -3
  10. data/lib/grape/exceptions/validation_errors.rb +42 -0
  11. data/lib/grape/http/request.rb +1 -1
  12. data/lib/grape/locale/en.yml +4 -3
  13. data/lib/grape/middleware/auth/base.rb +30 -0
  14. data/lib/grape/middleware/auth/basic.rb +2 -19
  15. data/lib/grape/middleware/auth/digest.rb +2 -19
  16. data/lib/grape/middleware/error.rb +10 -1
  17. data/lib/grape/middleware/formatter.rb +1 -1
  18. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  19. data/lib/grape/middleware/versioner/header.rb +2 -2
  20. data/lib/grape/path.rb +72 -0
  21. data/lib/grape/route.rb +6 -1
  22. data/lib/grape/validations.rb +25 -8
  23. data/lib/grape/validations/coerce.rb +1 -2
  24. data/lib/grape/validations/presence.rb +6 -2
  25. data/lib/grape/validations/regexp.rb +1 -2
  26. data/lib/grape/version.rb +1 -1
  27. data/spec/grape/api_spec.rb +71 -6
  28. data/spec/grape/entity_spec.rb +5 -5
  29. data/spec/grape/middleware/base_spec.rb +1 -1
  30. data/spec/grape/middleware/formatter_spec.rb +3 -3
  31. data/spec/grape/middleware/versioner/header_spec.rb +25 -0
  32. data/spec/grape/path_spec.rb +219 -0
  33. data/spec/grape/validations/coerce_spec.rb +31 -13
  34. data/spec/grape/validations/presence_spec.rb +12 -12
  35. data/spec/grape/validations/zh-CN.yml +4 -3
  36. data/spec/grape/validations_spec.rb +154 -10
  37. data/spec/support/versioned_helpers.rb +5 -2
  38. metadata +10 -45
@@ -2,7 +2,7 @@ module Grape
2
2
  class Request < Rack::Request
3
3
 
4
4
  def params
5
- @env['grape.request.params'] ||= begin
5
+ @env['grape.request.params'] = begin
6
6
  params = Hashie::Mash.new(super)
7
7
  if env['rack.routing_args']
8
8
  args = env['rack.routing_args'].dup
@@ -1,10 +1,11 @@
1
1
  en:
2
2
  grape:
3
3
  errors:
4
+ format: ! '%{attribute} %{message}'
4
5
  messages:
5
- coerce: 'invalid parameter: %{attribute}'
6
- presence: 'missing parameter: %{attribute}'
7
- regexp: 'invalid parameter: %{attribute}'
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
- attr_reader :authenticator
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
- attr_reader :authenticator
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
- handler.nil? ? handle_error(e) : self.instance_exec(e, &handler)
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
@@ -18,6 +18,11 @@ module Grape
18
18
  def to_s
19
19
  "version=#{route_version}, method=#{route_method}, path=#{route_path}"
20
20
  end
21
-
21
+
22
+ private
23
+ def to_ary
24
+ nil
25
+ end
26
+
22
27
  end
23
28
  end
@@ -89,10 +89,11 @@ module Grape
89
89
  class ParamsScope
90
90
  attr_accessor :element, :parent
91
91
 
92
- def initialize(api, element, parent, &block)
93
- @element = element
94
- @parent = parent
95
- @api = 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 requires(*attrs)
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
- ParamsScope.new(@api, element, self, &block)
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, nil, nil, &block)
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, :status => 400,
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, :status => 400,
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, :status => 400,
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
@@ -1,3 +1,3 @@
1
1
  module Grape
2
- VERSION = '0.5.0'
2
+ VERSION = '0.6.0'
3
3
  end
@@ -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!(:stack).and_return([{:middleware => [[ApiSpec::PhonyMiddleware, 'abc', 123]]}])
658
- subject.stub!(:settings).and_return(settings)
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!(:stack).and_return [
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!(:settings).and_return(settings)
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 = stub(: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