grape 1.7.1 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -1
  3. data/CONTRIBUTING.md +1 -1
  4. data/README.md +12 -4
  5. data/grape.gemspec +2 -2
  6. data/lib/grape/api.rb +2 -2
  7. data/lib/grape/content_types.rb +2 -8
  8. data/lib/grape/dsl/desc.rb +1 -1
  9. data/lib/grape/dsl/inside_route.rb +5 -5
  10. data/lib/grape/dsl/request_response.rb +2 -1
  11. data/lib/grape/dsl/settings.rb +2 -6
  12. data/lib/grape/endpoint.rb +19 -17
  13. data/lib/grape/error_formatter/base.rb +1 -1
  14. data/lib/grape/exceptions/base.rb +2 -2
  15. data/lib/grape/exceptions/missing_group_type.rb +1 -6
  16. data/lib/grape/exceptions/unsupported_group_type.rb +1 -6
  17. data/lib/grape/exceptions/validation_errors.rb +1 -6
  18. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +3 -3
  19. data/lib/grape/extensions/hash.rb +4 -7
  20. data/lib/grape/extensions/hashie/mash.rb +3 -3
  21. data/lib/grape/formatter/serializable_hash.rb +7 -7
  22. data/lib/grape/middleware/auth/base.rb +1 -1
  23. data/lib/grape/middleware/error.rb +1 -1
  24. data/lib/grape/middleware/formatter.rb +1 -1
  25. data/lib/grape/middleware/versioner/header.rb +11 -19
  26. data/lib/grape/router/route.rb +1 -3
  27. data/lib/grape/util/lazy_value.rb +3 -11
  28. data/lib/grape/util/strict_hash_configuration.rb +3 -4
  29. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  30. data/lib/grape/validations/params_scope.rb +8 -2
  31. data/lib/grape/validations/single_attribute_iterator.rb +3 -1
  32. data/lib/grape/validations/types/custom_type_coercer.rb +2 -16
  33. data/lib/grape/validations/validators/base.rb +9 -20
  34. data/lib/grape/validations/validators/default_validator.rb +2 -20
  35. data/lib/grape/validations/validators/multiple_params_base.rb +4 -8
  36. data/lib/grape/validations/validators/values_validator.rb +14 -5
  37. data/lib/grape/version.rb +1 -1
  38. data/lib/grape.rb +11 -3
  39. data/spec/grape/api/custom_validations_spec.rb +14 -57
  40. data/spec/grape/api_remount_spec.rb +36 -0
  41. data/spec/grape/api_spec.rb +10 -1
  42. data/spec/grape/dsl/desc_spec.rb +84 -87
  43. data/spec/grape/dsl/inside_route_spec.rb +6 -10
  44. data/spec/grape/dsl/request_response_spec.rb +21 -2
  45. data/spec/grape/endpoint_spec.rb +8 -8
  46. data/spec/grape/exceptions/body_parse_errors_spec.rb +40 -0
  47. data/spec/grape/exceptions/missing_group_type_spec.rb +5 -9
  48. data/spec/grape/exceptions/unsupported_group_type_spec.rb +5 -9
  49. data/spec/grape/grape_spec.rb +9 -0
  50. data/spec/grape/middleware/formatter_spec.rb +1 -1
  51. data/spec/grape/request_spec.rb +4 -14
  52. data/spec/grape/validations/multiple_attributes_iterator_spec.rb +6 -8
  53. data/spec/grape/validations/single_attribute_iterator_spec.rb +8 -9
  54. data/spec/grape/validations/validators/base_spec.rb +38 -0
  55. data/spec/grape/validations/validators/values_spec.rb +37 -0
  56. data/spec/grape/validations_spec.rb +7 -6
  57. data/spec/shared/deprecated_class_examples.rb +16 -0
  58. metadata +17 -22
  59. data/lib/grape/config.rb +0 -34
  60. data/lib/grape/extensions/deep_mergeable_hash.rb +0 -21
  61. data/lib/grape/extensions/deep_symbolize_hash.rb +0 -32
  62. data/spec/grape/config_spec.rb +0 -17
