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

Modules: Private Classes: NoRulesError

Constant Summary collapse

Rules =
T.type_alias(T::Hash[Symbol, T.untyped])
DecoratedClass =

T.class_of(T::Props), but that produces circular reference errors in some circumstances

T.type_alias(T.untyped)
DecoratedInstance =

Would be T::Props, but that produces circular reference errors in some circumstances

T.type_alias(T.untyped)
PropType =
T.type_alias(T.any(T::Types::Base, T::Props::CustomType))
PropTypeOrClass =
T.type_alias(T.any(PropType, Module))
TYPES_NOT_NEEDING_CLONE =

From T::Props::Utils.deep_clone_object, plus String

[TrueClass, FalseClass, NilClass, Symbol, String, Numeric]

Instance Method Summary collapse

Methods included from Sig

sig

Constructor Details

#initialize(klass) ⇒ Decorator

Returns a new instance of Decorator.



22
23
24
25
26
27
# File 'lib/types/props/decorator.rb', line 22

def initialize(klass)
  @class = klass
  klass.plugins.each do |mod|
    Private.apply_decorator_methods(mod, self)
  end
end

Instance Method Details

#add_prop_definition(prop, rules) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/types/props/decorator.rb', line 51

def add_prop_definition(prop, rules)
  prop = prop.to_sym
  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_propsObject



45
# File 'lib/types/props/decorator.rb', line 45

def all_props; props.keys; end

#decorated_classObject



84
# File 'lib/types/props/decorator.rb', line 84

def decorated_class; @class; end

#foreign_prop_get(instance, prop, foreign_class, rules = props[prop.to_sym], opts = {}) ⇒ Object



231
232
233
234
# File 'lib/types/props/decorator.rb', line 231

def foreign_prop_get(instance, prop, foreign_class, rules=props[prop.to_sym], opts={})
  return if !(value = prop_get(instance, prop, rules))
  foreign_class.load(value, {}, opts)
end

#get(instance, prop, rules = props[prop.to_sym]) ⇒ Object



99
100
101
102
103
# File 'lib/types/props/decorator.rb', line 99

def get(instance, prop, rules=props[prop.to_sym])
  # For backwards compatibility, fall back to reconstructing the accessor key
  # (though it would probably make more sense to raise in that case).
  instance.instance_variable_get(rules ? rules[:accessor_key] : '@' + prop.to_s) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
end

#model_inherited(child) ⇒ Object



786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
# File 'lib/types/props/decorator.rb', line 786

def model_inherited(child)
  child.extend(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).
    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)
  end
end

#mutate_prop_backdoor!(prop, key, value) ⇒ Object



38
39
40
41
42
# File 'lib/types/props/decorator.rb', line 38

def mutate_prop_backdoor!(prop, key, value)
  @props = props.merge(
    prop => props.fetch(prop).merge(key => value).freeze
  ).freeze
end

#plugin(mod) ⇒ Object



807
808
809
810
811
# File 'lib/types/props/decorator.rb', line 807

def plugin(mod)
  decorated_class.plugins << mod
  Private.apply_class_methods(mod, decorated_class)
  Private.apply_decorator_methods(mod, self)
end

#prop_defined(name, cls, rules = {}) ⇒ Object



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
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
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
# File 'lib/types/props/decorator.rb', line 332

def prop_defined(name, cls, rules={})
  if rules[:optional] == true
    T::Configuration.hard_assert_handler(
      'Use of `optional: true` is deprecated, please use `T.nilable(...)` instead.',
      storytime: {
        name: name,
        cls_or_args: cls.to_s,
        args: rules,
        klass: decorated_class.name,
      },
    )
  elsif rules[:optional] == false
    T::Configuration.hard_assert_handler(
      'Use of `optional: :false` is deprecated as it\'s the default value.',
      storytime: {
        name: name,
        cls_or_args: cls.to_s,
        args: rules,
        klass: decorated_class.name,
      },
    )
  elsif rules[:optional] == :on_load
    T::Configuration.hard_assert_handler(
      'Use of `optional: :on_load` is deprecated. You probably want `T.nilable(...)` with :raise_on_nil_write instead.',
      storytime: {
        name: name,
        cls_or_args: cls.to_s,
        args: rules,
        klass: decorated_class.name,
      },
    )
  elsif rules[:optional] == :existing
    T::Configuration.hard_assert_handler(
      'Use of `optional: :existing` is not allowed: you should use use T.nilable (http://go/optional)',
      storytime: {
        name: name,
        cls_or_args: cls.to_s,
        args: rules,
        klass: decorated_class.name,
      },
    )
  end

  if T::Utils::Nilable.is_union_with_nilclass(cls)
    # :_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 = type
  if !(type_object.singleton_class < T::Props::CustomType)
    type_object = smart_coerce(type_object, array: rules[:array], enum: rules[:enum])
  end

  prop_validate_definition!(name, cls, rules, type_object)

  # Retrive the possible underlying object with T.nilable.
  underlying_type_object = T::Utils::Nilable.get_underlying_type_object(type_object)
  type = T::Utils::Nilable.get_underlying_type(type)

  array_subdoc_type = array_subdoc_type(underlying_type_object)
  hash_value_subdoc_type = hash_value_subdoc_type(underlying_type_object)
  hash_key_custom_type = hash_key_custom_type(underlying_type_object)

  sensitivity_and_pii = {sensitivity: rules[:sensitivity]}
  if defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::Utils)
    sensitivity_and_pii = Opus::Sensitivity::Utils.normalize_sensitivity_and_pii_annotation(sensitivity_and_pii)
  end
  # 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 defined?(Opus) && defined?(Opus::Sensitivity) && defined?(Opus::Sensitivity::PIIable)
    if sensitivity_and_pii[:pii] && @class.is_a?(Class) && !@class.contains_pii?
      raise ArgumentError.new(
        'Cannot include a pii prop in a class that declares `contains_no_pii`'
      )
    end
  end

  needs_clone =
    if cls <= Array || cls <= Hash || cls <= Set
      shallow_clone_ok(underlying_type_object) ? :shallow : true
    else
      false
    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,
    # These are precomputed for performance
    # TODO: A lot of these are only needed by T::Props::Serializable or T::Struct
    # and can/should be moved accordingly.
    type_is_custom_type: cls.singleton_class < T::Props::CustomType,
    type_is_serializable: cls < T::Props::Serializable,
    type_is_array_of_serializable: !array_subdoc_type.nil?,
    type_is_hash_of_serializable_values: !hash_value_subdoc_type.nil?,
    type_is_hash_of_custom_type_keys: !hash_key_custom_type.nil?,
    type_object: type_object,
    type_needs_clone: needs_clone,
    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
  if type.is_a?(T::Types::TypedArray) && type.type.is_a?(T::Types::Simple)
    rules[:array] = type.type.raw_type
  elsif array_subdoc_type
    rules[:array] = array_subdoc_type
  end

  if rules[:type_is_serializable]
    rules[:serializable_subtype] = cls
  elsif array_subdoc_type
    rules[:serializable_subtype] = array_subdoc_type
  elsif hash_value_subdoc_type && hash_key_custom_type
    rules[:serializable_subtype] = {
      keys: hash_key_custom_type,
      values: hash_value_subdoc_type,
    }
  elsif hash_value_subdoc_type
    rules[:serializable_subtype] = hash_value_subdoc_type
  elsif hash_key_custom_type
    rules[:serializable_subtype] = hash_key_custom_type
  end

  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]

  if rules[:foreign] && rules[:foreign_hint_only]
    raise ArgumentError.new(":foreign and :foreign_hint_only are mutually exclusive.")
  end

  handle_foreign_option(name, cls, rules, rules[:foreign]) if rules[:foreign]
  handle_foreign_hint_only_option(cls, rules[:foreign_hint_only]) if rules[:foreign_hint_only]
  handle_redaction_option(name, rules[:redaction]) if rules[:redaction]
