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
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ Y2ZhNWZiZWQ4YjIwNmY5MDk1NmQxODgxM2FiZDA0NTE3ZDlmZmNiYQ==
5
+ data.tar.gz: !binary |-
6
+ MTE5MjU2MWY2MWZlNGI1ODQzNDExMDZjOTNmZjRiYzc3ZDY4ODZlMg==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ MDlhYjA4YWQ1OThhMGFhNTc3ZjUwNzI4N2FiNmY2ZmNlOGM3MjQ5YjE2OWMy
10
+ NzcxODEwZmY3Y2I5ZmVkOTk5ZWViMTNkZmE3NGM3MjMyOTAzZTdmMTg5MWZm
11
+ ZmI1Njk0ZjRlMGIwOGFkY2EzZDJhZTMxMDQ0MzcyMDM4Y2UxMGY=
12
+ data.tar.gz: !binary |-
13
+ OWU4MGUxOGJkYzRjYzhkZmQyMzFkMWU1NTk3ZTllMTRkZDkyOTA0YjcwODg3
14
+ YWNiNWY0ZDQzZWUxNmI4MmYxZGQzOWJmMTFiZDg0YTFiNzMyYzA5ODBhMDYz
15
+ ZWRiN2FmY2FhM2FjNjQ1ZWM2NjMwNWI1YmU1Y2NhYjA1NGI2MDg=
data/.travis.yml CHANGED
@@ -1,9 +1,5 @@
1
1
  rvm:
2
2
  - 2.0.0
3
- - 1.8.7
4
- - 1.9.2
5
3
  - 1.9.3
