Path: blob/main/SignalServiceKit/Storage/Database/SDSCodableModel/InheritableRecord.swift
1 views
//
// 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
}
}