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.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/lib/sorbet-runtime.rb +15 -3
  3. data/lib/types/_types.rb +26 -17
  4. data/lib/types/boolean.rb +1 -1
  5. data/lib/types/compatibility_patches.rb +65 -10
  6. data/lib/types/configuration.rb +93 -7
  7. data/lib/types/enum.rb +371 -0
  8. data/lib/types/generic.rb +2 -2
  9. data/lib/types/interface_wrapper.rb +4 -4
  10. data/lib/types/non_forcing_constants.rb +61 -0
  11. data/lib/types/private/abstract/data.rb +2 -2
  12. data/lib/types/private/abstract/declare.rb +3 -0
  13. data/lib/types/private/abstract/validate.rb +7 -7
  14. data/lib/types/private/casts.rb +27 -0
  15. data/lib/types/private/class_utils.rb +8 -5
  16. data/lib/types/private/methods/_methods.rb +80 -28
  17. data/lib/types/private/methods/call_validation.rb +5 -47
  18. data/lib/types/private/methods/decl_builder.rb +14 -56
  19. data/lib/types/private/methods/modes.rb +5 -7
  20. data/lib/types/private/methods/signature.rb +32 -18
  21. data/lib/types/private/methods/signature_validation.rb +29 -35
  22. data/lib/types/private/retry.rb +10 -0
  23. data/lib/types/private/sealed.rb +21 -1
  24. data/lib/types/private/types/type_alias.rb +31 -0
  25. data/lib/types/private/types/void.rb +4 -3
  26. data/lib/types/profile.rb +5 -1
  27. data/lib/types/props/_props.rb +3 -7
  28. data/lib/types/props/constructor.rb +29 -9
  29. data/lib/types/props/custom_type.rb +51 -27
  30. data/lib/types/props/decorator.rb +248 -405
  31. data/lib/types/props/generated_code_validation.rb +268 -0
  32. data/lib/types/props/has_lazily_specialized_methods.rb +92 -0
  33. data/lib/types/props/optional.rb +37 -41
  34. data/lib/types/props/plugin.rb +23 -1
  35. data/lib/types/props/pretty_printable.rb +3 -3
  36. data/lib/types/props/private/apply_default.rb +170 -0
  37. data/lib/types/props/private/deserializer_generator.rb +165 -0
  38. data/lib/types/props/private/parser.rb +32 -0
  39. data/lib/types/props/private/serde_transform.rb +186 -0
  40. data/lib/types/props/private/serializer_generator.rb +77 -0
  41. data/lib/types/props/private/setter_factory.rb +139 -0
  42. data/lib/types/props/serializable.rb +137 -192
  43. data/lib/types/props/type_validation.rb +19 -6
  44. data/lib/types/props/utils.rb +3 -7
  45. data/lib/types/props/weak_constructor.rb +51 -14
  46. data/lib/types/sig.rb +6 -6
  47. data/lib/types/types/attached_class.rb +37 -0
  48. data/lib/types/types/base.rb +26 -2
  49. data/lib/types/types/fixed_array.rb +28 -2
  50. data/lib/types/types/fixed_hash.rb +11 -10
  51. data/lib/types/types/intersection.rb +6 -0
  52. data/lib/types/types/noreturn.rb +4 -0
  53. data/lib/types/types/self_type.rb +4 -0
  54. data/lib/types/types/simple.rb +22 -1
  55. data/lib/types/types/t_enum.rb +38 -0
  56. data/lib/types/types/type_parameter.rb +1 -1
  57. data/lib/types/types/type_variable.rb +1 -1
  58. data/lib/types/types/typed_array.rb +7 -2
  59. data/lib/types/types/typed_enumerable.rb +28 -17
  60. data/lib/types/types/typed_enumerator.rb +7 -2
  61. data/lib/types/types/typed_hash.rb +8 -3
  62. data/lib/types/types/typed_range.rb +7 -2
  63. data/lib/types/types/typed_set.rb +7 -2
  64. data/lib/types/types/union.rb +37 -5
  65. data/lib/types/types/untyped.rb +4 -0
  66. data/lib/types/utils.rb +43 -11
  67. metadata +103 -11
  68. data/lib/types/private/error_handler.rb +0 -0
  69. data/lib/types/runtime_profiled.rb +0 -24
  70. 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
- decorator = self.class.decorator
20
- h = {}
21
-
22
- decorator.props.each do |prop, rules|
23
- hkey = rules[:serialized_form]
24
-
25
- val = decorator.get(self, prop, rules)
26
-
27
- if val.nil? && strict && !rules[:fully_optional]
28
- # If the prop was already missing during deserialization, that means the application
29
- # code already had to deal with a nil value, which means we wouldn't be accomplishing
30
- # much by raising here (other than causing an unnecessary breakage).
31
- if self.required_prop_missing_from_deserialize?(prop)
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
- decorator = self.class.decorator
121
-
122
- matching_props = 0
123
-
124
- decorator.props.each do |p, rules|
125
- hkey = rules[:serialized_form]
126
- val = hash[hkey]
127
- if val.nil?
128
- if T::Props::Utils.required_prop?(rules)
129
- val = decorator.get_default(rules, self.class)
130
- if val.nil?
131
- msg = "Tried to deserialize a required prop from a nil value. It's "\
132
- "possible that a nil value exists in the database, so you should "\
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
- if (subtype = rules[:serializable_subtype])
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
- # We compute extra_props this way specifically for performance
209
- if matching_props < hash.size
210
- pbsf = decorator.prop_by_serialized_forms
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 strict
214
- raise "Unknown properties for #{self.class.name}: #{h.keys.inspect}"
215
- else
216
- @_extra_props = h
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) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
244
- new_extra = new_val.instance_variable_get(:@_extra_props) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
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
- # @return [T::Boolean] Was this property missing during deserialize?
258
- def required_prop_missing_from_deserialize?(prop)
259
- return false if @_required_props_missing_from_deserialize.nil?
260
- @_required_props_missing_from_deserialize.include?(prop)
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
- # @return Marks this property as missing during deserialize
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
- def valid_props
278
- super + [
279
- :dont_store,
280
- :name,
281
- :raise_on_nil_write,
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
- get(instance, '_extra_props') || EMPTY_EXTRA_PROPS
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