@@ -10,6 +10,11 @@ module Grape
10
10
 
11
11
  include Grape::DSL::Parameters
12
12
 
13
+ # There are a number of documentation options on entities that don't have
14
+ # corresponding validators. Since there is nowhere that enumerates them all,
15
+ # we maintain a list of them here and skip looking up validators for them.
16
+ RESERVED_DOCUMENTATION_KEYWORDS = %i[as required param_type is_array format example].freeze
17
+
13
18
  class Attr
14
19
  attr_accessor :key, :scope
15
20
 
@@ -359,7 +364,8 @@ module Grape
359
364
  coerce_type validations, attrs, doc, opts
360
365
 
361
366
  validations.each do |type, options|
362
- next if type == :as
367
+ # Don't try to look up validators for documentation params that don't have one.
368
+ next if RESERVED_DOCUMENTATION_KEYWORDS.include?(type)
363
369
 
364
370
  validate(type, options, attrs, doc, opts)
365
371
  end
@@ -414,7 +420,7 @@ module Grape
414
420
 
415
421
  # but not special JSON types, which
416
422
  # already imply coercion method
417
- return unless [JSON, Array[JSON]].include? validations[:coerce]
423
+ return if [JSON, Array[JSON]].exclude? validations[:coerce]
418
424
 
419
425
  raise ArgumentError, 'coerce_with disallowed for type: JSON'
420
426
  end
@@ -6,8 +6,10 @@ module Grape
6
6
  private
7
7
 
8
8
  def yield_attributes(val, attrs)
9
+ return if skip?(val)
10
+
9
11
  attrs.each do |attr_name|
10
- yield val, attr_name, empty?(val), skip?(val)
12
+ yield val, attr_name, empty?(val)
11
13
  end
12
14
  end
13
15
 
@@ -141,7 +141,7 @@ module Grape
141
141
  lambda do |val|
142
142
  method.call(val).tap do |new_val|
143
143
  new_val.map do |item|
144
- item.is_a?(Hash) ? symbolize_keys(item) : item
144
+ item.is_a?(Hash) ? item.deep_symbolize_keys : item
145
145
  end
146
146
  end
147
147
  end
@@ -149,7 +149,7 @@ module Grape
149
149
  # Hash objects are processed directly
150
150
  elsif type == Hash
151
151
  lambda do |val|
152
- symbolize_keys method.call(val)
152
+ method.call(val).deep_symbolize_keys
153
153
  end
154
154
 
155
155
  # Simple types are not processed.
@@ -158,20 +158,6 @@ module Grape
158
158
  method
159
159
  end
160
160
  end
161
-
162
- def symbolize_keys!(hash)
163
- hash.each_key do |key|
164
- hash[key.to_sym] = hash.delete(key) if key.respond_to?(:to_sym)
165
- end
166
- hash
167
- end
168
-
169
- def symbolize_keys(hash)
170
- hash.inject({}) do |new_hash, (key, value)|
171
- new_key = key.respond_to?(:to_sym) ? key.to_sym : key
172
- new_hash.merge!(new_key => value)
173
- end
174
- end
175
161
  end
176
162
  end
177
163
  end
@@ -46,34 +46,23 @@ module Grape
46
46
  # there may be more than one error per field
47
47
  array_errors = []
48
48
 
49
- attributes.each do |val, attr_name, empty_val, skip_value|
50
- next if skip_value
49
+ attributes.each do |val, attr_name, empty_val|
51
50
  next if !@scope.required? && empty_val
52
51
  next unless @scope.meets_dependency?(val, params)
53
52
 
