sorbet-runtime 0.5.5400 → 0.5.5429

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4f9fd0690e5e4161f08d170c0ad34c991b419d7718cb6d3cc3db90036c35387
4
- data.tar.gz: acd9cfcc9cca1df5271b927d2dd85ffa0276337276a0df1ee522fe0c55dde9a4
3
+ metadata.gz: f7f54d40d5dbaf968c3ab004187df66db620ff62a74860b03f6c80f4cb59d0b2
4
+ data.tar.gz: 278a695d5bf4f93bfdd2a03f225850e0d7f54b6ee60fa9b838e07b16a55b0370
5
5
  SHA512:
6
- metadata.gz: c9935807087e676840c09e8e2c43b4c3bc25639264bb8ed793a0e677fce16742db55a14362a39e701da6e63b130ffa1213717e755a98695db849dc1c5ba9dcd5
7
- data.tar.gz: b0ee575a179bb1a6da3a10d3f40a16e27a94f739c28ca47bd174b9f7c854d90e5a5085bea444d715f1dc4fa12ffef98cdc98166c2af962ac06c5ff27dce0f911
6
+ metadata.gz: 99bc296b66501a1e58616bbeebef572e8b7154bbeab2405a2c92290a9331e57be59670b081f27003270e32998666a6363da2d38e2d1dfa4489a01c04f3c46a19
7
+ data.tar.gz: e7b6c400aaa036ad2b9fa50d43061785cc4e6fdce0e2afefda2dc515b5875ab7b42c741249d5fd3d603c6f37d517b340b52cbbb13084396b3ef54e45553f07dc
@@ -96,12 +96,18 @@ require_relative 'types/enum'
96
96
  # Props that run sigs statically so have to be after all the others :(
97
97
  require_relative 'types/props/private/setter_factory'
98
98
  require_relative 'types/props/private/apply_default'
99
+ require_relative 'types/props/has_lazily_specialized_methods'
99
100
  require_relative 'types/props/optional'
100
101
  require_relative 'types/props/weak_constructor'
101
102
  require_relative 'types/props/constructor'
102
103
  require_relative 'types/props/pretty_printable'
104
+ require_relative 'types/props/private/serde_transform'
105
+ require_relative 'types/props/private/deserializer_generator'
106
+ require_relative 'types/props/private/serializer_generator'
103
107
  require_relative 'types/props/serializable'
104
108
  require_relative 'types/props/type_validation'
109
+ require_relative 'types/props/private/parser'
110
+ require_relative 'types/props/generated_code_validation'
105
111
 
106
112
  require_relative 'types/struct'
107
113
 
@@ -80,5 +80,17 @@ module T::Props
80
80
  scalar_type?(val)
81
81
  end
82
82
  end
83
+
84
+ def self.checked_serialize(type, instance)
85
+ val = type.serialize(instance)
86
+ unless valid_serialization?(val, type)
87
+ msg = "#{type} did not serialize to a valid scalar type. It became a: #{val.class}"
88
+ if val.is_a?(Hash)
89
+ msg += "\nIf you want to store a structured Hash, consider using a T::Struct as your type."
90
+ end
91
+ raise T::Props::InvalidValueError.new(msg)
92
+ end
93
+ val
94
+ end
83
95
  end
84
96
  end
@@ -20,7 +20,7 @@ class T::Props::Decorator
20
20
  EMPTY_PROPS = T.let({}.freeze, T::Hash[Symbol, Rules])
21
21
  private_constant :EMPTY_PROPS
22
22
 
23
- sig {params(klass: T.untyped).void}
23
+ sig {params(klass: T.untyped).void.checked(:never)}
24
24
  def initialize(klass)
25
25
  @class = T.let(klass, T.all(Module, T::Props::ClassMethods))
26
26
  @class.plugins.each do |mod|
@@ -67,6 +67,7 @@ class T::Props::Decorator
67
67
  clobber_existing_method!
68
68
  extra
69
69
  optional
70
+ setter_validate
70
71
  _tnilable
71
72
  }.map {|k| [k, true]}.to_h.freeze, T::Hash[Symbol, T::Boolean])
72
73
  private_constant :VALID_RULE_KEYS
@@ -229,10 +230,12 @@ class T::Props::Decorator
229
230
  nil
230
231
  end
231
232
 
233
+ SAFE_NAME = /\A[A-Za-z_][A-Za-z0-9_-]*\z/
234
+
232
235
  # Used to validate both prop names and serialized forms
233
236
  sig {params(name: T.any(Symbol, String)).void}
234
237
  private def validate_prop_name(name)
235
- if !name.match?(/\A[A-Za-z_][A-Za-z0-9_-]*\z/)
238
+ if !name.match?(SAFE_NAME)
236
239
  raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
237
240
  end
238
241
  end
@@ -429,7 +432,7 @@ class T::Props::Decorator
429
432
  end
430
433
 
431
434
  handle_foreign_option(name, cls, rules, rules[:foreign]) if rules[:foreign]
