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 +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: 2a516534f055ea25f6c09a17ad4a9b2dddd9bb52092d3a7b11dfc9cee15caaf0
|
|
4
|
+
data.tar.gz: 9cfe46cb9aa4df2b8c87d9da885f97a76e85629432f9a569068ee8f5a985921c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5bd8c3e9d095a60d95810ae6f73acd084096c32a79025f199a6939757b38163b090eacd5b7aca62c78df816745a26d5d35f97e9b6c5c2d8dbb10d11e07966173
|
|
7
|
+
data.tar.gz: c54efa4d0e6f9a78bf6954d67715f45e940747d9855c59457dbd32c4550728664283eb332702d5b9b6653b3fc5d8eb4f58692e9d4c02ca3b127439b43aee4954
|
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.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-
|
|
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
|