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.
- checksums.yaml +5 -5
- data/lib/sorbet-runtime.rb +100 -0
- data/lib/types/_types.rb +245 -0
- data/lib/types/abstract_utils.rb +50 -0
- data/lib/types/boolean.rb +8 -0
- data/lib/types/compatibility_patches.rb +37 -0
- data/lib/types/configuration.rb +368 -0
- data/lib/types/generic.rb +23 -0
- data/lib/types/helpers.rb +31 -0
- data/lib/types/interface_wrapper.rb +158 -0
- data/lib/types/private/abstract/data.rb +36 -0
- data/lib/types/private/abstract/declare.rb +39 -0
- data/lib/types/private/abstract/hooks.rb +43 -0
- data/lib/types/private/abstract/validate.rb +128 -0
- data/lib/types/private/casts.rb +22 -0
- data/lib/types/private/class_utils.rb +102 -0
- data/lib/types/private/decl_state.rb +18 -0
- data/lib/types/private/error_handler.rb +37 -0
- data/lib/types/private/methods/_methods.rb +344 -0
- data/lib/types/private/methods/call_validation.rb +1177 -0
- data/lib/types/private/methods/decl_builder.rb +275 -0
- data/lib/types/private/methods/modes.rb +18 -0
- data/lib/types/private/methods/signature.rb +196 -0
- data/lib/types/private/methods/signature_validation.rb +232 -0
- data/lib/types/private/mixins/mixins.rb +27 -0
- data/lib/types/private/runtime_levels.rb +41 -0
- data/lib/types/private/types/not_typed.rb +23 -0
- data/lib/types/private/types/string_holder.rb +26 -0
- data/lib/types/private/types/void.rb +33 -0
- data/lib/types/profile.rb +27 -0
- data/lib/types/props/_props.rb +165 -0
- data/lib/types/props/constructor.rb +20 -0
- data/lib/types/props/custom_type.rb +84 -0
- data/lib/types/props/decorator.rb +826 -0
- data/lib/types/props/errors.rb +8 -0
- data/lib/types/props/optional.rb +73 -0
- data/lib/types/props/plugin.rb +15 -0
- data/lib/types/props/pretty_printable.rb +106 -0
- data/lib/types/props/serializable.rb +376 -0
- data/lib/types/props/type_validation.rb +98 -0
- data/lib/types/props/utils.rb +49 -0
- data/lib/types/props/weak_constructor.rb +30 -0
- data/lib/types/runtime_profiled.rb +36 -0
- data/lib/types/sig.rb +28 -0
- data/lib/types/struct.rb +8 -0
- data/lib/types/types/base.rb +141 -0
- data/lib/types/types/class_of.rb +38 -0
- data/lib/types/types/enum.rb +42 -0
- data/lib/types/types/fixed_array.rb +60 -0
- data/lib/types/types/fixed_hash.rb +59 -0
- data/lib/types/types/intersection.rb +36 -0
- data/lib/types/types/noreturn.rb +25 -0
- data/lib/types/types/proc.rb +51 -0
- data/lib/types/types/self_type.rb +31 -0
- data/lib/types/types/simple.rb +33 -0
- data/lib/types/types/type_member.rb +7 -0
- data/lib/types/types/type_parameter.rb +23 -0
- data/lib/types/types/type_template.rb +7 -0
- data/lib/types/types/type_variable.rb +31 -0
- data/lib/types/types/typed_array.rb +20 -0
- data/lib/types/types/typed_enumerable.rb +141 -0
- data/lib/types/types/typed_enumerator.rb +22 -0
- data/lib/types/types/typed_hash.rb +29 -0
- data/lib/types/types/typed_range.rb +22 -0
- data/lib/types/types/typed_set.rb +22 -0
- data/lib/types/types/union.rb +59 -0
- data/lib/types/types/untyped.rb +25 -0
- data/lib/types/utils.rb +223 -0
- 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
|