sorbet-runtime 0.4.4667 → 0.5.6189
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/lib/sorbet-runtime.rb +15 -3
- data/lib/types/_types.rb +26 -17
- data/lib/types/boolean.rb +1 -1
- data/lib/types/compatibility_patches.rb +65 -10
- data/lib/types/configuration.rb +93 -7
- data/lib/types/enum.rb +371 -0
- data/lib/types/generic.rb +2 -2
- data/lib/types/interface_wrapper.rb +4 -4
- data/lib/types/non_forcing_constants.rb +61 -0
- data/lib/types/private/abstract/data.rb +2 -2
- data/lib/types/private/abstract/declare.rb +3 -0
- data/lib/types/private/abstract/validate.rb +7 -7
- data/lib/types/private/casts.rb +27 -0
- data/lib/types/private/class_utils.rb +8 -5
- data/lib/types/private/methods/_methods.rb +80 -28
- data/lib/types/private/methods/call_validation.rb +5 -47
- data/lib/types/private/methods/decl_builder.rb +14 -56
- data/lib/types/private/methods/modes.rb +5 -7
- data/lib/types/private/methods/signature.rb +32 -18
- data/lib/types/private/methods/signature_validation.rb +29 -35
- data/lib/types/private/retry.rb +10 -0
- data/lib/types/private/sealed.rb +21 -1
- data/lib/types/private/types/type_alias.rb +31 -0
- data/lib/types/private/types/void.rb +4 -3
- data/lib/types/profile.rb +5 -1
- data/lib/types/props/_props.rb +3 -7
- data/lib/types/props/constructor.rb +29 -9
- data/lib/types/props/custom_type.rb +51 -27
- data/lib/types/props/decorator.rb +248 -405
- data/lib/types/props/generated_code_validation.rb +268 -0
- data/lib/types/props/has_lazily_specialized_methods.rb +92 -0
- data/lib/types/props/optional.rb +37 -41
- data/lib/types/props/plugin.rb +23 -1
- data/lib/types/props/pretty_printable.rb +3 -3
- data/lib/types/props/private/apply_default.rb +170 -0
- data/lib/types/props/private/deserializer_generator.rb +165 -0
- data/lib/types/props/private/parser.rb +32 -0
- data/lib/types/props/private/serde_transform.rb +186 -0
- data/lib/types/props/private/serializer_generator.rb +77 -0
- data/lib/types/props/private/setter_factory.rb +139 -0
- data/lib/types/props/serializable.rb +137 -192
- data/lib/types/props/type_validation.rb +19 -6
- data/lib/types/props/utils.rb +3 -7
- data/lib/types/props/weak_constructor.rb +51 -14
- data/lib/types/sig.rb +6 -6
- data/lib/types/types/attached_class.rb +37 -0
- data/lib/types/types/base.rb +26 -2
- data/lib/types/types/fixed_array.rb +28 -2
- data/lib/types/types/fixed_hash.rb +11 -10
- data/lib/types/types/intersection.rb +6 -0
- data/lib/types/types/noreturn.rb +4 -0
- data/lib/types/types/self_type.rb +4 -0
- data/lib/types/types/simple.rb +22 -1
- data/lib/types/types/t_enum.rb +38 -0
- data/lib/types/types/type_parameter.rb +1 -1
- data/lib/types/types/type_variable.rb +1 -1
- data/lib/types/types/typed_array.rb +7 -2
- data/lib/types/types/typed_enumerable.rb +28 -17
- data/lib/types/types/typed_enumerator.rb +7 -2
- data/lib/types/types/typed_hash.rb +8 -3
- data/lib/types/types/typed_range.rb +7 -2
- data/lib/types/types/typed_set.rb +7 -2
- data/lib/types/types/union.rb +37 -5
- data/lib/types/types/untyped.rb +4 -0
- data/lib/types/utils.rb +43 -11
- metadata +103 -11
- data/lib/types/private/error_handler.rb +0 -0
- data/lib/types/runtime_profiled.rb +0 -24
- data/lib/types/types/opus_enum.rb +0 -33
data/lib/types/props/plugin.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
# typed:
|
2
|
+
# typed: false
|
3
3
|
|
4
4
|
module T::Props::Plugin
|
5
5
|
include T::Props
|
@@ -12,4 +12,26 @@ module T::Props::Plugin
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
mixes_in_class_methods(ClassMethods)
|
15
|
+
|
16
|
+
module Private
|
17
|
+
# These need to be non-instance methods so we can use them without prematurely creating the
|
18
|
+
# child decorator in `model_inherited` (see comments there for details).
|
19
|
+
#
|
20
|
+
# The dynamic constant access below forces this file to be `typed: false`
|
21
|
+
def self.apply_class_methods(plugin, target)
|
22
|
+
if plugin.const_defined?('ClassMethods')
|
23
|
+
# FIXME: This will break preloading, selective test execution, etc if `mod::ClassMethods`
|
24
|
+
# is ever defined in a separate file from `mod`.
|
25
|
+
target.extend(plugin::ClassMethods)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.apply_decorator_methods(plugin, target)
|
30
|
+
if plugin.const_defined?('DecoratorMethods')
|
31
|
+
# FIXME: This will break preloading, selective test execution, etc if `mod::DecoratorMethods`
|
32
|
+
# is ever defined in a separate file from `mod`.
|
33
|
+
target.extend(plugin::DecoratorMethods)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
15
37
|
end
|
@@ -18,9 +18,9 @@ module T::Props::PrettyPrintable
|
|
18
18
|
module DecoratorMethods
|
19
19
|
extend T::Sig
|
20
20
|
|
21
|
-
sig {returns(T::
|
22
|
-
def
|
23
|
-
super
|
21
|
+
sig {params(key: Symbol).returns(T::Boolean).checked(:never)}
|
22
|
+
def valid_rule_key?(key)
|
23
|
+
super || key == :inspect
|
24
24
|
end
|
25
25
|
|
26
26
|
sig do
|
@@ -0,0 +1,170 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
module T::Props
|
5
|
+
module Private
|
6
|
+
class ApplyDefault
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Helpers
|
9
|
+
abstract!
|
10
|
+
|
11
|
+
# checked(:never) - O(object construction x prop count)
|
12
|
+
sig {returns(SetterFactory::SetterProc).checked(:never)}
|
13
|
+
attr_reader :setter_proc
|
14
|
+
|
15
|
+
# checked(:never) - We do this with `T.let` instead
|
16
|
+
sig {params(accessor_key: Symbol, setter_proc: SetterFactory::SetterProc).void.checked(:never)}
|
17
|
+
def initialize(accessor_key, setter_proc)
|
18
|
+
@accessor_key = T.let(accessor_key, Symbol)
|
19
|
+
@setter_proc = T.let(setter_proc, SetterFactory::SetterProc)
|
20
|
+
end
|
21
|
+
|
22
|
+
# checked(:never) - O(object construction x prop count)
|
23
|
+
sig {abstract.returns(T.untyped).checked(:never)}
|
24
|
+
def default; end
|
25
|
+
|
26
|
+
# checked(:never) - O(object construction x prop count)
|
27
|
+
sig {abstract.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
28
|
+
def set_default(instance); end
|
29
|
+
|
30
|
+
NO_CLONE_TYPES = T.let([TrueClass, FalseClass, NilClass, Symbol, Numeric, T::Enum].freeze, T::Array[Module])
|
31
|
+
|
32
|
+
# checked(:never) - Rules hash is expensive to check
|
33
|
+
sig {params(cls: Module, rules: T::Hash[Symbol, T.untyped]).returns(T.nilable(ApplyDefault)).checked(:never)}
|
34
|
+
def self.for(cls, rules)
|
35
|
+
accessor_key = rules.fetch(:accessor_key)
|
36
|
+
setter = rules.fetch(:setter_proc)
|
37
|
+
|
38
|
+
if rules.key?(:factory)
|
39
|
+
ApplyDefaultFactory.new(cls, rules.fetch(:factory), accessor_key, setter)
|
40
|
+
elsif rules.key?(:default)
|
41
|
+
default = rules.fetch(:default)
|
42
|
+
case default
|
43
|
+
when *NO_CLONE_TYPES
|
44
|
+
return ApplyPrimitiveDefault.new(default, accessor_key, setter)
|
45
|
+
when String
|
46
|
+
if default.frozen?
|
47
|
+
return ApplyPrimitiveDefault.new(default, accessor_key, setter)
|
48
|
+
end
|
49
|
+
when Array
|
50
|
+
if default.empty? && default.class == Array
|
51
|
+
return ApplyEmptyArrayDefault.new(accessor_key, setter)
|
52
|
+
end
|
53
|
+
when Hash
|
54
|
+
if default.empty? && default.default.nil? && T.unsafe(default).default_proc.nil? && default.class == Hash
|
55
|
+
return ApplyEmptyHashDefault.new(accessor_key, setter)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
ApplyComplexDefault.new(default, accessor_key, setter)
|
60
|
+
else
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ApplyFixedDefault < ApplyDefault
|
67
|
+
abstract!
|
68
|
+
|
69
|
+
# checked(:never) - We do this with `T.let` instead
|
70
|
+
sig {params(default: BasicObject, accessor_key: Symbol, setter_proc: SetterFactory::SetterProc).void.checked(:never)}
|
71
|
+
def initialize(default, accessor_key, setter_proc)
|
72
|
+
# FIXME: Ideally we'd check here that the default is actually a valid
|
73
|
+
# value for this field, but existing code relies on the fact that we don't.
|
74
|
+
#
|
75
|
+
# :(
|
76
|
+
#
|
77
|
+
# setter_proc.call(default)
|
78
|
+
@default = T.let(default, BasicObject)
|
79
|
+
super(accessor_key, setter_proc)
|
80
|
+
end
|
81
|
+
|
82
|
+
# checked(:never) - O(object construction x prop count)
|
83
|
+
sig {override.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
84
|
+
def set_default(instance)
|
85
|
+
instance.instance_variable_set(@accessor_key, default)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class ApplyPrimitiveDefault < ApplyFixedDefault
|
90
|
+
# checked(:never) - O(object construction x prop count)
|
91
|
+
sig {override.returns(T.untyped).checked(:never)}
|
92
|
+
attr_reader :default
|
93
|
+
end
|
94
|
+
|
95
|
+
class ApplyComplexDefault < ApplyFixedDefault
|
96
|
+
# checked(:never) - O(object construction x prop count)
|
97
|
+
sig {override.returns(T.untyped).checked(:never)}
|
98
|
+
def default
|
99
|
+
T::Props::Utils.deep_clone_object(@default)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Special case since it's so common, and a literal `[]` is meaningfully
|
104
|
+
# faster than falling back to ApplyComplexDefault or even calling
|
105
|
+
# `some_empty_array.dup`
|
106
|
+
class ApplyEmptyArrayDefault < ApplyDefault
|
107
|
+
# checked(:never) - O(object construction x prop count)
|
108
|
+
sig {override.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
109
|
+
def set_default(instance)
|
110
|
+
instance.instance_variable_set(@accessor_key, [])
|
111
|
+
end
|
112
|
+
|
113
|
+
# checked(:never) - O(object construction x prop count)
|
114
|
+
sig {override.returns(T::Array[T.untyped]).checked(:never)}
|
115
|
+
def default
|
116
|
+
[]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Special case since it's so common, and a literal `{}` is meaningfully
|
121
|
+
# faster than falling back to ApplyComplexDefault or even calling
|
122
|
+
# `some_empty_hash.dup`
|
123
|
+
class ApplyEmptyHashDefault < ApplyDefault
|
124
|
+
# checked(:never) - O(object construction x prop count)
|
125
|
+
sig {override.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
126
|
+
def set_default(instance)
|
127
|
+
instance.instance_variable_set(@accessor_key, {})
|
128
|
+
end
|
129
|
+
|
130
|
+
# checked(:never) - O(object construction x prop count)
|
131
|
+
sig {override.returns(T::Hash[T.untyped, T.untyped]).checked(:never)}
|
132
|
+
def default
|
133
|
+
{}
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
class ApplyDefaultFactory < ApplyDefault
|
138
|
+
# checked(:never) - We do this with `T.let` instead
|
139
|
+
sig do
|
140
|
+
params(
|
141
|
+
cls: Module,
|
142
|
+
factory: T.any(Proc, Method),
|
143
|
+
accessor_key: Symbol,
|
144
|
+
setter_proc: SetterFactory::SetterProc,
|
145
|
+
)
|
146
|
+
.void
|
147
|
+
.checked(:never)
|
148
|
+
end
|
149
|
+
def initialize(cls, factory, accessor_key, setter_proc)
|
150
|
+
@class = T.let(cls, Module)
|
151
|
+
@factory = T.let(factory, T.any(Proc, Method))
|
152
|
+
super(accessor_key, setter_proc)
|
153
|
+
end
|
154
|
+
|
155
|
+
# checked(:never) - O(object construction x prop count)
|
156
|
+
sig {override.params(instance: T.all(T::Props::Optional, Object)).void.checked(:never)}
|
157
|
+
def set_default(instance)
|
158
|
+
# Use the actual setter to validate the factory returns a legitimate
|
159
|
+
# value every time
|
160
|
+
instance.instance_exec(default, &@setter_proc)
|
161
|
+
end
|
162
|
+
|
163
|
+
# checked(:never) - O(object construction x prop count)
|
164
|
+
sig {override.returns(T.untyped).checked(:never)}
|
165
|
+
def default
|
166
|
+
@class.class_exec(&@factory)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
module T::Props
|
5
|
+
module Private
|
6
|
+
|
7
|
+
# Generates a specialized `deserialize` implementation for a subclass of
|
8
|
+
# T::Props::Serializable.
|
9
|
+
#
|
10
|
+
# The basic idea is that we analyze the props and for each prop, generate
|
11
|
+
# the simplest possible logic as a block of Ruby source, so that we don't
|
12
|
+
# pay the cost of supporting types like T:::Hash[CustomType, SubstructType]
|
13
|
+
# when deserializing a simple Integer. Then we join those together,
|
14
|
+
# with a little shared logic to be able to detect when we get input keys
|
15
|
+
# that don't match any prop.
|
16
|
+
module DeserializerGenerator
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
# Generate a method that takes a T::Hash[String, T.untyped] representing
|
20
|
+
# serialized props, sets instance variables for each prop found in the
|
21
|
+
# input, and returns the count of we props set (which we can use to check
|
22
|
+
# for unexpected input keys with minimal effect on the fast path).
|
23
|
+
sig do
|
24
|
+
params(
|
25
|
+
props: T::Hash[Symbol, T::Hash[Symbol, T.untyped]],
|
26
|
+
defaults: T::Hash[Symbol, T::Props::Private::ApplyDefault],
|
27
|
+
)
|
28
|
+
.returns(String)
|
29
|
+
.checked(:never)
|
30
|
+
end
|
31
|
+
def self.generate(props, defaults)
|
32
|
+
stored_props = props.reject {|_, rules| rules[:dont_store]}
|
33
|
+
parts = stored_props.map do |prop, rules|
|
34
|
+
# All of these strings should already be validated (directly or
|
35
|
+
# indirectly) in `validate_prop_name`, so we don't bother with a nice
|
36
|
+
# error message, but we double check here to prevent a refactoring
|
37
|
+
# from introducing a security vulnerability.
|
38
|
+
raise unless T::Props::Decorator::SAFE_NAME.match?(prop.to_s)
|
39
|
+
|
40
|
+
hash_key = rules.fetch(:serialized_form)
|
41
|
+
raise unless T::Props::Decorator::SAFE_NAME.match?(hash_key)
|
42
|
+
|
43
|
+
ivar_name = rules.fetch(:accessor_key).to_s
|
44
|
+
raise unless ivar_name.start_with?('@') && T::Props::Decorator::SAFE_NAME.match?(ivar_name[1..-1])
|
45
|
+
|
46
|
+
transformation = SerdeTransform.generate(
|
47
|
+
T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),
|
48
|
+
SerdeTransform::Mode::DESERIALIZE,
|
49
|
+
'val'
|
50
|
+
)
|
51
|
+
transformed_val = if transformation
|
52
|
+
# Rescuing exactly NoMethodError is intended as a temporary hack
|
53
|
+
# to preserve the semantics from before codegen. More generally
|
54
|
+
# we are inconsistent about typechecking on deser and need to decide
|
55
|
+
# our strategy here.
|
56
|
+
<<~RUBY
|
57
|
+
begin
|
58
|
+
#{transformation}
|
59
|
+
rescue NoMethodError => e
|
60
|
+
T::Configuration.soft_assert_handler(
|
61
|
+
'Deserialization error (probably unexpected stored type)',
|
62
|
+
storytime: {
|
63
|
+
klass: self.class,
|
64
|
+
prop: #{prop.inspect},
|
65
|
+
value: val,
|
66
|
+
error: e.message,
|
67
|
+
notify: 'djudd'
|
68
|
+
}
|
69
|
+
)
|
70
|
+
val
|
71
|
+
end
|
72
|
+
RUBY
|
73
|
+
else
|
74
|
+
'val'
|
75
|
+
end
|
76
|
+
|
77
|
+
nil_handler = generate_nil_handler(
|
78
|
+
prop: prop,
|
79
|
+
serialized_form: hash_key,
|
80
|
+
default: defaults[prop],
|
81
|
+
nilable_type: T::Props::Utils.optional_prop?(rules),
|
82
|
+
raise_on_nil_write: !!rules[:raise_on_nil_write],
|
83
|
+
)
|
84
|
+
|
85
|
+
<<~RUBY
|
86
|
+
val = hash[#{hash_key.inspect}]
|
87
|
+
#{ivar_name} = if val.nil?
|
88
|
+
found -= 1 unless hash.key?(#{hash_key.inspect})
|
89
|
+
#{nil_handler}
|
90
|
+
else
|
91
|
+
#{transformed_val}
|
92
|
+
end
|
93
|
+
RUBY
|
94
|
+
end
|
95
|
+
|
96
|
+
<<~RUBY
|
97
|
+
def __t_props_generated_deserialize(hash)
|
98
|
+
found = #{stored_props.size}
|
99
|
+
#{parts.join("\n\n")}
|
100
|
+
found
|
101
|
+
end
|
102
|
+
RUBY
|
103
|
+
end
|
104
|
+
|
105
|
+
# This is very similar to what we do in ApplyDefault, but has a few
|
106
|
+
# key differences that mean we don't just re-use the code:
|
107
|
+
#
|
108
|
+
# 1. Where the logic in construction is that we generate a default
|
109
|
+
# if & only if the prop key isn't present in the input, here we'll
|
110
|
+
# generate a default even to override an explicit nil, but only
|
111
|
+
# if the prop is actually required.
|
112
|
+
# 2. Since we're generating raw Ruby source, we can remove a layer
|
113
|
+
# of indirection for marginally better performance; this seems worth
|
114
|
+
# it for the common cases of literals and empty arrays/hashes.
|
115
|
+
# 3. We need to care about the distinction between `raise_on_nil_write`
|
116
|
+
# and actually non-nilable, where new-instance construction doesn't.
|
117
|
+
#
|
118
|
+
# So we fall back to ApplyDefault only when one of the cases just
|
119
|
+
# mentioned doesn't apply.
|
120
|
+
sig do
|
121
|
+
params(
|
122
|
+
prop: Symbol,
|
123
|
+
serialized_form: String,
|
124
|
+
default: T.nilable(ApplyDefault),
|
125
|
+
nilable_type: T::Boolean,
|
126
|
+
raise_on_nil_write: T::Boolean,
|
127
|
+
)
|
128
|
+
.returns(String)
|
129
|
+
.checked(:never)
|
130
|
+
end
|
131
|
+
private_class_method def self.generate_nil_handler(
|
132
|
+
prop:,
|
133
|
+
serialized_form:,
|
134
|
+
default:,
|
135
|
+
nilable_type:,
|
136
|
+
raise_on_nil_write:
|
137
|
+
)
|
138
|
+
if !nilable_type
|
139
|
+
case default
|
140
|
+
when NilClass
|
141
|
+
"self.class.decorator.raise_nil_deserialize_error(#{serialized_form.inspect})"
|
142
|
+
when ApplyPrimitiveDefault
|
143
|
+
literal = default.default
|
144
|
+
case literal
|
145
|
+
when String, Integer, Symbol, Float, TrueClass, FalseClass, NilClass
|
146
|
+
literal.inspect
|
147
|
+
else
|
148
|
+
"self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
|
149
|
+
end
|
150
|
+
when ApplyEmptyArrayDefault
|
151
|
+
'[]'
|
152
|
+
when ApplyEmptyHashDefault
|
153
|
+
'{}'
|
154
|
+
else
|
155
|
+
"self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
|
156
|
+
end
|
157
|
+
elsif raise_on_nil_write
|
158
|
+
"required_prop_missing_from_deserialize(#{prop.inspect})"
|
159
|
+
else
|
160
|
+
'nil'
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: false
|
3
|
+
|
4
|
+
module T::Props
|
5
|
+
module Private
|
6
|
+
module Parse
|
7
|
+
def parse(source)
|
8
|
+
@current_ruby ||= require_parser(:CurrentRuby)
|
9
|
+
@current_ruby.parse(source)
|
10
|
+
end
|
11
|
+
|
12
|
+
def s(type, *children)
|
13
|
+
@node ||= require_parser(:AST, :Node)
|
14
|
+
@node.new(type, children)
|
15
|
+
end
|
16
|
+
|
17
|
+
private def require_parser(*constants)
|
18
|
+
# This is an optional dependency for sorbet-runtime in general,
|
19
|
+
# but is required here
|
20
|
+
require 'parser/current'
|
21
|
+
|
22
|
+
# Hack to work around the static checker thinking the constant is
|
23
|
+
# undefined
|
24
|
+
cls = Kernel.const_get(:Parser, true)
|
25
|
+
while (const = constants.shift)
|
26
|
+
cls = cls.const_get(const, false)
|
27
|
+
end
|
28
|
+
cls
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,186 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
module T::Props
|
5
|
+
module Private
|
6
|
+
module SerdeTransform
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
class Serialize; end
|
10
|
+
private_constant :Serialize
|
11
|
+
class Deserialize; end
|
12
|
+
private_constant :Deserialize
|
13
|
+
ModeType = T.type_alias {T.any(Serialize, Deserialize)}
|
14
|
+
private_constant :ModeType
|
15
|
+
|
16
|
+
module Mode
|
17
|
+
SERIALIZE = T.let(Serialize.new.freeze, Serialize)
|
18
|
+
DESERIALIZE = T.let(Deserialize.new.freeze, Deserialize)
|
19
|
+
end
|
20
|
+
|
21
|
+
NO_TRANSFORM_TYPES = T.let(
|
22
|
+
[TrueClass, FalseClass, NilClass, Symbol, String].freeze,
|
23
|
+
T::Array[Module],
|
24
|
+
)
|
25
|
+
private_constant :NO_TRANSFORM_TYPES
|
26
|
+
|
27
|
+
sig do
|
28
|
+
params(
|
29
|
+
type: T::Types::Base,
|
30
|
+
mode: ModeType,
|
31
|
+
varname: String,
|
32
|
+
)
|
33
|
+
.returns(T.nilable(String))
|
34
|
+
.checked(:never)
|
35
|
+
end
|
36
|
+
def self.generate(type, mode, varname)
|
37
|
+
case type
|
38
|
+
when T::Types::TypedArray
|
39
|
+
inner = generate(type.type, mode, 'v')
|
40
|
+
if inner.nil?
|
41
|
+
"#{varname}.dup"
|
42
|
+
else
|
43
|
+
"#{varname}.map {|v| #{inner}}"
|
44
|
+
end
|
45
|
+
when T::Types::TypedSet
|
46
|
+
inner = generate(type.type, mode, 'v')
|
47
|
+
if inner.nil?
|
48
|
+
"#{varname}.dup"
|
49
|
+
else
|
50
|
+
"Set.new(#{varname}) {|v| #{inner}}"
|
51
|
+
end
|
52
|
+
when T::Types::TypedHash
|
53
|
+
keys = generate(type.keys, mode, 'k')
|
54
|
+
values = generate(type.values, mode, 'v')
|
55
|
+
if keys && values
|
56
|
+
"#{varname}.each_with_object({}) {|(k,v),h| h[#{keys}] = #{values}}"
|
57
|
+
elsif keys
|
58
|
+
"#{varname}.transform_keys {|k| #{keys}}"
|
59
|
+
elsif values
|
60
|
+
"#{varname}.transform_values {|v| #{values}}"
|
61
|
+
else
|
62
|
+
"#{varname}.dup"
|
63
|
+
end
|
64
|
+
when T::Types::Simple
|
65
|
+
raw = type.raw_type
|
66
|
+
if NO_TRANSFORM_TYPES.any? {|cls| raw <= cls}
|
67
|
+
nil
|
68
|
+
elsif raw <= Float
|
69
|
+
case mode
|
70
|
+
when Deserialize then "#{varname}.to_f"
|
71
|
+
when Serialize then nil
|
72
|
+
else T.absurd(mode)
|
73
|
+
end
|
74
|
+
elsif raw <= Numeric
|
75
|
+
nil
|
76
|
+
elsif raw < T::Props::Serializable
|
77
|
+
handle_serializable_subtype(varname, raw, mode)
|
78
|
+
elsif raw.singleton_class < T::Props::CustomType
|
79
|
+
handle_custom_type(varname, T.unsafe(raw), mode)
|
80
|
+
elsif T::Configuration.scalar_types.include?(raw.name)
|
81
|
+
# It's a bit of a hack that this is separate from NO_TRANSFORM_TYPES
|
82
|
+
# and doesn't check inheritance (like `T::Props::CustomType.scalar_type?`
|
83
|
+
# does), but it covers the main use case (pay-server's custom `Boolean`
|
84
|
+
# module) without either requiring `T::Configuration.scalar_types` to
|
85
|
+
# accept modules instead of strings (which produces load-order issues
|
86
|
+
# and subtle behavior changes) or eating the performance cost of doing
|
87
|
+
# an inheritance check by manually crawling a class hierarchy and doing
|
88
|
+
# string comparisons.
|
89
|
+
nil
|
90
|
+
else
|
91
|
+
"T::Props::Utils.deep_clone_object(#{varname})"
|
92
|
+
end
|
93
|
+
when T::Types::Union
|
94
|
+
non_nil_type = T::Utils.unwrap_nilable(type)
|
95
|
+
if non_nil_type
|
96
|
+
inner = generate(non_nil_type, mode, varname)
|
97
|
+
if inner.nil?
|
98
|
+
nil
|
99
|
+
else
|
100
|
+
"#{varname}.nil? ? nil : #{inner}"
|
101
|
+
end
|
102
|
+
elsif type.types.all? {|t| generate(t, mode, varname).nil?}
|
103
|
+
# Handle, e.g., T::Boolean
|
104
|
+
nil
|
105
|
+
else
|
106
|
+
# We currently deep_clone_object if the type was T.any(Integer, Float).
|
107
|
+
# When we get better support for union types (maybe this specific
|
108
|
+
# union type, because it would be a replacement for
|
109
|
+
# Chalk::ODM::DeprecatedNumemric), we could opt to special case
|
110
|
+
# this union to have no specific serde transform (the only reason
|
111
|
+
# why Float has a special case is because round tripping through
|
112
|
+
# JSON might normalize Floats to Integers)
|
113
|
+
"T::Props::Utils.deep_clone_object(#{varname})"
|
114
|
+
end
|
115
|
+
when T::Types::Intersection
|
116
|
+
dynamic_fallback = "T::Props::Utils.deep_clone_object(#{varname})"
|
117
|
+
|
118
|
+
# Transformations for any members of the intersection type where we
|
119
|
+
# know what we need to do and did not have to fall back to the
|
120
|
+
# dynamic deep clone method.
|
121
|
+
#
|
122
|
+
# NB: This deliberately does include `nil`, which means we know we
|
123
|
+
# don't need to do any transforming.
|
124
|
+
inner_known = type.types
|
125
|
+
.map {|t| generate(t, mode, varname)}
|
126
|
+
.reject {|t| t == dynamic_fallback}
|
127
|
+
.uniq
|
128
|
+
|
129
|
+
if inner_known.size != 1
|
130
|
+
# If there were no cases where we could tell what we need to do,
|
131
|
+
# e.g. if this is `T.all(SomethingWeird, WhoKnows)`, just use the
|
132
|
+
# dynamic fallback.
|
133
|
+
#
|
134
|
+
# If there were multiple cases and they weren't consistent, e.g.
|
135
|
+
# if this is `T.all(String, T::Array[Integer])`, the type is probably
|
136
|
+
# bogus/uninhabited, but use the dynamic fallback because we still
|
137
|
+
# don't have a better option, and this isn't the place to raise that
|
138
|
+
# error.
|
139
|
+
dynamic_fallback
|
140
|
+
else
|
141
|
+
# This is probably something like `T.all(String, SomeMarker)` or
|
142
|
+
# `T.all(SomeEnum, T.enum(SomeEnum::FOO))` and we should treat it
|
143
|
+
# like String or SomeEnum even if we don't know what to do with
|
144
|
+
# the rest of the type.
|
145
|
+
inner_known.first
|
146
|
+
end
|
147
|
+
when T::Types::Enum
|
148
|
+
generate(T::Utils.lift_enum(type), mode, varname)
|
149
|
+
else
|
150
|
+
"T::Props::Utils.deep_clone_object(#{varname})"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
|
155
|
+
private_class_method def self.handle_serializable_subtype(varname, type, mode)
|
156
|
+
case mode
|
157
|
+
when Serialize
|
158
|
+
"#{varname}.serialize(strict)"
|
159
|
+
when Deserialize
|
160
|
+
type_name = T.must(module_name(type))
|
161
|
+
"#{type_name}.from_hash(#{varname})"
|
162
|
+
else
|
163
|
+
T.absurd(mode)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
|
168
|
+
private_class_method def self.handle_custom_type(varname, type, mode)
|
169
|
+
case mode
|
170
|
+
when Serialize
|
171
|
+
"T::Props::CustomType.checked_serialize(#{varname})"
|
172
|
+
when Deserialize
|
173
|
+
type_name = T.must(module_name(type))
|
174
|
+
"#{type_name}.deserialize(#{varname})"
|
175
|
+
else
|
176
|
+
T.absurd(mode)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
sig {params(type: Module).returns(T.nilable(String)).checked(:never)}
|
181
|
+
private_class_method def self.module_name(type)
|
182
|
+
T::Configuration.module_name_mangler.call(type)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|