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.

Files changed (62) hide show
  1. checksums.yaml +5 -13
  2. data/.rubocop.yml +6 -6
  3. data/.travis.yml +11 -2
  4. data/CHANGELOG.md +23 -1
  5. data/Gemfile +11 -10
  6. data/Guardfile +5 -6
  7. data/README.md +194 -13
  8. data/UPGRADING.md +3 -3
  9. data/grape.gemspec +1 -1
  10. data/grape.png +0 -0
  11. data/lib/grape/api.rb +21 -13
  12. data/lib/grape/endpoint.rb +31 -13
  13. data/lib/grape/exceptions/validation.rb +2 -2
  14. data/lib/grape/locale/en.yml +2 -0
  15. data/lib/grape/middleware/auth/oauth2.rb +69 -65
  16. data/lib/grape/middleware/error.rb +4 -2
  17. data/lib/grape/middleware/formatter.rb +2 -2
  18. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  19. data/lib/grape/middleware/versioner/header.rb +3 -3
  20. data/lib/grape/util/hash_stack.rb +1 -1
  21. data/lib/grape/validations.rb +22 -8
  22. data/lib/grape/validations/default.rb +1 -1
  23. data/lib/grape/validations/exactly_one_of.rb +26 -0
  24. data/lib/grape/validations/mutual_exclusion.rb +25 -0
  25. data/lib/grape/validations/presence.rb +1 -1
  26. data/lib/grape/validations/regexp.rb +2 -1
  27. data/lib/grape/validations/values.rb +7 -1
  28. data/lib/grape/version.rb +1 -1
  29. data/spec/grape/api_spec.rb +390 -333
  30. data/spec/grape/endpoint_spec.rb +129 -99
  31. data/spec/grape/entity_spec.rb +47 -27
  32. data/spec/grape/exceptions/invalid_formatter_spec.rb +1 -1
  33. data/spec/grape/exceptions/invalid_versioner_option_spec.rb +1 -1
  34. data/spec/grape/exceptions/missing_mime_type_spec.rb +2 -2
  35. data/spec/grape/exceptions/missing_option_spec.rb +1 -1
  36. data/spec/grape/exceptions/unknown_options_spec.rb +1 -1
  37. data/spec/grape/exceptions/unknown_validator_spec.rb +1 -1
  38. data/spec/grape/middleware/auth/basic_spec.rb +3 -3
  39. data/spec/grape/middleware/auth/digest_spec.rb +4 -4
  40. data/spec/grape/middleware/auth/oauth2_spec.rb +11 -11
  41. data/spec/grape/middleware/base_spec.rb +9 -9
  42. data/spec/grape/middleware/error_spec.rb +4 -4
  43. data/spec/grape/middleware/exception_spec.rb +17 -17
  44. data/spec/grape/middleware/formatter_spec.rb +38 -38
  45. data/spec/grape/middleware/versioner/accept_version_header_spec.rb +11 -11
  46. data/spec/grape/middleware/versioner/header_spec.rb +39 -39
  47. data/spec/grape/middleware/versioner/param_spec.rb +10 -10
  48. data/spec/grape/middleware/versioner/path_spec.rb +7 -7
  49. data/spec/grape/middleware/versioner_spec.rb +4 -4
  50. data/spec/grape/path_spec.rb +6 -6
  51. data/spec/grape/util/hash_stack_spec.rb +22 -22
  52. data/spec/grape/validations/coerce_spec.rb +34 -34
  53. data/spec/grape/validations/default_spec.rb +16 -16
  54. data/spec/grape/validations/exactly_one_of_spec.rb +71 -0
  55. data/spec/grape/validations/mutual_exclusion_spec.rb +61 -0
  56. data/spec/grape/validations/presence_spec.rb +34 -34
  57. data/spec/grape/validations/regexp_spec.rb +11 -4
  58. data/spec/grape/validations/values_spec.rb +34 -20
  59. data/spec/grape/validations_spec.rb +300 -147
  60. data/spec/shared/versioning_examples.rb +18 -18
  61. data/spec/spec_helper.rb +2 -1
  62. 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.6.2, 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).
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.6.2.
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.2.0'
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.has_key?(:vendor)
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.has_key?(:with)
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.has_key?(:with)
215
+ handler ||= proc { options[:with] } if options.key?(:with)
216
216
 