432
- handle_foreign_hint_only_option(cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
435
+ handle_foreign_hint_only_option(name, cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
433
436
  handle_redaction_option(name, rules[:redaction]) if rules[:redaction]
434
437
  end
435
438
 
@@ -636,12 +639,13 @@ class T::Props::Decorator
636
639
 
637
640
  sig do
638
641
  params(
642
+ prop_name: Symbol,
639
643
  prop_cls: Module,
640
644
  foreign_hint_only: T.untyped,
641
645
  )
642
646
  .void
643
647
  end
644
- private def handle_foreign_hint_only_option(prop_cls, foreign_hint_only)
648
+ private def handle_foreign_hint_only_option(prop_name, prop_cls, foreign_hint_only)
645
649
  if ![String, Array].include?(prop_cls) && !(prop_cls.is_a?(T::Props::CustomType))
646
650
  raise ArgumentError.new(
647
651
  "`foreign_hint_only` can only be used with String or Array prop types"
@@ -652,6 +656,21 @@ class T::Props::Decorator
652
656
  :foreign_hint_only, foreign_hint_only,
653
657
  valid_type_msg: "an individual or array of a model class, or a Proc returning such."
654
658
  )
659
+
660
+ unless foreign_hint_only.is_a?(Proc)
661
+ T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign_hint_only}, notify: 'jerry')
662
+ Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign_hint_only`. In other words:
663
+
664
+ instead of `prop :foo, String, foreign_hint_only: FooModel`
665
+ use `prop :foo, String, foreign_hint_only: -> {FooModel}`
666
+
667
+ OR
668
+
669
+ instead of `prop :foo, String, foreign_hint_only: [FooModel, BarModel]`
670
+ use `prop :foo, String, foreign_hint_only: -> {[FooModel, BarModel]}`
671
+
672
+ MESSAGE
673
+ end
655
674
  end
656
675
 
657
676
  # checked(:never) - Rules hash is expensive to check
@@ -745,6 +764,16 @@ class T::Props::Decorator
745
764
  )
746
765
  end
747
766
 
767
+ unless foreign.is_a?(Proc)
768
+ T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign}, notify: 'jerry')
769
+ Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign`. In other words:
770
+
771
+ instead of `prop :foo, String, foreign: FooModel`
772
+ use `prop :foo, String, foreign: -> {FooModel}`
773
+
774
+ MESSAGE
775
+ end
776
+
748
777
  define_foreign_method(prop_name, rules, foreign)
749
778
  end
