sorbet-runtime 0.5.5413 → 0.5.5417
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/sorbet-runtime.rb +6 -0
- data/lib/types/props/custom_type.rb +12 -0
- data/lib/types/props/decorator.rb +3 -1
- data/lib/types/props/generated_code_validation.rb +251 -0
- data/lib/types/props/has_lazily_specialized_methods.rb +90 -0
- data/lib/types/props/private/deserializer_generator.rb +140 -0
- data/lib/types/props/private/parser.rb +32 -0
- data/lib/types/props/private/serde_transform.rb +131 -0
- data/lib/types/props/private/serializer_generator.rb +77 -0
- data/lib/types/props/serializable.rb +78 -187
- data/lib/types/utils.rb +15 -0
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c236407d857c6e24b8e71d59eb7ad8ec51040e48c0f24c56cee6d1385a9a92ab
|
4
|
+
data.tar.gz: 7a8ad4e57f69bfbff85589e862057345cebbb63c5ec5ed74268e902982cd6731
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e897902e9b86b94cb9c59ea2f07f5de20499bf0f178f4b2c0820df1a047c8010120c1fac1a56f977a0ec0b1493051898bfc648c9e54ce9379fc079870e747789
|
7
|
+
data.tar.gz: 7321f38a27966c9cfbfb66b8af11d4f2be9ebbb0b2d80cc5becafd516a2664c1167f57b102aa8a71b5deccd90a645c06237e4018429a0e7ffb5d46e3717e95da
|
data/lib/sorbet-runtime.rb
CHANGED
@@ -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?(
|
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
|
-
|
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
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
if
|
128
|
-
if
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
209
|
-
if
|
210
|
-
|
211
|
-
|
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
|
-
#
|
258
|
-
def
|
259
|
-
|
260
|
-
|
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
|
-
#
|
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.
|
4
|
+
version: 0.5.5417
|
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-
|
11
|
+
date: 2020-03-05 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
|