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,40 @@
1
+ # frozen_string_literal: true
2
+ # typed: false
3
+
4
+ module T::Props::Constructor
5
+ include T::Props::WeakConstructor
6
+ end
7
+
8
+ module T::Props::Constructor::DecoratorMethods
9
+ extend T::Sig
10
+
11
+ # Set values for all props that have no defaults. Override what `WeakConstructor`
12
+ # does in order to raise errors on nils instead of ignoring them.
13
+ #
14
+ # @return [Integer] A count of props that we successfully initialized (which
15
+ # we'll use to check for any unrecognized input.)
16
+ #
17
+ # checked(:never) - O(runtime object construction)
18
+ sig {params(instance: T::Props::Constructor, hash: T::Hash[Symbol, T.untyped]).returns(Integer).checked(:never)}
19
+ def construct_props_without_defaults(instance, hash)
20
+ # Use `each_pair` rather than `count` because, as of Ruby 2.6, the latter delegates to Enumerator
21
+ # and therefore allocates for each entry.
22
+ result = 0
23
+ @props_without_defaults&.each_pair do |p, setter_proc|
24
+ begin
25
+ val = hash[p]
26
+ instance.instance_exec(val, &setter_proc)
27
+ if val || hash.key?(p)
28
+ result += 1
29
+ end
30
+ rescue TypeError, T::Props::InvalidValueError
31
+ if !hash.key?(p)
32
+ raise ArgumentError.new("Missing required prop `#{p}` for class `#{instance.class.name}`")
33
+ else
34
+ raise
35
+ end
36
+ end
37
+ end
38
+ result
39
+ end
40
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
3
+
4
+ module T::Props
5
+ module CustomType
6
+ extend T::Sig
7
+ extend T::Helpers
8
+
9
+ abstract!
10
+
11
+ include Kernel # for `is_a?`
12
+
13
+ # Alias for backwards compatibility
14
+ sig(:final) do
15
+ params(
16
+ value: BasicObject,
17
+ )
18
+ .returns(T::Boolean)
19
+ .checked(:never)
20
+ end
21
+ def instance?(value)
22
+ self.===(value)
23
+ end
24
+
25
+ # Alias for backwards compatibility
26
+ sig(:final) do
27
+ params(
28
+ value: BasicObject,
29
+ )
30
+ .returns(T::Boolean)
31
+ .checked(:never)
32
+ end
33
+ def valid?(value)
34
+ instance?(value)
35
+ end
36
+
37
+ # Given an instance of this type, serialize that into a scalar type
38
+ # supported by T::Props.
39
+ #
40
+ # @param [Object] _instance
41
+ # @return An instance of one of T::Configuration.scalar_types
42
+ sig {abstract.params(instance: T.untyped).returns(T.untyped).checked(:never)}
43
+ def serialize(instance); end
44
+
45
+ # Given the serialized form of your type, this returns an instance
46
+ # of that custom type representing that value.
47
+ #
48
+ # @param scalar One of T::Configuration.scalar_types
49
+ # @return Object
50
+ sig {abstract.params(scalar: T.untyped).returns(T.untyped).checked(:never)}
51
+ def deserialize(scalar); end
52
+
53
+ sig {override.params(_base: Module).void}
54
+ def self.included(_base)
55
+ super
56
+
57
+ raise 'Please use "extend", not "include" to attach this module'
58
+ end
59
+
60
+ sig(:final) {params(val: Object).returns(T::Boolean).checked(:never)}
61
+ def self.scalar_type?(val)
62
+ # We don't need to check for val's included modules in
63
+ # T::Configuration.scalar_types, because T::Configuration.scalar_types
64
+ # are all classes.
65
+ klass = T.let(val.class, T.nilable(Class))
66
+ until klass.nil?
67
+ return true if T::Configuration.scalar_types.include?(klass.to_s)
68
+ klass = klass.superclass
69
+ end
70
+ false
71
+ end
72
+
73
+ # We allow custom types to serialize to Arrays, so that we can
74
+ # implement set-like fields that store a unique-array, but forbid
75
+ # hashes; Custom hash types should be implemented via an emebdded
76
+ # T::Struct (or a subclass like Chalk::ODM::Document) or via T.
77
+ sig(:final) {params(val: Object).returns(T::Boolean).checked(:never)}
78
+ def self.valid_serialization?(val)
79
+ case val
80
+ when Array
81
+ val.each do |v|
82
+ return false unless scalar_type?(v)
83
+ end
84
+
85
+ true
86
+ else
87
+ scalar_type?(val)
88
+ end
89
+ end
90
+
91
+ sig(:final) do
92
+ params(instance: Object)
93
+ .returns(T.untyped)
94
+ .checked(:never)
95
+ end
96
+ def self.checked_serialize(instance)
97
+ val = T.cast(instance.class, T::Props::CustomType).serialize(instance)
98
+ unless valid_serialization?(val)
99
+ msg = "#{instance.class} did not serialize to a valid scalar type. It became a: #{val.class}"
100
+ if val.is_a?(Hash)
101
+ msg += "\nIf you want to store a structured Hash, consider using a T::Struct as your type."
102
+ end
103
+ raise TypeError.new(msg)
104
+ end
105
+ val
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,672 @@
1
+ # frozen_string_literal: true
2
+ # typed: strict
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
+ DecoratedInstance = T.type_alias {Object} # Would be T::Props, but that produces circular reference errors in some circumstances
15
+ PropType = T.type_alias {T::Types::Base}
16
+ PropTypeOrClass = T.type_alias {T.any(PropType, Module)}
17
+
18
+ class NoRulesError < StandardError; end
19
+
20
+ EMPTY_PROPS = T.let({}.freeze, T::Hash[Symbol, Rules])
21
+ private_constant :EMPTY_PROPS
22
+
23
+ sig {params(klass: T.untyped).void.checked(:never)}
24
+ def initialize(klass)
25
+ @class = T.let(klass, T.all(Module, T::Props::ClassMethods))
26
+ @class.plugins.each do |mod|
27
+ T::Props::Plugin::Private.apply_decorator_methods(mod, self)
28
+ end
29
+ @props = T.let(EMPTY_PROPS, T::Hash[Symbol, Rules])
30
+ end
31
+
32
+ # checked(:never) - O(prop accesses)
33
+ sig {returns(T::Hash[Symbol, Rules]).checked(:never)}
34
+ attr_reader :props
35
+
36
+ sig {returns(T::Array[Symbol])}
37
+ def all_props; props.keys; end
38
+
39
+ # checked(:never) - O(prop accesses)
40
+ sig {params(prop: T.any(Symbol, String)).returns(Rules).checked(:never)}
41
+ def prop_rules(prop); props[prop.to_sym] || raise("No such prop: #{prop.inspect}"); end
42
+
43
+ # checked(:never) - Rules hash is expensive to check
44
+ sig {params(prop: Symbol, rules: Rules).void.checked(:never)}
45
+ def add_prop_definition(prop, rules)
46
+ override = rules.delete(:override)
47
+
48
+ if props.include?(prop) && !override
49
+ raise ArgumentError.new("Attempted to redefine prop #{prop.inspect} that's already defined without specifying :override => true: #{prop_rules(prop)}")
50
+ elsif !props.include?(prop) && override
51
+ raise ArgumentError.new("Attempted to override a prop #{prop.inspect} that doesn't already exist")
52
+ end
53
+
54
+ @props = @props.merge(prop => rules.freeze).freeze
55
+ end
56
+
57
+ VALID_RULE_KEYS = T.let(%i{
58
+ enum
59
+ foreign
60
+ foreign_hint_only
61
+ ifunset
62
+ immutable
63
+ override
64
+ redaction
65
+ sensitivity
66
+ without_accessors
67
+ clobber_existing_method!
68
+ extra
69
+ setter_validate
70
+ _tnilable
71
+ }.map {|k| [k, true]}.to_h.freeze, T::Hash[Symbol, T::Boolean])
72
+ private_constant :VALID_RULE_KEYS
73
+
74
+ sig {params(key: Symbol).returns(T::Boolean).checked(:never)}
75
+ def valid_rule_key?(key)
76
+ !!VALID_RULE_KEYS[key]
77
+ end
78
+
79
+ # checked(:never) - O(prop accesses)
80
+ sig {returns(T.all(Module, T::Props::ClassMethods)).checked(:never)}
81
+ def decorated_class; @class; end
82
+
83
+ # Accessors
84
+
85
+ # 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.
86
+ #
87
+ # checked(:never) - potentially O(prop accesses) depending on usage pattern
88
+ sig {params(prop: Symbol, val: T.untyped).void.checked(:never)}
89
+ def validate_prop_value(prop, val)
90
+ # We call `setter_proc` here without binding to an instance, so it'll run
91
+ # `instance_variable_set` if validation passes, but nothing will care.
92
+ # We only care about the validation.
93
+ prop_rules(prop).fetch(:setter_proc).call(val)
94
+ end
95
+
96
+ # For performance, don't use named params here.
97
+ # Passing in rules here is purely a performance optimization.
98
+ # Unlike the other methods that take rules, this one calls prop_rules for
99
+ # the default, which raises if the prop doesn't exist (this maintains
100
+ # preexisting behavior).
101
+ #
102
+ # Note this path is NOT used by generated setters on instances,
103
+ # which are defined using `setter_proc` directly.
104
+ #
105
+ # checked(:never) - O(prop accesses)
106
+ sig do
107
+ params(
108
+ instance: DecoratedInstance,
109
+ prop: Symbol,
110
+ val: T.untyped,
111
+ rules: Rules
112
+ )
113
+ .void
114
+ .checked(:never)
115
+ end
116
+ def prop_set(instance, prop, val, rules=prop_rules(prop))
117
+ instance.instance_exec(val, &rules.fetch(:setter_proc))
118
+ end
119
+ alias_method :set, :prop_set
120
+
121
+ # Only Models have any custom get logic but we need to call this on
122
+ # non-Models since we don't know at code gen time what we have.
123
+ sig do
124
+ params(
125
+ instance: DecoratedInstance,
126
+ prop: Symbol,
127
+ value: T.untyped
128
+ )
129
+ .returns(T.untyped)
130
+ .checked(:never)
131
+ end
132
+ def prop_get_logic(instance, prop, value)
133
+ value
134
+ end
135
+
136
+ # For performance, don't use named params here.
137
+ # Passing in rules here is purely a performance optimization.
138
+ #
139
+ # Note this path is NOT used by generated getters on instances,
140
+ # unless `ifunset` is used on the prop, or `prop_get` is overridden.
141
+ #
142
+ # checked(:never) - O(prop accesses)
143
+ sig do
144
+ params(
145
+ instance: DecoratedInstance,
146
+ prop: T.any(String, Symbol),
147
+ rules: Rules
148
+ )
149
+ .returns(T.untyped)
150
+ .checked(:never)
151
+ end
152
+ def prop_get(instance, prop, rules=prop_rules(prop))
153
+ val = instance.instance_variable_get(rules[:accessor_key])
154
+ if !val.nil?
155
+ val
156
+ else
157
+ if (d = rules[:ifunset])
158
+ T::Props::Utils.deep_clone_object(d)
159
+ else
160
+ nil
161
+ end
162
+ end
163
+ end
164
+
165
+ sig do
166
+ params(
167
+ instance: DecoratedInstance,
168
+ prop: T.any(String, Symbol),
169
+ rules: Rules
170
+ )
171
+ .returns(T.untyped)
172
+ .checked(:never)
173
+ end
174
+ def prop_get_if_set(instance, prop, rules=prop_rules(prop))
175
+ instance.instance_variable_get(rules[:accessor_key])
176
+ end
177
+ alias_method :get, :prop_get_if_set # Alias for backwards compatibility
178
+
179
+ # checked(:never) - O(prop accesses)
180
+ sig do
181
+ params(
182
+ instance: DecoratedInstance,
183
+ prop: Symbol,
184
+ foreign_class: Module,
185
+ rules: Rules,
186
+ opts: T::Hash[Symbol, T.untyped],
187
+ )
188
+ .returns(T.untyped)
189
+ .checked(:never)
190
+ end
191
+ def foreign_prop_get(instance, prop, foreign_class, rules=prop_rules(prop), opts={})
192
+ return if !(value = prop_get(instance, prop, rules))
193
+ T.unsafe(foreign_class).load(value, {}, opts)
194
+ end
195
+
196
+ # TODO: we should really be checking all the methods on `cls`, not just Object
197
+ BANNED_METHOD_NAMES = T.let(Object.instance_methods.to_set.freeze, T::Set[Symbol])
198
+
199
+ # checked(:never) - Rules hash is expensive to check
200
+ sig do
201
+ params(
202
+ name: Symbol,
203
+ cls: Module,
204
+ rules: Rules,
205
+ type: PropTypeOrClass
206
+ )
207
+ .void
208
+ .checked(:never)
209
+ end
210
+ def prop_validate_definition!(name, cls, rules, type)
211
+ validate_prop_name(name)
212
+
213
+ if rules.key?(:pii)
214
+ raise ArgumentError.new("The 'pii:' option for props has been renamed " \
215
+ "to 'sensitivity:' (in prop #{@class.name}.#{name})")
216
+ end
217
+
218
+ if rules.keys.any? {|k| !valid_rule_key?(k)}
219
+ raise ArgumentError.new("At least one invalid prop arg supplied in #{self}: #{rules.keys.inspect}")
220
+ end
221
+
222
+ if !(rules[:clobber_existing_method!]) && !(rules[:without_accessors])
223
+ if BANNED_METHOD_NAMES.include?(name.to_sym)
224
+ raise ArgumentError.new(
225
+ "#{name} can't be used as a prop in #{@class} because a method with " \
226
+ "that name already exists (defined by #{@class.instance_method(name).owner} " \
227
+ "at #{@class.instance_method(name).source_location || '<unknown>'}). " \
228
+ "(If using this name is unavoidable, try `without_accessors: true`.)"
229
+ )
230
+ end
231
+ end
232
+
233
+ extra = rules[:extra]
234
+ if !extra.nil? && !extra.is_a?(Hash)
235
+ raise ArgumentError.new("Extra metadata must be a Hash in prop #{@class.name}.#{name}")
236
+ end
237
+
238
+ nil
239
+ end
240
+
241
+ SAFE_NAME = /\A[A-Za-z_][A-Za-z0-9_-]*\z/
242
+
243
+ # Used to validate both prop names and serialized forms
244
+ sig {params(name: T.any(Symbol, String)).void}
245
+ private def validate_prop_name(name)
246
+ if !name.match?(SAFE_NAME)
247
+ raise ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")
248
+ end
249
+ end
250
+
251
+ # This converts the type from a T::Type to a regular old ruby class.
252
+ sig {params(type: T::Types::Base).returns(Module)}
253
+ private def convert_type_to_class(type)
254
+ case type
255
+ when T::Types::TypedArray, T::Types::FixedArray
256
+ Array
257
+ when T::Types::TypedHash, T::Types::FixedHash
258
+ Hash
259
+ when T::Types::TypedSet
260
+ Set
261
+ when T::Types::Union
262
+ # The below unwraps our T.nilable types for T::Props if we can.
263
+ # This lets us do things like specify: const T.nilable(String), foreign: Opus::DB::Model::Merchant
264
+ non_nil_type = T::Utils.unwrap_nilable(type)
265
+ if non_nil_type
266
+ convert_type_to_class(non_nil_type)
267
+ else
268
+ Object
269
+ end
270
+ when T::Types::Simple
271
+ type.raw_type
272
+ else
273
+ # This isn't allowed unless whitelisted_for_underspecification is
274
+ # true, due to the check in prop_validate_definition
275
+ Object
276
+ end
277
+ end
278
+
279
+ # checked(:never) - Rules hash is expensive to check
280
+ sig do
281
+ params(
282
+ name: T.any(Symbol, String),
283
+ cls: PropTypeOrClass,
284
+ rules: Rules,
285
+ )
286
+ .void
287
+ .checked(:never)
288
+ end
289
+ def prop_defined(name, cls, rules={})
290
+ cls = T::Utils.resolve_alias(cls)
291
+
292
+ if T::Utils::Nilable.is_union_with_nilclass(cls)
293
+ # :_tnilable is introduced internally for performance purpose so that clients do not need to call
294
+ # T::Utils::Nilable.is_tnilable(cls) again.
295
+ # It is strictly internal: clients should always use T::Props::Utils.required_prop?() or
296
+ # T::Props::Utils.optional_prop?() for checking whether a field is required or optional.
297
+ rules[:_tnilable] = true
298
+ end
299
+
300
+ name = name.to_sym
301
+ type = cls
302
+ if !cls.is_a?(Module)
303
+ cls = convert_type_to_class(cls)
304
+ end
305
+ type_object = smart_coerce(type, enum: rules[:enum])
306
+
307
+ prop_validate_definition!(name, cls, rules, type_object)
308
+
309
+ # Retrive the possible underlying object with T.nilable.
310
+ type = T::Utils::Nilable.get_underlying_type(type)
311
+
312
+ sensitivity_and_pii = {sensitivity: rules[:sensitivity]}
313
+ if defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::Utils)
314
+ sensitivity_and_pii = Opus::Sensitivity::Utils.normalize_sensitivity_and_pii_annotation(sensitivity_and_pii)
315
+ end
316
+ # We check for Class so this is only applied on concrete
317
+ # documents/models; We allow mixins containing props to not
318
+ # specify their PII nature, as long as every class into which they
319
+ # are ultimately included does.
320
+ #
321
+ if defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::PIIable)
322
+ if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !T.unsafe(@class).contains_pii?
323
+ raise ArgumentError.new(
324
+ 'Cannot include a pii prop in a class that declares `contains_no_pii`'
325
+ )
326
+ end
327
+ end
328
+
329
+ rules = rules.merge(
330
+ # TODO: The type of this element is confusing. We should refactor so that
331
+ # it can be always `type_object` (a PropType) or always `cls` (a Module)
332
+ type: type,
333
+ type_object: type_object,
334
+ accessor_key: "@#{name}".to_sym,
335
+ sensitivity: sensitivity_and_pii[:sensitivity],
336
+ pii: sensitivity_and_pii[:pii],
337
+ # extra arbitrary metadata attached by the code defining this property
338
+ extra: rules[:extra]&.freeze,
339
+ )
340
+
341
+ validate_not_missing_sensitivity(name, rules)
342
+
343
+ # for backcompat (the `:array` key is deprecated but because the name is
344
+ # so generic it's really hard to be sure it's not being relied on anymore)
345
+ if type.is_a?(T::Types::TypedArray)
346
+ inner = T::Utils::Nilable.get_underlying_type(type.type)
347
+ if inner.is_a?(Module)
348
+ rules[:array] = inner
349
+ end
350
+ end
351
+
352
+ rules[:setter_proc] = T::Props::Private::SetterFactory.build_setter_proc(@class, name, rules).freeze
353
+
354
+ add_prop_definition(name, rules)
355
+
356
+ # NB: using `without_accessors` doesn't make much sense unless you also define some other way to
357
+ # get at the property (e.g., Chalk::ODM::Document exposes `get` and `set`).
358
+ define_getter_and_setter(name, rules) unless rules[:without_accessors]
359
+
360
+ if rules[:foreign] && rules[:foreign_hint_only]
361
+ raise ArgumentError.new(":foreign and :foreign_hint_only are mutually exclusive.")
362
+ end
363
+
364
+ handle_foreign_option(name, cls, rules, rules[:foreign]) if rules[:foreign]
365
+ handle_foreign_hint_only_option(name, cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
366
+ handle_redaction_option(name, rules[:redaction]) if rules[:redaction]
367
+ end
368
+
369
+ # checked(:never) - Rules hash is expensive to check
370
+ sig {params(name: Symbol, rules: Rules).void.checked(:never)}
371
+ private def define_getter_and_setter(name, rules)
372
+ T::Configuration.without_ruby_warnings do
373
+ if !rules[:immutable]
374
+ if method(:prop_set).owner != T::Props::Decorator
375
+ @class.send(:define_method, "#{name}=") do |val|
376
+ T.unsafe(self.class).decorator.prop_set(self, name, val, rules)
377
+ end
378
+ else
379
+ # Fast path (~4x faster as of Ruby 2.6)
380
+ @class.send(:define_method, "#{name}=", &rules.fetch(:setter_proc))
381
+ end
382
+ end
383
+
384
+ if method(:prop_get).owner != T::Props::Decorator || rules.key?(:ifunset)
385
+ @class.send(:define_method, name) do
386
+ T.unsafe(self.class).decorator.prop_get(self, name, rules)
387
+ end
388
+ else
389
+ # Fast path (~30x faster as of Ruby 2.6)
390
+ @class.send(:attr_reader, name) # send is used because `attr_reader` is private in 2.4
391
+ end
392
+ end
393
+ end
394
+
395
+ sig do
396
+ params(type: PropTypeOrClass, enum: T.untyped)
397
+ .returns(T::Types::Base)
398
+ end
399
+ private def smart_coerce(type, enum:)
400
+ # Backwards compatibility for pre-T::Types style
401
+ type = T::Utils.coerce(type)
402
+ if enum.nil?
403
+ type
404
+ else
405
+ nonnil_type = T::Utils.unwrap_nilable(type)
406
+ if nonnil_type
407
+ T.nilable(T.all(nonnil_type, T.enum(enum)))
408
+ else
409
+ T.all(type, T.enum(enum))
410
+ end
411
+ end
412
+ end
413
+
414
+ # checked(:never) - Rules hash is expensive to check
415
+ sig {params(prop_name: Symbol, rules: Rules).void.checked(:never)}
416
+ private def validate_not_missing_sensitivity(prop_name, rules)
417
+ if rules[:sensitivity].nil?
418
+ if rules[:redaction]
419
+ T::Configuration.hard_assert_handler(
420
+ "#{@class}##{prop_name} has a 'redaction:' annotation but no " \
421
+ "'sensitivity:' annotation. This is probably wrong, because if a " \
422
+ "prop needs redaction then it is probably sensitive. Add a " \
423
+ "sensitivity annotation like 'sensitivity: Opus::Sensitivity::PII." \
424
+ "whatever', or explicitly override this check with 'sensitivity: []'."
425
+ )
426
+ end
427
+ # TODO(PRIVACYENG-982) Ideally we'd also check for 'password' and possibly
428
+ # other terms, but this interacts badly with ProtoDefinedDocument because
429
+ # the proto syntax currently can't declare "sensitivity: []"
430
+ if prop_name =~ /\bsecret\b/
431
+ T::Configuration.hard_assert_handler(
432
+ "#{@class}##{prop_name} has the word 'secret' in its name, but no " \
433
+ "'sensitivity:' annotation. This is probably wrong, because if a " \
434
+ "prop is named 'secret' then it is probably sensitive. Add a " \
435
+ "sensitivity annotation like 'sensitivity: Opus::Sensitivity::NonPII." \
436
+ "security_token', or explicitly override this check with " \
437
+ "'sensitivity: []'."
438
+ )
439
+ end
440
+ end
441
+ end
442
+
443
+ # Create "#{prop_name}_redacted" method
444
+ sig do
445
+ params(
446
+ prop_name: Symbol,
447
+ redaction: T.untyped,
448
+ )
449
+ .void
450
+ end
451
+ private def handle_redaction_option(prop_name, redaction)
452
+ redacted_method = "#{prop_name}_redacted"
453
+
454
+ @class.send(:define_method, redacted_method) do
455
+ value = self.public_send(prop_name)
456
+ Chalk::Tools::RedactionUtils.redact_with_directive(
457
+ value, redaction)
458
+ end
459
+ end
460
+
461
+ sig do
462
+ params(
463
+ option_sym: Symbol,
464
+ foreign: T.untyped,
465
+ valid_type_msg: String,
466
+ )
467
+ .void
468
+ end
469
+ private def validate_foreign_option(option_sym, foreign, valid_type_msg:)
470
+ if foreign.is_a?(Symbol) || foreign.is_a?(String)
471
+ raise ArgumentError.new(
472
+ "Using a symbol/string for `#{option_sym}` is no longer supported. Instead, use a Proc " \
473
+ "that returns the class, e.g., foreign: -> {Foo}"
474
+ )
475
+ end
476
+
477
+ if !foreign.is_a?(Proc) && !foreign.is_a?(Array) && !foreign.respond_to?(:load)
478
+ raise ArgumentError.new("The `#{option_sym}` option must be #{valid_type_msg}")
479
+ end
480
+ end
481
+
482
+ sig do
483
+ params(
484
+ prop_name: Symbol,
485
+ prop_cls: Module,
486
+ foreign_hint_only: T.untyped,
487
+ )
488
+ .void
489
+ end
490
+ private def handle_foreign_hint_only_option(prop_name, prop_cls, foreign_hint_only)
491
+ if ![String, Array].include?(prop_cls) && !(prop_cls.is_a?(T::Props::CustomType))
492
+ raise ArgumentError.new(
493
+ "`foreign_hint_only` can only be used with String or Array prop types"
494
+ )
495
+ end
496
+
497
+ validate_foreign_option(
498
+ :foreign_hint_only, foreign_hint_only,
499
+ valid_type_msg: "an individual or array of a model class, or a Proc returning such."
500
+ )
501
+
502
+ unless foreign_hint_only.is_a?(Proc)
503
+ T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign_hint_only}, notify: 'jerry')
504
+ Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign_hint_only`. In other words:
505
+
506
+ instead of `prop :foo, String, foreign_hint_only: FooModel`
507
+ use `prop :foo, String, foreign_hint_only: -> {FooModel}`
508
+
509
+ OR
510
+
511
+ instead of `prop :foo, String, foreign_hint_only: [FooModel, BarModel]`
512
+ use `prop :foo, String, foreign_hint_only: -> {[FooModel, BarModel]}`
513
+
514
+ MESSAGE
515
+ end
516
+ end
517
+
518
+ # checked(:never) - Rules hash is expensive to check
519
+ sig do
520
+ params(
521
+ prop_name: T.any(String, Symbol),
522
+ rules: Rules,
523
+ foreign: T.untyped,
524
+ )
525
+ .void
526
+ .checked(:never)
527
+ end
528
+ private def define_foreign_method(prop_name, rules, foreign)
529
+ fk_method = "#{prop_name}_"
530
+
531
+ # n.b. there's no clear reason *not* to allow additional options
532
+ # here, but we're baking in `allow_direct_mutation` since we
533
+ # *haven't* allowed additional options in the past and want to
534
+ # default to keeping this interface narrow.
535
+ @class.send(:define_method, fk_method) do |allow_direct_mutation: nil|
536
+ foreign = T.let(foreign, T.untyped)
537
+ if foreign.is_a?(Proc)
538
+ resolved_foreign = foreign.call
539
+ if !resolved_foreign.respond_to?(:load)
540
+ raise ArgumentError.new(
541
+ "The `foreign` proc for `#{prop_name}` must return a model class. " \
542
+ "Got `#{resolved_foreign.inspect}` instead."
543
+ )
544
+ end
545
+ # `foreign` is part of the closure state, so this will persist to future invocations
546
+ # of the method, optimizing it so this only runs on the first invocation.
547
+ foreign = resolved_foreign
548
+ end
549
+ if allow_direct_mutation.nil?
550
+ opts = {}
551
+ else
552
+ opts = {allow_direct_mutation: allow_direct_mutation}
553
+ end
554
+
555
+ T.unsafe(self.class).decorator.foreign_prop_get(self, prop_name, foreign, rules, opts)
556
+ end
557
+
558
+ force_fk_method = "#{fk_method}!"
559
+ @class.send(:define_method, force_fk_method) do |allow_direct_mutation: nil|
560
+ loaded_foreign = send(fk_method, allow_direct_mutation: allow_direct_mutation)
561
+ if !loaded_foreign
562
+ T::Configuration.hard_assert_handler(
563
+ 'Failed to load foreign model',
564
+ storytime: {method: force_fk_method, class: self.class}
565
+ )
566
+ end
567
+ loaded_foreign
568
+ end
569
+ end
570
+
571
+ # checked(:never) - Rules hash is expensive to check
572
+ sig do
573
+ params(
574
+ prop_name: Symbol,
575
+ prop_cls: Module,
576
+ rules: Rules,
577
+ foreign: T.untyped,
578
+ )
579
+ .void
580
+ .checked(:never)
581
+ end
582
+ private def handle_foreign_option(prop_name, prop_cls, rules, foreign)
583
+ validate_foreign_option(
584
+ :foreign, foreign, valid_type_msg: "a model class or a Proc that returns one"
585
+ )
586
+
587
+ if prop_cls != String
588
+ raise ArgumentError.new("`foreign` can only be used with a prop type of String")
589
+ end
590
+
591
+ if foreign.is_a?(Array)
592
+ # We don't support arrays with `foreign` because it's hard to both preserve ordering and
593
+ # keep them from being lurky performance hits by issuing a bunch of un-batched DB queries.
594
+ # We could potentially address that by porting over something like AmbiguousIDLoader.
595
+ raise ArgumentError.new(
596
+ "Using an array for `foreign` is no longer supported. Instead, use `foreign_hint_only` " \
597
+ "with an array or a Proc that returns an array, e.g., foreign_hint_only: -> {[Foo, Bar]}"
598
+ )
599
+ end
600
+
601
+ unless foreign.is_a?(Proc)
602
+ T::Configuration.soft_assert_handler(<<~MESSAGE, storytime: {prop: prop_name, value: foreign}, notify: 'jerry')
603
+ Please use a Proc that returns a model class instead of the model class itself as the argument to `foreign`. In other words:
604
+
605
+ instead of `prop :foo, String, foreign: FooModel`
606
+ use `prop :foo, String, foreign: -> {FooModel}`
607
+
608
+ MESSAGE
609
+ end
610
+
611
+ define_foreign_method(prop_name, rules, foreign)
612
+ end
613
+
614
+ # TODO: rename this to props_inherited
615
+ #
616
+ # This gets called when a module or class that extends T::Props gets included, extended,
617
+ # prepended, or inherited.
618
+ sig {params(child: Module).void.checked(:never)}
619
+ def model_inherited(child)
620
+ child.extend(T::Props::ClassMethods)
621
+ child = T.cast(child, T.all(Module, T::Props::ClassMethods))
622
+
623
+ child.plugins.concat(decorated_class.plugins)
624
+ decorated_class.plugins.each do |mod|
625
+ # NB: apply_class_methods must not be an instance method on the decorator itself,
626
+ # otherwise we'd have to call child.decorator here, which would create the decorator
627
+ # before any `decorator_class` override has a chance to take effect (see the comment below).
628
+ T::Props::Plugin::Private.apply_class_methods(mod, child)
629
+ end
630
+
631
+ props.each do |name, rules|
632
+ copied_rules = rules.dup
633
+ # NB: Calling `child.decorator` here is a timb bomb that's going to give someone a really bad
634
+ # time. Any class that defines props and also overrides the `decorator_class` method is going
635
+ # to reach this line before its override take effect, turning it into a no-op.
636
+ child.decorator.add_prop_definition(name, copied_rules)
637
+
638
+ # It's a bit tricky to support `prop_get` hooks added by plugins without
639
+ # sacrificing the `attr_reader` fast path or clobbering customized getters
640
+ # defined manually on a child.
641
+ #
642
+ # To make this work, we _do_ clobber getters defined on the child, but only if:
643
+ # (a) it's needed in order to support a `prop_get` hook, and
644
+ # (b) it's safe because the getter was defined by this file.
645
+ #
646
+ unless rules[:without_accessors]
647
+ if child.decorator.method(:prop_get).owner != method(:prop_get).owner &&
648
+ child.instance_method(name).source_location&.first == __FILE__
649
+ child.send(:define_method, name) do
650
+ T.unsafe(self.class).decorator.prop_get(self, name, rules)
651
+ end
652
+ end
653
+
654
+ unless rules[:immutable]
655
+ if child.decorator.method(:prop_set).owner != method(:prop_set).owner &&
656
+ child.instance_method("#{name}=").source_location&.first == __FILE__
657
+ child.send(:define_method, "#{name}=") do |val|
658
+ T.unsafe(self.class).decorator.prop_set(self, name, val, rules)
659
+ end
660
+ end
661
+ end
662
+ end
663
+ end
664
+ end
665
+
666
+ sig {params(mod: Module).void.checked(:never)}
667
+ def plugin(mod)
668
+ decorated_class.plugins << mod
669
+ T::Props::Plugin::Private.apply_class_methods(mod, decorated_class)
670
+ T::Props::Plugin::Private.apply_decorator_methods(mod, self)
671
+ end
672
+ end