118 lines
4.7 KiB
Swift
118 lines
4.7 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// Represents a type that may be initialized from a factory initializer that
|
|
/// used a "record type" to decide to initialize this type.
|
|
protocol FactoryInitializableFromRecordType {
|
|
/// The record type indicating that this type should be initialized.
|
|
static var recordType: UInt { get }
|
|
|
|
/// Initialize from the given ``Decoder``, at the request of an upstream
|
|
/// factory initializer.
|
|
///
|
|
/// This method may safely assume the upstream initializer belongs to its
|
|
/// superclass.
|
|
///
|
|
/// This method should call an appropriate `super.init`, passing a
|
|
/// superclass ``Decoder``.
|
|
init(forRecordTypeFactoryInitializationFrom decoder: Decoder) throws
|
|
}
|
|
|
|
/// Represents a type that should delegate its ``Decodable`` initialization to
|
|
/// another class (which, in practice, must be a subclass) via factory
|
|
/// initialization, using a "record type" to determine which subclass to
|
|
/// initialize itself as.
|
|
///
|
|
/// Why? Consider a class with many subclasses, which we may want to initialize
|
|
/// from a context in which we do not know the correct subclass for the data we
|
|
/// will pass to the initializer. For example, a `fetchAll()` method as follows:
|
|
///
|
|
/// ```swift
|
|
/// func fetchAll() -> [MyBaseClass] {
|
|
/// let decoders: [Data] = fetchDataBlobs()
|
|
/// return dataBlobs.map { .init($0) }
|
|
/// }
|
|
/// ```
|
|
///
|
|
/// Imagine that the various `Data` instances above should each be deserialized
|
|
/// as a different subclass of `MyBaseClass`. How do we know which subclass to
|
|
/// deserialize as, and how do we declare that in code?
|
|
///
|
|
/// ``NeedsFactoryInitializationFromRecordType`` works around this issue for
|
|
/// scenarios where our data is in a ``Decoder`` by requiring that each decoder
|
|
/// instance is known to contain a ``UInt`` "record type" that can be used to
|
|
/// pick which subclass to initialize.
|
|
///
|
|
/// ---
|
|
///
|
|
/// Note that this pattern (an initializer in a protocol extension) is required
|
|
/// to work around the fact that Swift does not support assignment to `self` in
|
|
/// class initializers.
|
|
///
|
|
/// See https://github.com/apple/swift/issues/47830 for more.
|
|
protocol NeedsFactoryInitializationFromRecordType: Decodable {
|
|
associatedtype CodingKeys: CodingKey
|
|
|
|
/// A key from which a `UInt` record type may be extracted from a given
|
|
/// `Decoder`.
|
|
static var recordTypeCodingKey: CodingKeys { get }
|
|
|
|
/// Determine the subclass of ourself to which we should delegate
|
|
/// initialization for a given `recordType`.
|
|
/// - Returns
|
|
/// The subclass type to initialize ourselves as. A `nil` result represents
|
|
/// an error state, such as no subclass matching `recordType`.
|
|
static func classToInitialize(
|
|
forRecordType recordType: UInt
|
|
) -> (any FactoryInitializableFromRecordType.Type)?
|
|
}
|
|
|
|
extension NeedsFactoryInitializationFromRecordType {
|
|
/// Extract a `UInt` record type from the given decoder, and use its value
|
|
/// to delegate initialization to a subclass.
|
|
///
|
|
/// Expects to be given a decoder for a subclass, and throws if the decoder
|
|
/// does not contain a `super` decoder that in turn contains a valid and
|
|
/// recognized record type.
|
|
public init(from subclassDecoder: Decoder) throws {
|
|
let subclassContainer = try subclassDecoder.container(keyedBy: CodingKeys.self)
|
|
let baseClassDecoder = try subclassContainer.superDecoder()
|
|
|
|
let container = try baseClassDecoder.container(keyedBy: CodingKeys.self)
|
|
let recordType = try container.decode(UInt.self, forKey: Self.recordTypeCodingKey)
|
|
|
|
guard let classToInitialize = Self.classToInitialize(forRecordType: recordType) else {
|
|
let errorMessage = "No class found to initialize for recordType: \(recordType)"
|
|
|
|
Logger.error(errorMessage)
|
|
|
|
throw DecodingError.dataCorrupted(.init(
|
|
codingPath: [Self.recordTypeCodingKey],
|
|
debugDescription: errorMessage
|
|
))
|
|
}
|
|
|
|
guard recordType == classToInitialize.recordType else {
|
|
let errorMessage = "Record type \(recordType) unexpectedly matched to class \(classToInitialize) with recordType \(classToInitialize.recordType)!"
|
|
|
|
Logger.error(errorMessage)
|
|
|
|
throw DecodingError.dataCorrupted(.init(
|
|
codingPath: [Self.recordTypeCodingKey],
|
|
debugDescription: errorMessage
|
|
))
|
|
}
|
|
|
|
let classInstance = try classToInitialize.init(forRecordTypeFactoryInitializationFrom: subclassDecoder)
|
|
|
|
guard let selfInstance = classInstance as? Self else {
|
|
owsFail("Factory-initialized class was not a \(Self.self)!")
|
|
}
|
|
|
|
self = selfInstance
|
|
}
|
|
}
|