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.
- checksums.yaml +7 -0
- data/lib/sorbet-runtime.rb +116 -0
- data/lib/types/_types.rb +285 -0
- data/lib/types/abstract_utils.rb +50 -0
- data/lib/types/boolean.rb +8 -0
- data/lib/types/compatibility_patches.rb +95 -0
- data/lib/types/configuration.rb +428 -0
- data/lib/types/enum.rb +349 -0
- data/lib/types/generic.rb +23 -0
- data/lib/types/helpers.rb +39 -0
- data/lib/types/interface_wrapper.rb +158 -0
- data/lib/types/non_forcing_constants.rb +51 -0
- data/lib/types/private/abstract/data.rb +36 -0
- data/lib/types/private/abstract/declare.rb +48 -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 +111 -0
- data/lib/types/private/decl_state.rb +30 -0
- data/lib/types/private/final.rb +51 -0
- data/lib/types/private/methods/_methods.rb +460 -0
- data/lib/types/private/methods/call_validation.rb +1149 -0
- data/lib/types/private/methods/decl_builder.rb +228 -0
- data/lib/types/private/methods/modes.rb +16 -0
- data/lib/types/private/methods/signature.rb +196 -0
- data/lib/types/private/methods/signature_validation.rb +229 -0
- data/lib/types/private/mixins/mixins.rb +27 -0
- data/lib/types/private/retry.rb +10 -0
- data/lib/types/private/runtime_levels.rb +56 -0
- data/lib/types/private/sealed.rb +65 -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/type_alias.rb +26 -0
- data/lib/types/private/types/void.rb +34 -0
- data/lib/types/profile.rb +31 -0
- data/lib/types/props/_props.rb +161 -0
- data/lib/types/props/constructor.rb +40 -0
- data/lib/types/props/custom_type.rb +108 -0
- data/lib/types/props/decorator.rb +672 -0
- data/lib/types/props/errors.rb +8 -0
- data/lib/types/props/generated_code_validation.rb +268 -0
- data/lib/types/props/has_lazily_specialized_methods.rb +92 -0
- data/lib/types/props/optional.rb +81 -0
- data/lib/types/props/plugin.rb +37 -0
- data/lib/types/props/pretty_printable.rb +107 -0
- data/lib/types/props/private/apply_default.rb +170 -0
- data/lib/types/props/private/deserializer_generator.rb +165 -0
- data/lib/types/props/private/parser.rb +32 -0
- data/lib/types/props/private/serde_transform.rb +192 -0
- data/lib/types/props/private/serializer_generator.rb +77 -0
- data/lib/types/props/private/setter_factory.rb +134 -0
- data/lib/types/props/serializable.rb +330 -0
- data/lib/types/props/type_validation.rb +111 -0
- data/lib/types/props/utils.rb +59 -0
- data/lib/types/props/weak_constructor.rb +67 -0
- data/lib/types/runtime_profiled.rb +24 -0
- data/lib/types/sig.rb +30 -0
- data/lib/types/struct.rb +18 -0
- data/lib/types/types/attached_class.rb +37 -0
- data/lib/types/types/base.rb +151 -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 +37 -0
- data/lib/types/types/noreturn.rb +29 -0
- data/lib/types/types/proc.rb +51 -0
- data/lib/types/types/self_type.rb +35 -0
- data/lib/types/types/simple.rb +33 -0
- data/lib/types/types/t_enum.rb +38 -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 +34 -0
- data/lib/types/types/typed_enumerable.rb +161 -0
- data/lib/types/types/typed_enumerator.rb +36 -0
- data/lib/types/types/typed_hash.rb +43 -0
- data/lib/types/types/typed_range.rb +26 -0
- data/lib/types/types/typed_set.rb +36 -0
- data/lib/types/types/union.rb +56 -0
- data/lib/types/types/untyped.rb +29 -0
- data/lib/types/utils.rb +217 -0
- 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
|