sorbet-runtime 0.5.5841

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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/lib/sorbet-runtime.rb +116 -0
  3. data/lib/types/_types.rb +285 -0
  4. data/lib/types/abstract_utils.rb +50 -0
  5. data/lib/types/boolean.rb +8 -0
  6. data/lib/types/compatibility_patches.rb +95 -0
  7. data/lib/types/configuration.rb +428 -0
  8. data/lib/types/enum.rb +349 -0
  9. data/lib/types/generic.rb +23 -0
  10. data/lib/types/helpers.rb +39 -0
  11. data/lib/types/interface_wrapper.rb +158 -0
  12. data/lib/types/non_forcing_constants.rb +51 -0
  13. data/lib/types/private/abstract/data.rb +36 -0
  14. data/lib/types/private/abstract/declare.rb +48 -0
  15. data/lib/types/private/abstract/hooks.rb +43 -0
  16. data/lib/types/private/abstract/validate.rb +128 -0
  17. data/lib/types/private/casts.rb +22 -0
  18. data/lib/types/private/class_utils.rb +111 -0
  19. data/lib/types/private/decl_state.rb +30 -0
  20. data/lib/types/private/final.rb +51 -0
  21. data/lib/types/private/methods/_methods.rb +460 -0
  22. data/lib/types/private/methods/call_validation.rb +1149 -0
  23. data/lib/types/private/methods/decl_builder.rb +228 -0
  24. data/lib/types/private/methods/modes.rb +16 -0
  25. data/lib/types/private/methods/signature.rb +196 -0
  26. data/lib/types/private/methods/signature_validation.rb +229 -0
  27. data/lib/types/private/mixins/mixins.rb +27 -0
  28. data/lib/types/private/retry.rb +10 -0
  29. data/lib/types/private/runtime_levels.rb +56 -0
  30. data/lib/types/private/sealed.rb +65 -0
  31. data/lib/types/private/types/not_typed.rb +23 -0
  32. data/lib/types/private/types/string_holder.rb +26 -0
  33. data/lib/types/private/types/type_alias.rb +26 -0
  34. data/lib/types/private/types/void.rb +34 -0
  35. data/lib/types/profile.rb +31 -0
  36. data/lib/types/props/_props.rb +161 -0
  37. data/lib/types/props/constructor.rb +40 -0
  38. data/lib/types/props/custom_type.rb +108 -0
  39. data/lib/types/props/decorator.rb +672 -0
  40. data/lib/types/props/errors.rb +8 -0
  41. data/lib/types/props/generated_code_validation.rb +268 -0
  42. data/lib/types/props/has_lazily_specialized_methods.rb +92 -0
  43. data/lib/types/props/optional.rb +81 -0
  44. data/lib/types/props/plugin.rb +37 -0
  45. data/lib/types/props/pretty_printable.rb +107 -0
  46. data/lib/types/props/private/apply_default.rb +170 -0
  47. data/lib/types/props/private/deserializer_generator.rb +165 -0
  48. data/lib/types/props/private/parser.rb +32 -0
  49. data/lib/types/props/private/serde_transform.rb +192 -0
  50. data/lib/types/props/private/serializer_generator.rb +77 -0
  51. data/lib/types/props/private/setter_factory.rb +134 -0
  52. data/lib/types/props/serializable.rb +330 -0
  53. data/lib/types/props/type_validation.rb +111 -0
  54. data/lib/types/props/utils.rb +59 -0
  55. data/lib/types/props/weak_constructor.rb +67 -0
  56. data/lib/types/runtime_profiled.rb +24 -0
  57. data/lib/types/sig.rb +30 -0
  58. data/lib/types/struct.rb +18 -0
  59. data/lib/types/types/attached_class.rb +37 -0
  60. data/lib/types/types/base.rb +151 -0
  61. data/lib/types/types/class_of.rb +38 -0
  62. data/lib/types/types/enum.rb +42 -0
  63. data/lib/types/types/fixed_array.rb +60 -0
  64. data/lib/types/types/fixed_hash.rb +59 -0
  65. data/lib/types/types/intersection.rb +37 -0
  66. data/lib/types/types/noreturn.rb +29 -0
  67. data/lib/types/types/proc.rb +51 -0
  68. data/lib/types/types/self_type.rb +35 -0
  69. data/lib/types/types/simple.rb +33 -0
  70. data/lib/types/types/t_enum.rb +38 -0
  71. data/lib/types/types/type_member.rb +7 -0
  72. data/lib/types/types/type_parameter.rb +23 -0
  73. data/lib/types/types/type_template.rb +7 -0
  74. data/lib/types/types/type_variable.rb +31 -0
  75. data/lib/types/types/typed_array.rb +34 -0
  76. data/lib/types/types/typed_enumerable.rb +161 -0
  77. data/lib/types/types/typed_enumerator.rb +36 -0
  78. data/lib/types/types/typed_hash.rb +43 -0
  79. data/lib/types/types/typed_range.rb +26 -0
  80. data/lib/types/types/typed_set.rb +36 -0
  81. data/lib/types/types/union.rb +56 -0
  82. data/lib/types/types/untyped.rb +29 -0
  83. data/lib/types/utils.rb +217 -0
  84. metadata +223 -0
