sorbet-runtime 0.5.5413 → 0.5.5420

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: ee0cee600d0c6c8dffef881a35e510ab5e4247e0d1e156f6d1ee83b7d626af4b
4
- data.tar.gz: e3f5c3dea96103665ac92c3b0519f47352d89aa4822acae16f96beb429ab548f
3
+ metadata.gz: 2a516534f055ea25f6c09a17ad4a9b2dddd9bb52092d3a7b11dfc9cee15caaf0
4
+ data.tar.gz: 9cfe46cb9aa4df2b8c87d9da885f97a76e85629432f9a569068ee8f5a985921c
5
5
  SHA512:
6
- metadata.gz: 7b035657a30fe89c2f598c21f07896e476217ea8708ee3569ff1c45b54dd9df3317547efa10137889a15fe01aa712f414b2a30ccdb636f51c01fb18a52a57f12
7
- data.tar.gz: b403812e28bc276d8514c0cfa1fabc856a2027b085bd78460cf018d5eb0c5f6383e066cb23976eef909c9e7121eb01745b4f702f24ae86cf10b426762ab0974d
6
+ metadata.gz: 5bd8c3e9d095a60d95810ae6f73acd084096c32a79025f199a6939757b38163b090eacd5b7aca62c78df816745a26d5d35f97e9b6c5c2d8dbb10d11e07966173
7
+ data.tar.gz: c54efa4d0e6f9a78bf6954d67715f45e940747d9855c59457dbd32c4550728664283eb332702d5b9b6653b3fc5d8eb4f58692e9d4c02ca3b127439b43aee4954
@@ -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
@@ -230,10 +230,12 @@ class T::Props::Decorator
230
230
  nil
231
231
  end
232
232
 
233
+ SAFE_NAME = /\A[A-Za-z_][A-Za-z0-9_-]*\z/
234
+
233
235
  # Used to validate both prop names and serialized forms
234
236
  sig {params(name: T.any(Symbol, String)).void}
235
237
  private def validate_prop_name(name)
236
- if !name.match?(/\A[A-Za-z_][A-Za-z0-9_-]*\z/)
238
+ if !name.match?(SAFE_NAME)
237
239
  raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
238
240
  end
239
241
  end
