sorbet-runtime 0.5.5841
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.
- checksums.yaml +7 -0
- data/lib/sorbet-runtime.rb +116 -0
- data/lib/types/_types.rb +285 -0
- data/lib/types/abstract_utils.rb +50 -0
- data/lib/types/boolean.rb +8 -0
- data/lib/types/compatibility_patches.rb +95 -0
- data/lib/types/configuration.rb +428 -0
- data/lib/types/enum.rb +349 -0
- data/lib/types/generic.rb +23 -0
- data/lib/types/helpers.rb +39 -0
- data/lib/types/interface_wrapper.rb +158 -0
- data/lib/types/non_forcing_constants.rb +51 -0
- data/lib/types/private/abstract/data.rb +36 -0
- data/lib/types/private/abstract/declare.rb +48 -0
- data/lib/types/private/abstract/hooks.rb +43 -0
- data/lib/types/private/abstract/validate.rb +128 -0
- data/lib/types/private/casts.rb +22 -0
- data/lib/types/private/class_utils.rb +111 -0
- data/lib/types/private/decl_state.rb +30 -0
- data/lib/types/private/final.rb +51 -0
- data/lib/types/private/methods/_methods.rb +460 -0
- data/lib/types/private/methods/call_validation.rb +1149 -0
- data/lib/types/private/methods/decl_builder.rb +228 -0
- data/lib/types/private/methods/modes.rb +16 -0
- data/lib/types/private/methods/signature.rb +196 -0
- data/lib/types/private/methods/signature_validation.rb +229 -0
- data/lib/types/private/mixins/mixins.rb +27 -0
- data/lib/types/private/retry.rb +10 -0
- data/lib/types/private/runtime_levels.rb +56 -0
- data/lib/types/private/sealed.rb +65 -0
- data/lib/types/private/types/not_typed.rb +23 -0
- data/lib/types/private/types/string_holder.rb +26 -0
- data/lib/types/private/types/type_alias.rb +26 -0
- data/lib/types/private/types/void.rb +34 -0
- data/lib/types/profile.rb +31 -0
- data/lib/types/props/_props.rb +161 -0
- data/lib/types/props/constructor.rb +40 -0
- data/lib/types/props/custom_type.rb +108 -0
- data/lib/types/props/decorator.rb +672 -0
- data/lib/types/props/errors.rb +8 -0
- 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 +81 -0
- data/lib/types/props/plugin.rb +37 -0
- data/lib/types/props/pretty_printable.rb +107 -0
- 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 +192 -0
- data/lib/types/props/private/serializer_generator.rb +77 -0
- data/lib/types/props/private/setter_factory.rb +134 -0
- data/lib/types/props/serializable.rb +330 -0
- data/lib/types/props/type_validation.rb +111 -0
- data/lib/types/props/utils.rb +59 -0
- data/lib/types/props/weak_constructor.rb +67 -0
- data/lib/types/runtime_profiled.rb +24 -0
- data/lib/types/sig.rb +30 -0
- data/lib/types/struct.rb +18 -0
- data/lib/types/types/attached_class.rb +37 -0
- data/lib/types/types/base.rb +151 -0
- data/lib/types/types/class_of.rb +38 -0
- data/lib/types/types/enum.rb +42 -0
- data/lib/types/types/fixed_array.rb +60 -0
- data/lib/types/types/fixed_hash.rb +59 -0
- data/lib/types/types/intersection.rb +37 -0
- data/lib/types/types/noreturn.rb +29 -0
- data/lib/types/types/proc.rb +51 -0
- data/lib/types/types/self_type.rb +35 -0
- data/lib/types/types/simple.rb +33 -0
- data/lib/types/types/t_enum.rb +38 -0
- data/lib/types/types/type_member.rb +7 -0
- data/lib/types/types/type_parameter.rb +23 -0
- data/lib/types/types/type_template.rb +7 -0
- data/lib/types/types/type_variable.rb +31 -0
- data/lib/types/types/typed_array.rb +34 -0
- data/lib/types/types/typed_enumerable.rb +161 -0
- data/lib/types/types/typed_enumerator.rb +36 -0
- data/lib/types/types/typed_hash.rb +43 -0
- data/lib/types/types/typed_range.rb +26 -0
- data/lib/types/types/typed_set.rb +36 -0
- data/lib/types/types/union.rb +56 -0
- data/lib/types/types/untyped.rb +29 -0
- data/lib/types/utils.rb +217 -0
- metadata +223 -0
@@ -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,192 @@
|
|
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
|
+
else
|
103
|
+
# Handle, e.g., T::Boolean
|
104
|
+
if type.types.all? {|t| generate(t, mode, varname).nil?}
|
105
|
+
nil
|
106
|
+
else
|
107
|
+
# We currently deep_clone_object if the type was T.any(Integer, Float).
|
108
|
+
# When we get better support for union types (maybe this specific
|
109
|
+
# union type, because it would be a replacement for
|
110
|
+
# Chalk::ODM::DeprecatedNumemric), we could opt to special case
|
111
|
+
# this union to have no specific serde transform (the only reason
|
112
|
+
# why Float has a special case is because round tripping through
|
113
|
+
# JSON might normalize Floats to Integers)
|
114
|
+
"T::Props::Utils.deep_clone_object(#{varname})"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
when T::Types::Intersection
|
118
|
+
dynamic_fallback = "T::Props::Utils.deep_clone_object(#{varname})"
|
119
|
+
|
120
|
+
# Transformations for any members of the intersection type where we
|
121
|
+
# know what we need to do and did not have to fall back to the
|
122
|
+
# dynamic deep clone method.
|
123
|
+
#
|
124
|
+
# NB: This deliberately does include `nil`, which means we know we
|
125
|
+
# don't need to do any transforming.
|
126
|
+
inner_known = type.types
|
127
|
+
.map {|t| generate(t, mode, varname)}
|
128
|
+
.reject {|t| t == dynamic_fallback}
|
129
|
+
.uniq
|
130
|
+
|
131
|
+
if inner_known.size != 1
|
132
|
+
# If there were no cases where we could tell what we need to do,
|
133
|
+
# e.g. if this is `T.all(SomethingWeird, WhoKnows)`, just use the
|
134
|
+
# dynamic fallback.
|
135
|
+
#
|
136
|
+
# If there were multiple cases and they weren't consistent, e.g.
|
137
|
+
# if this is `T.all(String, T::Array[Integer])`, the type is probably
|
138
|
+
# bogus/uninhabited, but use the dynamic fallback because we still
|
139
|
+
# don't have a better option, and this isn't the place to raise that
|
140
|
+
# error.
|
141
|
+
dynamic_fallback
|
142
|
+
else
|
143
|
+
# This is probably something like `T.all(String, SomeMarker)` or
|
144
|
+
# `T.all(SomeEnum, T.enum(SomeEnum::FOO))` and we should treat it
|
145
|
+
# like String or SomeEnum even if we don't know what to do with
|
146
|
+
# the rest of the type.
|
147
|
+
inner_known.first
|
148
|
+
end
|
149
|
+
when T::Types::Enum
|
150
|
+
generate(T::Utils.lift_enum(type), mode, varname)
|
151
|
+
else
|
152
|
+
"T::Props::Utils.deep_clone_object(#{varname})"
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
|
157
|
+
private_class_method def self.handle_serializable_subtype(varname, type, mode)
|
158
|
+
case mode
|
159
|
+
when Serialize
|
160
|
+
"#{varname}.serialize(strict)"
|
161
|
+
when Deserialize
|
162
|
+
type_name = T.must(module_name(type))
|
163
|
+
"#{type_name}.from_hash(#{varname})"
|
164
|
+
else
|
165
|
+
T.absurd(mode)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
|
170
|
+
private_class_method def self.handle_custom_type(varname, type, mode)
|
171
|
+
case mode
|
172
|
+
when Serialize
|
173
|
+
"T::Props::CustomType.checked_serialize(#{varname})"
|
174
|
+
when Deserialize
|
175
|
+
type_name = T.must(module_name(type))
|
176
|
+
"#{type_name}.deserialize(#{varname})"
|
177
|
+
else
|
178
|
+
T.absurd(mode)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Guard against overrides of `name` or `to_s`
|
183
|
+
MODULE_NAME = T.let(Module.instance_method(:name), UnboundMethod)
|
184
|
+
private_constant :MODULE_NAME
|
185
|
+
|
186
|
+
sig {params(type: Module).returns(T.nilable(String)).checked(:never)}
|
187
|
+
private_class_method def self.module_name(type)
|
188
|
+
MODULE_NAME.bind(type).call
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
module T::Props
|
5
|
+
module Private
|
6
|
+
|
7
|
+
# Generates a specialized `serialize` 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 serializing 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 SerializerGenerator
|
17
|
+
extend T::Sig
|
18
|
+
|
19
|
+
sig do
|
20
|
+
params(
|
21
|
+
props: T::Hash[Symbol, T::Hash[Symbol, T.untyped]],
|
22
|
+
)
|
23
|
+
.returns(String)
|
24
|
+
.checked(:never)
|
25
|
+
end
|
26
|
+
def self.generate(props)
|
27
|
+
stored_props = props.reject {|_, rules| rules[:dont_store]}
|
28
|
+
parts = stored_props.map do |prop, rules|
|
29
|
+
# All of these strings should already be validated (directly or
|
30
|
+
# indirectly) in `validate_prop_name`, so we don't bother with a nice
|
31
|
+
# error message, but we double check here to prevent a refactoring
|
32
|
+
# from introducing a security vulnerability.
|
33
|
+
raise unless T::Props::Decorator::SAFE_NAME.match?(prop.to_s)
|
34
|
+
|
35
|
+
hash_key = rules.fetch(:serialized_form)
|
36
|
+
raise unless T::Props::Decorator::SAFE_NAME.match?(hash_key)
|
37
|
+
|
38
|
+
ivar_name = rules.fetch(:accessor_key).to_s
|
39
|
+
raise unless ivar_name.start_with?('@') && T::Props::Decorator::SAFE_NAME.match?(ivar_name[1..-1])
|
40
|
+
|
41
|
+
transformed_val = SerdeTransform.generate(
|
42
|
+
T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),
|
43
|
+
SerdeTransform::Mode::SERIALIZE,
|
44
|
+
ivar_name
|
45
|
+
) || ivar_name
|
46
|
+
|
47
|
+
nil_asserter =
|
48
|
+
if rules[:fully_optional]
|
49
|
+
''
|
50
|
+
else
|
51
|
+
"required_prop_missing_from_serialize(#{prop.inspect}) if strict"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Don't serialize values that are nil to save space (both the
|
55
|
+
# nil value itself and the field name in the serialized BSON
|
56
|
+
# document)
|
57
|
+
<<~RUBY
|
58
|
+
if #{ivar_name}.nil?
|
59
|
+
#{nil_asserter}
|
60
|
+
else
|
61
|
+
h[#{hash_key.inspect}] = #{transformed_val}
|
62
|
+
end
|
63
|
+
RUBY
|
64
|
+
end
|
65
|
+
|
66
|
+
<<~RUBY
|
67
|
+
def __t_props_generated_serialize(strict)
|
68
|
+
h = {}
|
69
|
+
#{parts.join("\n\n")}
|
70
|
+
h
|
71
|
+
end
|
72
|
+
RUBY
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: strict
|
3
|
+
|
4
|
+
module T::Props
|
5
|
+
module Private
|
6
|
+
module SetterFactory
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
SetterProc = T.type_alias {T.proc.params(val: T.untyped).void}
|
10
|
+
ValidateProc = T.type_alias {T.proc.params(prop: Symbol, value: T.untyped).void}
|
11
|
+
|
12
|
+
sig do
|
13
|
+
params(
|
14
|
+
klass: T.all(Module, T::Props::ClassMethods),
|
15
|
+
prop: Symbol,
|
16
|
+
rules: T::Hash[Symbol, T.untyped]
|
17
|
+
)
|
18
|
+
.returns(SetterProc)
|
19
|
+
.checked(:never)
|
20
|
+
end
|
21
|
+
def self.build_setter_proc(klass, prop, rules)
|
22
|
+
# Our nil check works differently than a simple T.nilable for various
|
23
|
+
# reasons (including the `raise_on_nil_write` setting and the existence
|
24
|
+
# of defaults & factories), so unwrap any T.nilable and do a check
|
25
|
+
# manually.
|
26
|
+
non_nil_type = T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object))
|
27
|
+
accessor_key = rules.fetch(:accessor_key)
|
28
|
+
validate = rules[:setter_validate]
|
29
|
+
|
30
|
+
# It seems like a bug that this affects the behavior of setters, but
|
31
|
+
# some existing code relies on this behavior
|
32
|
+
has_explicit_nil_default = rules.key?(:default) && rules.fetch(:default).nil?
|
33
|
+
|
34
|
+
# Use separate methods in order to ensure that we only close over necessary
|
35
|
+
# variables
|
36
|
+
if !T::Props::Utils.need_nil_write_check?(rules) || has_explicit_nil_default
|
37
|
+
nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
|
38
|
+
else
|
39
|
+
non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
sig do
|
44
|
+
params(
|
45
|
+
prop: Symbol,
|
46
|
+
accessor_key: Symbol,
|
47
|
+
non_nil_type: T::Types::Base,
|
48
|
+
klass: T.all(Module, T::Props::ClassMethods),
|
49
|
+
validate: T.nilable(ValidateProc)
|
50
|
+
)
|
51
|
+
.returns(SetterProc)
|
52
|
+
end
|
53
|
+
private_class_method def self.non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
|
54
|
+
proc do |val|
|
55
|
+
if non_nil_type.valid?(val)
|
56
|
+
if validate
|
57
|
+
validate.call(prop, val)
|
58
|
+
end
|
59
|
+
instance_variable_set(accessor_key, val)
|
60
|
+
else
|
61
|
+
T::Props::Private::SetterFactory.raise_pretty_error(
|
62
|
+
klass,
|
63
|
+
prop,
|
64
|
+
non_nil_type,
|
65
|
+
val,
|
66
|
+
)
|
67
|
+
instance_variable_set(accessor_key, val)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
sig do
|
73
|
+
params(
|
74
|
+
prop: Symbol,
|
75
|
+
accessor_key: Symbol,
|
76
|
+
non_nil_type: T::Types::Base,
|
77
|
+
klass: T.all(Module, T::Props::ClassMethods),
|
78
|
+
validate: T.nilable(ValidateProc),
|
79
|
+
)
|
80
|
+
.returns(SetterProc)
|
81
|
+
end
|
82
|
+
private_class_method def self.nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
|
83
|
+
proc do |val|
|
84
|
+
if val.nil?
|
85
|
+
instance_variable_set(accessor_key, nil)
|
86
|
+
elsif non_nil_type.valid?(val)
|
87
|
+
if validate
|
88
|
+
validate.call(prop, val)
|
89
|
+
end
|
90
|
+
instance_variable_set(accessor_key, val)
|
91
|
+
else
|
92
|
+
T::Props::Private::SetterFactory.raise_pretty_error(
|
93
|
+
klass,
|
94
|
+
prop,
|
95
|
+
non_nil_type,
|
96
|
+
val,
|
97
|
+
)
|
98
|
+
instance_variable_set(accessor_key, val)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
sig do
|
104
|
+
params(
|
105
|
+
klass: T.all(Module, T::Props::ClassMethods),
|
106
|
+
prop: Symbol,
|
107
|
+
type: T.any(T::Types::Base, Module),
|
108
|
+
val: T.untyped,
|
109
|
+
)
|
110
|
+
.void
|
111
|
+
end
|
112
|
+
def self.raise_pretty_error(klass, prop, type, val)
|
113
|
+
base_message = "Can't set #{klass.name}.#{prop} to #{val.inspect} (instance of #{val.class}) - need a #{type}"
|
114
|
+
|
115
|
+
pretty_message = "Parameter '#{prop}': #{base_message}\n"
|
116
|
+
caller_loc = caller_locations&.find {|l| !l.to_s.include?('sorbet-runtime/lib/types/props')}
|
117
|
+
if caller_loc
|
118
|
+
pretty_message += "Caller: #{caller_loc.path}:#{caller_loc.lineno}\n"
|
119
|
+
end
|
120
|
+
|
121
|
+
T::Configuration.call_validation_error_handler(
|
122
|
+
nil,
|
123
|
+
message: base_message,
|
124
|
+
pretty_message: pretty_message,
|
125
|
+
kind: 'Parameter',
|
126
|
+
name: prop,
|
127
|
+
type: type,
|
128
|
+
value: val,
|
129
|
+
location: caller_loc,
|
130
|
+
)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,330 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
# typed: false
|
3
|
+
|
4
|
+
module T::Props::Serializable
|
5
|
+
include T::Props::Plugin
|
6
|
+
# Required because we have special handling for `optional: false`
|
7
|
+
include T::Props::Optional
|
8
|
+
# Required because we have special handling for extra_props
|
9
|
+
include T::Props::PrettyPrintable
|
10
|
+
|
11
|
+
# Serializes this object to a hash, suitable for conversion to
|
12
|
+
# JSON/BSON.
|
13
|
+
#
|
14
|
+
# @param strict [T::Boolean] (true) If false, do not raise an
|
15
|
+
# exception if this object has mandatory props with missing
|
16
|
+
# values.
|
17
|
+
# @return [Hash] A serialization of this object.
|
18
|
+
def serialize(strict=true)
|
19
|
+
begin
|
20
|
+
h = __t_props_generated_serialize(strict)
|
21
|
+
rescue => e
|
22
|
+
msg = self.class.decorator.message_with_generated_source_context(
|
23
|
+
e,
|
24
|
+
:__t_props_generated_serialize,
|
25
|
+
:generate_serialize_source
|
26
|
+
)
|
27
|
+
if msg
|
28
|
+
begin
|
29
|
+
raise e.class.new(msg)
|
30
|
+
rescue ArgumentError
|
31
|
+
raise TypeError.new(msg)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
raise
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
h.merge!(@_extra_props) if @_extra_props
|
39
|
+
h
|
40
|
+
end
|
41
|
+
|
42
|
+
private def __t_props_generated_serialize(strict)
|
43
|
+
# No-op; will be overridden if there are any props.
|
44
|
+
#
|
45
|
+
# To see the definition for class `Foo`, run `Foo.decorator.send(:generate_serialize_source)`
|
46
|
+
{}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Populates the property values on this object with the values
|
50
|
+
# from a hash. In general, prefer to use {.from_hash} to construct
|
51
|
+
# a new instance, instead of loading into an existing instance.
|
52
|
+
#
|
53
|
+
# @param hash [Hash<String, Object>] The hash to take property
|
54
|
+
# values from.
|
55
|
+
# @param strict [T::Boolean] (false) If true, raise an exception if
|
56
|
+
# the hash contains keys that do not correspond to any known
|
57
|
+
# props on this instance.
|
58
|
+
# @return [void]
|
59
|
+
def deserialize(hash, strict=false)
|
60
|
+
begin
|
61
|
+
hash_keys_matching_props = __t_props_generated_deserialize(hash)
|
62
|
+
rescue => e
|
63
|
+
msg = self.class.decorator.message_with_generated_source_context(
|
64
|
+
e,
|
65
|
+
:__t_props_generated_deserialize,
|
66
|
+
:generate_deserialize_source
|
67
|
+
)
|
68
|
+
if msg
|
69
|
+
begin
|
70
|
+
raise e.class.new(msg)
|
71
|
+
rescue ArgumentError
|
72
|
+
raise TypeError.new(msg)
|
73
|
+
end
|
74
|
+
else
|
75
|
+
raise
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
if hash.size > hash_keys_matching_props
|
80
|
+
serialized_forms = self.class.decorator.prop_by_serialized_forms
|
81
|
+
extra = hash.reject {|k, _| serialized_forms.key?(k)}
|
82
|
+
|
83
|
+
# `extra` could still be empty here if the input matches a `dont_store` prop;
|
84
|
+
# historically, we just ignore those
|
85
|
+
if !extra.empty?
|
86
|
+
if strict
|
87
|
+
raise "Unknown properties for #{self.class.name}: #{extra.keys.inspect}"
|
88
|
+
else
|
89
|
+
@_extra_props = extra
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
private def __t_props_generated_deserialize(hash)
|
96
|
+
# No-op; will be overridden if there are any props.
|
97
|
+
#
|
98
|
+
# To see the definition for class `Foo`, run `Foo.decorator.send(:generate_deserialize_source)`
|
99
|
+
0
|
100
|
+
end
|
101
|
+
|
102
|
+
# with() will clone the old object to the new object and merge the specified props to the new object.
|
103
|
+
def with(changed_props)
|
104
|
+
with_existing_hash(changed_props, existing_hash: self.serialize)
|
105
|
+
end
|
106
|
+
|
107
|
+
private def recursive_stringify_keys(obj)
|
108
|
+
if obj.is_a?(Hash)
|
109
|
+
new_obj = obj.class.new
|
110
|
+
obj.each do |k, v|
|
111
|
+
new_obj[k.to_s] = recursive_stringify_keys(v)
|
112
|
+
end
|
113
|
+
elsif obj.is_a?(Array)
|
114
|
+
new_obj = obj.map {|v| recursive_stringify_keys(v)}
|
115
|
+
else
|
116
|
+
new_obj = obj
|
117
|
+
end
|
118
|
+
new_obj
|
119
|
+
end
|
120
|
+
|
121
|
+
private def with_existing_hash(changed_props, existing_hash:)
|
122
|
+
serialized = existing_hash
|
123
|
+
new_val = self.class.from_hash(serialized.merge(recursive_stringify_keys(changed_props)))
|
124
|
+
old_extra = self.instance_variable_get(:@_extra_props) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
|
125
|
+
new_extra = new_val.instance_variable_get(:@_extra_props) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
|
126
|
+
if old_extra != new_extra
|
127
|
+
difference =
|
128
|
+
if old_extra
|
129
|
+
new_extra.reject {|k, v| old_extra[k] == v}
|
130
|
+
else
|
131
|
+
new_extra
|
132
|
+
end
|
133
|
+
raise ArgumentError.new("Unexpected arguments: input(#{changed_props}), unexpected(#{difference})")
|
134
|
+
end
|
135
|
+
new_val
|
136
|
+
end
|
137
|
+
|
138
|
+
# Asserts if this property is missing during strict serialize
|
139
|
+
private def required_prop_missing_from_serialize(prop)
|
140
|
+
if @_required_props_missing_from_deserialize&.include?(prop)
|
141
|
+
# If the prop was already missing during deserialization, that means the application
|
142
|
+
# code already had to deal with a nil value, which means we wouldn't be accomplishing
|
143
|
+
# much by raising here (other than causing an unnecessary breakage).
|
144
|
+
T::Configuration.log_info_handler(
|
145
|
+
"chalk-odm: missing required property in serialize",
|
146
|
+
prop: prop, class: self.class.name, id: self.class.decorator.get_id(self)
|
147
|
+
)
|
148
|
+
else
|
149
|
+
raise TypeError.new("#{self.class.name}.#{prop} not set for non-optional prop")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Marks this property as missing during deserialize
|
154
|
+
private def required_prop_missing_from_deserialize(prop)
|
155
|
+
@_required_props_missing_from_deserialize ||= Set[]
|
156
|
+
@_required_props_missing_from_deserialize << prop
|
157
|
+
nil
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
##############################################
|
163
|
+
|
164
|
+
# NB: This must stay in the same file where T::Props::Serializable is defined due to
|
165
|
+
# T::Props::Decorator#apply_plugin; see https://git.corp.stripe.com/stripe-internal/pay-server/blob/fc7f15593b49875f2d0499ffecfd19798bac05b3/chalk/odm/lib/chalk-odm/document_decorator.rb#L716-L717
|
166
|
+
module T::Props::Serializable::DecoratorMethods
|
167
|
+
include T::Props::HasLazilySpecializedMethods::DecoratorMethods
|
168
|
+
|
169
|
+
VALID_RULE_KEYS = {dont_store: true, name: true, raise_on_nil_write: true}.freeze
|
170
|
+
private_constant :VALID_RULE_KEYS
|
171
|
+
|
172
|
+
def valid_rule_key?(key)
|
173
|
+
super || VALID_RULE_KEYS[key]
|
174
|
+
end
|
175
|
+
|
176
|
+
def required_props
|
177
|
+
@class.props.select {|_, v| T::Props::Utils.required_prop?(v)}.keys
|
178
|
+
end
|
179
|
+
|
180
|
+
def prop_dont_store?(prop); prop_rules(prop)[:dont_store]; end
|
181
|
+
def prop_by_serialized_forms; @class.prop_by_serialized_forms; end
|
182
|
+
|
183
|
+
def from_hash(hash, strict=false)
|
184
|
+
raise ArgumentError.new("#{hash.inspect} provided to from_hash") if !(hash && hash.is_a?(Hash))
|
185
|
+
|
186
|
+
i = @class.allocate
|
187
|
+
i.deserialize(hash, strict)
|
188
|
+
|
189
|
+
i
|
190
|
+
end
|
191
|
+
|
192
|
+
def prop_serialized_form(prop)
|
193
|
+
prop_rules(prop)[:serialized_form]
|
194
|
+
end
|
195
|
+
|
196
|
+
def serialized_form_prop(serialized_form)
|
197
|
+
prop_by_serialized_forms[serialized_form.to_s] || raise("No such serialized form: #{serialized_form.inspect}")
|
198
|
+
end
|
199
|
+
|
200
|
+
def add_prop_definition(prop, rules)
|
201
|
+
rules[:serialized_form] = rules.fetch(:name, prop.to_s)
|
202
|
+
res = super
|
203
|
+
prop_by_serialized_forms[rules[:serialized_form]] = prop
|
204
|
+
enqueue_lazy_method_definition!(:__t_props_generated_serialize) {generate_serialize_source}
|
205
|
+
enqueue_lazy_method_definition!(:__t_props_generated_deserialize) {generate_deserialize_source}
|
206
|
+
res
|
207
|
+
end
|
208
|
+
|
209
|
+
private def generate_serialize_source
|
210
|
+
T::Props::Private::SerializerGenerator.generate(props)
|
211
|
+
end
|
212
|
+
|
213
|
+
private def generate_deserialize_source
|
214
|
+
T::Props::Private::DeserializerGenerator.generate(
|
215
|
+
props,
|
216
|
+
props_with_defaults || {},
|
217
|
+
)
|
218
|
+
end
|
219
|
+
|
220
|
+
def message_with_generated_source_context(error, generated_method, generate_source_method)
|
221
|
+
line_label = error.backtrace.find {|l| l.end_with?("in `#{generated_method}'")}
|
222
|
+
return unless line_label
|
223
|
+
|
224
|
+
line_num = line_label.split(':')[1]&.to_i
|
225
|
+
return unless line_num
|
226
|
+
|
227
|
+
source_lines = self.send(generate_source_method).split("\n")
|
228
|
+
previous_blank = source_lines[0...line_num].rindex(&:empty?) || 0
|
229
|
+
next_blank = line_num + (source_lines[line_num..-1]&.find_index(&:empty?) || 0)
|
230
|
+
context = " " + source_lines[(previous_blank + 1)...(next_blank)].join("\n ")
|
231
|
+
<<~MSG
|
232
|
+
Error in #{decorated_class.name}##{generated_method}: #{error.message}
|
233
|
+
at line #{line_num-previous_blank-1} in:
|
234
|
+
#{context}
|
235
|
+
MSG
|
236
|
+
end
|
237
|
+
|
238
|
+
def raise_nil_deserialize_error(hkey)
|
239
|
+
msg = "Tried to deserialize a required prop from a nil value. It's "\
|
240
|
+
"possible that a nil value exists in the database, so you should "\
|
241
|
+
"provide a `default: or factory:` for this prop (see go/optional "\
|
242
|
+
"for more details). If this is already the case, you probably "\
|
243
|
+
"omitted a required prop from the `fields:` option when doing a "\
|
244
|
+
"partial load."
|
245
|
+
storytime = {prop: hkey, klass: decorated_class.name}
|
246
|
+
|
247
|
+
# Notify the model owner if it exists, and always notify the API owner.
|
248
|
+
begin
|
249
|
+
if defined?(Opus) && defined?(Opus::Ownership) && decorated_class < Opus::Ownership
|
250
|
+
T::Configuration.hard_assert_handler(
|
251
|
+
msg,
|
252
|
+
storytime: storytime,
|
253
|
+
project: decorated_class.get_owner
|
254
|
+
)
|
255
|
+
end
|
256
|
+
ensure
|
257
|
+
T::Configuration.hard_assert_handler(msg, storytime: storytime)
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def prop_validate_definition!(name, cls, rules, type)
|
262
|
+
result = super
|
263
|
+
|
264
|
+
if (rules_name = rules[:name])
|
265
|
+
unless rules_name.is_a?(String)
|
266
|
+
raise ArgumentError.new("Invalid name in prop #{@class.name}.#{name}: #{rules_name.inspect}")
|
267
|
+
end
|
268
|
+
|
269
|
+
validate_prop_name(rules_name)
|
270
|
+
end
|
271
|
+
|
272
|
+
if !rules[:raise_on_nil_write].nil? && rules[:raise_on_nil_write] != true
|
273
|
+
raise ArgumentError.new("The value of `raise_on_nil_write` if specified must be `true` (given: #{rules[:raise_on_nil_write]}).")
|
274
|
+
end
|
275
|
+
|
276
|
+
result
|
277
|
+
end
|
278
|
+
|
279
|
+
def get_id(instance)
|
280
|
+
prop = prop_by_serialized_forms['_id']
|
281
|
+
if prop
|
282
|
+
get(instance, prop)
|
283
|
+
else
|
284
|
+
nil
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
EMPTY_EXTRA_PROPS = {}.freeze
|
289
|
+
private_constant :EMPTY_EXTRA_PROPS
|
290
|
+
|
291
|
+
def extra_props(instance)
|
292
|
+
instance.instance_variable_get(:@_extra_props) || EMPTY_EXTRA_PROPS
|
293
|
+
end
|
294
|
+
|
295
|
+
# @override T::Props::PrettyPrintable
|
296
|
+
private def inspect_instance_components(instance, multiline:, indent:)
|
297
|
+
if (extra_props = extra_props(instance)) && !extra_props.empty?
|
298
|
+
pretty_kvs = extra_props.map {|k, v| [k.to_sym, v.inspect]}
|
299
|
+
extra = join_props_with_pretty_values(pretty_kvs, multiline: false)
|
300
|
+
super + ["@_extra_props=<#{extra}>"]
|
301
|
+
else
|
302
|
+
super
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
|
308
|
+
##############################################
|
309
|
+
|
310
|
+
|
311
|
+
# NB: This must stay in the same file where T::Props::Serializable is defined due to
|
312
|
+
# T::Props::Decorator#apply_plugin; see https://git.corp.stripe.com/stripe-internal/pay-server/blob/fc7f15593b49875f2d0499ffecfd19798bac05b3/chalk/odm/lib/chalk-odm/document_decorator.rb#L716-L717
|
313
|
+
module T::Props::Serializable::ClassMethods
|
314
|
+
def prop_by_serialized_forms; @prop_by_serialized_forms ||= {}; end
|
315
|
+
|
316
|
+
# @!method self.from_hash(hash, strict)
|
317
|
+
#
|
318
|
+
# Allocate a new instance and call {#deserialize} to load a new
|
319
|
+
# object from a hash.
|
320
|
+
# @return [Serializable]
|
321
|
+
def from_hash(hash, strict=false)
|
322
|
+
self.decorator.from_hash(hash, strict)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Equivalent to {.from_hash} with `strict` set to true.
|
326
|
+
# @return [Serializable]
|
327
|
+
def from_hash!(hash)
|
328
|
+
self.decorator.from_hash(hash, true)
|
329
|
+
end
|
330
|
+
end
|