Class: T::Props::Decorator

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Methods included from Sig

sig

Methods included from T::Private::Methods::SingletonMethodHooks

#singleton_method_added

Methods included from T::Private::Methods::MethodHooks

#method_added

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

#propsObject (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_propsObject



55
56
57
# File 'lib/types/props/decorator.rb', line 55

def all_props
  props.keys
end

#decorated_classObject



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.get_underlying_type(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.get_underlying_type(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

Returns:



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