sorbet-coerce 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 459ea1a3f906360f8a0e0d33969f49110476a1aa8d1772a1adc4dc1fc704140c
4
- data.tar.gz: 7e2bf805fcb1d89de859312576d7e9e4eb782d03e4e7ff5929205c0fb676e377
3
+ metadata.gz: 07b8a2f1d773bf25d7abda4c1bfc6de014a7b41c9dde01b58ff27e4aaa5952a7
4
+ data.tar.gz: ea45ba6647ed640581243698193f9c7e24520d1ab2b2dea9009bf97ddee5309b
5
5
  SHA512:
6
- metadata.gz: c75dfa39c75773bd161fc67f2ee145192bc6431f58db621a39eadd2c02948d39a117f3d1a5c8ad277338cf1ab0342a6f4153f20326bd379e28e8a734ca1d190c
7
- data.tar.gz: d9c87f91c2586aac705089fe519d6c58e28854fe8cf87019468f46ad4421e8166d5f042c6f512c983d25b120e4b31f699f755706694a47a29c91b2d38c1a5a78
6
+ metadata.gz: d4052f15f7b98b91dcbc3e1eb086dd1a8f74694f73889d04f578af0cefae29ac87fb9f5d3ddc67d275f23b98335fa9865ddd0c483aba80788c3b41ebbb1b5f3b
7
+ data.tar.gz: 7d4145c0966431329cf292c6c7347db7dfbd6db3870ef44f0d5f876ecaf0e8e4395c466c5138556bc5972d439d08947d0de245334e7e618a7324edd3eaa22418
@@ -25,12 +25,12 @@ class TypeCoerce::Converter
25
25
  "#{name}#[#{@type.to_s}]"
26
26
  end
27
27
 
28
- def from(args, raise_coercion_error: nil)
28
+ def from(args, raise_coercion_error: nil, coerce_empty_to_nil: false)
29
29
  if raise_coercion_error.nil?
30
30
  raise_coercion_error = TypeCoerce::Configuration.raise_coercion_error
31
31
  end
32
32
 
33
- T.let(_convert(args, @type, raise_coercion_error), @type)
33
+ T.let(_convert(args, @type, raise_coercion_error, coerce_empty_to_nil), @type)
34
34
  end
35
35
 
36
36
  private
@@ -45,15 +45,19 @@ class TypeCoerce::Converter
45
45
  Time,
46
46
  ], T.untyped)
47
47
 
48
- def _convert(value, type, raise_coercion_error)
48
+ def _convert(value, type, raise_coercion_error, coerce_empty_to_nil)
49
49
  if type.is_a?(T::Types::Untyped)
50
50
  value
51
+ elsif type.is_a?(T::Types::ClassOf)
52
+ value
51
53
  elsif type.is_a?(T::Types::TypedArray)
52
- _convert_to_a(value, type.type, raise_coercion_error)
54
+ _convert_to_a(value, type.type, raise_coercion_error, coerce_empty_to_nil)
55
+ elsif type.is_a?(T::Types::FixedArray)
56
+ _convert_to_a(value, type.types, raise_coercion_error, coerce_empty_to_nil)
53
57
  elsif type.is_a?(T::Types::TypedSet)
54
- Set.new(_convert_to_a(value, type.type, raise_coercion_error))
58
+ Set.new(_convert_to_a(value, type.type, raise_coercion_error, coerce_empty_to_nil))
55
59
  elsif type.is_a?(T::Types::Simple)
56
- _convert(value, type.raw_type, raise_coercion_error)
60
+ _convert(value, type.raw_type, raise_coercion_error, coerce_empty_to_nil)
57
61
  elsif type.is_a?(T::Types::Union)
58
62
  true_idx = T.let(nil, T.nilable(Integer))
59
63
  false_idx = T.let(nil, T.nilable(Integer))
@@ -65,9 +69,7 @@ class TypeCoerce::Converter
65
69
  false_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == FalseClass
66
70
  end
67
71
 