6
- - jruby
7
- - rbx
8
- - ree
9
-
4
+ - jruby-19mode
5
+ - rbx-19mode
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ 0.6.0 (9/16/2013)
2
+ =================
3
+
4
+ #### Features
5
+
6
+ * Grape is no longer tested against Ruby 1.8.7.
7
+ * [#442](https://github.com/intridea/grape/issues/442): Enable incrementally building on top of a previous API version - [@dblock](https://github.com/dblock).
8
+ * [#442](https://github.com/intridea/grape/issues/442): API `version` can now take an array of multiple versions - [@dblock](https://github.com/dblock).
9
+ * [#444](https://github.com/intridea/grape/issues/444): Added `:en` as fallback locale for I18n - [@aew](https://github.com/aew).
10
+ * [#448](https://github.com/intridea/grape/pull/448): Adding POST style parameters for DELETE requests - [@dquimper](https://github.com/dquimper).
11
+ * [#450](https://github.com/intridea/grape/pull/450): Added option to pass an exception handler lambda as an argument to `rescue_from` - [@robertopedroso](https://github.com/robertopedroso).
12
+ * [#443](https://github.com/intridea/grape/pull/443): Let `requires` and `optional` take blocks that initialize new scopes - [@asross](https://github.com/asross).
13
+ * [#452](https://github.com/intridea/grape/pull/452): Added `with` as a hash option to specify handlers for `rescue_from` and `error_formatter` [@robertopedroso](https://github.com/robertopedroso).
14
+ * [#433](https://github.com/intridea/grape/issues/433), [#462](https://github.com/intridea/grape/issues/462): API change: validation errors are now collected and `Grape::Exceptions::ValidationErrors` is raised - [@stevschmid](https://github.com/stevschmid).
15
+ * Your contribution here.
16
+
17
+ #### Fixes
18
+
19
+ * [#428](https://github.com/intridea/grape/issues/428): Removes memoization from `Grape::Request` params to prevent middleware from freezing parameter values before `Formatter` can get them - [@mbleigh](https://github.com/mbleigh).
20
+
1
21
  0.5.0 (6/14/2013)
2
22
  =================
3
23
 
data/README.md CHANGED
@@ -10,6 +10,10 @@ content negotiation, versioning and much more.
10
10
 
11
11
  [![Build Status](https://travis-ci.org/intridea/grape.png?branch=master)](http://travis-ci.org/intridea/grape) [![Code Climate](https://codeclimate.com/github/intridea/grape.png)](https://codeclimate.com/github/intridea/grape)
12
12
 
13
+ ## Stable Release
14
+
15
+ You're reading the documentation for the stable release of Grape, 0.6.0.
16
+
13
17
  ## Project Resources
14
18
 
15
19
  * Need help? [Grape Google Group](http://groups.google.com/group/ruby-grape)
@@ -221,7 +225,7 @@ version 'v1', using: :header, vendor: 'twitter'
221
225
 
222
226
  Using this versioning strategy, clients should pass the desired version in the HTTP `Accept` head.
223
227
 
224
- curl -H Accept=application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline
228
+ curl -H Accept:application/vnd.twitter-v1+json http://localhost:9292/statuses/public_timeline
225
229
 
226
230
  By default, the first matching version is used when no `Accept` header is
227
231
  supplied. This behavior is similar to routing in Rails. To circumvent this default behavior,
@@ -236,7 +240,7 @@ version 'v1', using: :accept_version_header
236
240
 
237
241
  Using this versioning strategy, clients should pass the desired version in the HTTP `Accept-Version` header.
238
242
 
239
- curl -H "Accept-Version=v1" http://localhost:9292/statuses/public_timeline
243
+ curl -H "Accept-Version:v1" http://localhost:9292/statuses/public_timeline
240
244
 
241
245
  By default, the first matching version is used when no `Accept-Version` header is
242
246
  supplied. This behavior is similar to routing in Rails. To circumvent this default behavior,
@@ -329,6 +333,9 @@ params do
329
333
  group :media do
330
334
  requires :url
331
335
  end
336
+ optional :audio do
337
+ requires :mp3
338
+ end
332
339
  end
333
340
  put ':id' do
334
341
  # params[:id] is an Integer
@@ -346,8 +353,9 @@ params do
346
353
  end
347
354
  ```
348
355
 
349
- Parameters can be nested using `group`. In the above example, this means
350
- `params[:media][:url]` is required along with `params[:id]`.
356
+ Parameters can be nested using `group` or by calling `requires` or `optional` with a block.
357
+ In the above example, this means `params[:media][:url]` is required along with `params[:id]`,
358
+ and `params[:audio][:mp3]` is required only if `params[:audio]` is present.
351
359
 
352
360
  ### Namespace Validation and Coercion
353
361
 
@@ -379,7 +387,7 @@ The `namespace` method has a number of aliases, including: `group`, `resource`,
379
387
  class AlphaNumeric < Grape::Validations::Validator
380
388
  def validate_param!(attr_name, params)
381
389
  unless params[attr_name] =~ /^[[:alnum:]]+$/
382
- throw :error, status: 400, message: "#{attr_name}: must consist of alpha-numeric characters"
390
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message: "must consist of alpha-numeric characters"
383
391
  end
384
392
  end
385
393
  end
@@ -397,7 +405,7 @@ You can also create custom classes that take parameters.
397
405
  class Length < Grape::Validations::SingleOptionValidator
398
406
  def validate_param!(attr_name, params)
399
407
  unless params[attr_name].length <= @option
400
- throw :error, status: 400, message: "#{attr_name}: must be at the most #{@option} characters long"
408
+ raise Grape::Exceptions::Validation, param: @scope.full_name(attr_name), message: "must be at the most #{@option} characters long"
401
409
  end
402
410
  end
403
411
  end
@@ -411,20 +419,28 @@ end
411
419
 
412
420
  ### Validation Errors
413
421
 
414
- When validation and coercion errors occur an exception of type `Grape::Exceptions::Validation` is raised.
422
+ Validation and coercion errors are collected and an exception of type `Grape::Exceptions::ValidationErrors` is raised.
415
423
  If the exception goes uncaught it will respond with a status of 400 and an error message.
416
- You can rescue a `Grape::Exceptions::Validation` and respond with a custom response.
424
+ You can rescue a `Grape::Exceptions::ValidationErrors` and respond with a custom response.
417
425
 
418
426
  ```ruby
419
- rescue_from Grape::Exceptions::Validation do |e|
427
+ rescue_from Grape::Exceptions::ValidationErrors do |e|
420
428
  Rack::Response.new({
421
429
  'status' => e.status,
422
430
  'message' => e.message,
423
- 'param' => e.param
431
+ 'errors' => e.errors
424
432
  }.to_json, e.status)
425
433
  end
426
434
  ```
427
435
 
436
+ The validation errors are grouped by parameter name and can be accessed via ``Grape::Exceptions::ValidationErrors#errors``.
437
+
438
+ ### I18n
439
+
440
+ Grape supports I18n for parameter-related error messages, but will fallback to English if
441
+ translations for the default locale have not been provided. See [en.yml](lib/grape/locale/en.yml) for message keys.
442
+
443
+
428
444
  ## Headers
429
445
 
430
446
  Request headers are available through the `headers` helper or from `env` in their original form.
@@ -629,9 +645,23 @@ You can also return JSON formatted objects by raising error! and passing a hash
629
645
  instead of a message.
630
646
 
631
647
  ```ruby
632
- error! { "error" => "unexpected error", "detail" => "missing widget" }, 500
648
+ error!({ "error" => "unexpected error", "detail" => "missing widget" }, 500)
633
649
  ```
634
650
 
651
+ ### Handling 404
652
+
653
+ For Grape to handle all the 404s for your API, it can be useful to use a catch-all.
654
+ In its simplest form, it can be like:
655
+
656
+ ```ruby
657
+ route :any, '*path' do
658
+ error! # or something else
659
+ end
660
+ ```
661
+
662
+ It is very crucial to __define this endpoint at the very end of your API__, as it
663
+ literally accepts every request.
664
+
635
665
  ## Exception Handling
636
666
 
637
667
  Grape can be told to rescue all exceptions and return them in the API format.
@@ -1045,6 +1075,8 @@ You can use any Hypermedia representer, including [Roar](https://github.com/apot
1045
1075
  Roar renders JSON and works with the built-in Grape JSON formatter. Add `Roar::Representer::JSON`
1046
1076
  into your models or call `to_json` explicitly in your API implementation.
1047
1077
 
1078
+ Other alternatives include `ActiveModel::Serializers` via [grape-active_model_serializers](https://github.com/jrhe/grape-active_model_serializers).
1079
+
1048
1080
  ### Rabl
1049
1081
 
1050
1082
  You can use [Rabl](https://github.com/nesquena/rabl) templates with the help of the
data/lib/grape.rb CHANGED
@@ -22,8 +22,12 @@ I18n.load_path << File.expand_path('../grape/locale/en.yml', __FILE__)
22
22
  module Grape
23
23
  autoload :API, 'grape/api'
24
24
  autoload :Endpoint, 'grape/endpoint'
25
+
25
26
  autoload :Route, 'grape/route'
26
27
  autoload :Namespace, 'grape/namespace'
28
+
29
+ autoload :Path, 'grape/path'
30
+
27
31
  autoload :Cookies, 'grape/cookies'
28
32
  autoload :Validations, 'grape/validations'
29
33
  autoload :Request, 'grape/http/request'
@@ -31,6 +35,7 @@ module Grape
31
35
  module Exceptions
32
36
  autoload :Base, 'grape/exceptions/base'
33
37
  autoload :Validation, 'grape/exceptions/validation'
38
+ autoload :ValidationErrors, 'grape/exceptions/validation_errors'
34
39
  autoload :MissingVendorOption, 'grape/exceptions/missing_vendor_option'
35
40
  autoload :MissingMimeType, 'grape/exceptions/missing_mime_type'
36
41
  autoload :MissingOption, 'grape/exceptions/missing_option'
@@ -70,6 +75,7 @@ module Grape
70
75
 
71
76
  module Auth
72
77
  autoload :OAuth2, 'grape/middleware/auth/oauth2'
78
+ autoload :Base, 'grape/middleware/auth/base'
73
79
  autoload :Basic, 'grape/middleware/auth/basic'
74
80
  autoload :Digest, 'grape/middleware/auth/digest'
75
81
  end
data/lib/grape/api.rb CHANGED
@@ -6,14 +6,8 @@ module Grape
6
6
  extend Validations::ClassMethods
7
7
 
8
8
  class << self
9
- attr_reader :route_set
10
- attr_reader :versions
11
- attr_reader :routes
12
- attr_reader :settings
9
+ attr_reader :endpoints, :instance, :routes, :route_set, :settings, :versions
13
10
  attr_writer :logger
14
- attr_reader :endpoints
15
- attr_reader :mountings
16
- attr_reader :instance
17
11
 
18
12
  def logger(logger = nil)
19
13
  if logger
@@ -27,7 +21,6 @@ module Grape
27
21
  @settings = Grape::Util::HashStack.new
28
22
  @route_set = Rack::Mount::RouteSet.new
29
23
  @endpoints = []
30
- @mountings = []
31
24
  @routes = nil
32
25
  reset_validations!
33
26
  end
@@ -158,8 +151,14 @@ module Grape
158
151
  new_formatter ? set(:default_error_formatter, new_formatter) : settings[:default_error_formatter]
159
152
  end
160
153
 
161
- def error_formatter(format, new_formatter)
162
- settings.imbue(:error_formatters, format.to_sym => new_formatter)
154
+ def error_formatter(format, options)
155
+ if options.is_a?(Hash) && options.has_key?(:with)
156
+ formatter = options[:with]
157
+ else
158
+ formatter = options
159
+ end
160
+
161
+ settings.imbue(:error_formatters, format.to_sym => formatter)
163
162
  end
164
163
 
165
164
  # Specify additional content-types, e.g.:
@@ -195,13 +194,27 @@ module Grape
195
194
  # @param [Block] block Execution block to handle the given exception.
196
195
  # @param [Hash] options Options for the rescue usage.
197
196
  # @option options [Boolean] :backtrace Include a backtrace in the rescue response.
197
+ # @param [Proc] handler Execution proc to handle the given exception as an
198
+ # alternative to passing a block
198
199
  def rescue_from(*args, &block)
199
- if block_given?
200
+ if args.last.is_a?(Proc)
201
+ handler = args.pop
202
+ elsif block_given?
203
+ handler = block
204
+ end
205
+
206
+ options = args.last.is_a?(Hash) ? args.pop : {}
207
+ if options.has_key?(:with)
208
+ handler ||= proc { options[:with] }
209
+ end
210
+
211
+ if handler
200
212
  args.each do |arg|
201
- imbue(:rescue_handlers, { arg => block })
213
+ imbue(:rescue_handlers, { arg => handler })
202
214
  end
203
215
  end
204
- imbue(:rescue_options, args.pop) if args.last.is_a?(Hash)
216
+
217
+ imbue(:rescue_options, options)
205
218
  set(:rescue_all, true) and return if args.include?(:all)
206
219
  imbue(:rescued_errors, args)
207
220
  end
@@ -422,7 +435,7 @@ module Grape
422
435
  end
423
436
 
424
437
  def cascade(value = nil)
425
- value.nil? ?
438
+ value.nil? ?
426
439
  (settings.has_key?(:cascade) ? !! settings[:cascade] : true) :
427
440
  set(:cascade, value)
428
441
  end
@@ -33,27 +33,34 @@ module Grape
33
33
  end
34
34
 
35
35
  def initialize(settings, options = {}, &block)
36
+ require_option(options, :path)
37
+ require_option(options, :method)
38
+
36
39
  @settings = settings
40
+ @options = options
41
+
42
+ @options[:path] = Array(options[:path])
43
+ @options[:path] << '/' if options[:path].empty?
44
+
45
+ @options[:method] = Array(options[:method])
46
+ @options[:route_options] ||= {}
47
+
37
48
  if block_given?
38
- method_name = [
39
- options[:method],
40
- Namespace.joined_space(settings),
41
- settings.gather(:mount_path).join("/"),
42
- Array(options[:path]).join("/")
43
- ].join(" ")
44
49
  @source = block
45
50
  @block = self.class.generate_api_method(method_name, &block)
46
51
  end
47
- @options = options
48
-
49
- raise Grape::Exceptions::MissingOption.new(:path) unless options.key?(:path)
50
- options[:path] = Array(options[:path])
51
- options[:path] = ['/'] if options[:path].empty?
52
+ end
52
53
 
53
- raise Grape::Exceptions::MissingOption.new(:method) unless options.key?(:method)
54
- options[:method] = Array(options[:method])
54
+ def require_option(options, key)
55
+ options.has_key?(key) or raise Grape::Exceptions::MissingOption.new(key)
56
+ end
55
57
 
56
- options[:route_options] ||= {}
58
+ def method_name
59
+ [ options[:method],
60
+ Namespace.joined_space(settings),
61
+ settings.gather(:mount_path).join('/'),
62
+ options[:path].join('/')
63
+ ].join(" ")
57
64
  end
58
65
 
59
66
  def routes
@@ -120,24 +127,7 @@ module Grape
120
127
  end
121
128
 
122
129
  def prepare_path(path)
123
- parts = []
124
- parts << settings[:mount_path].to_s.split("/") if settings[:mount_path]
125
- parts << settings[:root_prefix].to_s.split("/") if settings[:root_prefix]
126
-
127
- uses_path_versioning = settings[:version] && settings[:version_options][:using] == :path
128
- namespace_is_empty = namespace && (namespace.to_s =~ /^\s*$/ || namespace.to_s == '/')
129
- path_is_empty = path && (path.to_s =~ /^\s*$/ || path.to_s == '/')
130
-
131
- parts << ':version' if uses_path_versioning
132
- if ! uses_path_versioning || (! namespace_is_empty || ! path_is_empty)
133
- parts << namespace.to_s if namespace
134
- parts << path.to_s if path
135
- format_suffix = '(.:format)'
136
- else
137
- format_suffix = '(/.:format)'
138
- end
139
- parts = parts.flatten.select { |part| part != '/' }
140
- Rack::Mount::Utils.normalize_path(parts.join('/') + format_suffix)
130
+ Path.prepare(path, namespace, settings)
141
131
  end
142
132
 
143
133
  def namespace
@@ -391,8 +381,17 @@ module Grape
391
381
  run_filters befores
392
382
 
393
383
  # Retieve validations from this namespace and all parent namespaces.
384
+ validation_errors = []
394
385
  settings.gather(:validations).each do |validator|
395
- validator.validate!(params)
386
+ begin
387
+ validator.validate!(params)
388
+ rescue Grape::Exceptions::Validation => e
389
+ validation_errors << e
390
+ end
391
+ end
392
+
393
+ if validation_errors.any?
394
+ raise Grape::Exceptions::ValidationErrors, errors: validation_errors
396
395
  end
397
396
 
398
397
  run_filters after_validations
@@ -433,7 +432,7 @@ module Grape
433
432
 
434
433
  if settings[:version]
435
434
  b.use Grape::Middleware::Versioner.using(settings[:version_options][:using]), {
436
- :versions => settings[:version],
435
+ :versions => settings[:version] ? settings[:version].flatten : nil,
437
436
  :version_options => settings[:version_options],
438
437
  :prefix => settings[:root_prefix]
439
438
  }
@@ -4,6 +4,7 @@ module Grape
4
4
 
5
5
  BASE_MESSAGES_KEY = 'grape.errors.messages'
6
6
  BASE_ATTRIBUTES_KEY = 'grape.errors.attributes'
7
+ FALLBACK_LOCALE = :en
7
8
 
8
9
  attr_reader :status, :message, :headers
9
10
 
@@ -54,11 +55,12 @@ module Grape
54
55
  end
55
56
 
56
57
  def translate_message(key, options = {})
57
- translate("#{BASE_MESSAGES_KEY}.#{key}", {:default => '' }.merge(options))
58
+ translate("#{BASE_MESSAGES_KEY}.#{key}", { :default => '' }.merge(options))
58
59
  end
59
60
 
60
61
  def translate(key, options = {})
61
- ::I18n.translate(key, options)
62
+ message = ::I18n.translate(key, options)
63
+ message.present? ? message : ::I18n.translate(key, options.merge({:locale => FALLBACK_LOCALE}))
62
64
  end
63
65
 
64
66
  end
@@ -6,11 +6,21 @@ module Grape
6
6
  attr_accessor :param
7
7
 
8
8
  def initialize(args = {})
9
- @param = args[:param].to_s if args.has_key? :param
10
- attribute = translate_attribute(@param)
11
- args[:message] = translate_message(args[:message_key], :attribute => attribute)
9
+ raise "Param is missing:" unless args.has_key? :param
10
+ @param = args[:param]
11
+ args[:message] = translate_message(args[:message_key]) if args.has_key? :message_key
12
12
  super
13
13
  end
14
+
15
+ # remove all the unnecessary stuff from Grape::Exceptions::Base like status
16
+ # and headers when converting a validation error to json or string
17
+ def as_json(*a)
18
+ self.to_s
19
+ end
20
+
21
+ def to_s
22
+ message
23
+ end
14
24
  end
15
25
  end
16
26
  end
@@ -0,0 +1,42 @@
1
+ require 'grape/exceptions/base'
2
+
3
+ module Grape
4
+ module Exceptions
5
+ class ValidationErrors < Grape::Exceptions::Base
6
+ include Enumerable
7
+
8
+ attr_reader :errors
9
+
10
+ def initialize(args = {})
11
+ @errors = {}
12
+ args[:errors].each do |validation_error|
13
+ @errors[validation_error.param] ||= []
14
+ @errors[validation_error.param] << validation_error
15
+ end
16
+ super message: full_messages.join(', '), status: 400
17
+ end
18
+
19
+ def each
20
+ errors.each_pair do |attribute, errors|
21
+ errors.each do |error|
22
+ yield attribute, error
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def full_messages
30
+ map { |attribute, error| full_message(attribute, error) }
31
+ end
32
+
33
+ def full_message(attribute, error)
34
+ I18n.t(:"grape.errors.format", {
35
+ default: "%{attribute} %{message}",
36
+ attribute: translate_attribute(attribute),
37
+ message: error.message
38
+ })
39
+ end
40
+ end
41
+ end
42
+ end