54
- begin
55
- validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name))
56
- rescue Grape::Exceptions::Validation => e
57
- array_errors << e
58
- end
53
+ validate_param!(attr_name, val) if @required || (val.respond_to?(:key?) && val.key?(attr_name))
54
+ rescue Grape::Exceptions::Validation => e
55
+ array_errors << e
59
56
  end
60
57
 
61
58
  raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
62
59
  end
63
60
 
64
- def self.convert_to_short_name(klass)
65
- ret = klass.name.gsub(/::/, '/')
66
- ret.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
67
- ret.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
68
- ret.tr!('-', '_')
69
- ret.downcase!
70
- File.basename(ret, '_validator')
71
- end
72
-
73
61
  def self.inherited(klass)
74
- return unless klass.name.present?
62
+ return if klass.name.blank?
75
63
 
76
- Validations.register_validator(convert_to_short_name(klass), klass)
64
+ short_validator_name = klass.name.demodulize.underscore.delete_suffix('_validator')
65
+ Validations.register_validator(short_validator_name, klass)
77
66
  end
78
67
 
79
68
  def message(default_key = nil)
@@ -95,8 +84,8 @@ module Grape
95
84
  end
96
85
 
97
86
  Grape::Validations::Base = Class.new(Grape::Validations::Validators::Base) do
98
- def initialize(*)
87
+ def self.inherited(*)
88
+ ActiveSupport::Deprecation.warn 'Grape::Validations::Base is deprecated! Use Grape::Validations::Validators::Base instead.'
99
89
  super
100
- warn '[DEPRECATION] `Grape::Validations::Base` is deprecated. Use `Grape::Validations::Validators::Base` instead.'
101
90
  end
102
91
  end
@@ -12,10 +12,10 @@ module Grape
12
12
  def validate_param!(attr_name, params)
13
13
  params[attr_name] = if @default.is_a? Proc
14
14
  @default.call
15
- elsif @default.frozen? || !duplicatable?(@default)
15
+ elsif @default.frozen? || !@default.duplicable?
16
16
  @default
17
17
  else
18
- duplicate(@default)
18
+ @default.dup
19
19
  end
20
20
  end
21
21
 
@@ -27,24 +27,6 @@ module Grape
27
27
  validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
28
28
  end
29
29
  end
30
-
31
- private
32
-
33
- # return true if we might be able to dup this object
34
- def duplicatable?(obj)
35
- !obj.nil? &&
36
- obj != true &&
37
- obj != false &&
38
- !obj.is_a?(Symbol) &&
39
- !obj.is_a?(Numeric)
40
- end
41
-
42
- # make a best effort to dup the object
43
- def duplicate(obj)
44
- obj.dup
45
- rescue TypeError
46
- obj
47
- end
48
30
  end
49
31
  end
50
32
  end
@@ -8,14 +8,10 @@ module Grape
8
8
  attributes = MultipleAttributesIterator.new(self, @scope, params)
9
9
  array_errors = []
10
10
 
11
- attributes.each do |resource_params, skip_value|
12
- next if skip_value
13
-
14
- begin
15
- validate_params!(resource_params)
16
- rescue Grape::Exceptions::Validation => e
17
- array_errors << e
18
- end
11
+ attributes.each do |resource_params|
12
+ validate_params!(resource_params)
13
+ rescue Grape::Exceptions::Validation => e
14
+ array_errors << e
19
15
  end
20
16
 
21
17
  raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
@@ -10,13 +10,11 @@ module Grape
10
10
  @values = options[:value]
11
11
  @proc = options[:proc]
12
12
 
13
- warn '[DEPRECATION] The values validator except option is deprecated. ' \
14
- 'Use the except validator instead.' if @excepts
13
+ ActiveSupport::Deprecation.warn('The values validator except option is deprecated. Use the except validator instead.') if @excepts
15
14
 
16
15
  raise ArgumentError, 'proc must be a Proc' if @proc && !@proc.is_a?(Proc)