750
779
 
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module T::Props
5
+ # Helper to validate generated code, to mitigate security concerns around
6
+ # `class_eval`. Not called by default; the expectation is this will be used
7
+ # in a test iterating over all T::Props::Serializable subclasses.
8
+ #
9
+ # We validate the exact expected structure of the generated methods as far
10
+ # as we can, and then where cloning produces an arbitrarily nested structure,
11
+ # we just validate a lack of side effects.
12
+ module GeneratedCodeValidation
13
+ extend Private::Parse
14
+
15
+ class ValidationError < RuntimeError; end
16
+
17
+ def self.validate_deserialize(source)
18
+ parsed = parse(source)
19
+
20
+ # def %<name>(hash)
21
+ # ...
22
+ # end
23
+ assert_equal(:def, parsed.type)
24
+ name, args, body = parsed.children
25
+ assert_equal(:__t_props_generated_deserialize, name)
26
+ assert_equal(s(:args, s(:arg, :hash)), args)
27
+
28
+ assert_equal(:begin, body.type)
29
+ init, *prop_clauses, ret = body.children
30
+
31
+ # found = %<prop_count>
32
+ # ...
33
+ # found
34
+ assert_equal(:lvasgn, init.type)
35
+ init_name, init_val = init.children
36
+ assert_equal(:found, init_name)
37
+ assert_equal(:int, init_val.type)
38
+ assert_equal(s(:lvar, :found), ret)
39
+
40
+ prop_clauses.each_with_index do |clause, i|
41
+ if i % 2 == 0
42
+ validate_deserialize_hash_read(clause)
43
+ else
44
+ validate_deserialize_ivar_set(clause)
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.validate_serialize(source)
50
+ parsed = parse(source)
51
+
52
+ # def %<name>(strict)
53
+ # ...
54
+ # end
55
+ assert_equal(:def, parsed.type)
56
+ name, args, body = parsed.children
57
+ assert_equal(:__t_props_generated_serialize, name)
58
+ assert_equal(s(:args, s(:arg, :strict)), args)
59
+
60
+ assert_equal(:begin, body.type)
61
+ init, *prop_clauses, ret = body.children
62
+
63
+ # h = {}
64
+ # ...
65
+ # h
66
+ assert_equal(s(:lvasgn, :h, s(:hash)), init)
67
+ assert_equal(s(:lvar, :h), ret)
68
+
69
+ prop_clauses.each do |clause|
70
+ validate_serialize_clause(clause)
71
+ end
72
+ end
73
+
74
+ private_class_method def self.validate_serialize_clause(clause)
75
+ assert_equal(:if, clause.type)
76
+ condition, if_body, else_body = clause.children
77
+
78
+ # if @%<accessor_key>.nil?
79
+ assert_equal(:send, condition.type)
80
+ receiver, method = condition.children
81
+ assert_equal(:ivar, receiver.type)
82
+ assert_equal(:nil?, method)
83
+
84
+ unless if_body.nil?
85
+ # required_prop_missing_from_serialize(%<prop>) if strict
86
+ assert_equal(:if, if_body.type)
87
+ if_strict_condition, if_strict_body, if_strict_else = if_body.children
88
+ assert_equal(s(:lvar, :strict), if_strict_condition)
89
+ assert_equal(:send, if_strict_body.type)
90
+ on_strict_receiver, on_strict_method, on_strict_arg = if_strict_body.children
91
+ assert_equal(nil, on_strict_receiver)
92
+ assert_equal(:required_prop_missing_from_serialize, on_strict_method)
93
+ assert_equal(:sym, on_strict_arg.type)
94
+ assert_equal(nil, if_strict_else)
95
+ end
96
+
97
+ # h[%<serialized_form>] = ...
98
+ assert_equal(:send, else_body.type)
99
+ receiver, method, h_key, h_val = else_body.children
100
+ assert_equal(s(:lvar, :h), receiver)
101
+ assert_equal(:[]=, method)
102
+ assert_equal(:str, h_key.type)
103
+
104
+ validate_lack_of_side_effects(h_val, whitelisted_methods_for_serialize)
105
+ end
106
+
107
+ private_class_method def self.validate_deserialize_hash_read(clause)
108
+ # val = hash[%<serialized_form>s]
109
+
110
+ assert_equal(:lvasgn, clause.type)
111
+ name, val = clause.children
112
+ assert_equal(:val, name)
113
+ assert_equal(:send, val.type)
114
+ receiver, method, arg = val.children
115
+ assert_equal(s(:lvar, :hash), receiver)
116
+ assert_equal(:[], method)
117
+ assert_equal(:str, arg.type)
118
+ end
119
+
120
+ private_class_method def self.validate_deserialize_ivar_set(clause)
121
+ # %<accessor_key>s = if val.nil?
122
+ # found -= 1 unless hash.key?(%<serialized_form>s)
123
+ # %<nil_handler>s
124
+ # else
125
+ # %<serialized_val>s
126
+ # end
127
+
128
+ assert_equal(:ivasgn, clause.type)
129
+ ivar_name, deser_val = clause.children
130
+ unless ivar_name.is_a?(Symbol)
131
+ raise ValidationError.new("Unexpected ivar: #{ivar_name}")
132
+ end
133
+
134
+ assert_equal(:if, deser_val.type)
135
+ condition, if_body, else_body = deser_val.children
136
+ assert_equal(s(:send, s(:lvar, :val), :nil?), condition)
137
+
138
+ assert_equal(:begin, if_body.type)
139
+ update_found, handle_nil = if_body.children
140
+ assert_equal(:if, update_found.type)
141
+ found_condition, found_if_body, found_else_body = update_found.children
142
+ assert_equal(:send, found_condition.type)
143
+ receiver, method, arg = found_condition.children
144
+ assert_equal(s(:lvar, :hash), receiver)
145
+ assert_equal(:key?, method)
146
+ assert_equal(:str, arg.type)
147
+ assert_equal(nil, found_if_body)
148
+ assert_equal(s(:op_asgn, s(:lvasgn, :found), :-, s(:int, 1)), found_else_body)
149
+
150
+ validate_deserialize_handle_nil(handle_nil)
151
+ validate_lack_of_side_effects(else_body, whitelisted_methods_for_deserialize)
152
+ end
153
+
154
+ private_class_method def self.validate_deserialize_handle_nil(node)
155
+ case node.type
156
+ when :hash, :array, :str, :sym, :int, :float, :true, :false, :nil
157
+ # Primitives are safe
158
+ when :send
159
+ receiver, method, arg = node.children
160
+ if receiver.nil?
161
+ # required_prop_missing_from_deserialize(%<prop>)
162
+ assert_equal(:required_prop_missing_from_deserialize, method)
163
+ assert_equal(:sym, arg.type)
164
+ elsif receiver == self_class_decorator
165
+ # self.class.decorator.raise_nil_deserialize_error(%<serialized_form>)
166
+ assert_equal(:raise_nil_deserialize_error, method)
167
+ assert_equal(:str, arg.type)
168
+ elsif method == :default
169
+ # self.class.decorator.props_with_defaults.fetch(%<prop>).default
170
+ assert_equal(:send, receiver.type)
171
+ inner_receiver, inner_method, inner_arg = receiver.children
172
+ assert_equal(
173
+ s(:send, self_class_decorator, :props_with_defaults),
174
+ inner_receiver,
175
+ )
176
+ assert_equal(:fetch, inner_method)
177
+ assert_equal(:sym, inner_arg.type)
178
+ else
179
+ raise ValidationError.new("Unexpected receiver in nil handler: #{node.inspect}")
180
+ end
181
+ else
182
+ raise ValidationError.new("Unexpected nil handler: #{node.inspect}")
183
+ end
184
+ end
185
+
186
+ private_class_method def self.self_class_decorator
187
+ @self_class_decorator ||= s(:send, s(:send, s(:self), :class), :decorator).freeze
188
+ end
189
+
190
+ private_class_method def self.validate_lack_of_side_effects(node, whitelisted_methods_by_receiver_type)
191
+ case node.type
192
+ when :const
193
+ # This is ok, because we'll have validated what method has been called
194
+ # if applicable
195
+ when :hash, :array, :str, :sym, :int, :float, :true, :false, :nil, :self
196
+ # Primitives & self are ok
197
+ when :lvar, :arg, :ivar
198
+ # Reading local & instance variables & arguments is ok
199
+ unless node.children.all? {|c| c.is_a?(Symbol)}
200
+ raise ValidationError.new("Unexpected child for #{node.type}: #{node.inspect}")
201
+ end
202
+ when :args, :mlhs, :block, :begin, :if
203
+ # Blocks etc are read-only if their contents are read-only
204
+ node.children.each {|c| validate_lack_of_side_effects(c, whitelisted_methods_by_receiver_type) if c}
205
+ when :send
206
+ # Sends are riskier so check a whitelist
207
+ receiver, method, *args = node.children
208
+ if receiver
209
+ if receiver.type == :send
210
+ key = receiver
211
+ else
212
+ key = receiver.type
213
+ validate_lack_of_side_effects(receiver, whitelisted_methods_by_receiver_type)
214
+ end
215
+
216
+ if !whitelisted_methods_by_receiver_type[key]&.include?(method)
217
+ raise ValidationError.new("Unexpected method #{method} called on #{receiver.inspect}")
218
+ end
219
+ end
220
+ args.each do |arg|
221
+ validate_lack_of_side_effects(arg, whitelisted_methods_by_receiver_type)
222
+ end
223
+ else
224
+ raise ValidationError.new("Unexpected node type #{node.type}: #{node.inspect}")
225
+ end
226
+ end
227
+
228
+ private_class_method def self.assert_equal(expected, actual)
229
+ if expected != actual
230
+ raise ValidationError.new("Expected #{expected}, got #{actual}")
231
+ end
232
+ end
233
+
234
+ # Method calls generated by SerdeTransform
235
+ private_class_method def self.whitelisted_methods_for_serialize
236
+ @whitelisted_methods_for_serialize ||= {
237
+ :lvar => %i{dup map transform_values transform_keys each_with_object nil? []= serialize},
238
+ :ivar => %i{dup map transform_values transform_keys each_with_object serialize},
239
+ :const => %i{checked_serialize deep_clone_object},
240
+ }
241
+ end
242
+
243
+ # Method calls generated by SerdeTransform
244
+ private_class_method def self.whitelisted_methods_for_deserialize
245
+ @whitelisted_methods_for_deserialize ||= {
246
+ :lvar => %i{dup map transform_values transform_keys each_with_object nil? []=},
247
+ :const => %i{deserialize from_hash deep_clone_object},
248
+ }
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module T::Props
5
+
6
+ # Helper for generating methods that replace themselves with a specialized
7
+ # version on first use. The main use case is when we want to generate a
8
+ # method using the full set of props on a class; we can't do that during
9
+ # prop definition because we have no way of knowing whether we are defining
10
+ # the last prop.
11
+ #
12
+ # See go/M8yrvzX2 (Stripe-internal) for discussion of security considerations.
13
+ # In outline, while `class_eval` is a bit scary, we believe that as long as
14
+ # all inputs are defined in version control (and this is enforced by calling
15
+ # `disable_lazy_evaluation!` appropriately), risk isn't significantly higher
16
+ # than with build-time codegen.
17
+ module HasLazilySpecializedMethods
18
+ extend T::Sig
19
+
20
+ class SourceEvaluationDisabled < RuntimeError
21
+ def initialize
22
+ super("Evaluation of lazily-defined methods is disabled")
23
+ end
24
+ end
25
+
26
+ # Disable any future evaluation of lazily-defined methods.
27
+ #
28
+ # This is intended to be called after startup but before interacting with
29
+ # the outside world, to limit attack surface for our `class_eval` use.
30
+ #
31
+ # Note it does _not_ prevent explicit calls to `eagerly_define_lazy_methods!`
32
+ # from working.
33
+ sig {void}
34
+ def self.disable_lazy_evaluation!
35
+ @lazy_evaluation_disabled ||= true
36
+ end
37
+
38
+ sig {returns(T::Boolean)}
39
+ def self.lazy_evaluation_enabled?
40
+ !@lazy_evaluation_disabled
41
+ end
42
+
43
+ module DecoratorMethods
44
+ extend T::Sig
45
+
46
+ sig {returns(T::Hash[Symbol, T.proc.returns(String)]).checked(:never)}
47
+ private def lazily_defined_methods
48
+ @lazily_defined_methods ||= {}
49
+ end
50
+
51
+ sig {params(name: Symbol).void}
52
+ private def eval_lazily_defined_method!(name)
53
+ if !HasLazilySpecializedMethods.lazy_evaluation_enabled?
54
+ raise SourceEvaluationDisabled.new
55
+ end
56
+
57
+ source = lazily_defined_methods.fetch(name).call
58
+
59
+ cls = decorated_class
60
+ cls.class_eval(source.to_s)
61
+ cls.send(:private, name)
62
+ end
63
+
64
+ sig {params(name: Symbol, blk: T.proc.returns(String)).void}
65
+ private def enqueue_lazy_method_definition!(name, &blk)
66
+ lazily_defined_methods[name] = blk
67
+
68
+ cls = decorated_class
69
+ cls.define_method(name) do |*args|
70
+ self.class.decorator.send(:eval_lazily_defined_method!, name)
71
+ send(name, *args)
72
+ end
73
+ cls.send(:private, name)
74
+ end
75
+
76
+ sig {void}
77
+ def eagerly_define_lazy_methods!
78
+ return if lazily_defined_methods.empty?
79
+
80
+ source = lazily_defined_methods.values.map(&:call).map(&:to_s).join("\n\n")
81
+
82
+ cls = decorated_class
83
+ cls.class_eval(source)
84
+ lazily_defined_methods.keys.each {|name| cls.send(:private, name)}
85
+ lazily_defined_methods.clear
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,140 @@
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
+ transformed_val = SerdeTransform.generate(
47
+ T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object)),
48
+ SerdeTransform::Mode::DESERIALIZE,
49
+ 'val'
50
+ ) || 'val'
51
+
52
+ nil_handler = generate_nil_handler(
53
+ prop: prop,
54
+ serialized_form: hash_key,
55
+ default: defaults[prop],
56
+ nilable_type: T::Props::Utils.optional_prop?(rules),
57
+ raise_on_nil_write: !!rules[:raise_on_nil_write],
58
+ )
59
+
60
+ <<~RUBY
61
+ val = hash[#{hash_key.inspect}]
62
+ #{ivar_name} = if val.nil?
63
+ found -= 1 unless hash.key?(#{hash_key.inspect})
64
+ #{nil_handler}
65
+ else
66
+ #{transformed_val}
67
+ end
68
+ RUBY
69
+ end
70
+
71
+ <<~RUBY
72
+ def __t_props_generated_deserialize(hash)
73
+ found = #{stored_props.size}
74
+ #{parts.join("\n\n")}
75
+ found
76
+ end
77
+ RUBY
78
+ end
79
+
80
+ # This is very similar to what we do in ApplyDefault, but has a few
81
+ # key differences that mean we don't just re-use the code:
82
+ #
83
+ # 1. Where the logic in construction is that we generate a default
84
+ # if & only if the prop key isn't present in the input, here we'll
85
+ # generate a default even to override an explicit nil, but only
86
+ # if the prop is actually required.
87
+ # 2. Since we're generating raw Ruby source, we can remove a layer
88
+ # of indirection for marginally better performance; this seems worth
89
+ # it for the common cases of literals and empty arrays/hashes.
90
+ # 3. We need to care about the distinction between `raise_on_nil_write`
91
+ # and actually non-nilable, where new-instance construction doesn't.
92
+ #
93
+ # So we fall back to ApplyDefault only when one of the cases just
94
+ # mentioned doesn't apply.
95
+ sig do
96
+ params(
97
+ prop: Symbol,
98
+ serialized_form: String,
99
+ default: T.nilable(ApplyDefault),
100
+ nilable_type: T::Boolean,
101
+ raise_on_nil_write: T::Boolean,
102
+ )
103
+ .returns(String)
104
+ .checked(:never)
105
+ end
106
+ private_class_method def self.generate_nil_handler(
107
+ prop:,
108
+ serialized_form:,
109
+ default:,
110
+ nilable_type:,
111
+ raise_on_nil_write:
112
+ )
113
+ if !nilable_type
114
+ case default
115
+ when NilClass
116
+ "self.class.decorator.raise_nil_deserialize_error(#{serialized_form.inspect})"
117
+ when ApplyPrimitiveDefault
118
+ literal = default.default
119
+ case literal
120
+ when String, Integer, Symbol, Float, TrueClass, FalseClass, NilClass
121
+ literal.inspect
122
+ else
123
+ "self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
124
+ end
125
+ when ApplyEmptyArrayDefault
126
+ '[]'
127
+ when ApplyEmptyHashDefault
128
+ '{}'
129
+ else
130
+ "self.class.decorator.props_with_defaults.fetch(#{prop.inspect}).default"
131
+ end
132
+ elsif raise_on_nil_write
133
+ "required_prop_missing_from_deserialize(#{prop.inspect})"
134
+ else
135
+ 'nil'
136
+ end
137
+ end
138
+ end
139
+ end
140
+ 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,136 @@
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, Numeric].freeze,
23
+ T::Array[Module],
24
+ )
25
+ private_constant :NO_TRANSFORM_TYPES
26
+
27
+ sig do
28
+ params(
29
+ type: T.any(T::Types::Base, Module),
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 < T::Props::Serializable
69
+ handle_serializable_subtype(varname, raw, mode)
70
+ elsif raw.singleton_class < T::Props::CustomType
71
+ handle_custom_type(varname, T.unsafe(raw), mode)
72
+ else
73
+ "T::Props::Utils.deep_clone_object(#{varname})"
74
+ end
75
+ when T::Types::Union
76
+ non_nil_type = T::Utils.unwrap_nilable(type)
77
+ if non_nil_type
78
+ inner = generate(non_nil_type, mode, varname)
79
+ if inner.nil?
80
+ nil
81
+ else
82
+ "#{varname}.nil? ? nil : #{inner}"
83
+ end
84
+ else
85
+ "T::Props::Utils.deep_clone_object(#{varname})"
86
+ end
87
+ when T::Types::Enum
88
+ generate(T::Utils.lift_enum(type), mode, varname)
89
+ else
90
+ if type.singleton_class < T::Props::CustomType
91
+ # Sometimes this comes wrapped in a T::Types::Simple and sometimes not
92
+ handle_custom_type(varname, T.unsafe(type), mode)
93
+ else
94
+ "T::Props::Utils.deep_clone_object(#{varname})"
95
+ end
96
+ end
97
+ end
98
+
99
+ sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
100
+ private_class_method def self.handle_serializable_subtype(varname, type, mode)
101
+ case mode
102
+ when Serialize
103
+ "#{varname}.serialize(strict)"
104
+ when Deserialize
105
+ type_name = T.must(module_name(type))
106
+ "#{type_name}.from_hash(#{varname})"
107
+ else
108
+ T.absurd(mode)
109
+ end
110
+ end
111
+
112
+ sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
113
+ private_class_method def self.handle_custom_type(varname, type, mode)
114
+ case mode
115
+ when Serialize
116
+ type_name = T.must(module_name(type))
117
+ "T::Props::CustomType.checked_serialize(#{type_name}, #{varname})"
118
+ when Deserialize
119
+ type_name = T.must(module_name(type))
120
+ "#{type_name}.deserialize(#{varname})"
121
+ else
122
+ T.absurd(mode)
123
+ end
124
+ end
125
+
126
+ # Guard against overrides of `name` or `to_s`
127
+ MODULE_NAME = T.let(Module.instance_method(:name), UnboundMethod)
128
+ private_constant :MODULE_NAME
129
+
130
+ sig {params(type: Module).returns(T.nilable(String)).checked(:never)}
131
+ private_class_method def self.module_name(type)
132
+ MODULE_NAME.bind(type).call
133
+ end
134
+ end
135
+ end
136
+ 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
+
@@ -7,6 +7,7 @@ module T::Props
7
7
  extend T::Sig
8
8
 
9
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}
10
11
 
