Class: T::Props::Decorator
- Inherits:
-
Object
- Object
- T::Props::Decorator
- Extended by:
- Sig
- Defined in:
- lib/types/props/decorator.rb
Overview
NB: This is not actually a decorator. It's just named that way for consistency with DocumentDecorator and ModelDecorator (which both seem to have been written with an incorrect understanding of the decorator pattern). These “decorators” should really just be static methods on private modules (we'd also want/need to replace decorator overrides in plugins with class methods that expose the necessary functionality).
Defined Under Namespace
Classes: NoRulesError
Constant Summary collapse
- Rules =
T.type_alias {T::Hash[Symbol, T.untyped]}
- DecoratedInstance =
Would be T::Props, but that produces circular reference errors in some circumstances
T.type_alias {Object}
- PropType =
T.type_alias {T::Types::Base}
- PropTypeOrClass =
T.type_alias {T.any(PropType, Module)}
- BANNED_METHOD_NAMES =
TODO: we should really be checking all the methods on `cls`, not just Object
T.let(Object.instance_methods.each_with_object({}) {|x, acc| acc[x] = true}.freeze, T::Hash[Symbol, TrueClass])
- SAFE_NAME =
T.let(/\A[A-Za-z_][A-Za-z0-9_-]*\z/.freeze, Regexp)
Instance Attribute Summary collapse
-
#props ⇒ Object
readonly
Returns the value of attribute props.
Instance Method Summary collapse
- #add_prop_definition(prop, rules) ⇒ Object
- #all_props ⇒ Object
- #decorated_class ⇒ Object
- #foreign_prop_get(instance, prop, foreign_class, rules = prop_rules(prop), opts = {}) ⇒ Object
-
#initialize(klass) ⇒ Decorator
constructor
A new instance of Decorator.
- #model_inherited(child) ⇒ Object
- #plugin(mod) ⇒ Object
- #prop_defined(name, cls, rules = {}) ⇒ Object
- #prop_get(instance, prop, rules = prop_rules(prop)) ⇒ Object
- #prop_get_if_set(instance, prop, rules = prop_rules(prop)) ⇒ Object (also: #get)
- #prop_get_logic(instance, prop, value) ⇒ Object
- #prop_rules(prop) ⇒ Object
- #prop_set(instance, prop, val, rules = prop_rules(prop)) ⇒ Object (also: #set)
- #prop_validate_definition!(name, cls, rules, type) ⇒ Object
- #valid_rule_key?(key) ⇒ Boolean
- #validate_prop_value(prop, val) ⇒ Object
Methods included from Sig
Constructor Details
#initialize(klass) ⇒ Decorator
Returns a new instance of Decorator.
24 25 26 27 28 29 30 |
# File 'lib/types/props/decorator.rb', line 24 def initialize(klass) @class = T.let(klass, T.all(Module, T::Props::ClassMethods)) @class.plugins.each do |mod| T::Props::Plugin::Private.apply_decorator_methods(mod, self) end @props = T.let(EMPTY_PROPS, T::Hash[Symbol, Rules]) end |
Instance Attribute Details
#props ⇒ Object (readonly)
Returns the value of attribute props.
34 35 36 |
# File 'lib/types/props/decorator.rb', line 34 def props @props end |
Instance Method Details
#add_prop_definition(prop, rules) ⇒ Object
49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/types/props/decorator.rb', line 49 def add_prop_definition(prop, rules) override = rules.delete(:override) if props.include?(prop) && !override raise ArgumentError.new("Attempted to redefine prop #{prop.inspect} that's already defined without specifying :override => true: #{prop_rules(prop)}") elsif !props.include?(prop) && override raise ArgumentError.new("Attempted to override a prop #{prop.inspect} that doesn't already exist") end @props = @props.merge(prop => rules.freeze).freeze end |
#all_props ⇒ Object
37 38 39 |
# File 'lib/types/props/decorator.rb', line 37 def all_props props.keys end |
#decorated_class ⇒ Object
84 85 86 |
# File 'lib/types/props/decorator.rb', line 84 def decorated_class @class end |
#foreign_prop_get(instance, prop, foreign_class, rules = prop_rules(prop), opts = {}) ⇒ Object
194 195 196 197 |
# File 'lib/types/props/decorator.rb', line 194 def foreign_prop_get(instance, prop, foreign_class, rules=prop_rules(prop), opts={}) return if !(value = prop_get(instance, prop, rules)) T.unsafe(foreign_class).load(value, {}, opts) end |
#model_inherited(child) ⇒ Object
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 |
# File 'lib/types/props/decorator.rb', line 600 def model_inherited(child) child.extend(T::Props::ClassMethods) child = T.cast(child, T.all(Module, T::Props::ClassMethods)) child.plugins.concat(decorated_class.plugins) decorated_class.plugins.each do |mod| # NB: apply_class_methods must not be an instance method on the decorator itself, # otherwise we'd have to call child.decorator here, which would create the decorator # before any `decorator_class` override has a chance to take effect (see the comment below). T::Props::Plugin::Private.apply_class_methods(mod, child) end props.each do |name, rules| copied_rules = rules.dup # NB: Calling `child.decorator` here is a timb bomb that's going to give someone a really bad # time. Any class that defines props and also overrides the `decorator_class` method is going # to reach this line before its override take effect, turning it into a no-op. child.decorator.add_prop_definition(name, copied_rules) # It's a bit tricky to support `prop_get` hooks added by plugins without # sacrificing the `attr_reader` fast path or clobbering customized getters # defined manually on a child. # # To make this work, we _do_ clobber getters defined on the child, but only if: # (a) it's needed in order to support a `prop_get` hook, and # (b) it's safe because the getter was defined by this file. # unless rules[:without_accessors] if clobber_getter?(child, name) child.send(:define_method, name) do T.unsafe(self.class).decorator.prop_get(self, name, rules) end end if !rules[:immutable] && clobber_setter?(child, name) child.send(:define_method, "#{name}=") do |val| T.unsafe(self.class).decorator.prop_set(self, name, val, rules) end end end end end |
#plugin(mod) ⇒ Object
656 657 658 659 660 |
# File 'lib/types/props/decorator.rb', line 656 def plugin(mod) decorated_class.plugins << mod T::Props::Plugin::Private.apply_class_methods(mod, decorated_class) T::Props::Plugin::Private.apply_decorator_methods(mod, self) end |
#prop_defined(name, cls, rules = {}) ⇒ Object
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 |
# File 'lib/types/props/decorator.rb', line 308 def prop_defined(name, cls, rules={}) cls = T::Utils.resolve_alias(cls) if prop_nilable?(cls, rules) # :_tnilable is introduced internally for performance purpose so that clients do not need to call # T::Utils::Nilable.is_tnilable(cls) again. # It is strictly internal: clients should always use T::Props::Utils.required_prop?() or # T::Props::Utils.optional_prop?() for checking whether a field is required or optional. rules[:_tnilable] = true end name = name.to_sym type = cls if !cls.is_a?(Module) cls = convert_type_to_class(cls) end type_object = smart_coerce(type, enum: rules[:enum]) prop_validate_definition!(name, cls, rules, type_object) # Retrive the possible underlying object with T.nilable. type = T::Utils::Nilable.(type) sensitivity_and_pii = {sensitivity: rules[:sensitivity]} normalize = T::Configuration.normalize_sensitivity_and_pii_handler if normalize sensitivity_and_pii = normalize.call(sensitivity_and_pii) # We check for Class so this is only applied on concrete # documents/models; We allow mixins containing props to not # specify their PII nature, as long as every class into which they # are ultimately included does. # if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !T.unsafe(@class).contains_pii? raise ArgumentError.new( 'Cannot include a pii prop in a class that declares `contains_no_pii`' ) end end rules = rules.merge( # TODO: The type of this element is confusing. We should refactor so that # it can be always `type_object` (a PropType) or always `cls` (a Module) type: type, type_object: type_object, accessor_key: "@#{name}".to_sym, sensitivity: sensitivity_and_pii[:sensitivity], pii: sensitivity_and_pii[:pii], # extra arbitrary metadata attached by the code defining this property extra: rules[:extra]&.freeze, ) validate_not_missing_sensitivity(name, rules) # for backcompat (the `:array` key is deprecated but because the name is # so generic it's really hard to be sure it's not being relied on anymore) if type.is_a?(T::Types::TypedArray) inner = T::Utils::Nilable.(type.type) if inner.is_a?(Module) rules[:array] = inner end end rules[:setter_proc] = T::Props::Private::SetterFactory.build_setter_proc(@class, name, rules).freeze add_prop_definition(name, rules) # NB: using `without_accessors` doesn't make much sense unless you also define some other way to # get at the property (e.g., Chalk::ODM::Document exposes `get` and `set`). define_getter_and_setter(name, rules) unless rules[:without_accessors] handle_foreign_option(name, cls, rules, rules[:foreign]) if rules[:foreign] handle_redaction_option(name, rules[:redaction]) if rules[:redaction] end |
#prop_get(instance, prop, rules = prop_rules(prop)) ⇒ Object
157 158 159 160 161 162 163 164 165 166 |
# File 'lib/types/props/decorator.rb', line 157 def prop_get(instance, prop, rules=prop_rules(prop)) val = instance.instance_variable_get(rules[:accessor_key]) if instance.instance_variable_defined?(rules[:accessor_key]) if !val.nil? val elsif (d = rules[:ifunset]) T::Props::Utils.deep_clone_object(d) else nil end end |
#prop_get_if_set(instance, prop, rules = prop_rules(prop)) ⇒ Object Also known as: get
177 178 179 |
# File 'lib/types/props/decorator.rb', line 177 def prop_get_if_set(instance, prop, rules=prop_rules(prop)) instance.instance_variable_get(rules[:accessor_key]) if instance.instance_variable_defined?(rules[:accessor_key]) end |
#prop_get_logic(instance, prop, value) ⇒ Object
137 138 139 |
# File 'lib/types/props/decorator.rb', line 137 def prop_get_logic(instance, prop, value) value end |
#prop_rules(prop) ⇒ Object
43 44 45 |
# File 'lib/types/props/decorator.rb', line 43 def prop_rules(prop) props[prop.to_sym] || raise("No such prop: #{prop.inspect}") end |
#prop_set(instance, prop, val, rules = prop_rules(prop)) ⇒ Object Also known as: set
121 122 123 |
# File 'lib/types/props/decorator.rb', line 121 def prop_set(instance, prop, val, rules=prop_rules(prop)) instance.instance_exec(val, &rules.fetch(:setter_proc)) end |
#prop_validate_definition!(name, cls, rules, type) ⇒ Object
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 |
# File 'lib/types/props/decorator.rb', line 213 def prop_validate_definition!(name, cls, rules, type) validate_prop_name(name) if rules.key?(:pii) raise ArgumentError.new("The 'pii:' option for props has been renamed " \ "to 'sensitivity:' (in prop #{@class.name}.#{name})") end if rules.keys.any? {|k| !valid_rule_key?(k)} raise ArgumentError.new("At least one invalid prop arg supplied in #{self}: #{rules.keys.inspect}") end if !rules[:clobber_existing_method!] && !rules[:without_accessors] && BANNED_METHOD_NAMES.include?(name.to_sym) raise ArgumentError.new( "#{name} can't be used as a prop in #{@class} because a method with " \ "that name already exists (defined by #{@class.instance_method(name).owner} " \ "at #{@class.instance_method(name).source_location || '<unknown>'}). " \ "(If using this name is unavoidable, try `without_accessors: true`.)" ) end extra = rules[:extra] if !extra.nil? && !extra.is_a?(Hash) raise ArgumentError.new("Extra metadata must be a Hash in prop #{@class.name}.#{name}") end nil end |
#valid_rule_key?(key) ⇒ Boolean
78 79 80 |
# File 'lib/types/props/decorator.rb', line 78 def valid_rule_key?(key) !!VALID_RULE_KEYS[key] end |
#validate_prop_value(prop, val) ⇒ Object
94 95 96 97 98 99 |
# File 'lib/types/props/decorator.rb', line 94 def validate_prop_value(prop, val) # We call `setter_proc` here without binding to an instance, so it'll run # `instance_variable_set` if validation passes, but nothing will care. # We only care about the validation. prop_rules(prop).fetch(:setter_proc).call(val) end |