grape 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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)