class T::Props::Decorator

functionality).
replace decorator overrides in plugins with class methods that expose the necessary
should really just be static methods on private modules (we’d also want/need to
with an incorrect understanding of the decorator pattern). These “decorators”
with DocumentDecorator and ModelDecorator (which both seem to have been written
NB: This is not actually a decorator. It’s just named that way for consistency

def add_prop_definition(prop, rules)

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

def all_props; props.keys; end

def all_props; props.keys; end

def array_subdoc_type(type)

def array_subdoc_type(type)
e.is_a?(T::Types::TypedArray)
ype = T::Utils.unwrap_nilable(type.type) || type.type
l_type.is_a?(T::Types::Simple) &&
(el_type.raw_type < T::Props::Serializable || el_type.raw_type.is_a?(T::Props::CustomType))
turn el_type.raw_type

def check_prop_type(prop, val, rules=prop_rules(prop))

def check_prop_type(prop, val, rules=prop_rules(prop))
bject = rules.fetch(:type_object)
 rules.fetch(:type)
: ideally we'd add `&& rules[:optional] != :existing` to this check
makes sense to treat those props required in this context), but we'd need
e sure that doesn't break any existing code first.
.nil?
T::Props::Utils.need_nil_write_check?(rules) || (rules.key?(:default) && rules[:default].nil?)
turn
ules[:raise_on_nil_write]
ise T::Props::InvalidValueError.new("Can't set #{@class.name}.#{prop} to #{val.inspect} " \
instance of #{val.class}) - need a #{type}")
rops::CustomType is not a real object based class so that we can not run real type check call.
rops::CustomType.valid?() is only a helper function call.
=
ype.is_a?(T::Props::CustomType) && T::Props::Utils.optional_prop?(rules)
pe.valid?(val)

pe_object.valid?(val)
lid
e T::Props::InvalidValueError.new("Can't set #{@class.name}.#{prop} to #{val.inspect} " \
instance of #{val.class}) - need a #{type_object}")

def convert_type_to_class(type)

def convert_type_to_class(type)
ype
::Types::TypedArray, T::Types::FixedArray
y
::Types::TypedHash, T::Types::FixedHash

::Types::TypedSet
::Types::Union
e below unwraps our T.nilable types for T::Props if we can.
is lets us do things like specify: const T.nilable(String), foreign: Opus::DB::Model::Merchant
nil_type = T::Utils.unwrap_nilable(type)
on_nil_type
nvert_type_to_class(non_nil_type)

ject
::Types::Simple
.raw_type
is isn't allowed unless whitelisted_for_underspecification is
ue, due to the check in prop_validate_definition
ct

def decorated_class; @class; end

def decorated_class; @class; end

def define_foreign_method(prop_name, rules, foreign)

def define_foreign_method(prop_name, rules, foreign)
hod = "#{prop_name}_"
 there's no clear reason *not* to allow additional options
, but we're baking in `allow_direct_mutation` since we
en't* allowed additional options in the past and want to
ult to keeping this interface narrow.
.send(:define_method, fk_method) do |allow_direct_mutation: nil|
oreign.is_a?(Proc)
solved_foreign = foreign.call
 !resolved_foreign.respond_to?(:load)
raise ArgumentError.new(
  "The `foreign` proc for `#{prop_name}` must return a model class. " \
  "Got `#{resolved_foreign.inspect}` instead."
)
d
`foreign` is part of the closure state, so this will persist to future invocations
of the method, optimizing it so this only runs on the first invocation.
reign = resolved_foreign
llow_direct_mutation.nil?
ts = {}

ts = {allow_direct_mutation: allow_direct_mutation}
.class.decorator.foreign_prop_get(self, prop_name, foreign, rules, opts)
fk_method = "#{fk_method}!"
.send(:define_method, force_fk_method) do |allow_direct_mutation: nil|
ed_foreign = send(fk_method, allow_direct_mutation: allow_direct_mutation)
loaded_foreign
:Configuration.hard_assert_handler(
'Failed to load foreign model',
storytime: {method: force_fk_method, class: self.class}
st(loaded_foreign)
.send(:define_method, "#{prop_name}_record") do |allow_direct_mutation: nil|
onfiguration.soft_assert_handler(
sing deprecated 'model.#{prop_name}_record' foreign key syntax. You should replace this with 'model.#{prop_name}_'",
tify: 'vasi'
(fk_method, allow_direct_mutation: allow_direct_mutation)

def define_getter_and_setter(name, rules)

def define_getter_and_setter(name, rules)
es[:immutable]
ss.send(:define_method, "#{name}=") do |_x|
ise T::Props::ImmutableProp.new("#{self.class}##{name} cannot be modified after creation.")
ss.send(:define_method, "#{name}=") do |x|
lf.class.decorator.prop_set(self, name, x, rules)
.send(:define_method, name) do
.class.decorator.prop_get(self, name, rules)

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

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

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

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

def handle_foreign_hint_only_option(prop_cls, foreign_hint_only)

def handle_foreign_hint_only_option(prop_cls, foreign_hint_only)
tring, Array].include?(prop_cls) && !(prop_cls.is_a?(T::Props::CustomType))
e ArgumentError.new(
foreign_hint_only` can only be used with String or Array prop types"
te_foreign_option(
eign_hint_only, foreign_hint_only,
d_type_msg: "an individual or array of a model class, or a Proc returning such."

def handle_foreign_option(prop_name, prop_cls, rules, foreign)

def handle_foreign_option(prop_name, prop_cls, rules, foreign)
te_foreign_option(
eign, foreign, valid_type_msg: "a model class or a Proc that returns one"
p_cls != String
e ArgumentError.new("`foreign` can only be used with a prop type of String")
eign.is_a?(Array)
 don't support arrays with `foreign` because it's hard to both preserve ordering and
ep them from being lurky performance hits by issuing a bunch of un-batched DB queries.
 could potentially address that by porting over something like AmbiguousIDLoader.
e ArgumentError.new(
sing an array for `foreign` is no longer supported. Instead, use `foreign_hint_only` " \
ith an array or a Proc that returns an array, e.g., foreign_hint_only: -> {[Foo, Bar]}"
_foreign_method(prop_name, rules, foreign)

def handle_redaction_option(prop_name, redaction)

def handle_redaction_option(prop_name, redaction)
ed_method = "#{prop_name}_redacted"
.send(:define_method, redacted_method) do
e = self.public_send(prop_name)
k::Tools::RedactionUtils.redact_with_directive(
lue, redaction)

def hash_key_custom_type(type)

def hash_key_custom_type(type)
e.is_a?(T::Types::TypedHash)
_type = T::Utils.unwrap_nilable(type.keys) || type.keys
eys_type.is_a?(T::Types::Simple) && keys_type.raw_type.is_a?(T::Props::CustomType)
turn keys_type.raw_type

def hash_value_subdoc_type(type)

def hash_value_subdoc_type(type)
e.is_a?(T::Types::TypedHash)
es_type = T::Utils.unwrap_nilable(type.values) || type.values
alues_type.is_a?(T::Types::Simple) &&
(values_type.raw_type < T::Props::Serializable || values_type.raw_type.is_a?(T::Props::CustomType))
turn values_type.raw_type

def initialize(klass)

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

def is_nilable?(type)

def is_nilable?(type)
 false if !type.is_a?(T::Types::Union)
ypes.any? {|t| t == T::Utils.coerce(NilClass)}

def model_inherited(child)

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

def mutate_prop_backdoor!(prop, key, value)

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

def plugin(mod)

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

def prop_defined(name, cls, rules={})

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

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

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

def prop_rules(prop); props[prop.to_sym] || raise("No such prop: #{prop.inspect}"); end

def prop_rules(prop); props[prop.to_sym] || raise("No such prop: #{prop.inspect}"); end

def prop_set(instance, prop, val, rules=prop_rules(prop))

def prop_set(instance, prop, val, rules=prop_rules(prop))
  check_prop_type(prop, val, T.must(rules))
  set(instance, prop, val, rules)
end

def prop_validate_definition!(name, cls, rules, type)

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

def props

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

def set(instance, prop, value, rules=props[prop.to_sym])

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

def shallow_clone_ok(type)

def shallow_clone_ok(type)
type =
ype.is_a?(T::Types::TypedArray)
pe.type
f type.is_a?(T::Types::TypedSet)
pe.type
f type.is_a?(T::Types::TypedHash)
pe.values
type.is_a?(T::Types::Simple) && TYPES_NOT_NEEDING_CLONE.any? do |cls|
r_type.raw_type <= cls

def smart_coerce(type, array:, enum:)

def smart_coerce(type, array:, enum:)
wards compatibility for pre-T::Types style
ray.nil? && !enum.nil?
e ArgumentError.new("Cannot specify both :array and :enum options")
!array.nil?
ype == Set
:Set[array]

:Array[array]
!enum.nil?
::Utils.unwrap_nilable(type)
nilable(T.enum(enum))

enum(enum)
tils.coerce(type)

def valid_props

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

def validate_foreign_option(option_sym, foreign, valid_type_msg:)

def validate_foreign_option(option_sym, foreign, valid_type_msg:)
eign.is_a?(Symbol) || foreign.is_a?(String)
e ArgumentError.new(
sing a symbol/string for `#{option_sym}` is no longer supported. Instead, use a Proc " \
hat returns the class, e.g., foreign: -> {Foo}"
reign.is_a?(Proc) && !foreign.is_a?(Array) && !foreign.respond_to?(:load)
e ArgumentError.new("The `#{option_sym}` option must be #{valid_type_msg}")

def validate_not_missing_sensitivity(prop_name, rules)

def validate_not_missing_sensitivity(prop_name, rules)
es[:sensitivity].nil?
ules[:redaction]
:Configuration.hard_assert_handler(
"#{@class}##{prop_name} has a 'redaction:' annotation but no " \
"'sensitivity:' annotation. This is probably wrong, because if a " \
"prop needs redaction then it is probably sensitive. Add a " \
"sensitivity annotation like 'sensitivity: Opus::Sensitivity::PII." \
"whatever', or explicitly override this check with 'sensitivity: []'."
DO(PRIVACYENG-982) Ideally we'd also check for 'password' and possibly
her terms, but this interacts badly with ProtoDefinedDocument because
e proto syntax currently can't declare "sensitivity: []"
rop_name =~ /\bsecret\b/
:Configuration.hard_assert_handler(
"#{@class}##{prop_name} has the word 'secret' in its name, but no " \
"'sensitivity:' annotation. This is probably wrong, because if a " \
"prop is named 'secret' then it is probably sensitive. Add a " \
"sensitivity annotation like 'sensitivity: Opus::Sensitivity::NonPII." \
"security_token', or explicitly override this check with " \
"'sensitivity: []'."

def validate_prop_name(name)

def validate_prop_name(name)
e !~ /\A[A-Za-z_][A-Za-z0-9_-]*\z/
e ArgumentError.new("Invalid prop name in #{@class.name}: #{name}")

def validate_prop_value(prop, val)

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