11
12
  sig do
12
13
  params(
@@ -31,6 +32,7 @@ module T::Props
31
32
  T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object))
32
33
  end
33
34
  accessor_key = rules.fetch(:accessor_key)
35
+ validate = rules[:setter_validate]
34
36
 
35
37
  # It seems like a bug that this affects the behavior of setters, but
36
38
  # some existing code relies on this behavior
@@ -39,9 +41,9 @@ module T::Props
39
41
  # Use separate methods in order to ensure that we only close over necessary
40
42
  # variables
41
43
  if !T::Props::Utils.need_nil_write_check?(rules) || has_explicit_nil_default
42
- nilable_proc(prop, accessor_key, non_nil_type)
44
+ nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
43
45
  else
44
- non_nil_proc(prop, accessor_key, non_nil_type)
46
+ non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
45
47
  end
46
48
  end
47
49
 
@@ -50,16 +52,21 @@ module T::Props
50
52
  prop: Symbol,
51
53
  accessor_key: Symbol,
52
54
  non_nil_type: T.any(T::Types::Base, T.all(T::Props::CustomType, Module)),
55
+ klass: T.all(Module, T::Props::ClassMethods),
56
+ validate: T.nilable(ValidateProc)
53
57
  )
54
58
  .returns(SetterProc)
