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
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
- #add_prop_definition(prop, rules) ⇒ Object
- #all_props ⇒ Object
- #decorated_class ⇒ Object
- #foreign_prop_get(instance, prop, foreign_class, rules = props[prop.to_sym], opts = {}) ⇒ Object
- #get(instance, prop, rules = props[prop.to_sym]) ⇒ Object
-
#initialize(klass) ⇒ Decorator
constructor
A new instance of Decorator.
- #model_inherited(child) ⇒ Object
- #mutate_prop_backdoor!(prop, key, value) ⇒ Object
- #plugin(mod) ⇒ Object
- #prop_defined(name, cls, rules = {}) ⇒ Object
- #prop_get(instance, prop, rules = props[prop.to_sym]) ⇒ Object
- #prop_rules(prop) ⇒ Object
- #prop_set(instance, prop, val, rules = prop_rules(prop)) ⇒ Object
- #prop_validate_definition!(name, cls, rules, type) ⇒ Object
- #props ⇒ Object
- #set(instance, prop, value, rules = props[prop.to_sym]) ⇒ Object
- #valid_props ⇒ Object
- #validate_prop_value(prop, val) ⇒ Object
Methods included from 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_props ⇒ Object
45 |
# File 'lib/types/props/decorator.rb', line 45 def all_props; props.keys; end |
#decorated_class ⇒ Object
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. = T::Utils::Nilable.(type_object) type = T::Utils::Nilable.(type) array_subdoc_type = array_subdoc_type() hash_value_subdoc_type = hash_value_subdoc_type() hash_key_custom_type = hash_key_custom_type() 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() ? :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 |
#props ⇒ Object
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_props ⇒ Object
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 |