17
16
 
18
- warn '[DEPRECATION] The values validator proc option is deprecated. ' \
19
- 'The lambda expression can now be assigned directly to values.' if @proc
17
+ ActiveSupport::Deprecation.warn('The values validator proc option is deprecated. The lambda expression can now be assigned directly to values.') if @proc
20
18
  else
21
19
  @excepts = nil
22
20
  @values = nil
@@ -45,7 +43,7 @@ module Grape
45
43
  unless check_values(param_array, attr_name)
46
44
 
47
45
  raise validation_exception(attr_name, message(:values)) \
48
- if @proc && !param_array.all? { |param| @proc.call(param) }
46
+ if @proc && !validate_proc(@proc, param_array)
49
47
  end
50
48
 
51
49
  private
@@ -70,6 +68,17 @@ module Grape
70
68
  param_array.none? { |param| excepts.include?(param) }
71
69
  end
72
70
 
71
+ def validate_proc(proc, param_array)
72
+ case proc.arity
73
+ when 0
74
+ param_array.all? { |_param| proc.call }
75
+ when 1
76
+ param_array.all? { |param| proc.call(param) }
77
+ else
78
+ raise ArgumentError, 'proc arity must be 0 or 1'
79
+ end
80
+ end
81
+
73
82
  def except_message
74
83
  options = instance_variable_get(:@option)
75
84
  options_key?(:except_message) ? options[:except_message] : message(:except_values)
data/lib/grape/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '1.7.1'
5
+ VERSION = '1.8.0'
6
6
  end
data/lib/grape.rb CHANGED
@@ -11,6 +11,7 @@ require 'bigdecimal'
11
11
  require 'date'
12
12
  require 'active_support'
13
13
  require 'active_support/concern'
14
+ require 'active_support/configurable'
14
15
  require 'active_support/version'
15
16
  require 'active_support/isolated_execution_state' if ActiveSupport::VERSION::MAJOR > 6
16
17
  require 'active_support/core_ext/array/conversions'
@@ -20,16 +21,21 @@ require 'active_support/core_ext/hash/conversions'
20
21
  require 'active_support/core_ext/hash/deep_merge'
21
22
  require 'active_support/core_ext/hash/except'
22
23
  require 'active_support/core_ext/hash/indifferent_access'
24
+ require 'active_support/core_ext/hash/keys'
23
25
  require 'active_support/core_ext/hash/reverse_merge'
24
26
  require 'active_support/core_ext/hash/slice'
25
27
  require 'active_support/core_ext/object/blank'
28
+ require 'active_support/core_ext/object/duplicable'
26
29
  require 'active_support/dependencies/autoload'
30
+ require 'active_support/deprecation'
31
+ require 'active_support/inflector'
27
32
  require 'active_support/notifications'
28
33
  require 'i18n'
29
34
 
30
35
  I18n.load_path << File.expand_path('grape/locale/en.yml', __dir__)
31
36
 
32
37
  module Grape
38
+ include ActiveSupport::Configurable
33
39
  extend ::ActiveSupport::Autoload
34
40
 
35
41
  eager_autoload do
@@ -92,8 +98,6 @@ module Grape
92
98
  module Extensions
93
99
  extend ::ActiveSupport::Autoload
94
100
  eager_autoload do
95
- autoload :DeepMergeableHash
96
- autoload :DeepSymbolizeHash
97
101
  autoload :Hash
98
102
  end
99
103
  module ActiveSupport
@@ -285,9 +289,13 @@ module Grape
285
289
  autoload :InvalidValue
286
290
  end
287
291
  end
292
+
293
+ configure do |config|
294
+ config.param_builder = Grape::Extensions::ActiveSupport::HashWithIndifferentAccess::ParamBuilder
295
+ config.compile_methods!
296
+ end
288
297
  end
289
298
 
290
- require 'grape/config'
291
299
  require 'grape/content_types'
