82 lines
3.4 KiB
Swift
82 lines
3.4 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// A type for a database row that corresponds to multiple concrete types.
|
|
///
|
|
/// This type decodes the recordType and then delegates the initialization
|
|
/// flow to the appropriate concrete type. That type must a subclass of the
|
|
/// type conforming to this protocol.
|
|
///
|
|
/// 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?
|
|
///
|
|
/// ``InheritableRecord`` works around this issue for scenarios where our
|
|
/// data is in a ``Decoder`` by requiring a `recordType` that can be used to
|
|
/// pick which subclass to initialize.
|
|
protocol InheritableRecord: Decodable {
|
|
/// A inheritance-supporting replacement for init(from decoder:).
|
|
///
|
|
/// Conforming types and subclasses should implement this method as they
|
|
/// would any other implementation of init(from decoder:).
|
|
init(inheritableDecoder decoder: Decoder) throws
|
|
|
|
/// 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 concreteType(forRecordType recordType: UInt) -> (any InheritableRecord.Type)?
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case recordType
|
|
}
|
|
|
|
/// 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.
|
|
extension InheritableRecord {
|
|
/// "Peek" the type of the record; invoke init(inheritableDecoder:).
|
|
///
|
|
/// This extension method provides a default implementation for init(from
|
|
/// decoder:) that extracts a `UInt` record type and uses its value to
|
|
/// delegate initialization to a subclass.
|
|
///
|
|
/// Types conforming to InheritableRecord MUST NOT override this method.
|
|
public init(from decoder: any Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
let recordType = try container.decode(UInt.self, forKey: .recordType)
|
|
|
|
guard let classToInitialize = Self.concreteType(forRecordType: recordType) else {
|
|
let errorMessage = "No class found to initialize for recordType: \(recordType)"
|
|
throw DecodingError.dataCorruptedError(forKey: .recordType, in: container, debugDescription: errorMessage)
|
|
}
|
|
|
|
let classInstance = try classToInitialize.init(inheritableDecoder: decoder)
|
|
guard let selfInstance = classInstance as? Self else {
|
|
let errorMessage = "Runtime type of \(recordType) isn't a \(Self.self)"
|
|
throw DecodingError.dataCorruptedError(forKey: .recordType, in: container, debugDescription: errorMessage)
|
|
}
|
|
self = selfInstance
|
|
}
|
|
}
|