sorbet-runtime 0.0.1.pre.prealpha → 0.4.4253

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 (69) hide show
  1. checksums.yaml +5 -5
  2. data/lib/sorbet-runtime.rb +100 -0
  3. data/lib/types/_types.rb +245 -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 +37 -0
  7. data/lib/types/configuration.rb +368 -0
  8. data/lib/types/generic.rb +23 -0
  9. data/lib/types/helpers.rb +31 -0
  10. data/lib/types/interface_wrapper.rb +158 -0
  11. data/lib/types/private/abstract/data.rb +36 -0
  12. data/lib/types/private/abstract/declare.rb +39 -0
  13. data/lib/types/private/abstract/hooks.rb +43 -0
  14. data/lib/types/private/abstract/validate.rb +128 -0
  15. data/lib/types/private/casts.rb +22 -0
  16. data/lib/types/private/class_utils.rb +102 -0
  17. data/lib/types/private/decl_state.rb +18 -0
  18. data/lib/types/private/error_handler.rb +37 -0
  19. data/lib/types/private/methods/_methods.rb +344 -0
  20. data/lib/types/private/methods/call_validation.rb +1177 -0
  21. data/lib/types/private/methods/decl_builder.rb +275 -0
  22. data/lib/types/private/methods/modes.rb +18 -0
  23. data/lib/types/private/methods/signature.rb +196 -0
  24. data/lib/types/private/methods/signature_validation.rb +232 -0
  25. data/lib/types/private/mixins/mixins.rb +27 -0
  26. data/lib/types/private/runtime_levels.rb +41 -0
  27. data/lib/types/private/types/not_typed.rb +23 -0
  28. data/lib/types/private/types/string_holder.rb +26 -0
  29. data/lib/types/private/types/void.rb +33 -0
  30. data/lib/types/profile.rb +27 -0
  31. data/lib/types/props/_props.rb +165 -0
  32. data/lib/types/props/constructor.rb +20 -0
  33. data/lib/types/props/custom_type.rb +84 -0
  34. data/lib/types/props/decorator.rb +826 -0
  35. data/lib/types/props/errors.rb +8 -0
  36. data/lib/types/props/optional.rb +73 -0
  37. data/lib/types/props/plugin.rb +15 -0
  38. data/lib/types/props/pretty_printable.rb +106 -0
  39. data/lib/types/props/serializable.rb +376 -0
  40. data/lib/types/props/type_validation.rb +98 -0
  41. data/lib/types/props/utils.rb +49 -0
  42. data/lib/types/props/weak_constructor.rb +30 -0
  43. data/lib/types/runtime_profiled.rb +36 -0
  44. data/lib/types/sig.rb +28 -0
  45. data/lib/types/struct.rb +8 -0
  46. data/lib/types/types/base.rb +141 -0
  47. data/lib/types/types/class_of.rb +38 -0
  48. data/lib/types/types/enum.rb +42 -0
  49. data/lib/types/types/fixed_array.rb +60 -0
  50. data/lib/types/types/fixed_hash.rb +59 -0
  51. data/lib/types/types/intersection.rb +36 -0
  52. data/lib/types/types/noreturn.rb +25 -0
  53. data/lib/types/types/proc.rb +51 -0
  54. data/lib/types/types/self_type.rb +31 -0
  55. data/lib/types/types/simple.rb +33 -0
  56. data/lib/types/types/type_member.rb +7 -0
  57. data/lib/types/types/type_parameter.rb +23 -0
  58. data/lib/types/types/type_template.rb +7 -0
  59. data/lib/types/types/type_variable.rb +31 -0
  60. data/lib/types/types/typed_array.rb +20 -0
  61. data/lib/types/types/typed_enumerable.rb +141 -0
  62. data/lib/types/types/typed_enumerator.rb +22 -0
  63. data/lib/types/types/typed_hash.rb +29 -0
  64. data/lib/types/types/typed_range.rb +22 -0
  65. data/lib/types/types/typed_set.rb +22 -0
  66. data/lib/types/types/union.rb +59 -0
  67. data/lib/types/types/untyped.rb +25 -0
  68. data/lib/types/utils.rb +223 -0
  69. metadata +122 -15
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module T::Props::Constructor
5
+ include T::Props::WeakConstructor
6
+
7
+ def initialize(hash={})
8
+ decorator = self.class.decorator
9
+
10
+ decorator.props.each do |prop, rules|
11
+ # It's important to explicitly compare against `true` here; the value can also be :existing or
12
+ # :on_load (which are truthy) but we don't want to treat those as optional in this context.
13
+ if T::Utils::Props.required_prop?(rules) && !decorator.has_default?(rules) && !hash.key?(prop)
14
+ raise ArgumentError.new("Missing required prop `#{prop}` for class `#{self.class}`")
15
+ end
16
+ end
17
+
18
+ super
19
+ end
20
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module T::Props
5
+ module CustomType
6
+ include Kernel # for `is_a?`
7
+
8
+ # Returns true if the given Ruby value can be assigned to a T::Props field
9
+ # of this type.
10
+ #
11
+ # @param [Object] _value
12
+ # @return T::Boolean
13
+ def instance?(_value)
14
+ raise NotImplementedError.new('Must override in included class')
15
+ end
16
+
17
+ # Alias for consistent interface with T::Types::Base
18
+ def valid?(value)
19
+ instance?(value)
20
+ end
21
+
22
+ # Given an instance of this type, serialize that into a scalar type
23
+ # supported by T::Props.
24
+ #
25
+ # @param [Object] _instance
26
+ # @return An instance of one of T::Configuration.scalar_types
27
+ def serialize(_instance)
28
+ raise NotImplementedError.new('Must override in included class')
29
+ end
30
+
31
+ # Given the serialized form of your type, this returns an instance
32
+ # of that custom type representing that value.
33
+ #
34
+ # @param _mongo_scalar One of T::Configuration.scalar_types
35
+ # @return Object
36
+ def deserialize(_mongo_scalar)
37
+ raise NotImplementedError.new('Must override in included class')
38
+ end
39
+
40
+ def self.included(_base)
41
+ super
42
+
43
+ raise 'Please use "extend", not "include" to attach this module'
44
+ end
45
+
46
+ def self.scalar_type?(val)
47
+ # We don't need to check for val's included modules in
48
+ # T::Configuration.scalar_types, because T::Configuration.scalar_types
49
+ # are all classes.
50
+ klass = val.class
51
+ until klass.nil?
52
+ return true if T::Configuration.scalar_types.include?(klass.to_s)
53
+ klass = klass.superclass
54
+ end
55
+ false
56
+ end
57
+
58
+ # We allow custom types to serialize to Arrays, so that we can
59
+ # implement set-like fields that store a unique-array, but forbid
60
+ # hashes; Custom hash types should be implemented via an emebdded
61
+ # T::Struct (or a subclass like Chalk::ODM::Document) or via T.
62
+ def self.valid_serialization?(val, type=nil)
63
+ if type&.name == 'Chalk::ODM::BsonTypes::BsonObject'
64
+ # Special case we allow for backwards compatibility with props formerly
65
+ # typed as "Object" or "Hash", which contain arbitrarily-nested BSON
66
+ # data (e.g. parsed API request bodies). In general, we aren't pushing
67
+ # to convert these to Chalk::ODM::BsonTypes - we'd rather delurk them -
68
+ # but this lets us convert events with these types to Proto.
69
+ return true
70
+ end
71
+
72
+ case val
73
+ when Array
74
+ val.each do |v|
75
+ return false unless scalar_type?(v)
76
+ end
77
+
78
+ true
79
+ else
80
+ scalar_type?(val)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,826 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ # NB: This is not actually a decorator. It's just named that way for consistency
5
+ # with DocumentDecorator and ModelDecorator (which both seem to have been written
6
+ # with an incorrect understanding of the decorator pattern). These "decorators"
7
+ # should really just be static methods on private modules (we'd also want/need to
8
+ # replace decorator overrides in plugins with class methods that expose the necessary
9
+ # functionality).
10
+ class T::Props::Decorator
11
+ extend T::Sig
12
+
13
+ Rules = T.type_alias(T::Hash[Symbol, T.untyped])
14
+ DecoratedClass = T.type_alias(T.untyped) # T.class_of(T::Props), but that produces circular reference errors in some circumstances
15
+ DecoratedInstance = T.type_alias(T.untyped) # Would be T::Props, but that produces circular reference errors in some circumstances
16
+ PropType = T.type_alias(T.any(T::Types::Base, T::Props::CustomType))
17
+ PropTypeOrClass = T.type_alias(T.any(PropType, Module))
18
+
19
+ class NoRulesError < StandardError; end
20
+
21
+ sig {params(klass: DecoratedClass).void}
22
+ def initialize(klass)
23
+ @class = klass
24
+ klass.plugins.each do |mod|
25
+ Private.apply_decorator_methods(mod, self)
26
+ end
27
+ end
28
+
29
+ # prop stuff
30
+ sig {returns(T::Hash[Symbol, Rules])}
31
+ def props
32
+ @props ||= {}.freeze
33
+ end
34
+
35
+ # Try to avoid using this; post-definition mutation of prop rules is
36
+ # surprising and hard to reason about.
37
+ sig {params(prop: Symbol, key: Symbol, value: T.untyped).void}
38
+ def mutate_prop_backdoor!(prop, key, value)
39
+ @props = props.merge(
40
+ prop => props.fetch(prop).merge(key => value).freeze
41
+ ).freeze
42
+ end
43
+
44
+ sig {returns(T::Array[Symbol])}
45
+ def all_props; props.keys; end
46
+
47
+ sig {params(prop: T.any(Symbol, String)).returns(Rules)}
48
+ def prop_rules(prop); props[prop.to_sym] || raise("No such prop: #{prop.inspect}"); end
49
+
50
+ sig {params(prop: Symbol, rules: Rules).void}
51
+ def add_prop_definition(prop, rules)
52
+ prop = prop.to_sym
53
+ override = rules.delete(:override)
54
+
55
+ if props.include?(prop) && !override
56
+ raise ArgumentError.new("Attempted to redefine prop #{prop.inspect} that's already defined without specifying :override => true: #{prop_rules(prop)}")
57
+ elsif !props.include?(prop) && override
58
+ raise ArgumentError.new("Attempted to override a prop #{prop.inspect} that doesn't already exist")
59
+ end
60
+
61
+ @props = @props.merge(prop => rules.freeze).freeze
62
+ end
63
+
64
+ sig {returns(T::Array[Symbol])}
65
+ def valid_props
66
+ %i{
67
+ enum
68
+ foreign
69
+ foreign_hint_only
70
+ ifunset
71
+ immutable
72
+ override
73
+ redaction
74
+ sensitivity
75
+ without_accessors
76
+ clobber_existing_method!
77
+ extra
78
+ optional
79
+ _tnilable
80
+ }
81
+ end
82
+
83
+ sig {returns(DecoratedClass)}
84
+ def decorated_class; @class; end
85
+
86
+ # Accessors
87
+
88
+ # For performance, don't use named params here.
89
+ # Passing in rules here is purely a performance optimization.
90
+ sig do
91
+ params(
92
+ instance: DecoratedInstance,
93
+ prop: T.any(String, Symbol),
94
+ rules: T.nilable(Rules)
95
+ )
96
+ .returns(T.untyped)
97
+ end
98
+ def get(instance, prop, rules=props[prop.to_sym])
99
+ # For backwards compatibility, fall back to reconstructing the accessor key
100
+ # (though it would probably make more sense to raise in that case).
101
+ instance.instance_variable_get(rules ? rules[:accessor_key] : '@' + prop.to_s) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
102
+ end
103
+
104
+ # For performance, don't use named params here.
105
+ # Passing in rules here is purely a performance optimization.
106
+ sig do
107
+ params(
108
+ instance: DecoratedInstance,
109
+ prop: Symbol,
110
+ value: T.untyped,
111
+ rules: T.nilable(Rules)
112
+ )
113
+ .void
114
+ end
115
+ def set(instance, prop, value, rules=props[prop.to_sym])
116
+ # For backwards compatibility, fall back to reconstructing the accessor key
117
+ # (though it would probably make more sense to raise in that case).
118
+ instance.instance_variable_set(rules ? rules[:accessor_key] : '@' + prop.to_s, value) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
119
+ end
120
+
121
+ # Use this to validate that a value will validate for a given prop. Useful for knowing whether a value can be set on a model without setting it.
122
+ sig {params(prop: Symbol, val: T.untyped).void}
123
+ def validate_prop_value(prop, val)
124
+ # This implements a 'public api' on document so that we don't allow callers to pass in rules
125
+ # Rules seem like an implementation detail so it seems good to now allow people to specify them manually.
126
+ check_prop_type(prop, val)
127
+ end
128
+
129
+ # Passing in rules here is purely a performance optimization.
130
+ sig {params(prop: Symbol, val: T.untyped, rules: Rules).void}
131
+ private def check_prop_type(prop, val, rules=prop_rules(prop))
132
+ type_object = rules.fetch(:type_object)
133
+ type = rules.fetch(:type)
134
+
135
+ # TODO: ideally we'd add `&& rules[:optional] != :existing` to this check
136
+ # (it makes sense to treat those props required in this context), but we'd need
137
+ # to be sure that doesn't break any existing code first.
138
+ if val.nil?
139
+ if !T::Props::Utils.need_nil_write_check?(rules) || (rules.key?(:default) && rules[:default].nil?)
140
+ return
141
+ end
142
+
143
+ if rules[:raise_on_nil_write]
144
+ raise T::Props::InvalidValueError.new("Can't set #{@class.name}.#{prop} to #{val.inspect} " \
145
+ "(instance of #{val.class}) - need a #{type}")
146
+ end
147
+ end
148
+
149
+ # T::Props::CustomType is not a real object based class so that we can not run real type check call.
150
+ # T::Props::CustomType.valid?() is only a helper function call.
151
+ valid =
152
+ if type.is_a?(T::Props::CustomType) && T::Utils::Props.optional_prop?(rules)
153
+ type.valid?(val)
154
+ else
155
+ type_object.valid?(val)
156
+ end
157
+ if !valid
158
+ raise T::Props::InvalidValueError.new("Can't set #{@class.name}.#{prop} to #{val.inspect} " \
159
+ "(instance of #{val.class}) - need a #{type_object}")
160
+ end
161
+ end
162
+
163
+ # For performance, don't use named params here.
164
+ # Passing in rules here is purely a performance optimization.
165
+ # Unlike the other methods that take rules, this one calls prop_rules for
166
+ # the default, which raises if the prop doesn't exist (this maintains
167
+ # preexisting behavior).
168
+ sig do
169
+ params(
170
+ instance: DecoratedInstance,
171
+ prop: Symbol,
172
+ val: T.untyped,
173
+ rules: T.nilable(Rules)
174
+ )
175
+ .void
176
+ end
177
+ def prop_set(instance, prop, val, rules=prop_rules(prop))
178
+ check_prop_type(prop, val, T.must(rules))
179
+ set(instance, prop, val, rules)
180
+ end
181
+
182
+ # For performance, don't use named params here.
183
+ # Passing in rules here is purely a performance optimization.
184
+ sig do
185
+ params(
186
+ instance: DecoratedInstance,
187
+ prop: T.any(String, Symbol),
188
+ rules: T.nilable(Rules)
189
+ )
190
+ .returns(T.untyped)
191
+ end
192
+ def prop_get(instance, prop, rules=props[prop.to_sym])
193
+ val = get(instance, prop, rules)
194
+
195
+ # NB: Do NOT change this to check `val.nil?` instead. BSON::ByteBuffer overrides `==` such
196
+ # that `== nil` can return true while `.nil?` returns false. Tests will break in mysterious
197
+ # ways. A special thanks to Ruby for enabling this type of bug.
198
+ #
199
+ # One side effect here is that _if_ a class (like BSON::ByteBuffer) defines ==
200
+ # in such a way that instances which are not `nil`, ie are not NilClass, nevertheless
201
+ # are `== nil`, then we will transparently convert such instances to `nil` on read.
202
+ # Yes, our code relies on this behavior (as of writing). :thisisfine:
203
+ if val != nil # rubocop:disable Style/NonNilCheck
204
+ val
205
+ else
206
+ raise NoRulesError.new if !rules
207
+ d = rules[:ifunset]
208
+ if d
209
+ T::Props::Utils.deep_clone_object(d)
210
+ else
211
+ nil
212
+ end
213
+ end
214
+ end
215
+
216
+ sig do
217
+ params(
218
+ instance: DecoratedInstance,
219
+ prop: Symbol,
220
+ foreign_class: Module,
221
+ rules: Rules,
222
+ opts: Hash
223
+ )
224
+ .returns(T.untyped)
225
+ end
226
+ def foreign_prop_get(instance, prop, foreign_class, rules=props[prop.to_sym], opts={})
227
+ return if !(value = prop_get(instance, prop, rules))
228
+ foreign_class.load(value, {}, opts)
229
+ end
230
+
231
+ sig do
232
+ params(
233
+ name: Symbol,
234
+ cls: Module,
235
+ rules: Rules,
236
+ type: PropTypeOrClass
237
+ )
238
+ .void
239
+ end
240
+ def prop_validate_definition!(name, cls, rules, type)
241
+ validate_prop_name(name)
242
+
243
+ if rules.key?(:pii)
244
+ raise ArgumentError.new("The 'pii:' option for props has been renamed " \
245
+ "to 'sensitivity:' (in prop #{@class.name}.#{name})")
246
+ end
247
+
248
+ if !(rules.keys - valid_props).empty?
249
+ raise ArgumentError.new("At least one invalid prop arg supplied in #{self}: #{rules.keys.inspect}")
250
+ end
251
+
252
+ if (array = rules[:array])
253
+ unless array.is_a?(Module)
254
+ raise ArgumentError.new("Bad class as subtype in prop #{@class.name}.#{name}: #{array.inspect}")
255
+ end
256
+ end
257
+
258
+ if !(rules[:clobber_existing_method!]) && !(rules[:without_accessors])
259
+ # TODO: we should really be checking all the methods on `cls`, not just Object
260
+ if Object.instance_methods.include?(name.to_sym)
261
+ raise ArgumentError.new(
262
+ "#{name} can't be used as a prop in #{@class} because a method with " \
263
+ "that name already exists (defined by #{@class.instance_method(name).owner} " \
264
+ "at #{@class.instance_method(name).source_location || '<unknown>'}). " \
265
+ "(If using this name is unavoidable, try `without_accessors: true`.)"
266
+ )
267
+ end
268
+ end
269
+
270
+ extra = rules[:extra]
271
+ if !extra.nil? && !extra.is_a?(Hash)
272
+ raise ArgumentError.new("Extra metadata must be a Hash in prop #{@class.name}.#{name}")
273
+ end
274
+
275
+ nil
276
+ end
277
+
278
+ private def validate_prop_name(name)
279
+ if name !~ /\A[A-Za-z_][A-Za-z0-9_-]*\z/
280
+ raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
281
+ end
282
+ end
283
+
284
+ # Check if this cls represents a T.nilable(type)
285
+ sig {params(type: PropTypeOrClass).returns(T::Boolean)}
286
+ private def is_nilable?(type)
287
+ return false if !type.is_a?(T::Types::Union)
288
+ type.types.any? {|t| t == T::Utils.coerce(NilClass)}
289
+ end
290
+
291
+ # This converts the type from a T::Type to a regular old ruby class.
292
+ sig {params(type: T::Types::Base).returns(Module)}
293
+ private def convert_type_to_class(type)
294
+ case type
295
+ when T::Types::TypedArray, T::Types::FixedArray
296
+ Array
297
+ when T::Types::TypedHash, T::Types::FixedHash
298
+ Hash
299
+ when T::Types::TypedSet
300
+ Set
301
+ when T::Types::Union
302
+ # The below unwraps our T.nilable types for T::Props if we can.
303
+ # This lets us do things like specify: const T.nilable(String), foreign: Opus::DB::Model::Merchant
304
+ non_nil_type = T::Utils.unwrap_nilable(type)
305
+ if non_nil_type
306
+ convert_type_to_class(non_nil_type)
307
+ else
308
+ Object
309
+ end
310
+ when T::Types::Simple
311
+ type.raw_type
312
+ else
313
+ # This isn't allowed unless whitelisted_for_underspecification is
314
+ # true, due to the check in prop_validate_definition
315
+ Object
316
+ end
317
+ end
318
+
319
+ sig do
320
+ params(
321
+ name: T.any(Symbol, String),
322
+ cls: PropTypeOrClass,
323
+ rules: Rules,
324
+ )
325
+ .void
326
+ end
327
+ def prop_defined(name, cls, rules={})
328
+ # TODO(jerry): Create similar soft assertions against false
329
+ if rules[:optional] == true
330
+ T::Configuration.hard_assert_handler(
331
+ 'Use of `optional: true` is deprecated, please use `T.nilable(...)` instead.',
332
+ storytime: {
333
+ name: name,
334
+ cls_or_args: cls.to_s,
335
+ args: rules,
336
+ klass: decorated_class.name,
337
+ },
338
+ )
339
+ elsif rules[:optional] == false
340
+ T::Configuration.hard_assert_handler(
341
+ 'Use of `optional: :false` is deprecated as it\'s the default value.',
342
+ storytime: {
343
+ name: name,
344
+ cls_or_args: cls.to_s,
345
+ args: rules,
346
+ klass: decorated_class.name,
347
+ },
348
+ )
349
+ elsif rules[:optional] == :on_load
350
+ T::Configuration.hard_assert_handler(
351
+ 'Use of `optional: :on_load` is deprecated. You probably want `T.nilable(...)` with :raise_on_nil_write instead.',
352
+ storytime: {
353
+ name: name,
354
+ cls_or_args: cls.to_s,
355
+ args: rules,
356
+ klass: decorated_class.name,
357
+ },
358
+ )
359
+ elsif rules[:optional] == :existing
360
+ T::Configuration.hard_assert_handler(
361
+ 'Use of `optional: :existing` is not allowed: you should use use T.nilable (http://go/optional)',
362
+ storytime: {
363
+ name: name,
364
+ cls_or_args: cls.to_s,
365
+ args: rules,
366
+ klass: decorated_class.name,
367
+ },
368
+ )
369
+ end
370
+
371
+ if T::Utils::Nilable.is_union_with_nilclass(cls)
372
+ # :_tnilable is introduced internally for performance purpose so that clients do not need to call
373
+ # T::Utils::Nilable.is_tnilable(cls) again.
374
+ # It is strictly internal: clients should always use T::Utils::Props.required_prop?() or
375
+ # T::Utils::Props.optional_prop?() for checking whether a field is required or optional.
376
+ rules[:_tnilable] = true
377
+ end
378
+
379
+ name = name.to_sym
380
+ type = cls
381
+ if !cls.is_a?(Module)
382
+ cls = convert_type_to_class(cls)
383
+ end
384
+ type_object = type
385
+ if !(type_object.singleton_class < T::Props::CustomType)
386
+ type_object = smart_coerce(type_object, array: rules[:array], enum: rules[:enum])
387
+ end
388
+
389
+ prop_validate_definition!(name, cls, rules, type_object)
390
+
391
+ # Retrive the possible underlying object with T.nilable.
392
+ underlying_type_object = T::Utils::Nilable.get_underlying_type_object(type_object)
393
+ type = T::Utils::Nilable.get_underlying_type(type)
394
+
395
+ array_subdoc_type = array_subdoc_type(underlying_type_object)
396
+ hash_value_subdoc_type = hash_value_subdoc_type(underlying_type_object)
397
+ hash_key_custom_type = hash_key_custom_type(underlying_type_object)
398
+
399
+ sensitivity_and_pii = {sensitivity: rules[:sensitivity]}
400
+ if defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::Utils)
401
+ sensitivity_and_pii = Opus::Sensitivity::Utils.normalize_sensitivity_and_pii_annotation(sensitivity_and_pii)
402
+ end
403
+ # We check for Class so this is only applied on concrete
404
+ # documents/models; We allow mixins containing props to not
405
+ # specify their PII nature, as long as every class into which they
406
+ # are ultimately included does.
407
+ #
408
+ if defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::PIIable)
409
+ if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !@class.contains_pii?
410
+ raise ArgumentError.new(
411
+ 'Cannot include a pii prop in a class that declares `contains_no_pii`'
412
+ )
413
+ end
414
+ end
415
+
416
+ needs_clone =
417
+ if cls <= Array || cls <= Hash || cls <= Set
418
+ shallow_clone_ok(underlying_type_object) ? :shallow : true
419
+ else
420
+ false
421
+ end
422
+
423
+ rules = rules.merge(
424
+ # TODO: The type of this element is confusing. We should refactor so that
425
+ # it can be always `type_object` (a PropType) or always `cls` (a Module)
426
+ type: type,
427
+ # These are precomputed for performance
428
+ # TODO: A lot of these are only needed by T::Props::Serializable or T::Struct
429
+ # and can/should be moved accordingly.
430
+ type_is_custom_type: cls.singleton_class < T::Props::CustomType,
431
+ type_is_serializable: cls < T::Props::Serializable,
432
+ type_is_array_of_serializable: !array_subdoc_type.nil?,
433
+ type_is_hash_of_serializable_values: !hash_value_subdoc_type.nil?,
434
+ type_is_hash_of_custom_type_keys: !hash_key_custom_type.nil?,
435
+ type_object: type_object,
436
+ type_needs_clone: needs_clone,
437
+ accessor_key: "@#{name}".to_sym,
438
+ sensitivity: sensitivity_and_pii[:sensitivity],
439
+ pii: sensitivity_and_pii[:pii],
440
+ # extra arbitrary metadata attached by the code defining this property
441
+ extra: rules[:extra]&.freeze,
442
+ )
443
+
444
+ validate_not_missing_sensitivity(name, rules)
445
+
446
+ # for backcompat
447
+ if type.is_a?(T::Types::TypedArray) && type.type.is_a?(T::Types::Simple)
448
+ rules[:array] = type.type.raw_type
449
+ elsif array_subdoc_type
450
+ rules[:array] = array_subdoc_type
451
+ end
452
+
453
+ if rules[:type_is_serializable]
454
+ rules[:serializable_subtype] = cls
455
+ elsif array_subdoc_type
456
+ rules[:serializable_subtype] = array_subdoc_type
457
+ elsif hash_value_subdoc_type && hash_key_custom_type
458
+ rules[:serializable_subtype] = {
459
+ keys: hash_key_custom_type,
460
+ values: hash_value_subdoc_type,
461
+ }
462
+ elsif hash_value_subdoc_type
463
+ rules[:serializable_subtype] = hash_value_subdoc_type
464
+ elsif hash_key_custom_type
465
+ rules[:serializable_subtype] = hash_key_custom_type
466
+ end
467
+
468
+ add_prop_definition(name, rules)
469
+ # NB: using `without_accessors` doesn't make much sense unless you also define some other way to
470
+ # get at the property (e.g., Chalk::ODM::Document exposes `get` and `set`).
471
+ define_getter_and_setter(name, rules) unless rules[:without_accessors]
472
+
473
+ if rules[:foreign] && rules[:foreign_hint_only]
474
+ raise ArgumentError.new(":foreign and :foreign_hint_only are mutually exclusive.")
475
+ end
476
+
477
+ handle_foreign_option(name, cls, rules, rules[:foreign]) if rules[:foreign]
478
+ handle_foreign_hint_only_option(cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
479
+ handle_redaction_option(name, rules[:redaction]) if rules[:redaction]
480
+ end
481
+
482
+ sig {params(name: Symbol, rules: Rules).void}
483
+ private def define_getter_and_setter(name, rules)
484
+ if rules[:immutable]
485
+ @class.send(:define_method, "#{name}=") do |_x|
486
+ raise T::Props::ImmutableProp.new("#{self.class}##{name} cannot be modified after creation.")
487
+ end
488
+ else
489
+ @class.send(:define_method, "#{name}=") do |x|
490
+ self.class.decorator.prop_set(self, name, x, rules)
491
+ end
492
+ end
493
+
494
+ @class.send(:define_method, name) do
495
+ self.class.decorator.prop_get(self, name, rules)
496
+ end
497
+ end
498
+
499
+ # returns the subdoc of the array type, or nil if it's not a Document type
500
+ sig do
501
+ params(type: PropType)
502
+ .returns(T.nilable(Module))
503
+ end
504
+ private def array_subdoc_type(type)
505
+ if type.is_a?(T::Types::TypedArray)
506
+ el_type = T::Utils.unwrap_nilable(type.type) || type.type
507
+
508
+ if el_type.is_a?(T::Types::Simple) &&
509
+ (el_type.raw_type < T::Props::Serializable || el_type.raw_type.is_a?(T::Props::CustomType))
510
+ return el_type.raw_type
511
+ end
512
+ end
513
+
514
+ nil
515
+ end
516
+
517
+ # returns the subdoc of the hash value type, or nil if it's not a Document type
518
+ sig do
519
+ params(type: PropType)
520
+ .returns(T.nilable(Module))
521
+ end
522
+ private def hash_value_subdoc_type(type)
523
+ if type.is_a?(T::Types::TypedHash)
524
+ values_type = T::Utils.unwrap_nilable(type.values) || type.values
525
+
526
+ if values_type.is_a?(T::Types::Simple) &&
527
+ (values_type.raw_type < T::Props::Serializable || values_type.raw_type.is_a?(T::Props::CustomType))
528
+ return values_type.raw_type
529
+ end
530
+ end
531
+
532
+ nil
533
+ end
534
+
535
+ # returns the type of the hash key, or nil. Any CustomType could be a key, but we only expect Opus::Enum right now.
536
+ sig do
537
+ params(type: PropType)
538
+ .returns(T.nilable(Module))
539
+ end
540
+ private def hash_key_custom_type(type)
541
+ if type.is_a?(T::Types::TypedHash)
542
+ keys_type = T::Utils.unwrap_nilable(type.keys) || type.keys
543
+
544
+ if keys_type.is_a?(T::Types::Simple) && keys_type.raw_type.is_a?(T::Props::CustomType)
545
+ return keys_type.raw_type
546
+ end
547
+ end
548
+
549
+ nil
550
+ end
551
+
552
+ # From T::Props::Utils.deep_clone_object, plus String
553
+ TYPES_NOT_NEEDING_CLONE = [TrueClass, FalseClass, NilClass, Symbol, String, Numeric]
554
+ if defined?(Opus) && defined?(Opus::Enum)
555
+ TYPES_NOT_NEEDING_CLONE << Opus::Enum
556
+ end
557
+
558
+ sig {params(type: PropType).returns(T::Boolean)}
559
+ private def shallow_clone_ok(type)
560
+ inner_type =
561
+ if type.is_a?(T::Types::TypedArray)
562
+ type.type
563
+ elsif type.is_a?(T::Types::TypedSet)
564
+ type.type
565
+ elsif type.is_a?(T::Types::TypedHash)
566
+ type.values
567
+ end
568
+
569
+ inner_type.is_a?(T::Types::Simple) && TYPES_NOT_NEEDING_CLONE.any? do |cls|
570
+ inner_type.raw_type <= cls
571
+ end
572
+ end
573
+
574
+ sig do
575
+ params(type: PropTypeOrClass, array: T.untyped, enum: T.untyped)
576
+ .returns(T::Types::Base)
577
+ end
578
+ private def smart_coerce(type, array:, enum:)
579
+ # Backwards compatibility for pre-T::Types style
580
+ if !array.nil? && !enum.nil?
581
+ raise ArgumentError.new("Cannot specify both :array and :enum options")
582
+ elsif !array.nil?
583
+ if type == Set
584
+ T::Set[array]
585
+ else
586
+ T::Array[array]
587
+ end
588
+ elsif !enum.nil?
589
+ if T::Utils.unwrap_nilable(type)
590
+ T.nilable(T.enum(enum))
591
+ else
592
+ T.enum(enum)
593
+ end
594
+ else
595
+ T::Utils.coerce(type)
596
+ end
597
+ end
598
+
599
+ sig {params(prop_name: Symbol, rules: Hash).void}
600
+ private def validate_not_missing_sensitivity(prop_name, rules)
601
+ if rules[:sensitivity].nil?
602
+ if rules[:redaction]
603
+ T::Configuration.hard_assert_handler(
604
+ "#{@class}##{prop_name} has a 'redaction:' annotation but no " \
605
+ "'sensitivity:' annotation. This is probably wrong, because if a " \
606
+ "prop needs redaction then it is probably sensitive. Add a " \
607
+ "sensitivity annotation like 'sensitivity: Opus::Sensitivity::PII." \
608
+ "whatever', or explicitly override this check with 'sensitivity: []'."
609
+ )
610
+ end
611
+ # TODO(PRIVACYENG-982) Ideally we'd also check for 'password' and possibly
612
+ # other terms, but this interacts badly with ProtoDefinedDocument because
613
+ # the proto syntax currently can't declare "sensitivity: []"
614
+ if prop_name =~ /\bsecret\b/
615
+ T::Configuration.hard_assert_handler(
616
+ "#{@class}##{prop_name} has the word 'secret' in its name, but no " \
617
+ "'sensitivity:' annotation. This is probably wrong, because if a " \
618
+ "prop is named 'secret' then it is probably sensitive. Add a " \
619
+ "sensitivity annotation like 'sensitivity: Opus::Sensitivity::NonPII." \
620
+ "security_token', or explicitly override this check with " \
621
+ "'sensitivity: []'."
622
+ )
623
+ end
624
+ end
625
+ end
626
+
627
+ # Create "#{prop_name}_redacted" method
628
+ sig do
629
+ params(
630
+ prop_name: Symbol,
631
+ redaction: Chalk::Tools::RedactionUtils::RedactionDirectiveSpec,
632
+ )
633
+ .void
634
+ end
635
+ private def handle_redaction_option(prop_name, redaction)
636
+ redacted_method = "#{prop_name}_redacted"
637
+
638
+ @class.send(:define_method, redacted_method) do
639
+ value = self.public_send(prop_name)
640
+ Chalk::Tools::RedactionUtils.redact_with_directive(
641
+ value, redaction)
642
+ end
643
+ end
644
+
645
+ sig do
646
+ params(
647
+ option_sym: Symbol,
648
+ foreign: T.untyped,
649
+ valid_type_msg: String,
650
+ )
651
+ .void
652
+ end
653
+ private def validate_foreign_option(option_sym, foreign, valid_type_msg:)
654
+ if foreign.is_a?(Symbol) || foreign.is_a?(String)
655
+ raise ArgumentError.new(
656
+ "Using a symbol/string for `#{option_sym}` is no longer supported. Instead, use a Proc " \
657
+ "that returns the class, e.g., foreign: -> {Foo}"
658
+ )
659
+ end
660
+
661
+ if !foreign.is_a?(Proc) && !foreign.is_a?(Array) && !foreign.respond_to?(:load)
662
+ raise ArgumentError.new("The `#{option_sym}` option must be #{valid_type_msg}")
663
+ end
664
+ end
665
+
666
+ sig do
667
+ params(
668
+ prop_cls: Module,
669
+ foreign_hint_only: T.untyped,
670
+ )
671
+ .void
672
+ end
673
+ private def handle_foreign_hint_only_option(prop_cls, foreign_hint_only)
674
+ if ![String, Array].include?(prop_cls) && !(prop_cls.is_a?(T::Props::CustomType))
675
+ raise ArgumentError.new(
676
+ "`foreign_hint_only` can only be used with String or Array prop types"
677
+ )
678
+ end
679
+
680
+ validate_foreign_option(
681
+ :foreign_hint_only, foreign_hint_only,
682
+ valid_type_msg: "an individual or array of a model class, or a Proc returning such."
683
+ )
684
+ end
685
+
686
+ sig do
687
+ params(
688
+ prop_name: T.any(String, Symbol),
689
+ rules: Rules,
690
+ foreign: T.untyped,
691
+ )
692
+ .void
693
+ end
694
+ private def define_foreign_method(prop_name, rules, foreign)
695
+ fk_method = "#{prop_name}_"
696
+
697
+ # n.b. there's no clear reason *not* to allow additional options
698
+ # here, but we're baking in `allow_direct_mutation` since we
699
+ # *haven't* allowed additional options in the past and want to
700
+ # default to keeping this interface narrow.
701
+ @class.send(:define_method, fk_method) do |allow_direct_mutation: nil|
702
+ if foreign.is_a?(Proc)
703
+ resolved_foreign = foreign.call
704
+ if !resolved_foreign.respond_to?(:load)
705
+ raise ArgumentError.new(
706
+ "The `foreign` proc for `#{prop_name}` must return a model class. " \
707
+ "Got `#{resolved_foreign.inspect}` instead."
708
+ )
709
+ end
710
+ # `foreign` is part of the closure state, so this will persist to future invocations
711
+ # of the method, optimizing it so this only runs on the first invocation.
712
+ foreign = resolved_foreign
713
+ end
714
+ if allow_direct_mutation.nil?
715
+ opts = {}
716
+ else
717
+ opts = {allow_direct_mutation: allow_direct_mutation}
718
+ end
719
+
720
+ self.class.decorator.foreign_prop_get(self, prop_name, foreign, rules, opts)
721
+ end
722
+
723
+ force_fk_method = "#{fk_method}!"
724
+ @class.send(:define_method, force_fk_method) do |allow_direct_mutation: nil|
725
+ loaded_foreign = send(fk_method, allow_direct_mutation: allow_direct_mutation)
726
+ if !loaded_foreign
727
+ T::Configuration.hard_assert_handler(
728
+ 'Failed to load foreign model',
729
+ storytime: {method: force_fk_method, class: self.class}
730
+ )
731
+ end
732
+ T.must(loaded_foreign)
733
+ end
734
+
735
+ @class.send(:define_method, "#{prop_name}_record") do |allow_direct_mutation: nil|
736
+ T::Configuration.soft_assert_handler(
737
+ "Using deprecated 'model.#{prop_name}_record' foreign key syntax. You should replace this with 'model.#{prop_name}_'",
738
+ notify: 'vasi'
739
+ )
740
+ send(fk_method, allow_direct_mutation: allow_direct_mutation)
741
+ end
742
+ end
743
+
744
+ sig do
745
+ params(
746
+ prop_name: Symbol,
747
+ prop_cls: Module,
748
+ rules: Rules,
749
+ foreign: T.untyped,
750
+ )
751
+ .void
752
+ end
753
+ private def handle_foreign_option(prop_name, prop_cls, rules, foreign)
754
+ validate_foreign_option(
755
+ :foreign, foreign, valid_type_msg: "a model class or a Proc that returns one"
756
+ )
757
+
758
+ if prop_cls != String
759
+ raise ArgumentError.new("`foreign` can only be used with a prop type of String")
760
+ end
761
+
762
+ if foreign.is_a?(Array)
763
+ # We don't support arrays with `foreign` because it's hard to both preserve ordering and
764
+ # keep them from being lurky performance hits by issuing a bunch of un-batched DB queries.
765
+ # We could potentially address that by porting over something like AmbiguousIDLoader.
766
+ raise ArgumentError.new(
767
+ "Using an array for `foreign` is no longer supported. Instead, use `foreign_hint_only` " \
768
+ "with an array or a Proc that returns an array, e.g., foreign_hint_only: -> {[Foo, Bar]}"
769
+ )
770
+ end
771
+
772
+ define_foreign_method(prop_name, rules, foreign)
773
+ end
774
+
775
+ # TODO: rename this to props_inherited
776
+ #
777
+ # This gets called when a module or class that extends T::Props gets included, extended,
778
+ # prepended, or inherited.
779
+ sig {params(child: DecoratedClass).void}
780
+ def model_inherited(child)
781
+ child.extend(T::Props::ClassMethods)
782
+ child.plugins.concat(decorated_class.plugins)
783
+
784
+ decorated_class.plugins.each do |mod|
785
+ # NB: apply_class_methods must not be an instance method on the decorator itself,
786
+ # otherwise we'd have to call child.decorator here, which would create the decorator
787
+ # before any `decorator_class` override has a chance to take effect (see the comment below).
788
+ Private.apply_class_methods(mod, child)
789
+ end
790
+
791
+ props.each do |name, rules|
792
+ copied_rules = rules.dup
793
+ # NB: Calling `child.decorator` here is a timb bomb that's going to give someone a really bad
794
+ # time. Any class that defines props and also overrides the `decorator_class` method is going
795
+ # to reach this line before its override take effect, turning it into a no-op.
796
+ child.decorator.add_prop_definition(name, copied_rules)
797
+ end
798
+ end
799
+
800
+ sig {params(mod: Module).void}
801
+ def plugin(mod)
802
+ decorated_class.plugins << mod
803
+ Private.apply_class_methods(mod, decorated_class)
804
+ Private.apply_decorator_methods(mod, self)
805
+ end
806
+
807
+ module Private
808
+ # These need to be non-instance methods so we can use them without prematurely creating the
809
+ # child decorator in `model_inherited` (see comments there for details).
810
+ def self.apply_class_methods(plugin, target)
811
+ if plugin.const_defined?('ClassMethods')
812
+ # FIXME: This will break preloading, selective test execution, etc if `mod::ClassMethods`
813
+ # is ever defined in a separate file from `mod`.
814
+ target.extend(plugin::ClassMethods) # rubocop:disable PrisonGuard/NoDynamicConstAccess
815
+ end
816
+ end
817
+
818
+ def self.apply_decorator_methods(plugin, target)
819
+ if plugin.const_defined?('DecoratorMethods')
820
+ # FIXME: This will break preloading, selective test execution, etc if `mod::DecoratorMethods`
821
+ # is ever defined in a separate file from `mod`.
822
+ target.extend(plugin::DecoratorMethods) # rubocop:disable PrisonGuard/NoDynamicConstAccess
823
+ end
824
+ end
825
+ end
826
+ end