292
300
 
293
301
  require 'grape/util/lazy_value'
@@ -1,48 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- describe Grape::Validations do
4
- context 'deprecated Grape::Validations::Base' do
5
- subject do
6
- Class.new(Grape::API) do
7
- params do
8
- requires :text, validator_with_old_base: true
9
- end
10
- get do
11
- end
12
- end
13
- end
14
-
15
- let(:validator_with_old_base) do
16
- Class.new(Grape::Validations::Base) do
17
- def validate_param!(_attr_name, _params)
18
- true
19
- end
20
- end
21
- end
22
-
23
- before do
24
- described_class.register_validator('validator_with_old_base', validator_with_old_base)
25
- allow(Warning).to receive(:warn)
26
- end
3
+ require 'shared/deprecated_class_examples'
27
4
 
28
- after do
29
- described_class.deregister_validator('validator_with_old_base')
30
- end
31
-
32
- def app
33
- subject
5
+ describe Grape::Validations do
6
+ describe 'Grape::Validations::Base' do
7
+ let(:deprecated_class) do
8
+ Class.new(Grape::Validations::Base)
34
9
  end
35
10
 
36
- it 'puts a deprecation warning' do
37
- expect(Warning).to receive(:warn) do |message|
38
- expect(message).to include('`Grape::Validations::Base` is deprecated')
39
- end
40
-
41
- get '/'
42
- end
11
+ it_behaves_like 'deprecated class'
43
12
  end
44
13
 
45
- context 'using a custom length validator' do
14
+ describe 'using a custom length validator' do
46
15
  subject do
47
16
  Class.new(Grape::API) do
48
17
  params do
@@ -64,6 +33,7 @@ describe Grape::Validations do
64
33
  end
65
34
  end
66
35
  end
36
+ let(:app) { Rack::Builder.new(subject) }
67
37
 
68
38
  before do
69
39
  described_class.register_validator('default_length', default_length_validator)
@@ -73,10 +43,6 @@ describe Grape::Validations do
73
43
  described_class.deregister_validator('default_length')
74
44
  end
75
45
 
76
- def app
77
- subject
78
- end
79
-
80
46
  it 'under 140 characters' do
81
47
  get '/', text: 'abc'
82
48
  expect(last_response.status).to eq 200
@@ -96,7 +62,7 @@ describe Grape::Validations do
96
62
  end
97
63
  end
98
64
 
99
- context 'using a custom body-only validator' do
65
+ describe 'using a custom body-only validator' do
100
66
  subject do
101
67
  Class.new(Grape::API) do
102
68
  params do
@@ -115,6 +81,7 @@ describe Grape::Validations do
115
81
  end
116
82
  end
117
83
  end
84
+ let(:app) { Rack::Builder.new(subject) }
118
85
 
119
86
  before do
120
87
  described_class.register_validator('in_body', in_body_validator)
@@ -124,10 +91,6 @@ describe Grape::Validations do
124
91
  described_class.deregister_validator('in_body')
125
92
  end
126
93
 
127
- def app
128
- subject
129
- end
130
-
131
94
  it 'allows field in body' do
132
95
  get '/', text: 'abc'
133
96
  expect(last_response.status).to eq 200
@@ -141,7 +104,7 @@ describe Grape::Validations do
141
104
  end
142
105
  end
143
106
 
144
- context 'using a custom validator with message_key' do
107
+ describe 'using a custom validator with message_key' do
145
108
  subject do
146
109
  Class.new(Grape::API) do
147
110
  params do
@@ -160,6 +123,7 @@ describe Grape::Validations do
160
123
  end
161
124
  end
162
125
  end
126
+ let(:app) { Rack::Builder.new(subject) }
163
127
 
164
128
  before do
165
129
  described_class.register_validator('with_message_key', message_key_validator)
@@ -169,10 +133,6 @@ describe Grape::Validations do
169
133
  described_class.deregister_validator('with_message_key')
