sorbet-coerce 0.1.1 → 0.1.6

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: 66e3f4b05deb745fbcff9b66bbb7ddfd1d10529ce292e9d27eaacc9bb7b8983f
4
- data.tar.gz: 8a3e7c12cf1233854e369f3c6d02f3182ed4977ffee07a8bdd5bc223f02d216b
3
+ metadata.gz: 9749ed0f8347a42911400212d8a30279876e10b1983daabc16167f819fdd76bd
4
+ data.tar.gz: 2866bbe6175228c14b563928bf44521608486ca7c1cb2aa6dc4bbf19aafd0bcf
5
5
  SHA512:
6
- metadata.gz: cbea704d16c04d30e695c5d65c0963e31288f1a70b5ba205fac12ca48bc65e8977882450927dc062ab26eab6d68c9f0861bf5f1931dec1ba9ebbe55a068c5f83
7
- data.tar.gz: '09dda06d9da527001aca7fbbb4bd311385647317150a2efb0fd4fa07e1bfaed22130d4e0d36c4d35e17908cb972203764039a9537314db0507cea0ae595652ba'
6
+ metadata.gz: d8f9f8e5086248a4db00ef26a0285fb9363817f552f77658d0ca8c0b5c373221df6c67116b9735fcd33d41831f6c49d139dfd1608e44adbe0cd5168335b863c5
7
+ data.tar.gz: a159fd6c75c112e71ad4e801ae759b01051f1a1e0ef2d02f875e40b4444b25b098a9049f3ab7dea596b5373c5e86759ba3a78da622978ef8147ba4883a51c444
@@ -53,13 +53,12 @@ module T::Private
53
53
  else
54
54
  _convert(value, type.types[nil_idx == 0 ? 1 : 0])
55
55
  end
56
+ elsif Object.const_defined?('T::Private::Types::TypeAlias') &&
57
+ type.is_a?(T::Private::Types::TypeAlias)
58
+ _convert(value, type.aliased_type)
56
59
  elsif type < T::Struct
57
60
  args = _build_args(value, type.props)
58
- begin
59
- type.new(args)
60
- rescue T::Props::InvalidValueError, ArgumentError
61
- nil
62
- end
61
+ type.new(args)
63
62
  else
64
63
  _convert_simple(value, type)
65
64
  end
@@ -67,7 +66,8 @@ module T::Private
67
66
 
68
67
  sig { params(value: T.untyped, type: T.untyped).returns(T.untyped) }
69
68
  def _convert_simple(value, type)
70
- return nil if value.nil?
69
+ return nil if _nil_like?(value, type)
70
+
71
71
  safe_type_rule = T.let(nil, T.untyped)
72
72
 
73
73
  if type == T::Boolean
@@ -81,30 +81,23 @@ module T::Private
81
81
  end
82
82
  SafeType::coerce(value, safe_type_rule)
83
83
  rescue SafeType::EmptyValueError, SafeType::CoercionError
84
- nil
84
+ value
85
85
  rescue SafeType::InvalidRuleError
86
- begin
87
- type.new(value)
88
- rescue
89
- nil
90
- end
86
+ type.new(value)
91
87
  end
92
88
 
93
89
  sig { params(ary: T.untyped, type: T.untyped).returns(T.untyped) }
94
90
  def _convert_to_a(ary, type)
95
- ary = [ary] unless ary.is_a?(::Array)
96
- T.send(
97
- 'let',
98
- ary.map { |value| _convert(value, type) },
99
- T.const_get('Array')[type],
100
- )
101
- rescue TypeError
102
- []
91
+ return [] if _nil_like?(ary, type)
92
+
93
+ # Checked by the T.let at root
94
+ ary.respond_to?(:map) ? ary.map { |value| _convert(value, type) } : ary
103
95
  end
104
96
 
105
97
  sig { params(args: T.untyped, props: T.untyped).returns(T.untyped) }
106
98
  def _build_args(args, props)
107
- return {} if args.nil?
99
+ return {} if _nil_like?(args, Hash)
100
+
108
101
  args.map { |name, value|
109
102
  key = name.to_sym
110
103
  [
@@ -114,5 +107,10 @@ module T::Private
114
107
  ]
115
108
  }.to_h.slice(*props.keys)
116
109
  end