@@ -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,90 @@
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
+ sig {void}
31
+ def self.disable_lazy_evaluation!
32
+ @lazy_evaluation_disabled ||= true
33
+ end
34
+
35
+ sig {returns(T::Boolean)}
36
+ def self.lazy_evaluation_enabled?
37
+ !@lazy_evaluation_disabled
38
+ end
39
+
40
+ module DecoratorMethods
41
+ extend T::Sig
42
+
43
+ sig {returns(T::Hash[Symbol, T.proc.returns(String)]).checked(:never)}
44
+ private def lazily_defined_methods
45
+ @lazily_defined_methods ||= {}
46
+ end
47
+
48
+ sig {params(name: Symbol).void}
49
+ private def eval_lazily_defined_method!(name)
50
+ if !HasLazilySpecializedMethods.lazy_evaluation_enabled?
51
+ raise SourceEvaluationDisabled.new
52
+ end
53
+
54
+ source = lazily_defined_methods.fetch(name).call
55
+
56
+ cls = decorated_class
57
+ cls.class_eval(source.to_s)
58
+ cls.send(:private, name)
59
+ end
60
+
61
+ sig {params(name: Symbol, blk: T.proc.returns(String)).void}
62
+ private def enqueue_lazy_method_definition!(name, &blk)
63
+ lazily_defined_methods[name] = blk
64
+
65
+ cls = decorated_class
66
+ cls.define_method(name) do |*args|
67
+ self.class.decorator.send(:eval_lazily_defined_method!, name)
68
+ send(name, *args)
69
+ end
70
+ cls.send(:private, name)
71
+ end
72
+
73
+ sig {void}
74
+ def eagerly_define_lazy_methods!
75
+ if !HasLazilySpecializedMethods.lazy_evaluation_enabled?
76
+ raise SourceEvaluationDisabled.new
77
+ elsif lazily_defined_methods.empty?
78
+ return
79
+ end
80
+
81
+ source = lazily_defined_methods.values.map(&:call).map(&:to_s).join("\n\n")
82
+
83
+ cls = decorated_class
84
+ cls.class_eval(source)
85
+ lazily_defined_methods.keys.each {|name| cls.send(:private, name)}
86
+ lazily_defined_methods.clear
87
+ end
88
+ end
89
+ end
90
+ 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,131 @@
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 Mode < T::Enum
10
+ enums do
11
+ SERIALIZE = new
12
+ DESERIALIZE = new
13
+ end
14
+ end
15
+
16
+ NO_TRANSFORM_TYPES = T.let(
17
+ [TrueClass, FalseClass, NilClass, Symbol, String, Numeric].freeze,
18
+ T::Array[Module],
19
+ )
20
+ private_constant :NO_TRANSFORM_TYPES
21
+
22
+ sig do
23
+ params(
24
+ type: T.any(T::Types::Base, Module),
25
+ mode: Mode,
26
+ varname: String,
27
+ )
28
+ .returns(T.nilable(String))
29
+ .checked(:never)
30
+ end
31
+ def self.generate(type, mode, varname)
32
+ case type
33
+ when T::Types::TypedArray
34
+ inner = generate(type.type, mode, 'v')
35
+ if inner.nil?
36
+ "#{varname}.dup"
37
+ else
38
+ "#{varname}.map {|v| #{inner}}"
39
+ end
40
+ when T::Types::TypedSet
41
+ inner = generate(type.type, mode, 'v')
42
+ if inner.nil?
43
+ "#{varname}.dup"
44
+ else
45
+ "Set.new(#{varname}) {|v| #{inner}}"
46
+ end
47
+ when T::Types::TypedHash
48
+ keys = generate(type.keys, mode, 'k')
49
+ values = generate(type.values, mode, 'v')
50
+ if keys && values
51
+ "#{varname}.each_with_object({}) {|(k,v),h| h[#{keys}] = #{values}}"
52
+ elsif keys
53
+ "#{varname}.transform_keys {|k| #{keys}}"
54
+ elsif values
55
+ "#{varname}.transform_values {|v| #{values}}"
56
+ else
57
+ "#{varname}.dup"
58
+ end
59
+ when T::Types::Simple
60
+ raw = type.raw_type
61
+ if NO_TRANSFORM_TYPES.any? {|cls| raw <= cls}
62
+ nil
63
+ elsif raw < T::Props::Serializable
64
+ handle_serializable_subtype(varname, raw, mode)
65
+ elsif raw.singleton_class < T::Props::CustomType
66
+ handle_custom_type(varname, T.unsafe(raw), mode)
67
+ else
68
+ "T::Props::Utils.deep_clone_object(#{varname})"
69
+ end
70
+ when T::Types::Union
71
+ non_nil_type = T::Utils.unwrap_nilable(type)
72
+ if non_nil_type
73
+ inner = generate(non_nil_type, mode, varname)
74
+ if inner.nil?
75
+ nil
76
+ else
77
+ "#{varname}.nil? ? nil : #{inner}"
78
+ end
79
+ else
80
+ "T::Props::Utils.deep_clone_object(#{varname})"
81
+ end
82
+ when T::Types::Enum
83
+ generate(T::Utils.lift_enum(type), mode, varname)
84
+ else
85
+ if type.singleton_class < T::Props::CustomType
86
+ # Sometimes this comes wrapped in a T::Types::Simple and sometimes not
87
+ handle_custom_type(varname, T.unsafe(type), mode)
88
+ else
89
+ "T::Props::Utils.deep_clone_object(#{varname})"
90
+ end
91
+ end
92
+ end
93
+
94
+ sig {params(varname: String, type: Module, mode: Mode).returns(String).checked(:never)}
95
+ private_class_method def self.handle_serializable_subtype(varname, type, mode)
96
+ case mode
97
+ when Mode::SERIALIZE
98
+ "#{varname}.serialize(strict)"
99
+ when Mode::DESERIALIZE
100
+ type_name = T.must(module_name(type))
101
+ "#{type_name}.from_hash(#{varname})"
102
+ else
103
+ T.absurd(mode)
104
+ end
105
+ end
106
+
107
+ sig {params(varname: String, type: Module, mode: Mode).returns(String).checked(:never)}
108
+ private_class_method def self.handle_custom_type(varname, type, mode)
109
+ case mode
110
+ when Mode::SERIALIZE
111
+ type_name = T.must(module_name(type))
112
+ "T::Props::CustomType.checked_serialize(#{type_name}, #{varname})"
113
+ when Mode::DESERIALIZE
114
+ type_name = T.must(module_name(type))
115
+ "#{type_name}.deserialize(#{varname})"
116
+ else
117
+ T.absurd(mode)
118
+ end
119
+ end
120
+
121
+ # Guard against overrides of `name` or `to_s`
122
+ MODULE_NAME = T.let(Module.instance_method(:name), UnboundMethod)
123
+ private_constant :MODULE_NAME
124
+
125
+ sig {params(type: Module).returns(T.nilable(String)).checked(:never)}
126
+ private_class_method def self.module_name(type)
127
+ MODULE_NAME.bind(type).call
128
+ end
129
+ end
130
+ end
131
+ 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
+
@@ -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.5413
4
+ version: 0.5.5420
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stripe
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-04 00:00:00.000000000 Z
11
+ date: 2020-03-06 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