170
134
  end
171
135
 
172
- def app
173
- subject
174
- end
175
-
176
136
  it 'fails with message' do
177
137
  get '/', text: 'foobar'
178
138
  expect(last_response.status).to eq 400
@@ -180,7 +140,7 @@ describe Grape::Validations do
180
140
  end
181
141
  end
182
142
 
183
- context 'using a custom request/param validator' do
143
+ describe 'using a custom request/param validator' do
184
144
  subject do
185
145
  Class.new(Grape::API) do
186
146
  params do
@@ -208,6 +168,7 @@ describe Grape::Validations do
208
168
  end
209
169
  end
210
170
  end
171
+ let(:app) { Rack::Builder.new(subject) }
211
172
 
212
173
  before do
213
174
  described_class.register_validator('admin', admin_validator)
@@ -217,10 +178,6 @@ describe Grape::Validations do
217
178
  described_class.deregister_validator('admin')
218
179
  end
219
180
 
220
- def app
221
- subject
222
- end
223
-
224
181
  it 'fail when non-admin user sets an admin field' do
225
182
  get '/', admin_field: 'tester', non_admin_field: 'toaster'
226
183
  expect(last_response.status).to eq 400
@@ -147,6 +147,42 @@ describe Grape::API do
147
147
  end
148
148
  end
149
149
 
150
+ context 'when the params are configured via a configuration' do
151
+ subject(:a_remounted_api) do
152
+ Class.new(described_class) do
153
+ params do
154
+ requires configuration[:required_attr_name], type: String
155
+ end
156
+
157
+ get(mounted { configuration[:endpoint] }) do
158
+ status 200
159
+ end
160
+ end
161
+ end
162
+
163
+ context 'when the configured param is my_attr' do
164
+ it 'requires the configured params' do
165
+ root_api.mount a_remounted_api, with: {
166
+ required_attr_name: 'my_attr',
167
+ endpoint: 'test'
168
+ }
169
+ get 'test?another_attr=1'
170
+ expect(last_response.status).to eq 400
171
+ get 'test?my_attr=1'
172
+ expect(last_response.status).to eq 200
173
+
174
+ root_api.mount a_remounted_api, with: {
175
+ required_attr_name: 'another_attr',
176
+ endpoint: 'test_b'
177
+ }
178
+ get 'test_b?another_attr=1'
179
+ expect(last_response.status).to eq 200
180
+ get 'test_b?my_attr=1'
181
+ expect(last_response.status).to eq 400
182
+ end
183
+ end
184
+ end
185
+
150
186
  context 'when executing a standard block within a `mounted` block with all dynamic params' do
151
187
  subject(:a_remounted_api) do
152
188
  Class.new(described_class) do
@@ -3311,7 +3311,7 @@ describe Grape::API do
3311
3311
  it 'is able to cascade' do
3312
3312
  subject.mount lambda { |env|
3313
3313
  headers = {}
3314
- headers['X-Cascade'] == 'pass' unless env['PATH_INFO'].include?('boo')
3314
+ headers['X-Cascade'] == 'pass' if env['PATH_INFO'].exclude?('boo')
3315
3315
  [200, headers, ['Farfegnugen']]
3316
3316
  } => '/'
3317
3317
 
@@ -4226,6 +4226,15 @@ describe Grape::API do
4226
4226
  end
4227
4227
  end
4228
4228
 
4229
+ it 'does not override methods inherited from Class' do
4230
+ Class.define_method(:test_method) {}
4231
+ subclass = Class.new(described_class)
4232
+ expect(subclass).not_to receive(:add_setup)
4233
+ subclass.test_method
4234
+ ensure
4235
+ Class.remove_method(:test_method)
4236
+ end
4237
+
4229
4238
  context 'overriding via composition' do
4230
4239
  module Inherited
4231
4240
  def inherited(api)