end

#prop_get(instance, prop, rules = props[prop.to_sym]) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/types/props/decorator.rb', line 196

def prop_get(instance, prop, rules=props[prop.to_sym])
  val = get(instance, prop, rules)

  # NB: Do NOT change this to check `val.nil?` instead. BSON::ByteBuffer overrides `==` such
  # that `== nil` can return true while `.nil?` returns false. Tests will break in mysterious
  # ways. A special thanks to Ruby for enabling this type of bug.
  #
  # One side effect here is that _if_ a class (like BSON::ByteBuffer) defines ==
  # in such a way that instances which are not `nil`, ie are not NilClass, nevertheless
  # are `== nil`, then we will transparently convert such instances to `nil` on read.
  # Yes, our code relies on this behavior (as of writing). :thisisfine:
  if val != nil # rubocop:disable Style/NonNilCheck
    val
  else
    raise NoRulesError.new if !rules
    d = rules[:ifunset]
    if d
      T::Props::Utils.deep_clone_object(d)
    else
      nil
    end
  end
end

#prop_rules(prop) ⇒ Object



48
# File 'lib/types/props/decorator.rb', line 48

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



180
181
182
183
# File 'lib/types/props/decorator.rb', line 180

def prop_set(instance, prop, val, rules=prop_rules(prop))
  check_prop_type(prop, val, T.must(rules))
  set(instance, prop, val, rules)
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
275
276
277
278
279
280
281
# 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 - valid_props).empty?
    raise ArgumentError.new("At least one invalid prop arg supplied in #{self}: #{rules.keys.inspect}")
  end

  if (array = rules[:array])
    unless array.is_a?(Module)
      raise ArgumentError.new("Bad class as subtype in prop #{@class.name}.#{name}: #{array.inspect}")
    end
  end

  if !(rules[:clobber_existing_method!]) && !(rules[:without_accessors])
    # TODO: we should really be checking all the methods on `cls`, not just Object
    if Object.instance_methods.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
  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

#propsObject



31
32
33
# File 'lib/types/props/decorator.rb', line 31

def props
  @props ||= {}.freeze
end

#set(instance, prop, value, rules = props[prop.to_sym]) ⇒ Object



117
118
119
120
121
# File 'lib/types/props/decorator.rb', line 117

def set(instance, prop, value, rules=props[prop.to_sym])
  # For backwards compatibility, fall back to reconstructing the accessor key
  # (though it would probably make more sense to raise in that case).
  instance.instance_variable_set(rules ? rules[:accessor_key] : '@' + prop.to_s, value) # rubocop:disable PrisonGuard/NoLurkyInstanceVariableAccess
end

#valid_propsObject



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/types/props/decorator.rb', line 65

def valid_props
  %i{
    enum
    foreign
    foreign_hint_only
    ifunset
    immutable
    override
    redaction
    sensitivity
    without_accessors
    clobber_existing_method!
    extra
    optional
    _tnilable
  }
end

#validate_prop_value(prop, val) ⇒ Object



125
126
127
128
129
# File 'lib/types/props/decorator.rb', line 125

def validate_prop_value(prop, val)
  # This implements a 'public api' on document so that we don't allow callers to pass in rules
  # Rules seem like an implementation detail so it seems good to now allow people to specify them manually.
  check_prop_type(prop, val)
end