110
+
111
+ sig { params(value: T.untyped, type: T.untyped).returns(T::Boolean) }
112
+ def _nil_like?(value, type)
113
+ value.nil? || (value == '' && type != String)
114
+ end
117
115
  end
118
116
  end
@@ -1,5 +1,4 @@
1
- # typed: strong
2
-
1
+ # typed: true
3
2
  module T
4
3
  module Coerce
5
4
  extend T::Sig
@@ -10,4 +9,12 @@ module T
10
9
  sig { params(args: T.untyped).returns(Elem) }
11
10
  def from(args); end
12
11
  end
12
+
13
+ module Private
14
+ module Types
15
+ class TypeAlias
16
+ def aliased_type; end
17
+ end
18
+ end
19
+ end
13
20
  end
@@ -0,0 +1,193 @@
1
+ # typed: ignore
2
+ require 'sorbet-coerce'
3
+ require 'sorbet-runtime'
4
+
5
+ describe T::Coerce do
6
+ context 'when given T::Struct' do
7
+ class ParamInfo < T::Struct
8
+ const :name, String
9
+ const :lvl, T.nilable(Integer)
10
+ const :skill_ids, T::Array[Integer]
11
+ end
12
+
13
+ class ParamInfo2 < T::Struct
14
+ const :a, Integer
15
+ const :b, Integer
16
+ const :notes, T::Array[String], default: []
17
+ end
18
+
19
+ class Param < T::Struct
20
+ const :id, Integer
21
+ const :role, String, default: 'wizard'
22
+ const :info, ParamInfo
23
+ const :opt, T.nilable(ParamInfo2)
24
+ end
25
+
26
+ class DefaultParams < T::Struct
27
+ const :a, Integer, default: 1
28
+ end
29
+
30
+ class CustomType
31
+ attr_reader :a
32
+
33
+ def initialize(a)
34
+ @a = a
35
+ end
36
+ end
37
+
38
+ class CustomType2
39
+ def self.new(a); 1; end
40
+ end
41
+
42
+ class UnsupportedCustomType
43
+ # Does not respond to new
44
+ end
45
+
46
+ let!(:param) {
47
+ T::Coerce[Param].new.from({
48
+ id: 1,
49
+ info: {
50
+ name: 'mango',
51
+ lvl: 100,
52
+ skill_ids: ['123', '456'],
53
+ },
54
+ opt: {
55
+ a: 1,
56
+ b: 2,
57
+ },
58
+ extra_attr: 'does not matter',
59
+ })
60
+ }
61
+
62
+ let!(:param2) {
63
+ T::Coerce[Param].new.from({
64
+ id: '2',
65
+ info: {
66
+ name: 'honeydew',
67
+ lvl: '5',
68
+ skill_ids: [],
69
+ },
70
+ opt: {
71
+ a: '1',
72
+ b: '2',
73
+ notes: [],
74
+ },
75
+ })
76
+ }
77
+
78
+ it 'reveals the right type' do
79
+ T.assert_type!(param, Param)
80
+ T.assert_type!(param.id, Integer)
81
+ T.assert_type!(param.info, ParamInfo)
82
+ T.assert_type!(param.info.name,String)
83
+ T.assert_type!(param.info.lvl, Integer)
84
+ T.assert_type!(param.opt, T.nilable(ParamInfo2))
85
+ end
86
+
87
+ it 'coerces correctly' do
88
+ expect(param.id).to eql 1
89
+ expect(param.role).to eql 'wizard'
90
+ expect(param.info.lvl).to eql 100
91
+ expect(param.info.name).to eql 'mango'
92
+ expect(param.info.skill_ids).to eql [123, 456]
93
+ expect(param.opt.notes).to eql []
94
+
95
+ expect(param2.id).to eql 2
96
+ expect(param2.info.name).to eql 'honeydew'
97
+ expect(param2.info.lvl).to eql 5
98
+ expect(param2.info.skill_ids).to eql []
99
+ expect(param2.opt.a).to eql 1
100
+ expect(param2.opt.b).to eql 2
101
+ expect(param2.opt.notes).to eql []
102
+
103
+ expect {
104
+ T::Coerce[Param].new.from({
105
+ id: 3,
106
+ info: {
107
+ # missing required name
108
+ lvl: 2,
109
+ },
110
+ })
111
+ }.to raise_error(ArgumentError)
112
+
113
+ expect(T::Coerce[DefaultParams].new.from(nil).a).to be 1
114
+ expect(T::Coerce[DefaultParams].new.from('').a).to be 1
115
+ end
116
+ end
117
+
118
+ context 'when the given T::Struct is invalid' do
119
+ class Param2 < T::Struct
120
+ const :id, Integer
121
+ const :info, T.any(Integer, String)
122
+ end
123
+
124
+ it 'raises an error' do
125
+ expect {
126
+ T::Coerce[Param2].new.from({id: 1, info: 1})
127
+ }.to raise_error(ArgumentError)
128
+ end
129
+ end
130
+
131
+ context 'when given primitive types' do
132
+ it 'reveals the right type' do
133
+ T.assert_type!(T::Coerce[Integer].new.from(1), Integer)
134
+ T.assert_type!(T::Coerce[Integer].new.from('1.0'), Integer)
135
+ T.assert_type!(T::Coerce[T.nilable(Integer)].new.from(nil), T.nilable(Integer))
136
+ end
137
+
138
+ it 'coreces correctly' do
139
+ expect{T::Coerce[Integer].new.from(nil)}.to raise_error(T::CoercionError)
140
+ expect(T::Coerce[T.nilable(Integer)].new.from(nil) || 1).to eql 1
141
+ expect(T::Coerce[Integer].new.from(2)).to eql 2
142
+ expect(T::Coerce[Integer].new.from('1.0')).to eql 1
143
+
144
+ expect{T::Coerce[T.nilable(Integer)].new.from('invalid integer string')}.to raise_error(T::CoercionError)
145
+ expect(T::Coerce[Float].new.from('1.0')).to eql 1.0
146
+
147
+ expect(T::Coerce[T::Boolean].new.from('false')).to be false
148
+ expect(T::Coerce[T::Boolean].new.from('true')).to be true
149
+
150
+ expect(T::Coerce[T.nilable(Integer)].new.from('')).to be nil
151
+ expect{T::Coerce[T.nilable(Integer)].new.from([])}.to raise_error(T::CoercionError)
152
+ expect(T::Coerce[T.nilable(String)].new.from('')).to eql ''
153
+ end
154
+ end
155
+
156
+ context 'when given custom types' do
157
+ it 'coerces correctly' do
158
+ T.assert_type!(T::Coerce[CustomType].new.from(a: 1), CustomType)
159
+ expect(T::Coerce[CustomType].new.from(1).a).to be 1
160
+
161
+ expect{T::Coerce[UnsupportedCustomType].new.from(1)}.to raise_error(ArgumentError)
162
+ expect{T::Coerce[CustomType2].new.from(1)}.to raise_error(T::CoercionError)
163
+ end
164
+ end
165
+
166
+ context 'when dealing with arries' do
167
+ it 'coreces correctly' do
168
+ expect(T::Coerce[T::Array[Integer]].new.from(nil)).to eql []
169
+ expect(T::Coerce[T::Array[Integer]].new.from('')).to eql []
170
+ expect{T::Coerce[T::Array[Integer]].new.from('not an array')}.to raise_error(T::CoercionError)
171
+ expect{T::Coerce[T::Array[Integer]].new.from('1')}.to raise_error(T::CoercionError)
172
+ expect(T::Coerce[T::Array[Integer]].new.from(['1', '2', '3'])).to eql [1, 2, 3]
173
+ expect{T::Coerce[T::Array[Integer]].new.from(['1', 'invalid', '3'])}.to raise_error(T::CoercionError)
174
+ expect{T::Coerce[T::Array[Integer]].new.from({a: 1})}.to raise_error(T::CoercionError)
175
+
176
+ infos = T::Coerce[T::Array[ParamInfo]].new.from([{name: 'a', skill_ids: []}])
177
+ T.assert_type!(infos, T::Array[ParamInfo])
178
+ expect(infos.first.name).to eql 'a'
179
+
180
+ infos = T::Coerce[T::Array[ParamInfo]].new.from([{name: 'b', skill_ids: []}])
181
+ T.assert_type!(infos, T::Array[ParamInfo])
182
+ expect(infos.first.name).to eql 'b'
183
+ end
184
+ end
185
+
186
+ context 'when given a type alias' do
187
+ MyType = T.type_alias(T::Boolean)
188
+
189
+ it 'coerces correctly' do
190
+ expect(T::Coerce[MyType].new.from('false')).to be false
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,79 @@
1
+ # typed: false
2
+ require 'sorbet-coerce'
3
+ require 'sorbet-runtime'
4
+
5
+ describe T::Coerce do
6
+ context 'when given nested types' do
7
+ class User < T::Struct
8
+ const :id, Integer
9
+ const :valid, T.nilable(T::Boolean)
10
+ end
11
+
12
+ class NestedParam < T::Struct
13
+ const :users, T::Array[User]
14
+ const :params, T.nilable(NestedParam)
15
+ end
16
+
17
+ it 'works with nest T::Struct' do
18
+ converted = T::Coerce[NestedParam].new.from({
19
+ users: [{id: '1'}],
20
+ params: {
21
+ users: [{id: '2', valid: 'true'}],
22
+ params: {
23
+ users: [{id: '3', valid: 'true'}],
24
+ },
25
+ },
26
+ })
27
+ # => <NestedParam
28
+ # params=<NestedParam
29
+ # params=<NestedParam
30
+ # params=nil,
31
+ # users=[<User id=3 valid=true>]
32
+ # >,
33
+ # users=[<User id=2 valid=true>]
34
+ # >,
35
+ # users=[<User id=1 valid=nil>]
36
+ # >
37
+
38
+ expect(converted.users.map(&:id)).to eql([1])
39
+ expect(converted.params.users.map(&:id)).to eql([2])
40
+ expect(converted.params.params.users.map(&:id)).to eql([3])
41
+ end
42
+
43
+ it 'works with nest T::Array' do
44
+ expect {
45
+ T::Coerce[T::Array[T.nilable(Integer)]].new.from(['1', 'invalid', '3'])
46
+ }.to raise_error(T::CoercionError)
47
+ expect(
48
+ T::Coerce[T::Array[T::Array[Integer]]].new.from([nil])
49
+ ).to eql([[]])
50
+ expect(
51
+ T::Coerce[T::Array[T::Array[Integer]]].new.from([['1'], ['2'], ['3']]),
52
+ ).to eql [[1], [2], [3]]
53
+
54
+ expect(T::Coerce[
55
+ T::Array[
56
+ T::Array[
57
+ T::Array[User]
58
+ ]
59
+ ]
60
+ ].new.from([[[{id: '1'}]]]).flatten.first.id).to eql(1)
61
+
62
+ expect(T::Coerce[
63
+ T::Array[
64
+ T::Array[
65
+ T::Array[
66
+ T::Array[
67
+ T::Array[User]
68
+ ]
69
+ ]
70
+ ]
71
+ ]
72
+ ].new.from([[[[[{id: 1}]]]]]).flatten.first.id).to eql 1
73
+
74
+ expect(T::Coerce[
75
+ T.nilable(T::Array[T.nilable(T::Array[T.nilable(User)])])
76
+ ].new.from([[{id: '1'}]]).flatten.map(&:id)).to eql([1])
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,54 @@
1
+ # typed: false
2
+ require 'sorbet-coerce'
3
+ require 'sorbet-runtime'
4
+
5
+ describe T::Coerce do
6
+ context 'when type errors are soft errors' do
7
+ let(:ignore_error) { Proc.new {} }
8
+
9
+ before(:each) do
10
+ allow(T::Configuration).to receive(
11
+ :inline_type_error_handler,
12
+ ).and_return(ignore_error)
13
+
14
+ allow(T::Configuration).to receive(
15
+ :call_validation_error_handler,
16
+ ).and_return(ignore_error)
17
+
18
+ allow(T::Configuration).to receive(
19
+ :sig_builder_error_handler,
20
+ ).and_return(ignore_error)
21
+ end
22
+
23
+ class ParamsWithSortError < T::Struct
24
+ const :a, Integer
25
+ end
26
+
27
+ class CustomTypeRaisesHardError
28
+ def initialize(value)
29
+ raise StandardError.new('value cannot be 1') if value == 1
30
+ end
31
+ end
32
+
33
+ class CustomTypeDoesNotRiaseHardError
34
+ def self.new(a); 1; end
35
+ end
36
+
37
+ it 'works as expected' do
38
+ invalid_arg = 'invalid integer string'
39
+ expect(T::Coerce[Integer].new.from(invalid_arg)).to eql(invalid_arg)
40
+ expect(T::Coerce[T::Array[Integer]].new.from(1)).to be 1
41
+ expect(T::Coerce[T::Array[Integer]].new.from(invalid_arg)).to eql(invalid_arg)
42
+ expect(T::Coerce[T::Array[Integer]].new.from({a: 1})).to eql([[:a, 1]])
43
+
44
+ expect {
45
+ T::Coerce[CustomTypeRaisesHardError].new.from(1)
46
+ }.to raise_error(StandardError)
47
+ expect(T::Coerce[CustomTypeDoesNotRiaseHardError].new.from(1)).to eql(1)
48
+
49
+ if Gem.loaded_specs['sorbet-runtime'].version >= Gem::Version.new('0.4.4948')
50
+ expect(T::Coerce[ParamsWithSortError].new.from({a: invalid_arg}).a).to eql(invalid_arg)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ require 'byebug'
3
+ require 'simplecov'
4
+
5
+ SimpleCov.start
6
+
7
+ if ENV['CI'] == 'true'
8
+ require 'codecov'
9
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
10
+ end
11
+
12
+ RSpec.configure do |config|
13
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
14
+ end
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.1.1
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chan Zuckerberg Initiative
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 0.4.4704
27
+ - !ruby/object:Gem::Dependency
28
+ name: polyfill
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.8'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.8'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: safe_type
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -62,40 +76,40 @@ dependencies:
62
76
  name: rspec