55
59
  end
56
- private_class_method def self.non_nil_proc(prop, accessor_key, non_nil_type)
60
+ private_class_method def self.non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
57
61
  proc do |val|
58
62
  if non_nil_type.valid?(val)
63
+ if validate
64
+ validate.call(prop, val)
65
+ end
59
66
  instance_variable_set(accessor_key, val)
60
67
  else
61
68
  T::Props::Private::SetterFactory.raise_pretty_error(
62
- T.unsafe(self.class),
69
+ klass,
63
70
  prop,
64
71
  non_nil_type,
65
72
  val,
@@ -73,18 +80,23 @@ module T::Props
73
80
  prop: Symbol,
74
81
  accessor_key: Symbol,
75
82
  non_nil_type: T.any(T::Types::Base, T.all(T::Props::CustomType, Module)),
83
+ klass: T.all(Module, T::Props::ClassMethods),
84
+ validate: T.nilable(ValidateProc),
76
85
  )
77
86
  .returns(SetterProc)
78
87
  end
79
- private_class_method def self.nilable_proc(prop, accessor_key, non_nil_type)
88
+ private_class_method def self.nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
80
89
  proc do |val|
81
90
  if val.nil?
82
91
  instance_variable_set(accessor_key, nil)
83
92
  elsif non_nil_type.valid?(val)
