sorbet-coerce 0.2.3 → 0.3.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: f267197b77c7b2c9ed7558ad7f94818fc54f9af5fc56c99c9d824af47e40bf78
4
- data.tar.gz: 04da4e2a3ebdde38cf4d2e6d60200c365d371250f9d33f588f67adc5b766c6b2
3
+ metadata.gz: b3a8032d0ba46f76ce47d4ec30e3e486005028d61a06c4f1047d6691f3e995f4
4
+ data.tar.gz: 6ec0fd090fbce8cb3889d69ce10bfa07b74b928425af2f9a4ca47bf60d1d1889
5
5
  SHA512:
6
- metadata.gz: 6ee1d39f9e9b12ebdb2d3f1f9c4066ef2f7707329dfe10ab3330fc33b9e943ae9560028ec467203efad12bad567eb5b226ae4669be0b378bf1897a3ca84fbe37
7
- data.tar.gz: a989b4a83745234e88e5c1ba5f26bda2d57be9ec0f61d26d4ece75f5db68bc88130fad68ec9fd43c6a4a44ae263231b65c8a0235bd34188b9c875f377ad9ddee
6
+ metadata.gz: 4ec5337779c9eece9610d73a037662d36d2dc9e50cfc77dd2762fb323f90dcb6edca41ee1337c12074243b1a1587450753a8afce49f5923aaf4ee1f88cc0bb2b
7
+ data.tar.gz: 84a9967bac7c2fb07d7c0f7530f1c4063a687019cc66e5787d0d69b1973884cf7e32d245ff2e99cb258abb963ae037f68e7dcc84c78d5c8f1b253d5b2f0b123a
@@ -1,4 +1,4 @@
1
- # typed: true
1
+ # typed: false
2
2
  module SafeType
3
3
  class CoercionError < StandardError; end
4
4
  end
@@ -15,13 +15,3 @@ module TypeCoerce
15
15
  class CoercionError < SafeType::CoercionError; end
16
16
  class ShapeError < SafeType::CoercionError; end
17
17
  end
18
-
19
- module T
20
- module Private
21
- module Types
22
- class TypeAlias
23
- def aliased_type; end
24
- end
25
- end
26
- end
27
- end
@@ -1,23 +1,8 @@
1
- # typed: strict
2
- require_relative 'configuration'
3
- require 'private/converter'
4
- require 'safe_type'
1
+ # typed: ignore
2
+ require 'sorbet-coerce/converter'
5
3
 
6
4
  module TypeCoerce
7
- class CoercionError < SafeType::CoercionError; end
8
- class ShapeError < SafeType::CoercionError; end
9
-
10
- define_singleton_method(:[]) do |type|
11
- Class.new(TypeCoerce::Private::Converter) do
12
- define_method(:to_s) { "#{name}#[#{type.to_s}]" }
13
-
14
- define_method(:from) do |args, raise_coercion_error: nil|
15
- if raise_coercion_error.nil?
16
- raise_coercion_error = TypeCoerce::Configuration.raise_coercion_error
17
- end
18
-
19
- T.send('let', send('_convert', args, type, raise_coercion_error), type)
20
- end
21
- end
5
+ def self.[](type)
6
+ TypeCoerce::Converter.new(type)
22
7
  end
23
8
  end