@@ -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,192 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module T::Props
5
+ module Private
6
+ module SerdeTransform
7
+ extend T::Sig
8
+
9
+ class Serialize; end
10
+ private_constant :Serialize
11
+ class Deserialize; end
12
+ private_constant :Deserialize
13
+ ModeType = T.type_alias {T.any(Serialize, Deserialize)}
14
+ private_constant :ModeType
15
+
16
+ module Mode
17
+ SERIALIZE = T.let(Serialize.new.freeze, Serialize)
18
+ DESERIALIZE = T.let(Deserialize.new.freeze, Deserialize)
19
+ end
20
+
21
+ NO_TRANSFORM_TYPES = T.let(
22
+ [TrueClass, FalseClass, NilClass, Symbol, String].freeze,
23
+ T::Array[Module],
24
+ )
25
+ private_constant :NO_TRANSFORM_TYPES
26
+
27
+ sig do
28
+ params(
29
+ type: T::Types::Base,
30
+ mode: ModeType,
31
+ varname: String,
32
+ )
33
+ .returns(T.nilable(String))
34
+ .checked(:never)
35
+ end
36
+ def self.generate(type, mode, varname)
37
+ case type
38
+ when T::Types::TypedArray
39
+ inner = generate(type.type, mode, 'v')
40
+ if inner.nil?
41
+ "#{varname}.dup"
42
+ else
43
+ "#{varname}.map {|v| #{inner}}"
44
+ end
45
+ when T::Types::TypedSet
46
+ inner = generate(type.type, mode, 'v')
47
+ if inner.nil?
48
+ "#{varname}.dup"
49
+ else
50
+ "Set.new(#{varname}) {|v| #{inner}}"
51
+ end
52
+ when T::Types::TypedHash
53
+ keys = generate(type.keys, mode, 'k')
54
+ values = generate(type.values, mode, 'v')
55
+ if keys && values
56
+ "#{varname}.each_with_object({}) {|(k,v),h| h[#{keys}] = #{values}}"
57
+ elsif keys
58
+ "#{varname}.transform_keys {|k| #{keys}}"
59
+ elsif values
60
+ "#{varname}.transform_values {|v| #{values}}"
61
+ else
62
+ "#{varname}.dup"
63
+ end
64
+ when T::Types::Simple
65
+ raw = type.raw_type
66
+ if NO_TRANSFORM_TYPES.any? {|cls| raw <= cls}
67
+ nil
68
+ elsif raw <= Float
69
+ case mode
70
+ when Deserialize then "#{varname}.to_f"
71
+ when Serialize then nil
72
+ else T.absurd(mode)
73
+ end
74
+ elsif raw <= Numeric
75
+ nil
76
+ elsif raw < T::Props::Serializable
77
+ handle_serializable_subtype(varname, raw, mode)
78
+ elsif raw.singleton_class < T::Props::CustomType
79
+ handle_custom_type(varname, T.unsafe(raw), mode)
80
+ elsif T::Configuration.scalar_types.include?(raw.name)
81
+ # It's a bit of a hack that this is separate from NO_TRANSFORM_TYPES
82
+ # and doesn't check inheritance (like `T::Props::CustomType.scalar_type?`
83
+ # does), but it covers the main use case (pay-server's custom `Boolean`
84
+ # module) without either requiring `T::Configuration.scalar_types` to
85
+ # accept modules instead of strings (which produces load-order issues
86
+ # and subtle behavior changes) or eating the performance cost of doing
87
+ # an inheritance check by manually crawling a class hierarchy and doing
88
+ # string comparisons.
89
+ nil
90
+ else
91
+ "T::Props::Utils.deep_clone_object(#{varname})"
92
+ end
93
+ when T::Types::Union
94
+ non_nil_type = T::Utils.unwrap_nilable(type)
95
+ if non_nil_type
96
+ inner = generate(non_nil_type, mode, varname)
97
+ if inner.nil?
98
+ nil
99
+ else
100
+ "#{varname}.nil? ? nil : #{inner}"
101
+ end
102
+ else
103
+ # Handle, e.g., T::Boolean
104
+ if type.types.all? {|t| generate(t, mode, varname).nil?}
105
+ nil
106
+ else
107
+ # We currently deep_clone_object if the type was T.any(Integer, Float).
108
+ # When we get better support for union types (maybe this specific
109
+ # union type, because it would be a replacement for
110
+ # Chalk::ODM::DeprecatedNumemric), we could opt to special case
111
+ # this union to have no specific serde transform (the only reason
112
+ # why Float has a special case is because round tripping through
113
+ # JSON might normalize Floats to Integers)
114
+ "T::Props::Utils.deep_clone_object(#{varname})"
115
+ end
116
+ end
117
+ when T::Types::Intersection
118
+ dynamic_fallback = "T::Props::Utils.deep_clone_object(#{varname})"
119
+
120
+ # Transformations for any members of the intersection type where we
121
+ # know what we need to do and did not have to fall back to the
122
+ # dynamic deep clone method.
123
+ #
124
+ # NB: This deliberately does include `nil`, which means we know we
125
+ # don't need to do any transforming.
126
+ inner_known = type.types
127
+ .map {|t| generate(t, mode, varname)}
128
+ .reject {|t| t == dynamic_fallback}
129
+ .uniq
130
+
131
+ if inner_known.size != 1
132
+ # If there were no cases where we could tell what we need to do,
133
+ # e.g. if this is `T.all(SomethingWeird, WhoKnows)`, just use the
134
+ # dynamic fallback.
135
+ #
136
+ # If there were multiple cases and they weren't consistent, e.g.
137
+ # if this is `T.all(String, T::Array[Integer])`, the type is probably
138
+ # bogus/uninhabited, but use the dynamic fallback because we still
139
+ # don't have a better option, and this isn't the place to raise that
140
+ # error.
141
+ dynamic_fallback
142
+ else
143
+ # This is probably something like `T.all(String, SomeMarker)` or
144
+ # `T.all(SomeEnum, T.enum(SomeEnum::FOO))` and we should treat it
145
+ # like String or SomeEnum even if we don't know what to do with
146
+ # the rest of the type.
147
+ inner_known.first
148
+ end
149
+ when T::Types::Enum
150
+ generate(T::Utils.lift_enum(type), mode, varname)
151
+ else
152
+ "T::Props::Utils.deep_clone_object(#{varname})"
153
+ end
154
+ end
155
+
156
+ sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
157
+ private_class_method def self.handle_serializable_subtype(varname, type, mode)
158
+ case mode
159
+ when Serialize
160
+ "#{varname}.serialize(strict)"
161
+ when Deserialize
162
+ type_name = T.must(module_name(type))
163
+ "#{type_name}.from_hash(#{varname})"
164
+ else
165
+ T.absurd(mode)
166
+ end
167
+ end
168
+
169
+ sig {params(varname: String, type: Module, mode: ModeType).returns(String).checked(:never)}
170
+ private_class_method def self.handle_custom_type(varname, type, mode)
171
+ case mode
172
+ when Serialize
173
+ "T::Props::CustomType.checked_serialize(#{varname})"
174
+ when Deserialize
175
+ type_name = T.must(module_name(type))
176
+ "#{type_name}.deserialize(#{varname})"
177
+ else
178
+ T.absurd(mode)
179
+ end
180
+ end
181
+
182
+ # Guard against overrides of `name` or `to_s`
183
+ MODULE_NAME = T.let(Module.instance_method(:name), UnboundMethod)
184
+ private_constant :MODULE_NAME
185
+
186
+ sig {params(type: Module).returns(T.nilable(String)).checked(:never)}
187
+ private_class_method def self.module_name(type)
188
+ MODULE_NAME.bind(type).call
189
+ end
190
+ end
191
+ end
192
+ 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
+
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module T::Props
5
+ module Private
6
+ module SetterFactory
7
+ extend T::Sig
8
+
9
+ SetterProc = T.type_alias {T.proc.params(val: T.untyped).void}
10
+ ValidateProc = T.type_alias {T.proc.params(prop: Symbol, value: T.untyped).void}
11
+
12
+ sig do
13
+ params(
14
+ klass: T.all(Module, T::Props::ClassMethods),
15
+ prop: Symbol,
16
+ rules: T::Hash[Symbol, T.untyped]
17
+ )
18
+ .returns(SetterProc)
19
+ .checked(:never)
20
+ end
21
+ def self.build_setter_proc(klass, prop, rules)
22
+ # Our nil check works differently than a simple T.nilable for various
23
+ # reasons (including the `raise_on_nil_write` setting and the existence
24
+ # of defaults & factories), so unwrap any T.nilable and do a check
25
+ # manually.
26
+ non_nil_type = T::Utils::Nilable.get_underlying_type_object(rules.fetch(:type_object))
27
+ accessor_key = rules.fetch(:accessor_key)
28
+ validate = rules[:setter_validate]
29
+
30
+ # It seems like a bug that this affects the behavior of setters, but
31
+ # some existing code relies on this behavior
32
+ has_explicit_nil_default = rules.key?(:default) && rules.fetch(:default).nil?
33
+
34
+ # Use separate methods in order to ensure that we only close over necessary
35
+ # variables
36
+ if !T::Props::Utils.need_nil_write_check?(rules) || has_explicit_nil_default
37
+ nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
38
+ else
39
+ non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
40
+ end
41
+ end
42
+
43
+ sig do
44
+ params(
45
+ prop: Symbol,
46
+ accessor_key: Symbol,
47
+ non_nil_type: T::Types::Base,
48
+ klass: T.all(Module, T::Props::ClassMethods),
49
+ validate: T.nilable(ValidateProc)
50
+ )
51
+ .returns(SetterProc)
52
+ end
53
+ private_class_method def self.non_nil_proc(prop, accessor_key, non_nil_type, klass, validate)
54
+ proc do |val|
55
+ if non_nil_type.valid?(val)
56
+ if validate
57
+ validate.call(prop, val)
58
+ end
59
+ instance_variable_set(accessor_key, val)
60
+ else
61
+ T::Props::Private::SetterFactory.raise_pretty_error(
62
+ klass,
63
+ prop,
64
+ non_nil_type,
65
+ val,
66
+ )
67
+ instance_variable_set(accessor_key, val)
68
+ end
69
+ end
70
+ end
71
+
72
+ sig do
73
+ params(
74
+ prop: Symbol,
75
+ accessor_key: Symbol,
76
+ non_nil_type: T::Types::Base,
77
+ klass: T.all(Module, T::Props::ClassMethods),
78
+ validate: T.nilable(ValidateProc),
79
+ )
80
+ .returns(SetterProc)
81
+ end
82
+ private_class_method def self.nilable_proc(prop, accessor_key, non_nil_type, klass, validate)
83
+ proc do |val|
84
+ if val.nil?
85
+ instance_variable_set(accessor_key, nil)
86
+ elsif non_nil_type.valid?(val)
87
+ if validate
88
+ validate.call(prop, val)
89
+ end
90
+ instance_variable_set(accessor_key, val)
91
+ else
92
+ T::Props::Private::SetterFactory.raise_pretty_error(
93
+ klass,
94
+ prop,
95
+ non_nil_type,
96
+ val,
97
+ )
98
+ instance_variable_set(accessor_key, val)
99
+ end
100
+ end
101
+ end
102
+
103
+ sig do
104
+ params(
105
+ klass: T.all(Module, T::Props::ClassMethods),
106
+ prop: Symbol,
107
+ type: T.any(T::Types::Base, Module),
108
+ val: T.untyped,
109
+ )
110
+ .void
111
+ end
112
+ def self.raise_pretty_error(klass, prop, type, val)
113
+ base_message = "Can't set #{klass.name}.#{prop} to #{val.inspect} (instance of #{val.class}) - need a #{type}"
114
+
115
+ pretty_message = "Parameter '#{prop}': #{base_message}\n"
116
+ caller_loc = caller_locations&.find {|l| !l.to_s.include?('sorbet-runtime/lib/types/props')}
117
+ if caller_loc
118
+ pretty_message += "Caller: #{caller_loc.path}:#{caller_loc.lineno}\n"
119
+ end
120
+
121
+ T::Configuration.call_validation_error_handler(
122
+ nil,
123
+ message: base_message,
124
+ pretty_message: pretty_message,
125
+ kind: 'Parameter',
126
+ name: prop,
127
+ type: type,
128
+ value: val,
129
+ location: caller_loc,
130
+ )
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,330 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module T::Props::Serializable
5
+ include T::Props::Plugin
6
+ # Required because we have special handling for `optional: false`
7
+ include T::Props::Optional
8
+ # Required because we have special handling for extra_props
9
+ include T::Props::PrettyPrintable
10
+
11
+ # Serializes this object to a hash, suitable for conversion to
12
+ # JSON/BSON.
13
+ #
14
+ # @param strict [T::Boolean] (true) If false, do not raise an
15
+ # exception if this object has mandatory props with missing
16
+ # values.
17
+ # @return [Hash] A serialization of this object.
18
+ def serialize(strict=true)
19
+ begin
20
+ h = __t_props_generated_serialize(strict)
21
+ rescue => e
22
+ msg = self.class.decorator.message_with_generated_source_context(
23
+ e,
24
+ :__t_props_generated_serialize,
25
+ :generate_serialize_source
26
+ )
27
+ if msg
28
+ begin
29
+ raise e.class.new(msg)
30
+ rescue ArgumentError
31
+ raise TypeError.new(msg)
32
+ end
33
+ else
34
+ raise
35
+ end
36
+ end
37
+
38
+ h.merge!(@_extra_props) if @_extra_props
39
+ h
40
+ end
41
+
42
+ private def __t_props_generated_serialize(strict)
43
+ # No-op; will be overridden if there are any props.
44
+ #
45
+ # To see the definition for class `Foo`, run `Foo.decorator.send(:generate_serialize_source)`
46
+ {}
47
+ end
48
+
49
+ # Populates the property values on this object with the values
50
+ # from a hash. In general, prefer to use {.from_hash} to construct
51
+ # a new instance, instead of loading into an existing instance.
52
+ #
53
+ # @param hash [Hash<String, Object>] The hash to take property
54
+ # values from.
55
+ # @param strict [T::Boolean] (false) If true, raise an exception if
56
+ # the hash contains keys that do not correspond to any known
57
+ # props on this instance.
58
+ # @return [void]
59
+ def deserialize(hash, strict=false)
60
+ begin
61
+ hash_keys_matching_props = __t_props_generated_deserialize(hash)
62
+ rescue => e
63
+ msg = self.class.decorator.message_with_generated_source_context(
64
+ e,
65
+ :__t_props_generated_deserialize,
66
+ :generate_deserialize_source
67
+ )
68
+ if msg
69
+ begin
70
+ raise e.class.new(msg)
71
+ rescue ArgumentError
72
+ raise TypeError.new(msg)
73
+ end
74
+ else
75
+ raise
76
+ end
77
+ end
78
+
79
+ if hash.size > hash_keys_matching_props
80
+ serialized_forms = self.class.decorator.prop_by_serialized_forms
81
+ extra = hash.reject {|k, _| serialized_forms.key?(k)}
82
+
83
+ # `extra` could still be empty here if the input matches a `dont_store` prop;
84
+ # historically, we just ignore those
85
+ if !extra.empty?
86
+ if strict
87
+ raise "Unknown properties for #{self.class.name}: #{extra.keys.inspect}"
88
+ else
89
+ @_extra_props = extra
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ private def __t_props_generated_deserialize(hash)
96
+ # No-op; will be overridden if there are any props.
97
+ #
98
+ # To see the definition for class `Foo`, run `Foo.decorator.send(:generate_deserialize_source)`
99
+ 0
100
+ end
101
+
102
+ # with() will clone the old object to the new object and merge the specified props to the new object.
103
+ def with(changed_props)
104
+ with_existing_hash(changed_props, existing_hash: self.serialize)
105
+ end
106
+
107
+ private def recursive_stringify_keys(obj)
108
+ if obj.is_a?(Hash)
109
+ new_obj = obj.class.new
110
+ obj.each do |k, v|
111
+ new_obj[k.to_s] = recursive_stringify_keys(v)
112
+ end
113
+ elsif obj.is_a?(Array)
114
+ new_obj = obj.map {|v| recursive_stringify_keys(v)}
115
+ else
116
+ new_obj = obj
117
+ end
118
+ new_obj
119
+ end
120
+
121
+ private def with_existing_hash(changed_props, existing_hash:)
122
+ serialized = existing_hash
123
+ new_val = self.class.from_hash(serialized.merge(recursive_stringify_keys(changed_props)))
124
+ old_extra = self.instance_variable_get(:@_extra_props) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
125
+ new_extra = new_val.instance_variable_get(:@_extra_props) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
126
+ if old_extra != new_extra
127
+ difference =
128
+ if old_extra
129
+ new_extra.reject {|k, v| old_extra[k] == v}
130
+ else
131
+ new_extra
132
+ end
133
+ raise ArgumentError.new("Unexpected arguments: input(#{changed_props}), unexpected(#{difference})")
134
+ end
135
+ new_val
136
+ end
137
+
138
+ # Asserts if this property is missing during strict serialize
139
+ private def required_prop_missing_from_serialize(prop)
140
+ if @_required_props_missing_from_deserialize&.include?(prop)
141
+ # If the prop was already missing during deserialization, that means the application
142
+ # code already had to deal with a nil value, which means we wouldn't be accomplishing
143
+ # much by raising here (other than causing an unnecessary breakage).
144
+ T::Configuration.log_info_handler(
145
+ "chalk-odm: missing required property in serialize",
146
+ prop: prop, class: self.class.name, id: self.class.decorator.get_id(self)
147
+ )
148
+ else
149
+ raise TypeError.new("#{self.class.name}.#{prop} not set for non-optional prop")
150
+ end
151
+ end
152
+
153
+ # Marks this property as missing during deserialize
154
+ private def required_prop_missing_from_deserialize(prop)
155
+ @_required_props_missing_from_deserialize ||= Set[]
156
+ @_required_props_missing_from_deserialize << prop
157
+ nil
158
+ end
159
+ end
160
+
161
+
162
+ ##############################################
163
+
164
+ # NB: This must stay in the same file where T::Props::Serializable is defined due to
165
+ # T::Props::Decorator#apply_plugin; see https://git.corp.stripe.com/stripe-internal/pay-server/blob/fc7f15593b49875f2d0499ffecfd19798bac05b3/chalk/odm/lib/chalk-odm/document_decorator.rb#L716-L717
166
+ module T::Props::Serializable::DecoratorMethods
167
+ include T::Props::HasLazilySpecializedMethods::DecoratorMethods
168
+
169
+ VALID_RULE_KEYS = {dont_store: true, name: true, raise_on_nil_write: true}.freeze
170
+ private_constant :VALID_RULE_KEYS
171
+
172
+ def valid_rule_key?(key)
173
+ super || VALID_RULE_KEYS[key]
174
+ end
175
+
176
+ def required_props
177
+ @class.props.select {|_, v| T::Props::Utils.required_prop?(v)}.keys
178
+ end
179
+
180
+ def prop_dont_store?(prop); prop_rules(prop)[:dont_store]; end
181
+ def prop_by_serialized_forms; @class.prop_by_serialized_forms; end
182
+
183
+ def from_hash(hash, strict=false)
184
+ raise ArgumentError.new("#{hash.inspect} provided to from_hash") if !(hash && hash.is_a?(Hash))
185
+
186
+ i = @class.allocate
187
+ i.deserialize(hash, strict)
188
+
189
+ i
190
+ end
191
+
192
+ def prop_serialized_form(prop)
193
+ prop_rules(prop)[:serialized_form]
194
+ end
195
+
196
+ def serialized_form_prop(serialized_form)
197
+ prop_by_serialized_forms[serialized_form.to_s] || raise("No such serialized form: #{serialized_form.inspect}")
198
+ end
199
+
200
+ def add_prop_definition(prop, rules)
201
+ rules[:serialized_form] = rules.fetch(:name, prop.to_s)
202
+ res = super
203
+ prop_by_serialized_forms[rules[:serialized_form]] = prop
204
+ enqueue_lazy_method_definition!(:__t_props_generated_serialize) {generate_serialize_source}
205
+ enqueue_lazy_method_definition!(:__t_props_generated_deserialize) {generate_deserialize_source}
206
+ res
207
+ end
208
+
209
+ private def generate_serialize_source
210
+ T::Props::Private::SerializerGenerator.generate(props)
211
+ end
212
+
213
+ private def generate_deserialize_source
214
+ T::Props::Private::DeserializerGenerator.generate(
215
+ props,
216
+ props_with_defaults || {},
217
+ )
218
+ end
219
+
220
+ def message_with_generated_source_context(error, generated_method, generate_source_method)
221
+ line_label = error.backtrace.find {|l| l.end_with?("in `#{generated_method}'")}
222
+ return unless line_label
223
+
224
+ line_num = line_label.split(':')[1]&.to_i
225
+ return unless line_num
226
+
227
+ source_lines = self.send(generate_source_method).split("\n")
228
+ previous_blank = source_lines[0...line_num].rindex(&:empty?) || 0
229
+ next_blank = line_num + (source_lines[line_num..-1]&.find_index(&:empty?) || 0)
230
+ context = " " + source_lines[(previous_blank + 1)...(next_blank)].join("\n ")
231
+ <<~MSG
232
+ Error in #{decorated_class.name}##{generated_method}: #{error.message}
233
+ at line #{line_num-previous_blank-1} in:
234
+ #{context}
235
+ MSG
236
+ end
237
+
238
+ def raise_nil_deserialize_error(hkey)
239
+ msg = "Tried to deserialize a required prop from a nil value. It's "\
240
+ "possible that a nil value exists in the database, so you should "\
241
+ "provide a `default: or factory:` for this prop (see go/optional "\
242
+ "for more details). If this is already the case, you probably "\
243
+ "omitted a required prop from the `fields:` option when doing a "\
244
+ "partial load."
245
+ storytime = {prop: hkey, klass: decorated_class.name}
246
+
247
+ # Notify the model owner if it exists, and always notify the API owner.
248
+ begin
249
+ if defined?(Opus) && defined?(Opus::Ownership) && decorated_class < Opus::Ownership
250
+ T::Configuration.hard_assert_handler(
251
+ msg,
252
+ storytime: storytime,
253
+ project: decorated_class.get_owner
254
+ )
255
+ end
256
+ ensure
257
+ T::Configuration.hard_assert_handler(msg, storytime: storytime)
258
+ end
259
+ end
260
+
261
+ def prop_validate_definition!(name, cls, rules, type)
262
+ result = super
263
+
264
+ if (rules_name = rules[:name])
265
+ unless rules_name.is_a?(String)
266
+ raise ArgumentError.new("Invalid name in prop #{@class.name}.#{name}: #{rules_name.inspect}")
267
+ end
268
+
269
+ validate_prop_name(rules_name)
270
+ end
271
+
272
+ if !rules[:raise_on_nil_write].nil? && rules[:raise_on_nil_write] != true
273
+ raise ArgumentError.new("The value of `raise_on_nil_write` if specified must be `true` (given: #{rules[:raise_on_nil_write]}).")
274
+ end
275
+
276
+ result
277
+ end
278
+
279
+ def get_id(instance)
280
+ prop = prop_by_serialized_forms['_id']
281
+ if prop
282
+ get(instance, prop)
283
+ else
284
+ nil
285
+ end
286
+ end
287
+
288
+ EMPTY_EXTRA_PROPS = {}.freeze
289
+ private_constant :EMPTY_EXTRA_PROPS
290
+
291
+ def extra_props(instance)
292
+ instance.instance_variable_get(:@_extra_props) || EMPTY_EXTRA_PROPS
293
+ end
294
+
295
+ # @override T::Props::PrettyPrintable
296
+ private def inspect_instance_components(instance, multiline:, indent:)
297
+ if (extra_props = extra_props(instance)) && !extra_props.empty?
298
+ pretty_kvs = extra_props.map {|k, v| [k.to_sym, v.inspect]}
299
+ extra = join_props_with_pretty_values(pretty_kvs, multiline: false)
300
+ super + ["@_extra_props=<#{extra}>"]
301
+ else
302
+ super
303
+ end
304
+ end
305
+ end
306
+
307
+
308
+ ##############################################
309
+
310
+
311
+ # NB: This must stay in the same file where T::Props::Serializable is defined due to
312
+ # 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
313
+ module T::Props::Serializable::ClassMethods
314
+ def prop_by_serialized_forms; @prop_by_serialized_forms ||= {}; end
315
+
316
+ # @!method self.from_hash(hash, strict)
317
+ #
318
+ # Allocate a new instance and call {#deserialize} to load a new
319
+ # object from a hash.
320
+ # @return [Serializable]
321
+ def from_hash(hash, strict=false)
322
+ self.decorator.from_hash(hash, strict)
323
+ end
324
+
325
+ # Equivalent to {.from_hash} with `strict` set to true.
326
+ # @return [Serializable]
327
+ def from_hash!(hash)
328
+ self.decorator.from_hash(hash, true)
329
+ end
330
+ end