93
+ if validate
94
+ validate.call(prop, val)
95
+ end
84
96
  instance_variable_set(accessor_key, val)
85
97
  else
86
98
  T::Props::Private::SetterFactory.raise_pretty_error(
87
- T.unsafe(self.class),
99
+ klass,
88
100
  prop,
89
101
  non_nil_type,
90
102
  val,
@@ -16,96 +16,18 @@ module T::Props::Serializable
16
16
  # values.
17
17
  # @return [Hash] A serialization of this object.
18
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)
98
- end
99
- end
100
-
101
- h[hkey] = val
102
- end
103
-
19
+ h = __t_props_generated_serialize(strict)
104
20
  h.merge!(@_extra_props) if @_extra_props
105
-
106
21
  h
107
22
  end
108
23
 
24
+ private def __t_props_generated_serialize(strict)
25
+ # No-op; will be overridden if there are any props.
26
+ #
27
+ # To see the definition for class `Foo`, run `Foo.decorator.send(:generate_serialize_source)`
28
+ {}
29
+ end
30
+
109
31
  # Populates the property values on this object with the values
110
32
  # from a hash. In general, prefer to use {.from_hash} to construct
111
33
  # a new instance, instead of loading into an existing instance.
@@ -117,105 +39,28 @@ module T::Props::Serializable
117
39
  # props on this instance.