63
77
  requirement: !ruby/object:Gem::Requirement
64
78
  requirements:
65
- - - ">="
79
+ - - "~>"
66
80
  - !ruby/object:Gem::Version
67
81
  version: '3.8'
68
- - - "~>"
82
+ - - ">="
69
83
  - !ruby/object:Gem::Version
70
84
  version: '3.8'
71
85
  type: :development
72
86
  prerelease: false
73
87
  version_requirements: !ruby/object:Gem::Requirement
74
88
  requirements:
75
- - - ">="
89
+ - - "~>"
76
90
  - !ruby/object:Gem::Version
77
91
  version: '3.8'
78
- - - "~>"
92
+ - - ">="
79
93
  - !ruby/object:Gem::Version
80
94
  version: '3.8'
81
95
  - !ruby/object:Gem::Dependency
82
96
  name: byebug
83
97
  requirement: !ruby/object:Gem::Requirement
84
98
  requirements:
85
- - - ">="
99
+ - - "~>"
86
100
  - !ruby/object:Gem::Version
87
101
  version: 11.0.1
88
- - - "~>"
102
+ - - ">="
89
103
  - !ruby/object:Gem::Version
90
104
  version: 11.0.1
91
105
  type: :development
92
106
  prerelease: false
93
107
  version_requirements: !ruby/object:Gem::Requirement
94
108
  requirements:
95
- - - ">="
109
+ - - "~>"
96
110
  - !ruby/object:Gem::Version
97
111
  version: 11.0.1
98
- - - "~>"
112
+ - - ">="
99
113
  - !ruby/object:Gem::Version
100
114
  version: 11.0.1
101
115
  description:
@@ -106,7 +120,11 @@ extra_rdoc_files: []
106
120
  files:
107
121
  - lib/private/converter.rb
108
122
  - lib/sorbet-coerce.rb
109
- - lib/sorbet-coerce.rbi
123
+ - rbi/sorbet-coerce.rbi
124
+ - spec/coerce_spec.rb
125
+ - spec/nested_spec.rb
126
+ - spec/soft_error_spec.rb
127
+ - spec/spec_helper.rb
110
128
  homepage: https://github.com/chanzuckerberg/sorbet-coerce
111
129
  licenses:
112
130
  - MIT
@@ -127,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
145
  version: '0'
128
146
  requirements: []
129
147
  rubyforge_project:
130
- rubygems_version: 2.7.9
148
+ rubygems_version: 2.7.7
131
149
  signing_key:
132
150
  specification_version: 4
133
151
  summary: A type coercion lib works with Sorbet's static type checker and type definitions;