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, T::Module[T.anything]) }
- OverrideRules =
T.type_alias { T::Hash[Symbol, {allow_incompatible: T::Boolean}] }
- OVERRIDE_TRUE =
T.let({ reader: {allow_incompatible: false}.freeze, writer: {allow_incompatible: false}.freeze, }.freeze, OverrideRules)
- OVERRIDE_READER =
T.let({ reader: {allow_incompatible: false}.freeze, }.freeze, OverrideRules)
- OVERRIDE_WRITER =
T.let({ writer: {allow_incompatible: false}.freeze, }.freeze, OverrideRules)
- OVERRIDE_EMPTY =
T.let({}.freeze, OverrideRules)
- 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], checked: false)
- SAFE_NAME =
T.let(/\A[A-Za-z_][A-Za-z0-9_-]*\z/, Regexp, checked: false)
- SAFE_ACCESSOR_KEY_NAME =
Should be exactly the same as ‘SAFE_NAME`, but with a leading `@`.
T.let(/\A@[A-Za-z_][A-Za-z0-9_-]*\z/, Regexp, checked: false)
Instance Attribute Summary collapse
-
#props ⇒ Object
readonly
Returns the value of attribute props.
Instance Method Summary collapse
- #add_prop_definition(name, 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
Methods included from T::Private::Methods::SingletonMethodHooks
Methods included from T::Private::Methods::MethodHooks
Constructor Details
#initialize(klass) ⇒ Decorator
Returns a new instance of Decorator.
42 43 44 45 46 47 48 |
# File 'lib/types/props/decorator.rb', line 42 def initialize(klass) @class = T.let(klass, DecoratedClassType) @class.plugins.each do |mod| T::Props::Plugin::Private.apply_decorator_methods(mod, self) end @props = T.let(EMPTY_PROPS, T::Hash[Symbol, Rules], checked: false) end |
Instance Attribute Details
#props ⇒ Object (readonly)
Returns the value of attribute props.
52 53 54 |
# File 'lib/types/props/decorator.rb', line 52 def props @props end |
Instance Method Details
#add_prop_definition(name, rules) ⇒ Object
67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/types/props/decorator.rb', line 67 def add_prop_definition(name, rules) override = rules.delete(:override) if props.include?(name) && !override raise ArgumentError.new("Attempted to redefine prop #{name.inspect} on class #{@class} that's already defined without specifying :override => true: #{prop_rules(name)}") end # dup/store/freeze rather than @props.merge(name => rules.freeze).freeze: # merge would allocate a temporary single-entry Hash and re-insert every # existing entry. The published hash stays frozen between adds. new_props = @props.dup new_props[name] = rules.freeze @props = new_props.freeze end |
#all_props ⇒ Object
55 56 57 |
# File 'lib/types/props/decorator.rb', line 55 def all_props props.keys end |
#decorated_class ⇒ Object
113 114 115 |
# File 'lib/types/props/decorator.rb', line 113 def decorated_class @class end |
#foreign_prop_get(instance, prop, foreign_class, rules = prop_rules(prop), opts = {}) ⇒ Object
226 227 228 229 |
# File 'lib/types/props/decorator.rb', line 226 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
651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 |
# File 'lib/types/props/decorator.rb', line 651 def model_inherited(child) child.extend(T::Props::ClassMethods) child = T.cast(child, DecoratedClassType) 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 parent_props = props # Return before child.decorator below: forcing decorator creation for a # prop-less parent would defeat any decorator_class override (see the NB). return if parent_props.empty? # NB: Calling `child.decorator` here is a time 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 = child.decorator # Computed once for all props: the owner comparison cannot change inside # the loop below (it only defines prop accessors on child), and each # Object#method call allocates a fresh Method. clobber_getters = child_decorator.method(:prop_get).owner != method(:prop_get).owner clobber_setters = child_decorator.method(:prop_set).owner != method(:prop_set).owner parent_props.each do |name, rules| copied_rules = rules.dup 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_getters && child.instance_method(name).source_location&.first == __FILE__ child.send(:define_method, name) do T.unsafe(self.class).decorator.prop_get(self, name, rules) end end if !rules[:immutable] && clobber_setters && child.instance_method("#{name}=").source_location&.first == __FILE__ 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
754 755 756 757 758 |
# File 'lib/types/props/decorator.rb', line 754 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
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 |
# File 'lib/types/props/decorator.rb', line 370 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) # Retrieve the possible underlying object with T.nilable. type = T::Utils::Nilable.(type) rules_sensitivity = rules[:sensitivity] sensitivity_and_pii = {sensitivity: rules_sensitivity} if !rules_sensitivity.nil? 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 define pii prop `#{@class}##{name}` because `#{@class}` is `contains_no_pii`" ) end end end rules[:type] = type rules[:type_object] = type_object rules[:accessor_key] = "@#{name}".to_sym rules[:sensitivity] = sensitivity_and_pii[:sensitivity] rules[:pii] = sensitivity_and_pii[:pii] rules[:extra] = rules[:extra]&.freeze # extra arbitrary metadata attached by the code defining this property # 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 setter_proc, value_validate_proc, bound_setter_proc = T::Props::Private::SetterFactory.build_setter_proc(@class, name, rules) setter_proc.freeze value_validate_proc.freeze bound_setter_proc.freeze rules[:value_validate_proc] = value_validate_proc # :setter_proc is a pre-existing rules key and is preserved as-is: the # 1-arity, self-bound proc used only to define the generated `name=` # instance setter via `define_method(&proc)` (its fast path). The net-new # :_bound_setter_proc is the 2-arity (instance, val) equivalent that every # other write goes through without instance_exec's per-call self-rebinding; # it is underscore-prefixed (like :_tnilable) so external code that replays # rules hashes back into prop() filters it out as internal. rules[:setter_proc] = setter_proc rules[:_bound_setter_proc] = bound_setter_proc validate_overrides(name, rules) 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
185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/types/props/decorator.rb', line 185 def prop_get(instance, prop, rules=prop_rules(prop)) # `instance_variable_get` will return nil if the variable doesn't exist # which is what we want to have happen for the logic below. val = instance.instance_variable_get(rules[:accessor_key]) if !val.nil? val elsif (d = rules[:ifunset]) T::Props::Utils.deep_clone(d) else nil end end |
#prop_get_if_set(instance, prop, rules = prop_rules(prop)) ⇒ Object Also known as: get
207 208 209 210 211 |
# File 'lib/types/props/decorator.rb', line 207 def prop_get_if_set(instance, prop, rules=prop_rules(prop)) # `instance_variable_get` will return nil if the variable doesn't exist # which is what we want to have happen for the return value here. instance.instance_variable_get(rules[:accessor_key]) end |
#prop_get_logic(instance, prop, value) ⇒ Object
165 166 167 |
# File 'lib/types/props/decorator.rb', line 165 def prop_get_logic(instance, prop, value) value end |
#prop_rules(prop) ⇒ Object
61 62 63 |
# File 'lib/types/props/decorator.rb', line 61 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
147 148 149 150 151 |
# File 'lib/types/props/decorator.rb', line 147 def prop_set(instance, prop, val, rules=prop_rules(prop)) # The 2-arity bound proc does the same validation/assignment as the # 1-arity setter proc without instance_exec's per-call self-rebinding. rules.fetch(:_bound_setter_proc).call(instance, val) end |
#prop_validate_definition!(name, cls, rules, type) ⇒ Object
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
# File 'lib/types/props/decorator.rb', line 245 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) } invalid_keys = rules.keys.reject { |k| valid_rule_key?(k) } suffix = invalid_keys.size == 1 ? "" : "s" raise ArgumentError.new("Invalid prop arg#{suffix} supplied in #{self}: #{invalid_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
107 108 109 |
# File 'lib/types/props/decorator.rb', line 107 def valid_rule_key?(key) !!VALID_RULE_KEYS[key] end |
#validate_prop_value(prop, val) ⇒ Object
123 124 125 |
# File 'lib/types/props/decorator.rb', line 123 def validate_prop_value(prop, val) prop_rules(prop).fetch(:value_validate_proc).call(val) end |