68
- raise ArgumentError.new(
69
- 'the only supported union types are T.nilable and T::Boolean',
70
- ) unless (
72
+ return value unless (
71
73
  (!true_idx.nil? && !false_idx.nil? && !nil_idx.nil?) || # T.nilable(T::Boolean)
72
74
  (type.types.length == 2 && (
73
75
  !nil_idx.nil? || (!true_idx.nil? && !false_idx.nil?) # T.nilable || T::Boolean
@@ -75,12 +77,12 @@ class TypeCoerce::Converter
75
77
  )
76
78
 
77
79
  if !true_idx.nil? && !false_idx.nil?
78
- _convert_simple(value, T::Boolean, raise_coercion_error)
80
+ _convert_simple(value, T::Boolean, raise_coercion_error, coerce_empty_to_nil)
79
81
  else
80
- _convert(value, type.types[nil_idx == 0 ? 1 : 0], raise_coercion_error)
82
+ _convert(value, type.types[nil_idx == 0 ? 1 : 0], raise_coercion_error, coerce_empty_to_nil)
81
83
  end
82
84
  elsif type.is_a?(T::Types::TypedHash)
83
- return {} if _nil_like?(value, type)
85
+ return {} if _nil_like?(value, type, coerce_empty_to_nil)
84
86
 
85
87
  unless value.respond_to?(:map)
86
88
  raise TypeCoerce::ShapeError.new(value, type)
@@ -88,30 +90,34 @@ class TypeCoerce::Converter
88
90
 
89
91
  value.map do |k, v|
90
92
  [
91
- _convert(k, type.keys, raise_coercion_error),
92
- _convert(v, type.values, raise_coercion_error),
93
+ _convert(k, type.keys, raise_coercion_error, coerce_empty_to_nil),
94
+ _convert(v, type.values, raise_coercion_error, coerce_empty_to_nil),
93
95
  ]
94
96
  end.to_h
95
97
  elsif Object.const_defined?('T::Private::Types::TypeAlias') &&
96
98
  type.is_a?(T::Private::Types::TypeAlias)
97
- _convert(value, type.aliased_type, raise_coercion_error)
98
- elsif type.respond_to?(:<) && type < T::Struct
99
- return value if value.is_a?(type)
100
-
101
- args = _build_args(value, type, raise_coercion_error)
102
- type.new(args)
103
- elsif type.respond_to?(:<) && type < T::Enum
104
- return value if value.is_a?(type)
105
-
106
- _convert_enum(value, type, raise_coercion_error)
99
+ _convert(value, type.aliased_type, raise_coercion_error, coerce_empty_to_nil)
100
+ elsif type.is_a?(Class) || type.is_a?(Module)
101
+ return coerce_nil(value, type, coerce_empty_to_nil) if value.is_a?(type)
102
+
103
+ if type < T::Struct
104
+ args = _build_args(value, type, raise_coercion_error, coerce_empty_to_nil)
105
+ type.new(args)
106
+ elsif type < T::Enum
107
+ _convert_enum(value, type, raise_coercion_error, coerce_empty_to_nil)
108
+ else
109
+ _convert_simple(value, type, raise_coercion_error, coerce_empty_to_nil)
110
+ end
107
111
  else
108
- return value if value.is_a?(type)
109
-
110
- _convert_simple(value, type, raise_coercion_error)
112
+ if raise_coercion_error
113
+ raise TypeCoerce::CoercionError.new(value, type)
114
+ else
115
+ value
116
+ end
111
117
  end
112
118
  end
113
119
 
114
- def _convert_enum(value, type, raise_coercion_error)
120
+ def _convert_enum(value, type, raise_coercion_error, coerce_empty_to_nil)
115
121
  if raise_coercion_error
116
122
  type.deserialize(value)
117
123
  else
@@ -121,8 +127,8 @@ class TypeCoerce::Converter
121
127
  raise TypeCoerce::CoercionError.new(value, type)
122
128
  end
123
129
 
124
- def _convert_simple(value, type, raise_coercion_error)
125
- return nil if _nil_like?(value, type)
130
+ def _convert_simple(value, type, raise_coercion_error, coerce_empty_to_nil)
131
+ return nil if _nil_like?(value, type, coerce_empty_to_nil)
126
132
 
127
133
  safe_type_rule = T.let(nil, T.untyped)
128
134
 
@@ -151,18 +157,24 @@ class TypeCoerce::Converter
151
157
  end
152
158
  end
153
159
 
154
- def _convert_to_a(ary, type, raise_coercion_error)
155
- return [] if _nil_like?(ary, type)
160
+ def _convert_to_a(ary, type, raise_coercion_error, coerce_empty_to_nil)
161
+ return [] if _nil_like?(ary, type, coerce_empty_to_nil)
156
162
 
157
163
  unless ary.respond_to?(:map)
158
164
  raise TypeCoerce::ShapeError.new(ary, type)
159
165
  end
160
166
 
161
- ary.map { |value| _convert(value, type, raise_coercion_error) }
167
+ ary.map.with_index do |value, i|
168
+ if type.is_a?(Array)
169
+ _convert(value, type[i], raise_coercion_error, coerce_empty_to_nil)
170
+ else
171
+ _convert(value, type, raise_coercion_error, coerce_empty_to_nil)
172
+ end
173
+ end
162
174
  end
163
175
 
164
- def _build_args(args, type, raise_coercion_error)
165
- return {} if _nil_like?(args, Hash)
176
+ def _build_args(args, type, raise_coercion_error, coerce_empty_to_nil)
177
+ return {} if _nil_like?(args, Hash, coerce_empty_to_nil)
166
178
 
167
179
  unless args.respond_to?(:each_pair)
168
180
  raise TypeCoerce::ShapeError.new(args, type)
@@ -174,12 +186,22 @@ class TypeCoerce::Converter
174
186
  [
175
187
  key,
176
188
  (!props.include?(key) || value.nil?) ?
177
- nil : _convert(value, props[key][:type], raise_coercion_error),
189
+ nil : _convert(value, props[key][:type], raise_coercion_error, coerce_empty_to_nil),
178
190
  ]
179
191
  }.to_h.slice(*props.keys)
180
192
  end
181
193
 
182
- def _nil_like?(value, type)
183
- value.nil? || (value == '' && type != String)
194
+ def _nil_like?(value, type, coerce_empty_to_nil)
195
+ return true if value.nil?
196
+ return true if value == '' && coerce_empty_to_nil
197
+ return true if value == '' && type != String
198
+
199
+ false
200
+ end
201
+
202
+ def coerce_nil(value, type, coerce_empty_to_nil)
203
+ return nil if _nil_like?(value, type, coerce_empty_to_nil)
204
+
205
+ value
184
206
  end
185
207
  end
data/spec/coerce_spec.rb CHANGED
@@ -47,6 +47,14 @@ describe TypeCoerce do
47
47
  const :myenum, TestEnum
48
48
  end
49
49
 
50
+ class WithNilableString < T::Struct
51
+ const :myvalue, T.nilable(String)
52
+ end
53
+
54
+ class WithNilableInteger < T::Struct
55
+ const :myvalue, T.nilable(Integer)
56
+ end
57
+
50
58
  class CustomType
51
59
  attr_reader :a
52
60
 
@@ -63,6 +71,22 @@ describe TypeCoerce do
63
71
  # Does not respond to new
64
72
  end
65
73
 
74
+ class WithSupportedUnion < T::Struct
75
+ const :nilable, T.nilable(String)
76
+ const :nilable_boolean, T.nilable(T::Boolean)
77
+ end
78
+
79
+ class WithUnsupportedUnion < T::Struct
80
+ const :union, T.any(String, Integer)
81
+ end
82
+
83
+ class WithFixedArray < T::Struct
84
+ const :arr, [Integer, String, Integer]
85
+ end
86
+
87
+ class CustomString < String
88
+ end
89
+
66
90
  let!(:param) {
67
91
  TypeCoerce[Param].new.from({
68
92
  id: 1,
@@ -144,12 +168,12 @@ describe TypeCoerce do
144
168
  context 'when the given T::Struct is invalid' do
145
169
  class Param2 < T::Struct
146
170
  const :id, Integer
147
- const :info, T.any(Integer, String)
171
+ const :param, Param
148
172
  end
149
173
 
150
174
  it 'raises an error' do
151
175
  expect {
152
- TypeCoerce[Param2].new.from({id: 1, info: 1})
176
+ TypeCoerce[Param2].new.from({id: 1, info: {}})
153
177
  }.to raise_error(ArgumentError)
154
178
  end
155
179
  end
@@ -192,7 +216,34 @@ describe TypeCoerce do
192
216
  end
193
217
  end
194
218
 
195
- context 'when dealing with arries' do
219
+ context 'when given union types' do
220
+ context 'supported union types' do
221
+ it 'coerces correctly' do
222
+ coerced = TypeCoerce[WithSupportedUnion].new.from({
223
+ nilable: 2,
224
+ nilable_boolean: 'true'
225
+ })
226
+ expect(coerced.nilable).to eq('2')
227
+ expect(coerced.nilable_boolean).to eq(true)
228
+ end
229
+ end
230
+
231
+ context 'unsupported union types' do
232
+ it 'keeps the values as-is' do
233
+ coerced = TypeCoerce[WithUnsupportedUnion].new.from({union: 'a'})
234
+ expect(coerced.union).to eq('a')
235
+
236
+ coerced = TypeCoerce[WithUnsupportedUnion].new.from({union: 2})
237
+ expect(coerced.union).to eq(2)
238
+
239
+ expect do
240
+ TypeCoerce[WithUnsupportedUnion].new.from({union: nil})
241
+ end.to raise_error(TypeError)
242
+ end
243
+ end
244
+ end
245
+
246
+ context 'when dealing with arrays' do
196
247
  it 'coreces correctly' do
197
248
  expect(TypeCoerce[T::Array[Integer]].new.from(nil)).to eql []
198
249
  expect(TypeCoerce[T::Array[Integer]].new.from('')).to eql []
@@ -273,18 +324,18 @@ describe TypeCoerce do
273
324
 
274
325
  context 'when dealing with enums' do
275
326
  it 'coerces a serialized enum correctly' do
276
- coerced = TypeCoerce[WithEnum].new.from(myenum: "test")
327
+ coerced = TypeCoerce[WithEnum].new.from({myenum: "test"})
277
328
  expect(coerced.myenum).to eq(TestEnum::Test)
278
329
  end
279
330
 
280
331
  it 'handles a real enum correctly' do
281
- coerced = TypeCoerce[WithEnum].new.from(myenum: TestEnum::Test)
332
+ coerced = TypeCoerce[WithEnum].new.from({myenum: TestEnum::Test})
282
333
  expect(coerced.myenum).to eq(TestEnum::Test)
283
334
  end
284
335
 
285
336
  it 'handles bad enum' do
286
337
  expect {
287
- TypeCoerce[WithEnum].new.from(myenum: "bad_key")
338
+ TypeCoerce[WithEnum].new.from({myenum: "bad_key"})
288
339
  }.to raise_error(TypeCoerce::CoercionError)
289
340
  end
290
341
  end
@@ -295,4 +346,68 @@ describe TypeCoerce do
295
346
  obj = CustomType.new(1)
296
347
  expect(TypeCoerce[T::Hash[String, T.untyped]].new.from({a: obj})).to eq({'a' => obj})
297
348
  end
349
+
350
+ it 'works with T::Types::FixedArray' do
351
+ type = T.type_alias { [Integer, String, Integer] }
352
+ coerced = TypeCoerce[type].new.from(['1', 2, '3'])
353
+ expect(coerced).to eql([1, '2', 3])
354
+
355
+ coerced = TypeCoerce[WithFixedArray].new.from({ arr: ['1', 2, '3'] })
356
+ expect(coerced.arr).to eql([1, '2', 3])
357
+ end
358
+
359
+ context 'when dealing with T.class_of' do
360
+ it 'keeps the value as-is' do
361
+ string_class_type = T.class_of(String)
362
+ expect(TypeCoerce[string_class_type].new.from(String)).to eql(String)
363
+ expect(TypeCoerce[string_class_type].new.from(CustomString)).to eql(CustomString)
364
+ expect do
365
+ TypeCoerce[string_class_type].new.from(Integer)
366
+ end.to raise_error(TypeError)
367
+ expect do
368
+ TypeCoerce[string_class_type].new.from('a')
369
+ end.to raise_error(TypeError)
370
+ end
371
+ end
372
+
373
+ context 'when dealing with unknown types' do
374
+ context 'raise_coercion_error is enabled' do
375
+ it 'raises error' do
376
+ expect do
377
+ TypeCoerce['a'].new.from('a', raise_coercion_error: true)
378
+ end.to raise_error(TypeCoerce::CoercionError)
379
+ end
380
+ end
381
+ context 'raise_coercion_error is disabled' do
382
+ it 'keeps the value as-is (and let Sorbet raise error)' do
383
+ expect do
384
+ TypeCoerce['a'].new.from('a', raise_coercion_error: false)
385
+ end.to raise_error(/must be an T::Types::Base/i)
386
+ end
387
+ end
388
+ end
389
+
390
+ context 'when dealing with coercing empty strings' do
391
+ context 'when flag is set' do
392
+ it 'coerces empty strings to nil from a simple type' do
393
+ expect(TypeCoerce[T.nilable(String)].new.from('', coerce_empty_to_nil: true)).to be_nil
394
+ end
395
+
396
+ it 'coerces empty strings to nil from a struct' do
397
+ coerced = TypeCoerce[WithNilableString].new.from({myvalue: ''}, coerce_empty_to_nil: true)
398
+ expect(coerced.myvalue).to eql(nil)
399
+ end
400
+ end
401
+
402
+ context 'when flag is not set' do
403
+ it 'coerces empty strings to nil from a simple type' do
404
+ expect(TypeCoerce[T.nilable(String)].new.from('', coerce_empty_to_nil: false)).to eql('')
405
+ end
406
+
407
+ it 'coerces empty strings to nil from a struct' do
408
+ coerced = TypeCoerce[WithNilableString].new.from({myvalue: ''}, coerce_empty_to_nil: false)
409
+ expect(coerced.myvalue).to eql('')
410
+ end
411
+ end
412
+ end
298
413
  end
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: false
2
2
  require 'sorbet-coerce'
3
3
 
4
4
  T.assert_type!(TypeCoerce[Integer].new.from('1'), Integer)
data/spec/spec_helper.rb CHANGED
@@ -1,13 +1,10 @@
1
1
  # typed: strict
2
- require 'byebug'
3
- require 'simplecov'
2
+ require "byebug"
3
+ require "simplecov"
4
+ require "simplecov-cobertura"
4
5
 
5
6
  SimpleCov.start
6
-
7
- if ENV['CI'] == 'true'
8
- require 'codecov'
9
- SimpleCov.formatter = SimpleCov::Formatter::Codecov
10
- end
7
+ SimpleCov.formatter = SimpleCov::Formatter::CoberturaFormatter
11
8
 
12
9
  RSpec.configure do |config|
13
10
  config.expect_with(:rspec) { |c| c.syntax = :expect }
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-coerce
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chan Zuckerberg Initiative
@@ -76,40 +76,40 @@ dependencies:
76
76
  name: rspec
77
77
  requirement: !ruby/object:Gem::Requirement
78
78
  requirements:
79
- - - ">="
79
+ - - "~>"
80
80
  - !ruby/object:Gem::Version
81
81
  version: '3.8'
82
- - - "~>"
82
+ - - ">="
83
83
  - !ruby/object:Gem::Version
84
84
  version: '3.8'
85
85
  type: :development
86
86
  prerelease: false
87
87
  version_requirements: !ruby/object:Gem::Requirement
88
88
  requirements:
89
- - - ">="
89
+ - - "~>"
90
90
  - !ruby/object:Gem::Version
91
91
  version: '3.8'
92
- - - "~>"
92
+ - - ">="
93
93
  - !ruby/object:Gem::Version
94
94
  version: '3.8'
95
95
  - !ruby/object:Gem::Dependency
96
96
  name: byebug
97
97
  requirement: !ruby/object:Gem::Requirement
98
98
  requirements:
99
- - - ">="
99
+ - - "~>"
100
100
  - !ruby/object:Gem::Version
101
101
  version: 11.0.1
102
- - - "~>"
102
+ - - ">="
103
103
  - !ruby/object:Gem::Version
104
104
  version: 11.0.1
105
105
  type: :development
106
106
  prerelease: false
107
107
  version_requirements: !ruby/object:Gem::Requirement
108
108
  requirements:
109
- - - ">="
109
+ - - "~>"
110
110
  - !ruby/object:Gem::Version
111
111
  version: 11.0.1
112
- - - "~>"
112
+ - - ">="
113
113
  - !ruby/object:Gem::Version
114
114
  version: 11.0.1
115
115
  description:
@@ -146,7 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
146
  - !ruby/object:Gem::Version
147
147
  version: '0'
148
148
  requirements: []
149
- rubygems_version: 3.0.8
149
+ rubygems_version: 3.1.6
150
150
  signing_key:
151
151
  specification_version: 4
152
152
  summary: A type coercion lib works with Sorbet's static type checker and type definitions;