@@ -0,0 +1,183 @@
1
+ # typed: ignore
2
+ require 'polyfill'
3
+ require 'safe_type'
4
+ require 'set'
5
+ require 'sorbet-runtime'
6
+ require 'sorbet-coerce/configuration'
7
+
8
+ using Polyfill(Hash: %w[#slice])
9
+
10
+ module TypeCoerce
11
+ class CoercionError < SafeType::CoercionError; end
12
+ class ShapeError < SafeType::CoercionError; end
13
+ end
14
+
15
+ class TypeCoerce::Converter
16
+ def initialize(type)
17
+ @type = type
18
+ end
19
+
20
+ def new
21
+ self
22
+ end
23
+
24
+ def to_s
25
+ "#{name}#[#{@type.to_s}]"
26
+ end
27
+
28
+ def from(args, raise_coercion_error: nil)
29
+ if raise_coercion_error.nil?
30
+ raise_coercion_error = TypeCoerce::Configuration.raise_coercion_error
31
+ end
32
+
33
+ T.let(_convert(args, @type, raise_coercion_error), @type)
34
+ end
35
+
36
+ private
37
+
38
+ PRIMITIVE_TYPES = T.let(::Set[
39
+ Date,
40
+ DateTime,
41
+ Float,
42
+ Integer,
43
+ String,
44
+ Symbol,
45
+ Time,
46
+ ], T.untyped)
47
+
48
+ def _convert(value, type, raise_coercion_error)
49
+ if type.is_a?(T::Types::Untyped)
50
+ value
51
+ elsif type.is_a?(T::Types::TypedArray)
52
+ _convert_to_a(value, type.type, raise_coercion_error)
53
+ elsif type.is_a?(T::Types::TypedSet)
54
+ Set.new(_convert_to_a(value, type.type, raise_coercion_error))
55
+ elsif type.is_a?(T::Types::Simple)
56
+ _convert(value, type.raw_type, raise_coercion_error)
57
+ elsif type.is_a?(T::Types::Union)
58
+ true_idx = T.let(nil, T.nilable(Integer))
59
+ false_idx = T.let(nil, T.nilable(Integer))
60
+ nil_idx = T.let(nil, T.nilable(Integer))
61
+
62
+ type.types.each_with_index do |t, i|
63
+ nil_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == NilClass
64
+ true_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == TrueClass
65
+ false_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == FalseClass
66
+ end
67
+
68
+ raise ArgumentError.new(
69
+ 'the only supported union types are T.nilable and T::Boolean',
70
+ ) unless (
71
+ (!true_idx.nil? && !false_idx.nil? && !nil_idx.nil?) || # T.nilable(T::Boolean)
72
+ (type.types.length == 2 && (
73
+ !nil_idx.nil? || (!true_idx.nil? && !false_idx.nil?) # T.nilable || T::Boolean
74
+ ))
75
+ )
76
+
77
+ if !true_idx.nil? && !false_idx.nil?
78
+ _convert_simple(value, T::Boolean, raise_coercion_error)
79
+ else
80
+ _convert(value, type.types[nil_idx == 0 ? 1 : 0], raise_coercion_error)
81
+ end
82
+ elsif type.is_a?(T::Types::TypedHash)
83
+ return {} if _nil_like?(value, type)
84
+
85
+ unless value.respond_to?(:map)
86
+ raise TypeCoerce::ShapeError.new(value, type)
87
+ end
88
+
89
+ value.map do |k, v|
90
+ [
91
+ _convert(k, type.keys, raise_coercion_error),
92
+ _convert(v, type.values, raise_coercion_error),
93
+ ]
94
+ end.to_h
95
+ elsif Object.const_defined?('T::Private::Types::TypeAlias') &&
96
+ 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)
107
+ else
108
+ return value if value.is_a?(type)
109
+
110
+ _convert_simple(value, type, raise_coercion_error)
111
+ end
112
+ end
113
+
114
+ def _convert_enum(value, type, raise_coercion_error)
115
+ if raise_coercion_error
116
+ type.deserialize(value)
117
+ else
118
+ type.try_deserialize(value)
119
+ end
120
+ rescue KeyError
121
+ raise TypeCoerce::CoercionError.new(value, type)
122
+ end
123
+
124
+ def _convert_simple(value, type, raise_coercion_error)
125
+ return nil if _nil_like?(value, type)
126
+
127
+ safe_type_rule = T.let(nil, T.untyped)
128
+
129
+ if type == T::Boolean
130
+ safe_type_rule = SafeType::Boolean.strict
131
+ elsif value.is_a?(type)
132
+ return value
133
+ elsif PRIMITIVE_TYPES.include?(type)
134
+ safe_type_rule = SafeType.const_get(type.name).strict
135
+ else
136
+ safe_type_rule = type
137
+ end
138
+
139
+ if safe_type_rule.is_a?(SafeType::Rule)
140
+ SafeType::coerce(value, safe_type_rule)
141
+ else
142
+ type.new(value)
143
+ end
144
+ rescue SafeType::EmptyValueError, SafeType::CoercionError
145
+ if raise_coercion_error
146
+ raise TypeCoerce::CoercionError.new(value, type)
147
+ else
148
+ nil
149
+ end
150
+ end
151
+
152
+ def _convert_to_a(ary, type, raise_coercion_error)
153
+ return [] if _nil_like?(ary, type)
154
+
155
+ unless ary.respond_to?(:map)
156
+ raise TypeCoerce::ShapeError.new(ary, type)
157
+ end
158
+
159
+ ary.map { |value| _convert(value, type, raise_coercion_error) }
160
+ end
161
+
162
+ def _build_args(args, type, raise_coercion_error)
163
+ return {} if _nil_like?(args, Hash)
164
+
165
+ unless args.respond_to?(:each_pair)
166
+ raise TypeCoerce::ShapeError.new(args, type)
167
+ end
168
+
169
+ props = type.props
170
+ args.map { |name, value|
171
+ key = name.to_sym
172
+ [
173
+ key,
174
+ (!props.include?(key) || value.nil?) ?
175
+ nil : _convert(value, props[key][:type], raise_coercion_error),
176
+ ]
177
+ }.to_h.slice(*props.keys)
178
+ end
179
+
180
+ def _nil_like?(value, type)
181
+ value.nil? || (value == '' && type != String)
182
+ end
183
+ end
@@ -31,6 +31,21 @@ describe TypeCoerce do
31
31
  const :myhash, T::Hash[String, Integer]
32
32
  end
33
33
 
34
+ class HashParamsWithDefault < T::Struct
35
+ const :myhash, T::Hash[String, Integer], default: Hash['a' => 1]
36
+ end
37
+
38
+ class TestEnum < T::Enum
39
+ enums do
40
+ Test = new
41
+ Other = new
42
+ end
43
+ end
44
+
45
+ class WithEnum < T::Struct
46
+ const :myenum, TestEnum
47
+ end
48
+
34
49
  class CustomType
35
50
  attr_reader :a
36
51
 
@@ -95,6 +110,7 @@ describe TypeCoerce do
95
110
  expect(param.info.name).to eql 'mango'
96
111
  expect(param.info.skill_ids).to eql [123, 456]
97
112
  expect(param.opt.notes).to eql []
113
+ expect(TypeCoerce[Param].new.from(param)).to eq(param)
98
114
 
99
115
  expect(param2.id).to eql 2
100
116
  expect(param2.info.name).to eql 'honeydew'
@@ -159,8 +175,10 @@ describe TypeCoerce do
159
175
 
160
176
  context 'when given custom types' do
161
177
  it 'coerces correctly' do
162
- T.assert_type!(TypeCoerce[CustomType].new.from(a: 1), CustomType)
163
- expect(TypeCoerce[CustomType].new.from(1).a).to be 1
178
+ obj = TypeCoerce[CustomType].new.from(1)
179
+ T.assert_type!(obj, CustomType)
180
+ expect(obj.a).to be 1
181
+ expect(TypeCoerce[CustomType].new.from(obj)).to be obj
164
182
 
165
183
  expect{TypeCoerce[UnsupportedCustomType].new.from(1)}.to raise_error(ArgumentError)
166
184
  # CustomType2.new(anything) returns Integer 1; 1.is_a?(CustomType2) == false
@@ -194,6 +212,8 @@ describe TypeCoerce do
194
212
 
195
213
  context 'when dealing with hashes' do
196
214
  it 'coreces correctly' do
215
+ expect(TypeCoerce[T::Hash[T.untyped, T.untyped]].new.from(nil)).to eql({})
216
+
197
217
  expect(TypeCoerce[T::Hash[String, T::Boolean]].new.from({
198
218
  a: 'true',
199
219
  b: 'false',
@@ -206,6 +226,7 @@ describe TypeCoerce do
206
226
  myhash: {'a' => '1', 'b' => '2'},
207
227
  }).myhash).to eql({'a' => 1, 'b' => 2})
208
228
 
229
+ expect(TypeCoerce[HashParamsWithDefault].new.from({}).myhash).to eql({'a' => 1})
209
230
 
210
231
  expect {
211
232
  TypeCoerce[T::Hash[String, T::Boolean]].new.from({
@@ -244,6 +265,24 @@ describe TypeCoerce do
244
265
  end
245
266
  end
246
267
 
268
+ context 'when dealing with enums' do
269
+ it 'coerces a serialized enum correctly' do
270
+ coerced = TypeCoerce[WithEnum].new.from(myenum: "test")
271
+ expect(coerced.myenum).to eq(TestEnum::Test)
272
+ end
273
+
274
+ it 'handles a real enum correctly' do
275
+ coerced = TypeCoerce[WithEnum].new.from(myenum: TestEnum::Test)
276
+ expect(coerced.myenum).to eq(TestEnum::Test)
277
+ end
278
+
279
+ it 'handles bad enum' do
280
+ expect {
281
+ TypeCoerce[WithEnum].new.from(myenum: "bad_key")
282
+ }.to raise_error(TypeCoerce::CoercionError)
283
+ end
284
+ end
285
+
247
286
  it 'works with T.untyped' do
248
287
  expect(TypeCoerce[T.untyped].new.from(1)).to eql 1
249
288
 
@@ -75,5 +75,14 @@ describe TypeCoerce do
75
75
  T.nilable(T::Array[T.nilable(T::Array[T.nilable(User)])])
76
76
  ].new.from([[{id: '1'}]]).flatten.map(&:id)).to eql([1])
77
77
  end
78
+
79
+ it 'works with nested T::Hash' do
80
+ expect(
81
+ TypeCoerce[T::Hash[Symbol, T::Hash[Symbol, Integer]]].new.from({
82
+ a: nil,
83
+ b: {c: '1'}
84
+ })
85
+ ).to eql({a: {}, b: {c: 1}})
86
+ end
78
87
  end
79
88
  end
@@ -4,6 +4,12 @@ require 'sorbet-runtime'
4
4
 
5
5
  describe TypeCoerce do
6
6
  context 'when type errors are soft errors' do
7
+ class SoftErrorTestEnum < T::Enum
8
+ enums do
9
+ Other = new
10
+ end
11
+ end
12
+
7
13
  let(:ignore_error) { Proc.new {} }
8
14
 
9
15
  before(:each) do
@@ -48,6 +54,7 @@ describe TypeCoerce do
48
54
 
49
55
  it 'works as expected' do
50
56
  expect(TypeCoerce[Integer].new.from(invalid_arg)).to eql(nil)
57
+ expect(TypeCoerce[SoftErrorTestEnum].new.from('bad_key')).to eql(nil)
51
58
 
52
59
  expect{TypeCoerce[T::Array[Integer]].new.from(1)}.to raise_error(TypeCoerce::ShapeError)
53
60
  expect(TypeCoerce[T::Array[Integer]].new.from({a: 1})).to eql([nil])
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.2.3
4
+ version: 0.3.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:
@@ -118,10 +118,10 @@ executables: []
118
118
  extensions: []
119
119
  extra_rdoc_files: []
120
120
  files:
121
- - lib/configuration.rb
122
- - lib/private/converter.rb
121
+ - lib/bundled_rbi/sorbet-coerce.rbi
123
122
  - lib/sorbet-coerce.rb
124
- - rbi/sorbet-coerce.rbi
123
+ - lib/sorbet-coerce/configuration.rb
124
+ - lib/sorbet-coerce/converter.rb
125
125
  - spec/coerce_spec.rb
126
126
  - spec/nested_spec.rb
127
127
  - spec/soft_error_spec.rb
@@ -146,8 +146,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
146
  - !ruby/object:Gem::Version
147
147
  version: '0'
148
148
  requirements: []
149
- rubyforge_project:
150
- rubygems_version: 2.7.7
149
+ rubygems_version: 3.0.8
151
150
  signing_key:
152
151
  specification_version: 4
153
152
  summary: A type coercion lib works with Sorbet's static type checker and type definitions;
@@ -1,144 +0,0 @@
1
- # typed: strict
2
- require 'safe_type'
3
- require 'set'
4
- require 'sorbet-runtime'
5
- require 'polyfill'
6
-
7
- using Polyfill(Hash: %w[#slice])
8
-
9
- module TypeCoerce; end
10
-
11
- module TypeCoerce::Private
12
- class Converter
13
- extend T::Sig
14
-
15
- PRIMITIVE_TYPES = T.let(::Set[
16
- Date,
17
- DateTime,
18
- Float,
19
- Integer,
20
- String,
21
- Symbol,
22
- Time,
23
- ], T.untyped)
24
-
25
- protected
26
- sig { params(value: T.untyped, type: T.untyped, raise_coercion_error: T::Boolean).returns(T.untyped) }
27
- def _convert(value, type, raise_coercion_error)
28
- if type.is_a?(T::Types::Untyped)
29
- value
30
- elsif type.is_a?(T::Types::TypedArray)
31
- _convert_to_a(value, type.type, raise_coercion_error)
32
- elsif type.is_a?(T::Types::TypedSet)
33
- Set.new(_convert_to_a(value, type.type, raise_coercion_error))
34
- elsif type.is_a?(T::Types::Simple)
35
- _convert(value, type.raw_type, raise_coercion_error)
36
- elsif type.is_a?(T::Types::Union)
37
- true_idx = T.let(nil, T.nilable(Integer))
38
- false_idx = T.let(nil, T.nilable(Integer))
39
- nil_idx = T.let(nil, T.nilable(Integer))
40
-
41
- type.types.each_with_index do |t, i|
42
- nil_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == NilClass
43
- true_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == TrueClass
44
- false_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == FalseClass
45
- end
46
-
47
- raise ArgumentError.new(
48
- 'the only supported union types are T.nilable and T::Boolean',
49
- ) unless (
50
- (!true_idx.nil? && !false_idx.nil? && !nil_idx.nil?) || # T.nilable(T::Boolean)
51
- (type.types.length == 2 && (
52
- !nil_idx.nil? || (!true_idx.nil? && !false_idx.nil?) # T.nilable || T::Boolean
53
- ))
54
- )
55
-
56
- if !true_idx.nil? && !false_idx.nil?
57
- _convert_simple(value, T::Boolean, raise_coercion_error)
58
- else
59
- _convert(value, type.types[nil_idx == 0 ? 1 : 0], raise_coercion_error)
60
- end
61
- elsif type.is_a?(T::Types::TypedHash)
62
- unless value.respond_to?(:map)
63
- raise TypeCoerce::ShapeError.new(value, type)
64
- end
65
-
66
- value.map do |k, v|
67
- [
68
- _convert(k, type.keys, raise_coercion_error),
69
- _convert(v, type.values, raise_coercion_error),
70
- ]
71
- end.to_h
72
- elsif Object.const_defined?('T::Private::Types::TypeAlias') &&
73
- type.is_a?(T::Private::Types::TypeAlias)
74
- _convert(value, type.aliased_type, raise_coercion_error)
75
- elsif type < T::Struct
76
- args = _build_args(value, type, raise_coercion_error)
77
- type.new(args)
78
- else
79
- _convert_simple(value, type, raise_coercion_error)
80
- end
81
- end
82
-
83
- sig { params(value: T.untyped, type: T.untyped, raise_coercion_error: T::Boolean).returns(T.untyped) }
84
- def _convert_simple(value, type, raise_coercion_error)
85
- return nil if _nil_like?(value, type)
86
-
87
- safe_type_rule = T.let(nil, T.untyped)
88
-
89
- if type == T::Boolean
90
- safe_type_rule = SafeType::Boolean.strict
91
- elsif value.is_a?(type)
92
- return value
93
- elsif PRIMITIVE_TYPES.include?(type)
94
- safe_type_rule = SafeType.const_get(type.name).strict
95
- else
96
- safe_type_rule = type
97
- end
98
- SafeType::coerce(value, safe_type_rule)
99
- rescue SafeType::EmptyValueError, SafeType::CoercionError
100
- if raise_coercion_error
101
- raise TypeCoerce::CoercionError.new(value, type)
102
- else
103
- nil
104
- end
105
- rescue SafeType::InvalidRuleError
106
- type.new(value)
107
- end
108
-
109
- sig { params(ary: T.untyped, type: T.untyped, raise_coercion_error: T::Boolean).returns(T.untyped) }
110
- def _convert_to_a(ary, type, raise_coercion_error)
111
- return [] if _nil_like?(ary, type)
112
-
113
- unless ary.respond_to?(:map)
114
- raise TypeCoerce::ShapeError.new(ary, type)
115
- end
116
-
117
- ary.map { |value| _convert(value, type, raise_coercion_error) }
118
- end
119
-
120
- sig { params(args: T.untyped, type: T.untyped, raise_coercion_error: T::Boolean).returns(T.untyped) }
121
- def _build_args(args, type, raise_coercion_error)
122
- return {} if _nil_like?(args, Hash)
123
-
124
- unless args.respond_to?(:each_pair)
125
- raise TypeCoerce::ShapeError.new(args, type)
126
- end
127
-
128
- props = type.props
129
- args.map { |name, value|
130
- key = name.to_sym
131
- [
132
- key,
133
- (!props.include?(key) || value.nil?) ?
134
- nil : _convert(value, props[key][:type], raise_coercion_error),
135
- ]
136
- }.to_h.slice(*props.keys)
137
- end
138
-
139
- sig { params(value: T.untyped, type: T.untyped).returns(T::Boolean) }
140
- def _nil_like?(value, type)
141
- value.nil? || (value == '' && type != String)
142
- end
143
- end
144
- end