118
40
  # @return [void]
119
41
  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)
154
- end
155
-
156
- matching_props += 1 if hash.key?(hkey)
157
- 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)
42
+ hash_keys_matching_props = __t_props_generated_deserialize(hash)
43
+ if hash.size > hash_keys_matching_props
44
+ serialized_forms = self.class.decorator.prop_by_serialized_forms
45
+ extra = hash.reject {|k, _| serialized_forms.key?(k)}
46
+
47
+ # `extra` could still be empty here if the input matches a `dont_store` prop;
48
+ # historically, we just ignore those
49
+ if !extra.empty?
50
+ if strict
51
+ raise "Unknown properties for #{self.class.name}: #{extra.keys.inspect}"
52
+ else
53
+ @_extra_props = extra
200
54
  end
201
-
202
- matching_props += 1
203
55
  end
204
-
205
- self.instance_variable_set(rules[:accessor_key], val) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
206
56
  end
57
+ end
207
58
 
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)}
212
-
213
- if strict
214
- raise "Unknown properties for #{self.class.name}: #{h.keys.inspect}"
215
- else
216
- @_extra_props = h
217
- end
218
- end
59
+ private def __t_props_generated_deserialize(hash)
60
+ # No-op; will be overridden if there are any props.
61
+ #
62
+ # To see the definition for class `Foo`, run `Foo.decorator.send(:generate_deserialize_source)`
63
+ 0
219
64
  end
220
65
 
221
66
  # with() will clone the old object to the new object and merge the specified props to the new object.
@@ -254,14 +99,23 @@ module T::Props::Serializable
254
99
  new_val
255
100
  end
256
101
 
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)
102
+ # Asserts if this property is missing during strict serialize
103
+ private def required_prop_missing_from_serialize(prop)
104
+ if @_required_props_missing_from_deserialize&.include?(prop)
105
+ # If the prop was already missing during deserialization, that means the application
106
+ # code already had to deal with a nil value, which means we wouldn't be accomplishing
107
+ # much by raising here (other than causing an unnecessary breakage).
108
+ T::Configuration.log_info_handler(
109
+ "chalk-odm: missing required property in serialize",
110
+ prop: prop, class: self.class.name, id: self.class.decorator.get_id(self)
111
+ )
112
+ else
113
+ raise T::Props::InvalidValueError.new("#{self.class.name}.#{prop} not set for non-optional prop")
114
+ end
261
115
  end
262
116
 
