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
@@ -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,139 @@
|
|
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
|
+
# this use of recursively_valid? is intentional: unlike for
|
56
|
+
# methods, we want to make sure data at the 'edge'
|
57
|
+
# (e.g. models that go into databases or structs serialized
|
58
|
+
# from disk) are correct, so we use more thorough runtime
|
59
|
+
# checks there
|
60
|
+
if non_nil_type.recursively_valid?(val)
|
61
|
+
validate&.call(prop, val)
|
62
|
+
else
|
63
|
+
T::Props::Private::SetterFactory.raise_pretty_error(
|
64
|
+
klass,
|
65
|
+
prop,
|
66
|
+
non_nil_type,
|
67
|
+
val,
|
68
|
+
)
|
69
|
+
end
|
70
|
+
instance_variable_set(accessor_key, val)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
sig do
|
75
|
+
params(
|
76
|
+
prop: Symbol,
|
77
|
+
accessor_key: Symbol,
|
78
|
+
non_nil_type: T::Types::Base,
|
79
|
+
klass: T.all(Module, T::Props::ClassMethods),
|
80
|
+
validate: T.nilable(ValidateProc),
|
81
|
+
)
|
82
|
+
.returns(SetterProc)
|
83
|
+
end
|
84
|
+
private_class_method def self.nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
|
85
|
+
proc do |val|
|
86
|
+
if val.nil?
|
87
|
+
instance_variable_set(accessor_key, nil)
|
88
|
+
# this use of recursively_valid? is intentional: unlike for
|
89
|
+
# methods, we want to make sure data at the 'edge'
|
90
|
+
# (e.g. models that go into databases or structs serialized
|
91
|
+
# from disk) are correct, so we use more thorough runtime
|
92
|
+
# checks there
|
93
|
+
elsif non_nil_type.recursively_valid?(val)
|
94
|
+
validate&.call(prop, val)
|
95
|
+
instance_variable_set(accessor_key, val)
|
96
|
+
else
|
97
|
+
T::Props::Private::SetterFactory.raise_pretty_error(
|
98
|
+
klass,
|
99
|
+
prop,
|
100
|
+
non_nil_type,
|
101
|
+
val,
|
102
|
+
)
|
103
|
+
instance_variable_set(accessor_key, val)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
sig do
|
109
|
+
params(
|
110
|
+
klass: T.all(Module, T::Props::ClassMethods),
|
111
|
+
prop: Symbol,
|
112
|
+
type: T.any(T::Types::Base, Module),
|
113
|
+
val: T.untyped,
|
114
|
+
)
|
115
|
+
.void
|
116
|
+
end
|
117
|
+
def self.raise_pretty_error(klass, prop, type, val)
|
118
|
+
base_message = "Can't set #{klass.name}.#{prop} to #{val.inspect} (instance of #{val.class}) - need a #{type}"
|
119
|
+
|
120
|
+
pretty_message = "Parameter '#{prop}': #{base_message}\n"
|
121
|
+
caller_loc = caller_locations&.find {|l| !l.to_s.include?('sorbet-runtime/lib/types/props')}
|
122
|
+
if caller_loc
|
123
|
+
pretty_message += "Caller: #{caller_loc.path}:#{caller_loc.lineno}\n"
|
124
|
+
end
|
125
|
+
|
126
|
+
T::Configuration.call_validation_error_handler(
|
127
|
+
nil,
|
128
|
+
message: base_message,
|
129
|
+
pretty_message: pretty_message,
|
130
|
+
kind: 'Parameter',
|
131
|
+
name: prop,
|
132
|
+
type: type,
|
133
|
+
value: val,
|
134
|
+
location: caller_loc,
|
135
|
+
)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -15,97 +15,37 @@ module T::Props::Serializable
|
|
15
15
|
# exception if this object has mandatory props with missing
|
16
16
|
# values.
|
17
17
|
# @return [Hash] A serialization of this object.
|
18
|
-
def serialize(strict=true)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
if
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
T::Configuration.log_info_handler(
|
33
|
-
"chalk-odm: missing required property in serialize",
|
34
|
-
prop: prop, class: self.class.name, id: decorator.get_id(self)
|
35
|
-
)
|
36
|
-
else
|
37
|
-
raise T::Props::InvalidValueError.new("#{self.class.name}.#{prop} not set for non-optional prop")
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
# Don't serialize values that are nil to save space (both the
|
42
|
-
# nil value itself and the field name in the serialized BSON
|
43
|
-
# document)
|
44
|
-
next if rules[:dont_store] || val.nil?
|
45
|
-
|
46
|
-
if rules[:serializable_subtype]
|
47
|
-
if rules[:type_is_serializable]
|
48
|
-
val = val.serialize(strict)
|
49
|
-
elsif rules[:type_is_array_of_serializable]
|
50
|
-
if (subtype = rules[:serializable_subtype]).is_a?(T::Props::CustomType)
|
51
|
-
val = val.map {|el| el && subtype.serialize(el)}
|
52
|
-
else
|
53
|
-
val = val.map {|el| el && el.serialize(strict)}
|
54
|
-
end
|
55
|
-
elsif rules[:type_is_hash_of_serializable_values] && rules[:type_is_hash_of_custom_type_keys]
|
56
|
-
key_subtype = rules[:serializable_subtype][:keys]
|
57
|
-
value_subtype = rules[:serializable_subtype][:values]
|
58
|
-
if value_subtype.is_a?(T::Props::CustomType)
|
59
|
-
val = val.each_with_object({}) do |(key, value), result|
|
60
|
-
result[key_subtype.serialize(key)] = value && value_subtype.serialize(value)
|
61
|
-
end
|
62
|
-
else
|
63
|
-
val = val.each_with_object({}) do |(key, value), result|
|
64
|
-
result[key_subtype.serialize(key)] = value && value.serialize(strict)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
elsif rules[:type_is_hash_of_serializable_values]
|
68
|
-
value_subtype = rules[:serializable_subtype]
|
69
|
-
if value_subtype.is_a?(T::Props::CustomType)
|
70
|
-
val = val.transform_values {|v| v && value_subtype.serialize(v)}
|
71
|
-
else
|
72
|
-
val = val.transform_values {|v| v && v.serialize(strict)}
|
73
|
-
end
|
74
|
-
elsif rules[:type_is_hash_of_custom_type_keys]
|
75
|
-
key_subtype = rules[:serializable_subtype]
|
76
|
-
val = val.each_with_object({}) do |(key, value), result|
|
77
|
-
result[key_subtype.serialize(key)] = value
|
78
|
-
end
|
79
|
-
end
|
80
|
-
elsif rules[:type_is_custom_type]
|
81
|
-
val = rules[:type].serialize(val)
|
82
|
-
|
83
|
-
unless T::Props::CustomType.valid_serialization?(val, rules[:type])
|
84
|
-
msg = "#{rules[:type]} did not serialize to a valid scalar type. It became a: #{val.class}"
|
85
|
-
if val.is_a?(Hash)
|
86
|
-
msg += "\nIf you want to store a structured Hash, consider using a T::Struct as your type."
|
87
|
-
end
|
88
|
-
raise T::Props::InvalidValueError.new(msg)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
needs_clone = rules[:type_needs_clone]
|
93
|
-
if needs_clone
|
94
|
-
if needs_clone == :shallow
|
95
|
-
val = val.dup
|
96
|
-
else
|
97
|
-
val = T::Props::Utils.deep_clone_object(val)
|
18
|
+
def serialize(strict=true) # rubocop:disable Style/OptionalBooleanParameter (changing this API is unfortunately not feasible)
|
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)
|
98
32
|
end
|
33
|
+
else
|
34
|
+
raise
|
99
35
|
end
|
100
|
-
|
101
|
-
h[hkey] = val
|
102
36
|
end
|
103
37
|
|
104
38
|
h.merge!(@_extra_props) if @_extra_props
|
105
|
-
|
106
39
|
h
|
107
40
|
end
|
108
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
|
+
|
109
49
|
# Populates the property values on this object with the values
|
110
50
|
# from a hash. In general, prefer to use {.from_hash} to construct
|
111
51
|
# a new instance, instead of loading into an existing instance.
|
@@ -116,108 +56,49 @@ module T::Props::Serializable
|
|
116
56
|
# the hash contains keys that do not correspond to any known
|
117
57
|
# props on this instance.
|
118
58
|
# @return [void]
|
119
|
-
def deserialize(hash, strict=false)
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
"provide a `default: or factory:` for this prop (see go/optional "\
|
134
|
-
"for more details). If this is already the case, you probably "\
|
135
|
-
"omitted a required prop from the `fields:` option when doing a "\
|
136
|
-
"partial load."
|
137
|
-
storytime = {prop: hkey, klass: self.class.name}
|
138
|
-
|
139
|
-
# Notify the model owner if it exists, and always notify the API owner.
|
140
|
-
begin
|
141
|
-
if defined?(Opus) && defined?(Opus::Ownership) && decorator.decorated_class < Opus::Ownership
|
142
|
-
T::Configuration.hard_assert_handler(
|
143
|
-
msg,
|
144
|
-
storytime: storytime,
|
145
|
-
project: decorator.decorated_class.get_owner
|
146
|
-
)
|
147
|
-
end
|
148
|
-
ensure
|
149
|
-
T::Configuration.hard_assert_handler(msg, storytime: storytime)
|
150
|
-
end
|
151
|
-
end
|
152
|
-
elsif rules[:need_nil_read_check]
|
153
|
-
self.required_prop_missing_from_deserialize(p)
|
59
|
+
def deserialize(hash, strict=false) # rubocop:disable Style/OptionalBooleanParameter (changing this API is unfortunately not feasible)
|
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)
|
154
73
|
end
|
155
|
-
|
156
|
-
matching_props += 1 if hash.key?(hkey)
|
157
74
|
else
|
158
|
-
|
159
|
-
val =
|
160
|
-
if rules[:type_is_array_of_serializable]
|
161
|
-
if subtype.is_a?(T::Props::CustomType)
|
162
|
-
val.map {|el| el && subtype.deserialize(el)}
|
163
|
-
else
|
164
|
-
val.map {|el| el && subtype.from_hash(el)}
|
165
|
-
end
|
166
|
-
elsif rules[:type_is_hash_of_serializable_values] && rules[:type_is_hash_of_custom_type_keys]
|
167
|
-
key_subtype = subtype[:keys]
|
168
|
-
values_subtype = subtype[:values]
|
169
|
-
if values_subtype.is_a?(T::Props::CustomType)
|
170
|
-
val.each_with_object({}) do |(key, value), result|
|
171
|
-
result[key_subtype.deserialize(key)] = value && values_subtype.deserialize(value)
|
172
|
-
end
|
173
|
-
else
|
174
|
-
val.each_with_object({}) do |(key, value), result|
|
175
|
-
result[key_subtype.deserialize(key)] = value && values_subtype.from_hash(value)
|
176
|
-
end
|
177
|
-
end
|
178
|
-
elsif rules[:type_is_hash_of_serializable_values]
|
179
|
-
if subtype.is_a?(T::Props::CustomType)
|
180
|
-
val.transform_values {|v| v && subtype.deserialize(v)}
|
181
|
-
else
|
182
|
-
val.transform_values {|v| v && subtype.from_hash(v)}
|
183
|
-
end
|
184
|
-
elsif rules[:type_is_hash_of_custom_type_keys]
|
185
|
-
val.map do |key, value|
|
186
|
-
[subtype.deserialize(key), value]
|
187
|
-
end.to_h
|
188
|
-
else
|
189
|
-
subtype.from_hash(val)
|
190
|
-
end
|
191
|
-
elsif (needs_clone = rules[:type_needs_clone])
|
192
|
-
val =
|
193
|
-
if needs_clone == :shallow
|
194
|
-
val.dup
|
195
|
-
else
|
196
|
-
T::Props::Utils.deep_clone_object(val)
|
197
|
-
end
|
198
|
-
elsif rules[:type_is_custom_type]
|
199
|
-
val = rules[:type].deserialize(val)
|
200
|
-
end
|
201
|
-
|
202
|
-
matching_props += 1
|
75
|
+
raise
|
203
76
|
end
|
204
|
-
|
205
|
-
self.instance_variable_set(rules[:accessor_key], val) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
|
206
77
|
end
|
207
78
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
h = hash.reject {|k, _| pbsf.key?(k)}
|
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)}
|
212
82
|
|
213
|
-
if
|
214
|
-
|
215
|
-
|
216
|
-
|
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
|
217
91
|
end
|
218
92
|
end
|
219
93
|
end
|
220
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
|
+
|
221
102
|
# with() will clone the old object to the new object and merge the specified props to the new object.
|
222
103
|
def with(changed_props)
|
223
104
|
with_existing_hash(changed_props, existing_hash: self.serialize)
|
@@ -240,8 +121,8 @@ module T::Props::Serializable
|
|
240
121
|
private def with_existing_hash(changed_props, existing_hash:)
|
241
122
|
serialized = existing_hash
|
242
123
|
new_val = self.class.from_hash(serialized.merge(recursive_stringify_keys(changed_props)))
|
243
|
-
old_extra = self.instance_variable_get(:@_extra_props)
|
244
|
-
new_extra = new_val.instance_variable_get(:@_extra_props)
|
124
|
+
old_extra = self.instance_variable_get(:@_extra_props)
|
125
|
+
new_extra = new_val.instance_variable_get(:@_extra_props)
|
245
126
|
if old_extra != new_extra
|
246
127
|
difference =
|
247
128
|
if old_extra
|
@@ -254,14 +135,23 @@ module T::Props::Serializable
|
|
254
135
|
new_val
|
255
136
|
end
|
256
137
|
|
257
|
-
#
|
258
|
-
def
|
259
|
-
|
260
|
-
|
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
|
261
151
|
end
|
262
152
|
|
263
|
-
#
|
264
|
-
def required_prop_missing_from_deserialize(prop)
|
153
|
+
# Marks this property as missing during deserialize
|
154
|
+
private def required_prop_missing_from_deserialize(prop)
|
265
155
|
@_required_props_missing_from_deserialize ||= Set[]
|
266
156
|
@_required_props_missing_from_deserialize << prop
|
267
157
|
nil
|
@@ -274,12 +164,13 @@ end
|
|
274
164
|
# NB: This must stay in the same file where T::Props::Serializable is defined due to
|
275
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
|
276
166
|
module T::Props::Serializable::DecoratorMethods
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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]
|
283
174
|
end
|
284
175
|
|
285
176
|
def required_props
|
@@ -289,7 +180,7 @@ module T::Props::Serializable::DecoratorMethods
|
|
289
180
|
def prop_dont_store?(prop); prop_rules(prop)[:dont_store]; end
|
290
181
|
def prop_by_serialized_forms; @class.prop_by_serialized_forms; end
|
291
182
|
|
292
|
-
def from_hash(hash, strict=false)
|
183
|
+
def from_hash(hash, strict=false) # rubocop:disable Style/OptionalBooleanParameter (changing this API is unfortunately not feasible)
|
293
184
|
raise ArgumentError.new("#{hash.inspect} provided to from_hash") if !(hash && hash.is_a?(Hash))
|
294
185
|
|
295
186
|
i = @class.allocate
|
@@ -310,9 +201,63 @@ module T::Props::Serializable::DecoratorMethods
|
|
310
201
|
rules[:serialized_form] = rules.fetch(:name, prop.to_s)
|
311
202
|
res = super
|
312
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}
|
313
206
|
res
|
314
207
|
end
|
315
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
|
+
|
316
261
|
def prop_validate_definition!(name, cls, rules, type)
|
317
262
|
result = super
|
318
263
|
|
@@ -344,7 +289,7 @@ module T::Props::Serializable::DecoratorMethods
|
|
344
289
|
private_constant :EMPTY_EXTRA_PROPS
|
345
290
|
|
346
291
|
def extra_props(instance)
|
347
|
-
|
292
|
+
instance.instance_variable_get(:@_extra_props) || EMPTY_EXTRA_PROPS
|
348
293
|
end
|
349
294
|
|
350
295
|
# @override T::Props::PrettyPrintable
|
@@ -373,7 +318,7 @@ module T::Props::Serializable::ClassMethods
|
|
373
318
|
# Allocate a new instance and call {#deserialize} to load a new
|
374
319
|
# object from a hash.
|
375
320
|
# @return [Serializable]
|
376
|
-
def from_hash(hash, strict=false)
|
321
|
+
def from_hash(hash, strict=false) # rubocop:disable Style/OptionalBooleanParameter (changing this API is unfortunately not feasible)
|
377
322
|
self.decorator.from_hash(hash, strict)
|
378
323
|
end
|
379
324
|
|