217
- handler_type = !!options[:rescue_subclasses] ? :rescue_handlers : :base_only_rescue_handlers
218
- imbue handler_type, Hash[args.map { |arg| [arg, handler] }]
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
- imbue(:rescue_options, options)
229
+ imbue handler_type, Hash[args.map { |arg| [arg, handler] }]
230
+ end
221
231
 
222
- set(:rescue_all, true) if args.include?(:all)
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.has_key?(:cascade) ? !!settings[:cascade] : true
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.has_key?(:cascade)
552
- return !!self.class.settings[:version_options][:cascade] if self.class.settings[:version_options] && self.class.settings[:version_options].has_key?(:cascade)
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(', ')
@@ -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.has_key?(key)
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
- path_info: route.route_compiled,
82
- request_method: method,
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{ version format }
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
- object_instance = object.respond_to?(:first) ? object.first : object
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
- object_instance.class.ancestors.each do |potential|
346
+ object_class.ancestors.each do |potential|
332
347
  entity_class ||= (settings[:representations] || {})[potential]
333
348
  end
334
349
 
335
- entity_class ||= object_instance.class.const_get(:Entity) if object_instance.class.const_defined?(:Entity) && object_instance.class.const_get(:Entity).respond_to?(:represent)
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
- # Retieve validations from this namespace and all parent namespaces.
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.has_key? :param
9
+ raise "Param is missing:" unless args.key? :param
10
10
  @param = args[:param]
11
- args[:message] = translate_message(args[:message_key]) if args.has_key? :message_key
11
+ args[:message] = translate_message(args[:message_key]) if args.key? :message_key
12
12
  super
13
13
  end
14
14
 
@@ -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::Middleware::Auth
2
- # OAuth 2.0 authorization for Grape APIs.
3
- class OAuth2 < Grape::Middleware::Base
4
- def default_options
5
- {
6
- token_class: 'AccessToken',
7
- realm: 'OAuth API',
8
- parameter: %w(bearer_token oauth_token access_token),
9
- accepted_headers: %w(HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION REDIRECT_X_HTTP_AUTHORIZATION),
10
- header: [/Bearer (.*)/i, /OAuth (.*)/i],
11
- required: true
12
- }
13
- end
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
- def before
16
- verify_token(token_parameter || token_header)
17
- end
17
+ def before
18
+ verify_token(token_parameter || token_header)
19
+ end
18
20
 
19
- def request
20
- @request ||= Grape::Request.new(env)
21
- end
21
+ def request
22
+ @request ||= Grape::Request.new(env)
23
+ end
22
24
 
23
- def params
24
- @params ||= request.params
25
- end
25
+ def params
26
+ @params ||= request.params
27
+ end
26
28
 
27
- def token_parameter
28
- Array(options[:parameter]).each do |p|
29
- return params[p] if params[p]
30
- end
31
- nil
32
- end
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
- def token_header
35
- return false unless authorization_header
36
- Array(options[:header]).each do |regexp|
37
- return $1 if authorization_header =~ regexp
38
- end
39
- nil
40
- end
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
- def authorization_header
43
- options[:accepted_headers].each do |head|
44
- return env[head] if env[head]
45
- end
46
- nil
47
- end
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
- def token_class
50
- @klass ||= eval(options[:token_class]) # rubocop:disable Eval
51
- end
51
+ def token_class
52
+ @klass ||= eval(options[:token_class]) # rubocop:disable Eval
53
+ end
52
54
 
53
- def verify_token(token)
54
- token = token_class.verify(token)
55
- if token
56
- if token.respond_to?(:expired?) && token.expired?
57
- error_out(401, 'invalid_grant')
58
- else
59
- if !token.respond_to?(:permission_for?) || token.permission_for?(env)
60
- env['api.token'] = token
61
- else
62
- error_out(403, 'insufficient_scope')
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
- def error_out(status, error)
71
- throw :error,
72
- message: error,
73
- status: status,
74
- headers: {
75
- 'WWW-Authenticate' => "OAuth realm='#{options[:realm]}', error='#{error}'"
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 = options[:base_only_rescue_handlers][klass] || options[:base_only_rescue_handlers][:all] unless 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
- )x
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].has_key?(:cascade)
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) || has_version?(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].has_key?(:cascade)
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 has_version?(media_type)
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