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, 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], checked: false)
SAFE_NAME =
T.let(/\A[A-Za-z_][A-Za-z0-9_-]*\z/.freeze, Regexp, checked: false)

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Sig

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], checked: false)
end

Instance Attribute Details

#propsObject (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} on class #{@class} 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} on class #{@class} that doesn't already exist")
  end

  @props = @props.merge(prop => rules.freeze).freeze
end

#all_propsObject



37
38
39
# File 'lib/types/props/decorator.rb', line 37

def all_props
  props.keys
end

#decorated_classObject



92
93
94
# File 'lib/types/props/decorator.rb', line 92

def decorated_class
  @class
end

#foreign_prop_get(instance, prop, foreign_class, rules = prop_rules(prop), opts = {}) ⇒ Object



202
203
204
205
# File 'lib/types/props/decorator.rb', line 202

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



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
642
643
644
645
646
647
648
649
650
651
652
653
654
# File 'lib/types/props/decorator.rb', line 613

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



669
670
671
672
673
# File 'lib/types/props/decorator.rb', line 669

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



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
382
383
384
385
386
387
388
389
390
391
# File 'lib/types/props/decorator.rb', line 318

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.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 include a pii prop in a class that declares `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

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



165
166
167
168
169
170
171
172
173
174
# File 'lib/types/props/decorator.rb', line 165

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



185
186
187
# File 'lib/types/props/decorator.rb', line 185

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



145
146
147
# File 'lib/types/props/decorator.rb', line 145

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



129
130
131
# File 'lib/types/props/decorator.rb', line 129

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



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/types/props/decorator.rb', line 221

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

Returns:



86
87
88
# File 'lib/types/props/decorator.rb', line 86

def valid_rule_key?(key)
  !!VALID_RULE_KEYS[key]
end

#validate_prop_value(prop, val) ⇒ Object



102
103
104
105
106
107
# File 'lib/types/props/decorator.rb', line 102

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