Swift 5.6, aiming at Swift 6.0. So constantly refactoring and trying things out.
The user defines a schema.
That schema is stored within the database.
The database does it thing like optimise column order or whatever.
The user can provide a dictionary of key-value pairs (or that is the result of a SQL statement):
["id": RowID(rawValue: 42), "flag": true, "name" : "kitty"]
This dictionary can pass through the encoder.
The user can provide a struct like Animal which passes through the encoder.
The encoder returns a dictionary of [String: Element]
pairs.
Element is something that can be stored within the DB.
It's encoded once, written many times: the row itself, indices, views, whatever.
These are all containers that together from a table.
Are keys always processed in the same order by Codable? I genuinely don't know.
But doesn't really matter, I reserve the right to store the columns on disk as I see fit.
It has to be a struct because it has to be Sendable
.
Nested structs? Make a graph. Maybe at some point this can be relaxed and a nested struct just gets its own container. But that depends on how well I get a handle on encoders.
Point being: I want this encoder to turn a lovely swift struct into [String: Element]
which I can process.
So kitty becomes:
["id": Element(.numeric(8), .numeric(42)),
"flag": Element(.flag(true), .none),
"name", Element(.text(5), .text("kitty"))]`
The database row is flat, so id: ["RawValue" : 42]
wouldn't work.
struct RowID : Sendable
{
let rawValue: Int
}
extension RowID : RawRepresentable
{
}
extension RowID : Codable
{
}
private struct Animal : Codable, Equatable
{
let id: RowID
let flag: Bool
let name: String
}
private let cat = Animal(id: 42, flag: true, name: "kitty")
func testRowID() throws
{
let value = RowID(rawValue: 42)
XCTAssertNotNil(try encoder.encode(value))
XCTAssertEqual(try encoder.encode(value), ValueEncoder.Element(value: 42))
}
func testValue() throws
{
let encoded = try encoder.encode(cat)
XCTAssertEqual(encoded["id"], .init(value: cat.id.rawValue))
XCTAssertEqual(encoded["name"], .init(value: cat.name))
}
func testOptionalValue() throws
{
let value : Animal? = cat
let encoded : [String: ValueEncoder.Element] = try encoder.encode(value)
XCTAssertEqual(encoded["id"], .init(value: cat.id.rawValue))
XCTAssertEqual(encoded["name"], .init(value: cat.name))
}
final class ValueEncoder
{
//MARK: definitions
typealias Element = PackageWriter.Element
//MARK: properties
fileprivate var stack: Element? = nil
// fileprivate var array: [Element]? = nil
fileprivate var content: [String : Element] = [:]
/* required */
let codingPath: [CodingKey] = [] //might be useful for nested structs?
let userInfo: [CodingUserInfoKey : Any] = [:] //might be useful for encoding date strategies or so?
}
extension ValueEncoder
{
//a standalone value has no key, so just return the element
func encode(_ value: (any Encodable)?) throws -> Element
{
defer{ stack = nil }
try value?.encode(to: self)
guard let element = stack else { return .init() }
return element
}
func encode(_ value: any Encodable) throws -> [String: Element]
{
try value.encode(to: self)
return content
}
// func encode<T>(_ value: T) throws -> Element
// where T: Encodable & RawRepresentable, T.RawValue : Encodable
// {
// try encode(value.rawValue)
// }
}
extension ValueEncoder : Encoder
{
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key>
where Key : CodingKey
{
.init(ValueEncoderContainer(encoder: self))
}
func unkeyedContainer() -> UnkeyedEncodingContainer { fatalError() } // { UnkeyedValueEncoderContainer(encoder: self) }
func singleValueContainer() -> SingleValueEncodingContainer { SingleValueEncoderContainer(encoder: self) }
}
private struct ValueEncoderContainer<Key>
where Key: CodingKey
{
//MARK: properties
let encoder: ValueEncoder
var singular: SingleValueEncoderContainer
/* required */
let codingPath: [CodingKey] = []
//MARK: init
fileprivate init(encoder: ValueEncoder)
{
self.encoder = encoder
self.singular = .init(encoder: encoder)
}
}
extension ValueEncoderContainer : KeyedEncodingContainerProtocol
{
mutating func superEncoder() -> Encoder { encoder }
mutating func superEncoder(forKey key: Key) -> Encoder { encoder }
mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { fatalError() }
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer<NestedKey>
where NestedKey: CodingKey
{
encoder.container(keyedBy: keyType)
}
}
extension ValueEncoderContainer
{
/* optionals */
mutating func encodeNil(forKey key: Key) throws
{
try self.singular.encodeNil()
try wrap(key: key)
}
/* booleans */
mutating func encode(_ value: Bool, forKey key: Key) throws
{
try self.singular.encode(value)
try wrap(key: key)
}
/* strings */
mutating func encode(_ value: String, forKey key: Key) throws
{
try self.singular.encode(value)
try wrap(key: key)
}
/* integers */
mutating func encode<T>(_ value: T, forKey key: Key) throws
where T: Encodable & FixedWidthInteger
{
try self.singular.encode(value)
try wrap(key: key)
}
/* floating points */
mutating func encode(_ value: Double, forKey key: Key) throws
{
try encode(value.bitPattern, forKey: key)
}
// mutating func encode(_ value: Date, forKey key: Key) throws
// {
// try encode(value.timeIntervalSinceReferenceDate, forKey: key)
// }
/* generic */
// mutating func encode<T>(_ value: T, forKey key: Key) throws
// where T: Encodable & RawRepresentable, T.RawValue: Encodable & FixedWidthInteger
// {
// try encode(value.rawValue, forKey: key)
// }
mutating func encode<T>(_ value: T, forKey key: Key) throws
where T: Encodable
{
switch value
{
// case let date as Date: try encode(date, forKey: key)
// case let rowID as RowID: try encode(rowID.rawValue, forKey: key)
default:
throw Error.unsupported(String(describing: type(of: value)))
}
}
}
private extension ValueEncoderContainer
{
func wrap(key: Key) throws
{
guard let element = self.encoder.stack else { throw KeyError.unknown(key) }
encoder.content[key.stringValue] = element
encoder.stack = nil
}
}
private struct SingleValueEncoderContainer
{
//MARK: properties
let encoder : ValueEncoder
/* required */
let codingPath: [CodingKey] = []
//MARK: init
fileprivate init(encoder: ValueEncoder)
{
self.encoder = encoder
}
}
extension SingleValueEncoderContainer : SingleValueEncodingContainer
{
/* optionals */
mutating func encodeNil() throws
{
encoder.stack = .init()
}
/* booleans */
mutating func encode(_ value: Bool) throws
{
encoder.stack = .init(value: value)
}
/* strings */
mutating func encode(_ value: String) throws
{
encoder.stack = .init(value: value)
}
/* integers */
mutating func encode<T>(_ value: T) throws
where T: Encodable & FixedWidthInteger
{
encoder.stack = .init(value: value)
}
/* floating points */
mutating func encode(_ value: Float32) throws
{
try encode(value.bitPattern)
}
mutating func encode(_ value: Float64) throws //repetitive :(
{
try encode(value.bitPattern)
}
// mutating func encode(_ value: Date) throws
// {
// try encode(value.timeIntervalSinceReferenceDate)
// }
/* generic */
mutating func encode<T>(_ value: T) throws
where T : Encodable
{
try value.encode(to: encoder)
}
}
I got to this point thanks to NSBlog and JSONEncoder
.
Without those examples, it's very unclear that you need encode
functions in ValueEncoder
to kick of this thing. Naively you start with ValueEncoder
conforming to those protocols.
Are the encodeIfPresent
functions required, actually?
Yes, I can get it work for RowID
specifically but if the user has some custom RawRepresentable
value then the schema would just be "custom" and not "custom.rawValue" or whatever. It should be flattened in the case of RawRepresentable
.