grape 0.10.1 → 0.11.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -1
  4. data/CHANGELOG.md +18 -0
  5. data/Gemfile +1 -1
  6. data/README.md +124 -9
  7. data/UPGRADING.md +66 -0
  8. data/gemfiles/rails_3.gemfile +1 -1
  9. data/gemfiles/rails_4.gemfile +1 -1
  10. data/lib/grape.rb +4 -0
  11. data/lib/grape/api.rb +1 -5
  12. data/lib/grape/dsl/inside_route.rb +2 -1
  13. data/lib/grape/dsl/parameters.rb +20 -9
  14. data/lib/grape/dsl/routing.rb +11 -1
  15. data/lib/grape/error_formatter/base.rb +1 -1
  16. data/lib/grape/exceptions/invalid_accept_header.rb +10 -0
  17. data/lib/grape/exceptions/invalid_message_body.rb +10 -0
  18. data/lib/grape/exceptions/missing_group_type.rb +10 -0
  19. data/lib/grape/exceptions/unsupported_group_type.rb +10 -0
  20. data/lib/grape/http/request.rb +1 -0
  21. data/lib/grape/locale/en.yml +11 -0
  22. data/lib/grape/middleware/base.rb +1 -1
  23. data/lib/grape/middleware/formatter.rb +2 -0
  24. data/lib/grape/middleware/versioner/header.rb +14 -11
  25. data/lib/grape/parser/json.rb +3 -0
  26. data/lib/grape/parser/xml.rb +3 -0
  27. data/lib/grape/validations/params_scope.rb +20 -4
  28. data/lib/grape/validations/validators/coerce.rb +4 -1
  29. data/lib/grape/validations/validators/values.rb +1 -1
  30. data/lib/grape/version.rb +1 -1
  31. data/spec/grape/api_spec.rb +3 -3
  32. data/spec/grape/dsl/parameters_spec.rb +11 -11
  33. data/spec/grape/dsl/routing_spec.rb +13 -4
  34. data/spec/grape/endpoint_spec.rb +2 -2
  35. data/spec/grape/exceptions/body_parse_errors_spec.rb +105 -0
  36. data/spec/grape/exceptions/invalid_accept_header_spec.rb +330 -0
  37. data/spec/grape/integration/rack_spec.rb +32 -0
  38. data/spec/grape/middleware/base_spec.rb +20 -0
  39. data/spec/grape/middleware/versioner/header_spec.rb +74 -96
  40. data/spec/grape/validations/params_scope_spec.rb +124 -0
  41. data/spec/grape/validations/validators/allow_blank_spec.rb +102 -0
  42. data/spec/grape/validations/validators/values_spec.rb +45 -1
  43. data/spec/grape/validations_spec.rb +54 -16
  44. data/spec/shared/versioning_examples.rb +32 -0
  45. metadata +61 -51
@@ -35,10 +35,20 @@ module Grape
35
35
  fail Grape::Exceptions::MissingVendorOption.new if options[:using] == :header && !options.key?(:vendor)
36
36
 
37
37
  @versions = versions | args
38
- nest(block) do
38
+
39
+ if block_given?
40
+ within_namespace do
41
+ namespace_inheritable(:version, args)
42
+ namespace_inheritable(:version_options, options)
43
+
44
+ instance_eval(&block)
45
+ end
46
+ else
39
47
  namespace_inheritable(:version, args)
40
48
  namespace_inheritable(:version_options, options)
41
49
  end
50
+
51
+ # reset_validations!
42
52
  end
43
53
 
44
54
  @versions.last unless @versions.nil?
@@ -41,7 +41,7 @@ module Grape
41
41
  http_codes = env['rack.routing_args'][:route_info].route_http_codes || []
42
42
  found_code = http_codes.find do |http_code|
43
43
  (http_code[0].to_i == env['api.endpoint'].status) && http_code[2].respond_to?(:represent)
44
- end
44
+ end if env['api.endpoint'].request
45
45
 
46
46
  presenter = found_code[2] if found_code
47
47
  end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class InvalidAcceptHeader < Base
5
+ def initialize(message, headers)
6
+ super(message: compose_message('invalid_accept_header', message: message), status: 406, headers: headers)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class InvalidMessageBody < Base
5
+ def initialize(body_format)
6
+ super(message: compose_message('invalid_message_body', body_format: body_format), status: 400)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class MissingGroupTypeError < Base
5
+ def initialize
6
+ super(message: compose_message('missing_group_type'))
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+ module Grape
3
+ module Exceptions
4
+ class UnsupportedGroupTypeError < Base
5
+ def initialize
6
+ super(message: compose_message('unsupported_group_type'))
7
+ end
8
+ end
9
+ end
10
+ end
@@ -7,6 +7,7 @@ module Grape
7
7
  args = env['rack.routing_args'].dup
8
8
  # preserve version from query string parameters
9
9
  args.delete(:version)
10
+ args.delete(:route_info)
10
11
  params.deep_merge!(args)
11
12
  end
12
13
  params
@@ -33,4 +33,15 @@ en:
33
33
  at_least_one: 'are missing, at least one parameter must be provided'
34
34
  exactly_one: 'are missing, exactly one parameter must be provided'
35
35
  all_or_none: 'provide all or none of parameters'
36
+ missing_group_type: 'group type is required'
37
+ unsupported_group_type: 'group type must be Array or Hash'
38
+ invalid_message_body:
39
+ problem: "message body does not match declared format"
40
+ resolution:
41
+ "when specifying %{body_format} as content-type, you must pass valid
42
+ %{body_format} in the request's 'body'
43
+ "
44
+ invalid_accept_header:
45
+ problem: 'Invalid accept header'
46
+ resolution: '%{message}'
36
47
 
@@ -37,7 +37,7 @@ module Grape
37
37
  end
38
38
 
39
39
  def response
40
- Rack::Response.new(@app_response)
40
+ Rack::Response.new(@app_response[2], @app_response[0], @app_response[1])
41
41
  end
42
42
 
43
43
  def content_type_for(format)
@@ -81,6 +81,8 @@ module Grape
81
81
  end
82
82
  env['rack.request.form_input'] = env['rack.input']
83
83
  end
84
+ rescue Grape::Exceptions::Base => e
85
+ raise e
84
86
  rescue StandardError => e
85
87
  throw :error, status: 400, message: e.message
86
88
  end
@@ -16,23 +16,19 @@ module Grape
16
16
  # env['api.subtype'] => 'vnd.mycompany-v1+json'
17
17
  # env['api.vendor] => 'mycompany'
18
18
  # env['api.version] => 'v1'
19
- # env['api.format] => 'format'
19
+ # env['api.format] => 'json'
20
20
  #
21
21
  # If version does not match this route, then a 406 is raised with
22
22
  # X-Cascade header to alert Rack::Mount to attempt the next matched
23
23
  # route.
24
24
  class Header < Base
25
25
  def before
26
- begin
27
- header = Rack::Accept::MediaType.new env['HTTP_ACCEPT']
28
- rescue RuntimeError => e
29
- throw :error, status: 406, headers: error_headers, message: e.message
30
- end
26
+ header = rack_accept_header
31
27
 
32
28
  if strict?
33
29
  # If no Accept header:
34
30
  if header.qvalues.empty?
35
- throw :error, status: 406, headers: error_headers, message: 'Accept header must be set.'
31
+ fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must be set.', error_headers)
36
32
  end
37
33
  # Remove any acceptable content types with ranges.
38
34
  header.qvalues.reject! do |media_type, _|
@@ -40,7 +36,8 @@ module Grape
40
36
  end
41
37
  # If all Accept headers included a range:
42
38
  if header.qvalues.empty?
43
- throw :error, status: 406, headers: error_headers, message: 'Accept header must not contain ranges ("*").'
39
+ fail Grape::Exceptions::InvalidAcceptHeader.new('Accept header must not contain ranges ("*").',
40
+ error_headers)
44
41
  end
45
42
  end
46
43
 
@@ -58,10 +55,10 @@ module Grape
58
55
  end
59
56
  # If none of the available content types are acceptable:
60
57
  elsif strict?
61
- throw :error, status: 406, headers: error_headers, message: '406 Not Acceptable'
58
+ fail Grape::Exceptions::InvalidAcceptHeader.new('406 Not Acceptable', error_headers)
62
59
  # If all acceptable content types specify a vendor or version that doesn't exist:
63
60
  elsif header.values.all? { |header_value| has_vendor?(header_value) || version?(header_value) }
64
- throw :error, status: 406, headers: error_headers, message: 'API vendor or version not found.'
61
+ fail Grape::Exceptions::InvalidAcceptHeader.new('API vendor or version not found.', error_headers)
65
62
  end
66
63
  end
67
64
 
@@ -83,7 +80,13 @@ module Grape
83
80
  available_media_types << media_type
84
81
  end
85
82
 
86
- available_media_types = available_media_types.flatten
83
+ available_media_types.flatten
84
+ end
85
+
86
+ def rack_accept_header
87
+ Rack::Accept::MediaType.new env['HTTP_ACCEPT']
88
+ rescue RuntimeError => e
89
+ raise Grape::Exceptions::InvalidAcceptHeader.new(e.message, error_headers)
87
90
  end
88
91
 
89
92
  def versions
@@ -4,6 +4,9 @@ module Grape
4
4
  class << self
5
5
  def call(object, env)
6
6
  MultiJson.load(object)
7
+ rescue MultiJson::ParseError
8
+ # handle JSON parsing errors via the rescue handlers or provide error message
9
+ raise Grape::Exceptions::InvalidMessageBody, 'application/json'
7
10
  end
8
11
  end
9
12
  end
@@ -4,6 +4,9 @@ module Grape
4
4
  class << self
5
5
  def call(object, env)
6
6
  MultiXml.parse(object)
7
+ rescue MultiXml::ParseError
8
+ # handle XML parsing errors via the rescue handlers or provide error message
9
+ raise Grape::Exceptions::InvalidMessageBody, 'application/xml'
7
10
  end
8
11
  end
9
12
  end
@@ -64,14 +64,29 @@ module Grape
64
64
  end
65
65
  end
66
66
 
67
+ def require_optional_fields(context, opts)
68
+ optional_fields = opts[:using].keys
69
+ optional_fields -= Array(opts[:except]) unless context == :all
70
+ optional_fields.each do |field|
71
+ field_opts = opts[:using][field]
72
+ optional(field, field_opts) if field_opts
73
+ end
74
+ end
75
+
67
76
  def validate_attributes(attrs, opts, &block)
68
- validations = { presence: true }
69
- validations.merge!(opts) if opts
77
+ validations = opts.clone
70
78
  validations[:type] ||= Array if block
71
79
  validates(attrs, validations)
72
80
  end
73
81
 
74
82
  def new_scope(attrs, optional = false, &block)
83
+ # if required params are grouped and no type or unsupported type is provided, raise an error
84
+ type = attrs[1] ? attrs[1][:type] : nil
85
+ if attrs.first && !optional
86
+ fail Grape::Exceptions::MissingGroupTypeError.new if type.nil?
87
+ fail Grape::Exceptions::UnsupportedGroupTypeError.new unless [Array, Hash].include?(type)
88
+ end
89
+
75
90
  opts = attrs[1] || { type: Array }
76
91
  ParamsScope.new(api: @api, element: attrs.first, parent: self, optional: optional, type: opts[:type], &block)
77
92
  end
@@ -141,7 +156,7 @@ module Grape
141
156
 
142
157
  def guess_coerce_type(coerce_type, values)
143
158
  return coerce_type if !values || values.is_a?(Proc)
144
- return values.first.class if coerce_type == Array && !values.empty?
159
+ return values.first.class if coerce_type == Array && (values.is_a?(Range) || !values.empty?)
145
160
  coerce_type
146
161
  end
147
162
 
@@ -167,7 +182,8 @@ module Grape
167
182
  return unless coerce_type && values
168
183
  return if values.is_a?(Proc)
169
184
  coerce_type = coerce_type.first if coerce_type.kind_of?(Array)
170
- if values.any? { |v| !v.kind_of?(coerce_type) }
185
+ value_types = values.is_a?(Range) ? [values.begin, values.end] : values
186
+ if value_types.any? { |v| !v.kind_of?(coerce_type) }
171
187
  fail Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values)
172
188
  end
173
189
  end
@@ -29,9 +29,12 @@ module Grape
29
29
  # allow nil, to ignore when a parameter is absent
30
30
  return true if val.nil?
31
31
  if klass == Virtus::Attribute::Boolean
32
- val.is_a?(TrueClass) || val.is_a?(FalseClass)
32
+ val.is_a?(TrueClass) || val.is_a?(FalseClass) || (val.is_a?(String) && val.empty?)
33
33
  elsif klass == Rack::Multipart::UploadedFile
34
34
  val.is_a?(Hashie::Mash) && val.key?(:tempfile)
35
+ elsif [DateTime, Date, Numeric].any?{ |vclass| vclass >= klass }
36
+ return true if val.is_a?(String) && val.empty?
37
+ val.is_a?(klass)
35
38
  else
36
39
  val.is_a?(klass)
37
40
  end
@@ -11,7 +11,7 @@ module Grape
11
11
 
12
12
  values = @values.is_a?(Proc) ? @values.call : @values
13
13
  param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
14
- unless (param_array - values).empty?
14
+ unless param_array.all? { |param| values.include?(param) }
15
15
  fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message_key: :values
16
16
  end
17
17
  end
@@ -1,3 +1,3 @@
1
1
  module Grape
2
- VERSION = '0.10.1'
2
+ VERSION = '0.11.0'
3
3
  end
@@ -2166,11 +2166,11 @@ describe Grape::API do
2166
2166
  it 'groups nested params and prevents overwriting of params with same name in different groups' do
2167
2167
  subject.desc 'method'
2168
2168
  subject.params do
2169
- group :group1 do
2169
+ group :group1, type: Array do
2170
2170
  optional :param1, desc: 'group1 param1 desc'
2171
2171
  requires :param2, desc: 'group1 param2 desc'
2172
2172
  end
2173
- group :group2 do
2173
+ group :group2, type: Array do
2174
2174
  optional :param1, desc: 'group2 param1 desc'
2175
2175
  requires :param2, desc: 'group2 param2 desc'
2176
2176
  end
@@ -2190,7 +2190,7 @@ describe Grape::API do
2190
2190
  subject.desc 'nesting'
2191
2191
  subject.params do
2192
2192
  requires :root_param, desc: 'root param'
2193
- group :nested do
2193
+ group :nested, type: Array do
2194
2194
  requires :nested_param, desc: 'nested param'
2195
2195
  end
2196
2196
  end
@@ -11,7 +11,7 @@ module Grape
11
11
  end
12
12
 
13
13
  # rubocop:disable TrivialAccessors
14
- def validate_attrs
14
+ def validate_attributes_reader
15
15
  @validate_attributes
16
16
  end
17
17
  # rubocop:enable TrivialAccessors
@@ -21,7 +21,7 @@ module Grape
21
21
  end
22
22
 
23
23
  # rubocop:disable TrivialAccessors
24
- def push_declared_paras
24
+ def push_declared_params_reader
25
25
  @push_declared_params
26
26
  end
27
27
  # rubocop:enable TrivialAccessors
@@ -31,7 +31,7 @@ module Grape
31
31
  end
32
32
 
33
33
  # rubocop:disable TrivialAccessors
34
- def valids
34
+ def validates_reader
35
35
  @validates
36
36
  end
37
37
  # rubocop:enable TrivialAccessors
@@ -57,8 +57,8 @@ module Grape
57
57
  it 'adds a required parameter' do
58
58
  subject.requires :id, type: Integer, desc: 'Identity.'
59
59
 
60
- expect(subject.validate_attrs).to eq([[:id], { type: Integer, desc: 'Identity.' }])
61
- expect(subject.push_declared_paras).to eq([[:id]])
60
+ expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.', presence: true }])
61
+ expect(subject.push_declared_params_reader).to eq([[:id]])
62
62
  end
63
63
  end
64
64
 
@@ -66,8 +66,8 @@ module Grape
66
66
  it 'adds an optional parameter' do
67
67
  subject.optional :id, type: Integer, desc: 'Identity.'
68
68
 
69
- expect(subject.valids).to eq([[:id], { type: Integer, desc: 'Identity.' }])
70
- expect(subject.push_declared_paras).to eq([[:id]])
69
+ expect(subject.validate_attributes_reader).to eq([[:id], { type: Integer, desc: 'Identity.' }])
70
+ expect(subject.push_declared_params_reader).to eq([[:id]])
71
71
  end
72
72
  end
73
73
 
@@ -75,7 +75,7 @@ module Grape
75
75
  it 'adds an mutally exclusive parameter validation' do
76
76
  subject.mutually_exclusive :media, :audio
77
77
 
78
- expect(subject.valids).to eq([[:media, :audio], { mutual_exclusion: true }])
78
+ expect(subject.validates_reader).to eq([[:media, :audio], { mutual_exclusion: true }])
79
79
  end
80
80
  end
81
81
 
@@ -83,7 +83,7 @@ module Grape
83
83
  it 'adds an exactly of one parameter validation' do
84
84
  subject.exactly_one_of :media, :audio
85
85
 
86
- expect(subject.valids).to eq([[:media, :audio], { exactly_one_of: true }])
86
+ expect(subject.validates_reader).to eq([[:media, :audio], { exactly_one_of: true }])
87
87
  end
88
88
  end
89
89
 
@@ -91,7 +91,7 @@ module Grape
91
91
  it 'adds an at least one of parameter validation' do
92
92
  subject.at_least_one_of :media, :audio
93
93
 
94
- expect(subject.valids).to eq([[:media, :audio], { at_least_one_of: true }])
94
+ expect(subject.validates_reader).to eq([[:media, :audio], { at_least_one_of: true }])
95
95
  end
96
96
  end
97
97
 
@@ -99,7 +99,7 @@ module Grape
99
99
  it 'adds an all or none of parameter validation' do
100
100
  subject.all_or_none_of :media, :audio
101
101
 
102
- expect(subject.valids).to eq([[:media, :audio], { all_or_none_of: true }])
102
+ expect(subject.validates_reader).to eq([[:media, :audio], { all_or_none_of: true }])
103
103
  end
104
104
  end
105
105
 
@@ -14,8 +14,13 @@ module Grape
14
14
  let(:options) { { a: :b } }
15
15
  let(:path) { '/dummy' }
16
16
 
17
- xdescribe '.version' do
18
- it 'does some thing'
17
+ describe '.version' do
18
+ it 'sets a version for route' do
19
+ version = 'v1'
20
+ expect(subject).to receive(:namespace_inheritable).with(:version, [version])
21
+ expect(subject).to receive(:namespace_inheritable).with(:version_options, using: :path)
22
+ expect(subject.version(version)).to eq(version)
23
+ end
19
24
  end
20
25
 
21
26
  describe '.prefix' do
@@ -140,8 +145,12 @@ module Grape
140
145
  it 'does some thing'
141
146
  end
142
147
 
143
- xdescribe '.versions' do
144
- it 'does some thing'
148
+ describe '.versions' do
149
+ it 'returns last defined version' do
150
+ subject.version 'v1'
151
+ subject.version 'v2'
152
+ expect(subject.version).to eq('v2')
153
+ end
145
154
  end
146
155
  end
147
156
  end
@@ -386,9 +386,9 @@ describe Grape::Endpoint do
386
386
 
387
387
  expect(last_response.status).to eq(200)
388
388
  expect(inner_params[:first]).to eq 'present'
389
- expect(inner_params[:nested].keys).to eq [:fourth, :fifth, :nested_nested]
389
+ expect(inner_params[:nested].keys).to eq %w(fourth fifth nested_nested)
390
390
  expect(inner_params[:nested][:fourth]).to eq ''
391
- expect(inner_params[:nested][:nested_nested].keys).to eq [:sixth, :seven]
391
+ expect(inner_params[:nested][:nested_nested].keys).to eq %w(sixth seven)
392
392
  expect(inner_params[:nested][:nested_nested][:sixth]).to eq 'sixth'
393
393
  end
394
394
  end
@@ -0,0 +1,105 @@
1
+ require 'spec_helper'
2
+
3
+ describe Grape::Exceptions::ValidationErrors do
4
+ context 'api with rescue_from :all handler' do
5
+ subject { Class.new(Grape::API) }
6
+ before {
7
+ subject.rescue_from :all do |e|
8
+ rack_response 'message was processed', 400
9
+ end
10
+ subject.params do
11
+ requires :beer
12
+ end
13
+ subject.post '/beer' do
14
+ 'beer received'
15
+ end
16
+ }
17
+
18
+ def app
19
+ subject
20
+ end
21
+
22
+ context 'with content_type json' do
23
+ it 'can recover from failed body parsing' do
24
+ post '/beer', 'test', 'CONTENT_TYPE' => 'application/json'
25
+ expect(last_response.status).to eq 400
26
+ expect(last_response.body).to eq('message was processed')
27
+ end
28
+ end
29
+
30
+ context 'with content_type xml' do
31
+ it 'can recover from failed body parsing' do
32
+ post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml'
33
+ expect(last_response.status).to eq 400
34
+ expect(last_response.body).to eq('message was processed')
35
+ end
36
+ end
37
+
38
+ context 'with content_type text' do
39
+ it 'can recover from failed body parsing' do
40
+ post '/beer', 'test', 'CONTENT_TYPE' => 'text/plain'
41
+ expect(last_response.status).to eq 400
42
+ expect(last_response.body).to eq('message was processed')
43
+ end
44
+ end
45
+
46
+ context 'with no specific content_type' do
47
+ it 'can recover from failed body parsing' do
48
+ post '/beer', 'test', {}
49
+ expect(last_response.status).to eq 400
50
+ expect(last_response.body).to eq('message was processed')
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'api without a rescue handler' do
56
+ subject { Class.new(Grape::API) }
57
+ before {
58
+ subject.params do
59
+ requires :beer
60
+ end
61
+ subject.post '/beer' do
62
+ 'beer received'
63
+ end
64
+ }
65
+
66
+ def app
67
+ subject
68
+ end
69
+
70
+ context 'and with content_type json' do
71
+ it 'can recover from failed body parsing' do
72
+ post '/beer', 'test', 'CONTENT_TYPE' => 'application/json'
73
+ expect(last_response.status).to eq 400
74
+ expect(last_response.body).to include('message body does not match declared format')
75
+ expect(last_response.body).to include('application/json')
76
+ end
77
+ end
78
+
79
+ context 'with content_type xml' do
80
+ it 'can recover from failed body parsing' do
81
+ post '/beer', 'test', 'CONTENT_TYPE' => 'application/xml'
82
+ expect(last_response.status).to eq 400
83
+ expect(last_response.body).to include('message body does not match declared format')
84
+ expect(last_response.body).to include('application/xml')
85
+ end
86
+ end
87
+
88
+ context 'with content_type text' do
89
+ it 'can recover from failed body parsing' do
90
+ post '/beer', 'test', 'CONTENT_TYPE' => 'text/plain'
91
+ expect(last_response.status).to eq 400
92
+ expect(last_response.body).to eq('beer is missing')
93
+ end
94
+ end
95
+
96
+ context 'and with no specific content_type' do
97
+ it 'can recover from failed body parsing' do
98
+ post '/beer', 'test', {}
99
+ expect(last_response.status).to eq 400
100
+ # plain response with text/html
101
+ expect(last_response.body).to eq('beer is missing')
102
+ end
103
+ end
104
+ end
105
+ end