263
- # @return Marks this property as missing during deserialize
264
- def required_prop_missing_from_deserialize(prop)
117
+ # Marks this property as missing during deserialize
118
+ private def required_prop_missing_from_deserialize(prop)
265
119
  @_required_props_missing_from_deserialize ||= Set[]
266
120
  @_required_props_missing_from_deserialize << prop
267
121
  nil
@@ -274,6 +128,7 @@ end
274
128
  # NB: This must stay in the same file where T::Props::Serializable is defined due to
275
129
  # 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
130
  module T::Props::Serializable::DecoratorMethods
131
+ include T::Props::HasLazilySpecializedMethods::DecoratorMethods
277
132
 
278
133
  VALID_RULE_KEYS = {dont_store: true, name: true, raise_on_nil_write: true}.freeze
279
134
  private_constant :VALID_RULE_KEYS
@@ -310,9 +165,45 @@ module T::Props::Serializable::DecoratorMethods
310
165
  rules[:serialized_form] = rules.fetch(:name, prop.to_s)
311
166
  res = super
312
167
  prop_by_serialized_forms[rules[:serialized_form]] = prop
168
+ enqueue_lazy_method_definition!(:__t_props_generated_serialize) {generate_serialize_source}
169
+ enqueue_lazy_method_definition!(:__t_props_generated_deserialize) {generate_deserialize_source}
313
170
  res
314
171
  end
315
172
 
173
+ private def generate_serialize_source
174
+ T::Props::Private::SerializerGenerator.generate(props)
175
+ end
176
+
177
+ private def generate_deserialize_source
178
+ T::Props::Private::DeserializerGenerator.generate(
179
+ props,
180
+ props_with_defaults || {},
181
+ )
182
+ end
183
+
184
+ def raise_nil_deserialize_error(hkey)
185
+ msg = "Tried to deserialize a required prop from a nil value. It's "\
186
+ "possible that a nil value exists in the database, so you should "\
187
+ "provide a `default: or factory:` for this prop (see go/optional "\
188
+ "for more details). If this is already the case, you probably "\
189
+ "omitted a required prop from the `fields:` option when doing a "\
190
+ "partial load."
191
+ storytime = {prop: hkey, klass: decorated_class.name}
192
+
193
+ # Notify the model owner if it exists, and always notify the API owner.
194
+ begin
195
+ if defined?(Opus) && defined?(Opus::Ownership) && decorated_class < Opus::Ownership
196
+ T::Configuration.hard_assert_handler(
197
+ msg,
198
+ storytime: storytime,
199
+ project: decorated_class.get_owner
200
+ )
201
+ end
202
+ ensure
203
+ T::Configuration.hard_assert_handler(msg, storytime: storytime)
204
+ end
205
+ end
206
+
316
207
  def prop_validate_definition!(name, cls, rules, type)
317
208
  result = super
318
209
 
data/lib/types/utils.rb CHANGED
@@ -151,6 +151,21 @@ module T::Utils
151
151
  "#{start_part}#{ellipsis}#{end_part}"
152
152
  end
153
153
 
154
+ def self.lift_enum(enum)
155
+ unless enum.is_a?(T::Types::Enum)
156
+ raise ArgumentError.new("#{enum.inspect} is not a T.enum")
157
+ end
158
+
159
+ classes = enum.values.map(&:class).uniq
160
+ if classes.empty?
161
+ T.untyped
162
+ elsif classes.length > 1
163
+ T::Types::Union.new(classes)
164
+ else
165
+ T::Types::Simple.new(classes.first)
166
+ end
167
+ end
168
+
154
169
  module Nilable
155
170
  # :is_union_type, T::Boolean: whether the type is an T::Types::Union type
156
171
  # :non_nilable_type, Class: if it is an T.nilable type, the corresponding underlying type; otherwise, nil.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-runtime
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.5400
4
+ version: 0.5.5429
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stripe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-26 00:00:00.000000000 Z
11
+ date: 2020-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: parser
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '2.7'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.7'
97
111
  description: Sorbet's runtime type checking component
98
112
  email:
99
113
  executables: []
@@ -137,10 +151,16 @@ files:
137
151
  - lib/types/props/custom_type.rb
138
152
  - lib/types/props/decorator.rb
139
153
  - lib/types/props/errors.rb
154
+ - lib/types/props/generated_code_validation.rb
155
+ - lib/types/props/has_lazily_specialized_methods.rb
140
156
  - lib/types/props/optional.rb
141
157
  - lib/types/props/plugin.rb
142
158
  - lib/types/props/pretty_printable.rb
143
159
  - lib/types/props/private/apply_default.rb
160
+ - lib/types/props/private/deserializer_generator.rb
161
+ - lib/types/props/private/parser.rb
162
+ - lib/types/props/private/serde_transform.rb
163
+ - lib/types/props/private/serializer_generator.rb
144
164
  - lib/types/props/private/setter_factory.rb
145
165
  - lib/types/props/serializable.rb
146
166
  - lib/types/props/type_validation.rb