Last active
March 9, 2021 18:26
-
-
Save BigZaphod/4814485a26b62931b4bc82c5d1adf403 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Created by Sean Heber (@BigZaphod) on 10/12/19. | |
// License: BSD | |
import Foundation | |
//==--------------------------------------------------------- | |
/// This implementation requires the ability to initialize an | |
/// instance of a type before it can decode it. This is to support | |
/// reference types automatically so that each instance is | |
/// only stored once in the archive. Decoding a reference | |
/// requires the ability to first make an instance and then | |
/// fill it in with the values that are read from the archive | |
/// in a seperate step because Swift is very strict and has | |
/// strong opinions about these things. | |
/// | |
public protocol Archivable { | |
/// You'll need this. You can likely get this for free from Swift if you use structs or final classes and provide initial values for all your properties. | |
init() | |
/// Generally you don't have to provide this - use the default implementations and the property wrappers. | |
func write(to archive: ArchiveWriter) throws | |
/// Generally you don't have to provide this - use the default implementations and the property wrappers. | |
mutating func read(from archive: ArchiveReader) throws | |
} | |
public protocol PolymorphicArchivable: Archivable {} | |
public protocol ArchivablePropertyWrapper: CustomDebugStringConvertible { | |
associatedtype ValueType | |
var wrappedValue: ValueType { get set } | |
} | |
//==--------------------------------------------------------- | |
/// The @Archive property wrapper is the magic that makes it possible to | |
/// "automatically" encode and decode all of your properties just by conforming | |
/// your type to Archivable. Mark every property you want to archive with the | |
/// property wrapper and the implementation should do the rest. | |
/// | |
@propertyWrapper | |
public struct Archive<ValueType: Archivable>: ArchivablePropertyWrapper, _ArchivableProperty { | |
public init(wrappedValue initialValue: ValueType) { | |
wrapper = .init(value: initialValue) | |
} | |
public var wrappedValue: ValueType { | |
get { wrapper.value } | |
set { | |
if isKnownUniquelyReferenced(&wrapper) { | |
wrapper.value = newValue | |
} else { | |
wrapper = .init(value: newValue) | |
} | |
} | |
} | |
fileprivate func writeWrappedValue(to archive: ArchiveWriter) throws { try archive.write(wrapper.value) } | |
fileprivate func readWrappedValue(from archive: ArchiveReader) throws { wrapper.value = try archive.read(ValueType.self) } | |
private var wrapper: _ArchiveWrapper<ValueType> | |
} | |
@propertyWrapper | |
public struct ArchiveWeak<ValueType: Archivable & AnyObject>: ArchivablePropertyWrapper, _ArchivableProperty { | |
public init(wrappedValue initialValue: ValueType?) { | |
wrapper = .init(value: initialValue) | |
} | |
public var wrappedValue: ValueType? { | |
get { wrapper.value } | |
set { | |
if isKnownUniquelyReferenced(&wrapper) { | |
wrapper.value = newValue | |
} else { | |
wrapper = .init(value: newValue) | |
} | |
} | |
} | |
fileprivate func writeWrappedValue(to archive: ArchiveWriter) throws { try archive.write(wrapper.value) } | |
fileprivate func readWrappedValue(from archive: ArchiveReader) throws { wrapper.value = try archive.read(ValueType?.self) } | |
private var wrapper: _ArchiveWeakWrapper<ValueType> | |
} | |
extension ArchivablePropertyWrapper { | |
public var debugDescription: String { | |
return String(reflecting: wrappedValue) | |
} | |
} | |
extension Equatable where Self: ArchivablePropertyWrapper, ValueType: Equatable { | |
public static func == (lhs: Self, rhs: Self) -> Bool { | |
return lhs.wrappedValue == rhs.wrappedValue | |
} | |
} | |
extension Hashable where Self: ArchivablePropertyWrapper, ValueType: Hashable { | |
public func hash(into hasher: inout Hasher) { | |
wrappedValue.hash(into: &hasher) | |
} | |
} | |
extension Comparable where Self: ArchivablePropertyWrapper, ValueType: Comparable { | |
public static func < (lhs: Self, rhs: Self) -> Bool { lhs.wrappedValue < rhs.wrappedValue } | |
public static func <= (lhs: Self, rhs: Self) -> Bool { lhs.wrappedValue <= rhs.wrappedValue } | |
public static func >= (lhs: Self, rhs: Self) -> Bool { lhs.wrappedValue >= rhs.wrappedValue } | |
public static func > (lhs: Self, rhs: Self) -> Bool { lhs.wrappedValue > rhs.wrappedValue } | |
} | |
extension CustomStringConvertible where Self: ArchivablePropertyWrapper, ValueType: CustomStringConvertible { | |
public var description: String { | |
return wrappedValue.description | |
} | |
} | |
extension Archive: Equatable where ValueType: Equatable {} | |
extension Archive: Hashable where ValueType: Hashable {} | |
extension Archive: Comparable where ValueType: Comparable {} | |
extension Archive: CustomStringConvertible where ValueType: CustomStringConvertible {} | |
extension ArchiveWeak: Equatable where ValueType: Equatable {} | |
extension ArchiveWeak: Hashable where ValueType: Hashable {} | |
//==--------------------------------------------------------- | |
// Internals | |
//==--------------------------------------------------------- | |
private protocol _ArchivableProperty { | |
func writeWrappedValue(to archive: ArchiveWriter) throws | |
func readWrappedValue(from archive: ArchiveReader) throws | |
} | |
private final class _ArchiveWrapper<ValueType> { | |
init(value initialValue: ValueType) { value = initialValue } | |
var value: ValueType | |
} | |
private final class _ArchiveWeakWrapper<ValueType: AnyObject> { | |
init(value initialValue: ValueType?) { value = initialValue } | |
weak var value: ValueType? | |
} | |
extension Archivable { | |
// this finds all of the @Archive-able properties and returns them with their names | |
private func namedArchivableProperties() throws -> [String : _ArchivableProperty] { | |
var properties: [String : _ArchivableProperty] = [:] | |
var nextMirror: Mirror? = Mirror(reflecting: self) | |
while let mirror = nextMirror { | |
for (propertyName, propertyValue) in mirror.children { | |
if let name = propertyName, let property = propertyValue as? _ArchivableProperty { | |
properties[name] = property | |
} | |
} | |
nextMirror = mirror.superclassMirror | |
guard nextMirror == nil || self is PolymorphicArchivable else { | |
throw ArchivingError.nonPolymorphicArchivableType(type(of: self as Any)) | |
} | |
} | |
return properties | |
} | |
public func write(to archive: ArchiveWriter) throws { | |
let properties = try self.namedArchivableProperties() | |
try archive.writeTable(properties.keys) { (key: String) in | |
if let property = properties[key] { | |
try property.writeWrappedValue(to: archive) | |
} | |
} | |
} | |
public mutating func read(from archive: ArchiveReader) throws { | |
let properties = try self.namedArchivableProperties() | |
try archive.readTable { (key: String) in | |
if let property = properties[key] { | |
try property.readWrappedValue(from: archive) | |
} | |
} | |
} | |
} | |
//==--------------------------------------------------------- | |
// numerics that can read/write their raw bytes directly: | |
//==--------------------------------------------------------- | |
extension Int8: Archivable {} | |
extension UInt8: Archivable {} | |
extension Int16: Archivable {} | |
extension UInt16: Archivable {} | |
extension Int32: Archivable {} | |
extension UInt32: Archivable {} | |
extension Int64: Archivable {} | |
extension UInt64: Archivable {} | |
extension Float32: Archivable {} | |
extension Float64: Archivable {} | |
// fixed width integers will always use bigEndian | |
extension Archivable where Self: FixedWidthInteger { | |
public func write(to archive: ArchiveWriter) throws { | |
archive.writeUnsafeBytes(of: bigEndian) | |
} | |
public mutating func read(from archive: ArchiveReader) throws { | |
try archive.readUnsafeBytes(into: &self) | |
self = bigEndian | |
} | |
} | |
// all other numeric types just read/write their raw bytes in whatever order | |
extension Archivable where Self: Numeric { | |
public func write(to archive: ArchiveWriter) throws { | |
archive.writeUnsafeBytes(of: self) | |
} | |
public mutating func read(from archive: ArchiveReader) throws { | |
try archive.readUnsafeBytes(into: &self) | |
} | |
} | |
//==--------------------------------------------------------- | |
// Types that are encoded in terms of other archivable types: | |
//==--------------------------------------------------------- | |
// encoded as a UInt8 | |
extension Bool: Archivable { | |
public mutating func read(from archive: ArchiveReader) throws { try self = (archive.read(UInt8.self) > 0) } | |
public func write(to archive: ArchiveWriter) throws { try archive.write(UInt8(self ? 1 : 0)) } | |
} | |
// Int is always stored as an Int64 | |
extension Int: Archivable { | |
public mutating func read(from archive: ArchiveReader) throws { try self = Int(archive.read(Int64.self)) } | |
public func write(to archive: ArchiveWriter) throws { try archive.write(Int64(self)) } | |
} | |
// UInt is always stored as a UInt64 | |
extension UInt: Archivable { | |
public mutating func read(from archive: ArchiveReader) throws { try self = UInt(archive.read(UInt64.self)) } | |
public func write(to archive: ArchiveWriter) throws { try archive.write(UInt64(self)) } | |
} | |
// string is special because of built-in support for | |
// uniquing them so they are only stored once. therefore | |
// encoding/decoding is done in ArchiveWriter/ArchiveReader | |
extension String: Archivable { | |
public mutating func read(from archive: ArchiveReader) throws { try self = archive.readString() } | |
public func write(to archive: ArchiveWriter) throws { try archive.writeString(self) } | |
} | |
extension Collection where Self: Archivable { | |
public func write(to archive: ArchiveWriter) throws { | |
try archive.write(count) | |
try forEach(archive.write) | |
} | |
} | |
extension RangeReplaceableCollection where Self: Archivable { | |
public mutating func read(from archive: ArchiveReader) throws { | |
let count = try archive.read(Int.self) | |
removeAll(keepingCapacity: true) | |
reserveCapacity(count) | |
for _ in 0..<count { try append(archive.read(Element.self)) } | |
} | |
} | |
// read and write implementations are inherited from RangeReplaceableCollection and Collection | |
extension Array: Archivable {} | |
extension Data: Archivable {} | |
// Data slice is special and has built-in support for reading | |
// so it can reference the input data without making a copy. | |
// write implementation is inherited from Collection | |
extension Slice: Archivable where Base == Data { | |
public mutating func read(from archive: ArchiveReader) throws { | |
self = try archive.readDataSlice() | |
} | |
} | |
// write implementation is inherited from Collection | |
extension Set: Archivable where Element: Archivable { | |
public mutating func read(from archive: ArchiveReader) throws { | |
let count = try archive.read(Int.self) | |
removeAll(keepingCapacity: true) | |
reserveCapacity(count) | |
for _ in 0..<count { | |
try insert(archive.read(Element.self)) | |
} | |
} | |
} | |
extension Dictionary: Archivable where Key: Archivable { | |
public func write(to archive: ArchiveWriter) throws { | |
try archive.write(count) | |
for (key, value) in self { | |
try archive.write(key) | |
try archive.write(value) | |
} | |
} | |
public mutating func read(from archive: ArchiveReader) throws { | |
let count = try archive.read(Int.self) | |
removeAll(keepingCapacity: true) | |
reserveCapacity(count) | |
for _ in 0..<count { | |
let key = try archive.read(Key.self) | |
let value = try archive.read(Value.self) | |
self[key] = value | |
} | |
} | |
} | |
private protocol ArchivableOptional: Archivable {} | |
extension Optional: ArchivableOptional { | |
public init() { | |
self = .none | |
} | |
public func write(to archive: ArchiveWriter) throws { | |
if let value = self { | |
try archive.write(true) | |
try archive.write(value) | |
} else { | |
try archive.write(false) | |
} | |
} | |
public mutating func read(from archive: ArchiveReader) throws { | |
if try archive.read(Bool.self) { | |
try self = .some(archive.read(Wrapped.self)) | |
} else { | |
self = .none | |
} | |
} | |
} | |
// enums, OptionSets, etc can be archived automatically | |
// if they are represented by a raw value that is archivable | |
extension Archivable where Self: RawRepresentable, RawValue: Archivable { | |
public func write(to archive: ArchiveWriter) throws { try archive.write(rawValue) } | |
public mutating func read(from archive: ArchiveReader) throws { | |
guard let value = Self.init(rawValue: try archive.read(RawValue.self)) else { | |
throw ArchivingError.readFailed | |
} | |
self = value | |
} | |
} | |
/// Errors that may be thrown by the archiving system. | |
public enum ArchivingError: Error { | |
case incompatibleArchiver | |
case writeFailed | |
case readFailed | |
case notArchivable(Any.Type) | |
case cannotWriteUnregisteredPolymorphicArchivableType(Any.Type) | |
case cannotReadUnregisteredPolymorphicArchivableType(String) // your base class or root protocol may need to be PolymorphicArchivable or you forgot to call ArchivablePolymorphicType.register() | |
case cannotReadType(Any.Type) | |
case nonPolymorphicArchivableType(Any.Type) | |
} | |
/// This takes an object/value and converts it and everything it references into an archive. | |
public final class ArchiveWriter { | |
public static let encodingVersion: Int = 2 | |
public var userInfo: Any? | |
public static func data<T: Archivable>(for value: T, as type: T.Type, userInfo: Any? = nil) throws -> Data { | |
try ArchiveWriter(userInfo).encodeRoot(value) | |
} | |
private init(_ userInfo: Any?) { | |
self.userInfo = userInfo | |
} | |
private var buffers: [Data] = [] | |
private var encodedReferenceIDs: [ObjectIdentifier : Int] = [:] | |
private var encodedStringIDs: [String : Int] = [:] | |
private var encodedValues: [Data] = [] | |
private func encodeRoot<T: Archivable>(_ value: T) throws -> Data { | |
try encodeBox { | |
// first write the simplest header ever | |
try write(ArchiveWriter.encodingVersion as Int) | |
// box up the value - this process will find and encode all of the strings and other objects it depends on | |
let boxedValue = try encodeBox({ try write(value) }) | |
// write the objects and strings that were generated while boxing | |
try write(encodedValues) | |
// finally write the value's box | |
try write(boxedValue) | |
} | |
} | |
private func encodeBox(_ block: () throws -> Void) throws -> Data { | |
buffers.append(Data(capacity: 128)) | |
try block() | |
return buffers.removeLast() | |
} | |
private func writeReference<T>(_ obj: T) throws { | |
let ref = ObjectIdentifier(obj as AnyObject) | |
if let idx = encodedReferenceIDs[ref] { | |
try write(idx) | |
} else { | |
let idx = encodedValues.count | |
try write(idx) | |
encodedReferenceIDs[ref] = idx | |
// this is very important! | |
// while boxing the value, we may end up back inside this function and the count needs | |
// to be correct or else the index numbers will be off. this ensures there's a slot in | |
// the array at the right place once the boxing is finished. | |
encodedValues.append(Data()) | |
encodedValues[idx] = try encodeBox { | |
try writeValue(obj) | |
} | |
} | |
} | |
private func writeValue<T>(_ value: T) throws { | |
if let optionalValue = value as? ArchivableOptional { | |
try optionalValue.write(to: self) | |
} else if let polymorphicArchivableValue = value as? PolymorphicArchivable { | |
let valueType = type(of: polymorphicArchivableValue) | |
guard let typeName = ArchivablePolymorphicType.registeredName(for: valueType) else { | |
throw ArchivingError.cannotWriteUnregisteredPolymorphicArchivableType(valueType) | |
} | |
try writeString(typeName) | |
try polymorphicArchivableValue.write(to: self) | |
} else if let archivableValue = value as? Archivable { | |
try archivableValue.write(to: self) | |
} else { | |
throw ArchivingError.notArchivable(type(of: value)) | |
} | |
} | |
fileprivate func writeString(_ string: String) throws { | |
if let idx = encodedStringIDs[string] { | |
try write(idx) | |
} else { | |
let idx = encodedValues.count | |
try write(idx) | |
encodedStringIDs[string] = idx | |
encodedValues.append(Data(string.utf8)) | |
} | |
} | |
public func writeUnsafeBytes<T>(of value: T) { | |
withUnsafeBytes(of: value) { | |
buffers[buffers.count - 1].append($0.bindMemory(to: UInt8.self).baseAddress!, count: MemoryLayout<T>.size) | |
} | |
} | |
public func write<T>(_ value: T) throws { | |
if type(of: value) is AnyClass { | |
try writeReference(value) | |
} else { | |
try writeValue(value) | |
} | |
} | |
public func writeTable<TableKeys: Collection>(_ keys: TableKeys, valueWriter: (String) throws -> Void) throws where TableKeys.Element == String { | |
try write(keys.count) | |
for key in keys { | |
try write(encodeBox({ | |
try writeString(key) | |
try valueWriter(key) | |
})) | |
} | |
} | |
} | |
/// This is the opposite of the ArchiveWriter. | |
public final class ArchiveReader { | |
public var userInfo: Any? | |
public static func read<T: Archivable>(_ type: T.Type, from source: Data, userInfo: Any? = nil) throws -> T { | |
try ArchiveReader(userInfo).decodeRoot(type, from: source) | |
} | |
private init(_ userInfo: Any?) { | |
self.userInfo = userInfo | |
} | |
private var slices: [Slice<Data>] = [] | |
private var encodedValues: [Slice<Data>] = [] | |
private var decodedValues: [Int : Archivable] = [:] | |
private func decodeRoot<T: Archivable>(_ type: T.Type, from data: Data) throws -> T { | |
try decodeBox(Slice(data)) { | |
// immediately read the header | |
let encodingVersion = try read(Int.self) | |
// we only support this version for now | |
guard encodingVersion == ArchiveWriter.encodingVersion else { throw ArchivingError.incompatibleArchiver } | |
// read the dependencies (strings and other objects) | |
encodedValues = try read([Slice<Data>].self) | |
// now read the root value from inside its box | |
return try decodeBox(read(Slice<Data>.self)) { | |
try read(T.self) | |
} | |
} | |
} | |
private func decodeBox<T>(_ box: Slice<Data>, block: () throws -> T) rethrows -> T { | |
slices.append(box) | |
defer { slices.removeLast() } | |
return try block() | |
} | |
private func readReference(of type: Any.Type) throws -> Archivable { | |
let idx = try read(Int.self) | |
if let obj = decodedValues[idx] { | |
return obj | |
} | |
return try decodeBox(encodedValues[idx]) { | |
let archivableType = try readArchivedType(for: type) | |
var obj = archivableType.init() | |
decodedValues[idx] = obj | |
try obj.read(from: self) | |
return obj | |
} | |
} | |
private func readArchivedType(for type: Any.Type) throws -> Archivable.Type { | |
// try to read an encoded name if the type is known to be PolymorphicArchivable or if the type is NOT known to be Archivable | |
// this catches scenerios where the type is actually a .Protocol or something because we are reading without a known concrete type. | |
if type is PolymorphicArchivable.Type || !(type is Archivable.Type) { | |
let typeName = try readString() | |
guard let polymorphicType = ArchivablePolymorphicType.registeredType(for: typeName) else { | |
throw ArchivingError.cannotReadUnregisteredPolymorphicArchivableType(typeName) | |
} | |
return polymorphicType | |
} | |
guard let archivableType = type as? Archivable.Type else { | |
throw ArchivingError.cannotReadType(type) | |
} | |
return archivableType | |
} | |
fileprivate func readString() throws -> String { | |
let idx = try read(Int.self) | |
if let value = decodedValues[idx] { | |
guard let str = value as? String else { | |
throw ArchivingError.readFailed | |
} | |
return str | |
} | |
guard let str = String(bytes: encodedValues[idx], encoding: .utf8) else { | |
throw ArchivingError.readFailed | |
} | |
decodedValues[idx] = str | |
return str | |
} | |
fileprivate func readDataSlice() throws -> Slice<Data> { | |
let count = try read(Int.self) | |
let slice = slices.removeLast() | |
defer { slices.append(slice.dropFirst(count)) } | |
return slice[slice.startIndex ..< slice.startIndex + count] | |
} | |
public func readUnsafeBytes<T>(into value: inout T) throws { | |
let count = MemoryLayout<T>.size | |
let slice = slices.removeLast() | |
guard slice.count >= count else { | |
throw ArchivingError.readFailed | |
} | |
withUnsafeMutableBytes(of: &value) { | |
slice.base.copyBytes(to: $0.bindMemory(to: UInt8.self).baseAddress!, from: slice.startIndex..<(slice.startIndex + count)) | |
} | |
slices.append(slice.dropFirst(count)) | |
} | |
public func read<T>(_ type: T.Type) throws -> T { | |
var archivableValue: Archivable | |
if type is AnyClass { | |
archivableValue = try readReference(of: type) | |
} else { | |
let archivableType = try readArchivedType(for: type) | |
archivableValue = archivableType.init() | |
try archivableValue.read(from: self) | |
} | |
guard let recastValue = archivableValue as? T else { | |
throw ArchivingError.readFailed | |
} | |
return recastValue | |
} | |
public func readTable(_ valueReader: (String) throws -> Void) throws { | |
let count = try read(Int.self) | |
for _ in 0..<count { | |
let box = try read(Slice<Data>.self) | |
try decodeBox(box) { | |
let key = try readString() | |
try valueReader(key) | |
} | |
} | |
} | |
} | |
public enum ArchivablePolymorphicType { | |
public static func register(_ type: PolymorphicArchivable.Type) { | |
let typeName = String(reflecting: type) | |
let identifier = ObjectIdentifier(type) | |
// ignore the registration if this type is already registered | |
if registeredName(for: type) == typeName, registeredType(for: typeName) == type { | |
return | |
} | |
// sanity checks | |
precondition(typeForName[typeName] == nil) | |
precondition(nameForType[identifier] == nil) | |
// register | |
typeForName[typeName] = type | |
nameForType[identifier] = typeName | |
} | |
fileprivate static func registeredName(for type: Any.Type) -> String? { | |
return nameForType[ObjectIdentifier(type)] | |
} | |
fileprivate static func registeredType(for name: String) -> PolymorphicArchivable.Type? { | |
return typeForName[name] | |
} | |
private static var typeForName: [String : PolymorphicArchivable.Type] = [:] | |
private static var nameForType: [ObjectIdentifier : String] = [:] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// First let's define an enum for some spaceship size classes we will need later. | |
// The `Archivable` protocol will automatically work for enums as long as the `RawType` | |
// of the enum is an `Archivable` type. In this case the enum is set as a `String` which | |
// is given an `Archivable` conformance by the `Archivable` implementation itself. | |
enum ShipSizeClass: String, Archivable { | |
case galaxy, excelsior, constitution | |
// this default initializer is unfortunately needed by `Archivable` because all `Archivable` | |
// types must have an empty initializer implemented. This requirement exists in order to | |
// support encoding graphs of objects that have circular references. Since Swift essentially | |
// integrates both `alloc` and `init` into a single initialization step, I could not find any | |
// other way to do this without a requirement along these lines. | |
// | |
// If you use structs or final classes, Swift will generate an empty initializer for you as | |
// long as you define defaults for all of your properties (which is super nice), but no such | |
// convenience exists (or is usually reasonable) for enums, so we have to do it ourselves | |
// and cringe a little about it. If you hide it in an extension you can pretend it doesn't | |
// actually exist and move on with your life..... maybe.... | |
init() { self = .galaxy } | |
} | |
// This protocol will act as a kind of "abstract base class" for our ships. Since we want to have | |
// an array of `[Ship]` in our space stations, we need to make all Ship's a `PolymorphicArchivable` | |
// type or else the archiver will not be able to correctly encode and decode the array elements. | |
// | |
// Note that this same consideration is needed when using classes that will have subclasses - so | |
// be aware of this. If you want to have arrays of a base type that will have subtype instances | |
// in that array, for example, then those types will need to use `PolymorphicArchivable`. In my | |
// experiments with this so far, it's not needed too often if you keep the data structures simple. | |
protocol Ship: PolymorphicArchivable { | |
var registry: String { get set } | |
var sizeClass: ShipSizeClass { get set } | |
} | |
// Let's define some ship types! | |
// (This is a pretty silly way to do this, but it's just an example. :P) | |
// Since `Ship` is already `PolymorphicArchivable`, we don't need to explicitly conform each type to it. | |
// However each property we want to encode in our archive *does* need to be marked with the `@Archive` | |
// property wrapper so it knows which things to actually write to the archive. Unfortunately Swift has | |
// no way to require a property declared in a protocol to adopt a specific property wrapper, so there's | |
// some duplication here that, in theory, shouldn't be necessary - but it's not too bad. Using a base | |
// class instead of a base protocol arrangement would avoid that if you really wanted to, I suppose. | |
// Any properties that are not marked with `@Archive` will not be saved or restored by the system. | |
struct Intrepid: Ship { | |
@Archive var registry = "NCC-38907" | |
@Archive var sizeClass: ShipSizeClass = .excelsior | |
} | |
final class Excalibur: Ship { | |
@Archive var registry = "NCC-1664" | |
@Archive var sizeClass: ShipSizeClass = .constitution | |
} | |
struct Enterprise: Ship { | |
@Archive var registry = "NCC-1701-D" | |
@Archive var sizeClass: ShipSizeClass = .galaxy | |
} | |
// Now we need a space station for all of these ships to visit! | |
// This is where the `PolymorphicArchivable` really comes into play. Here we have an array of | |
// docked ships - but it's defined as `[Ship]`. You may notice that this array's elements are not | |
// a specific concrete type (like just `[Excalibur]` or `[Enterprise]` or whatever) but instead | |
// simply defined as containing instances of types that are known to conform to `Ship`. Each | |
// individual value in this array could even be of totally different underlying types. Eagle-eyed | |
// readers may have noticed that `Excalibur` is actually defined as a class type while all of the | |
// others are structs - and yet this still works! I don't think you can do this with `Codable` | |
// unless you use wrapper objects of your own (or perhaps use a property wrapper implementation | |
// that does something like that for you automatically). | |
final class Station: Archivable { | |
@Archive var docked: [Ship] = [] | |
} | |
func example() { | |
// Any polymorphic type needs to be registered before it will archive/unarchive. This is required because | |
// Swift has no way (that I know of) to translate a string name of a type into a Type instance. If the runtime | |
// ever gets this ability, this step would no longer be necessary. | |
// Attempting to archive a polymorphic type that isn't registered will result throw an error so it's easy to catch. | |
ArchivablePolymorphicType.register(Intrepid.self) | |
ArchivablePolymorphicType.register(Excalibur.self) | |
ArchivablePolymorphicType.register(Enterprise.self) | |
// Let's make some stuff to archive! | |
let originalStarBase = Station() | |
originalStarBase.docked.append(Enterprise()) | |
originalStarBase.docked.append(Excalibur()) | |
originalStarBase.docked.append(Intrepid()) | |
// prints: ["NCC-1701-D", "NCC-1664", "NCC-38907"] | |
print(originalStarBase.docked.map({ $0.registry })) | |
// Let's do some archiving shenanigans... | |
do { | |
// make an archive of the original starbase | |
let archivedData = try ArchiveWriter.data(for: originalStarBase, as: Station.self) | |
// unarchive the data into a new instance of the previously archived starbase | |
let restoredStarBase = try ArchiveReader.read(Station.self, from: archivedData) | |
// as one would expect, the same ships are still docked in the same order: | |
// ["NCC-1701-D", "NCC-1664", "NCC-38907"] | |
print(restoredStarBase.docked.map({ $0.registry })) | |
// No errors! | |
print("Made it so!") | |
} catch let err { | |
print(err) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment