DataLireCoder swift package
This commit is contained in:
29
Sources/DLCCommon/Extensions/SQLiteRow.swift
Normal file
29
Sources/DLCCommon/Extensions/SQLiteRow.swift
Normal file
@@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
public extension SQLiteRow {
|
||||
func contains(_ key: CodingKey) -> Bool {
|
||||
if let index = key.intValue {
|
||||
0..<count ~= index
|
||||
} else {
|
||||
contains(key.stringValue)
|
||||
}
|
||||
}
|
||||
|
||||
subscript(key: CodingKey) -> Value? {
|
||||
get {
|
||||
if let index = key.intValue {
|
||||
self[index].value
|
||||
} else {
|
||||
self[key.stringValue]
|
||||
}
|
||||
}
|
||||
set {
|
||||
if let index = key.intValue {
|
||||
self[self[index].column] = newValue
|
||||
} else {
|
||||
self[key.stringValue] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
Sources/DLCCommon/Structures/RowCodingKey.swift
Normal file
20
Sources/DLCCommon/Structures/RowCodingKey.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
|
||||
public struct RowCodingKey: CodingKey, Equatable {
|
||||
// MARK: - Properties
|
||||
|
||||
public let stringValue: String
|
||||
public let intValue: Int?
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
public init(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
self.intValue = nil
|
||||
}
|
||||
|
||||
public init(intValue: Int) {
|
||||
self.stringValue = "Index \(intValue)"
|
||||
self.intValue = intValue
|
||||
}
|
||||
}
|
||||
102
Sources/DLCDecoder/Classes/KeyedContainer.swift
Normal file
102
Sources/DLCDecoder/Classes/KeyedContainer.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
final class KeyedContainer<Decoder: RowDecoder & KeyCheckingDecoder, Key: CodingKey>: Container, KeyedDecodingContainerProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
let decoder: Decoder
|
||||
let codingPath: [any CodingKey]
|
||||
let allKeys: [Key]
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(
|
||||
decoder: Decoder,
|
||||
codingPath: [any CodingKey],
|
||||
allKeys: [Key]
|
||||
) {
|
||||
self.decoder = decoder
|
||||
self.codingPath = codingPath
|
||||
self.allKeys = allKeys
|
||||
}
|
||||
|
||||
// MARK: - Container Methods
|
||||
|
||||
func contains(_ key: Key) -> Bool {
|
||||
decoder.contains(key)
|
||||
}
|
||||
|
||||
func decodeNil(forKey key: Key) throws -> Bool {
|
||||
try decoder.decodeNil(for: key)
|
||||
}
|
||||
|
||||
func decode<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T {
|
||||
switch type {
|
||||
case is Date.Type:
|
||||
try decoder.decodeDate(for: key) as! T
|
||||
case let type as SQLiteRawRepresentable.Type:
|
||||
try decoder.decode(type, for: key) as! T
|
||||
default:
|
||||
try T(from: decoder.decoder(for: key))
|
||||
}
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy type: NestedKey.Type,
|
||||
forKey key: Key
|
||||
) throws -> KeyedDecodingContainer<NestedKey> {
|
||||
let info = """
|
||||
Attempted to decode a nested keyed container for key '\(key.stringValue)',
|
||||
but the value cannot be represented as a keyed container.
|
||||
"""
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
KeyedDecodingContainer<NestedKey>.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(
|
||||
forKey key: Key
|
||||
) throws -> any UnkeyedDecodingContainer {
|
||||
let info = """
|
||||
Attempted to decode a nested unkeyed container for key '\(key.stringValue)',
|
||||
but the value cannot be represented as an unkeyed container.
|
||||
"""
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
UnkeyedDecodingContainer.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
func superDecoder() throws -> any Swift.Decoder {
|
||||
let info = """
|
||||
Attempted to get a superDecoder,
|
||||
but SQLiteRowDecoder does not support superDecoding.
|
||||
"""
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(Swift.Decoder.self, context)
|
||||
}
|
||||
|
||||
func superDecoder(forKey key: Key) throws -> any Swift.Decoder {
|
||||
let info = """
|
||||
Attempted to get a superDecoder for key '\(key.stringValue)',
|
||||
but SQLiteRowDecoder does not support nested structures.
|
||||
"""
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(Swift.Decoder.self, context)
|
||||
}
|
||||
}
|
||||
104
Sources/DLCDecoder/Classes/MultiRowDecoder.swift
Normal file
104
Sources/DLCDecoder/Classes/MultiRowDecoder.swift
Normal file
@@ -0,0 +1,104 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
public final class MultiRowDecoder: RowDecoder {
|
||||
// MARK: - Properties
|
||||
|
||||
public let dateDecoder: any DateDecoder
|
||||
public let sqliteData: [SQLiteRow]
|
||||
public let codingPath: [any CodingKey]
|
||||
public let userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
public var count: Int? { sqliteData.count }
|
||||
|
||||
// MARK: Inits
|
||||
|
||||
public init(
|
||||
dateDecoder: any DateDecoder,
|
||||
sqliteData: [SQLiteRow],
|
||||
codingPath: [any CodingKey],
|
||||
userInfo: [CodingUserInfoKey: Any]
|
||||
) {
|
||||
self.dateDecoder = dateDecoder
|
||||
self.sqliteData = sqliteData
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
// MARK: Methods
|
||||
|
||||
public func decodeNil(for key: any CodingKey) throws -> Bool {
|
||||
let info = "Attempted to decode nil, but it's not supported for an array of rows."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.dataCorrupted(context)
|
||||
}
|
||||
|
||||
public func decodeDate(for key: any CodingKey) throws -> Date {
|
||||
return try decode(Date.self, for: key)
|
||||
}
|
||||
|
||||
public func decode<T: SQLiteRawRepresentable>(
|
||||
_ type: T.Type,
|
||||
for key: any CodingKey
|
||||
) throws -> T {
|
||||
let info = "Expected a type of \(type), but found an array of rows."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(type, context)
|
||||
}
|
||||
|
||||
public func decoder(for key: any CodingKey) throws -> any Decoder {
|
||||
guard let index = key.intValue else {
|
||||
let info = "Expected an integer key, but found a non-integer key."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.keyNotFound(key, context)
|
||||
}
|
||||
return SingleRowDecoder(
|
||||
dateDecoder: dateDecoder,
|
||||
sqliteData: sqliteData[index],
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo
|
||||
)
|
||||
}
|
||||
|
||||
public func container<Key: CodingKey>(
|
||||
keyedBy type: Key.Type
|
||||
) throws -> KeyedDecodingContainer<Key> {
|
||||
let info = "Expected a keyed container, but found an array of rows."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
KeyedDecodingContainer<Key>.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
public func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||
UnkeyedContainer(
|
||||
decoder: self,
|
||||
codingPath: codingPath
|
||||
)
|
||||
}
|
||||
|
||||
public func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||
let info = "Expected a single value container, but found an array of rows."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
SingleValueDecodingContainer.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
135
Sources/DLCDecoder/Classes/SingleRowDecoder.swift
Normal file
135
Sources/DLCDecoder/Classes/SingleRowDecoder.swift
Normal file
@@ -0,0 +1,135 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCCommon
|
||||
|
||||
public final class SingleRowDecoder: RowDecoder, KeyCheckingDecoder {
|
||||
// MARK: - Properties
|
||||
|
||||
public let dateDecoder: any DateDecoder
|
||||
public let sqliteData: SQLiteRow
|
||||
public let codingPath: [any CodingKey]
|
||||
public let userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
public var count: Int? { sqliteData.count }
|
||||
|
||||
// MARK: Inits
|
||||
|
||||
public init(
|
||||
dateDecoder: any DateDecoder,
|
||||
sqliteData: SQLiteRow,
|
||||
codingPath: [any CodingKey],
|
||||
userInfo: [CodingUserInfoKey: Any]
|
||||
) {
|
||||
self.dateDecoder = dateDecoder
|
||||
self.sqliteData = sqliteData
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
public func contains(_ key: any CodingKey) -> Bool {
|
||||
sqliteData.contains(key)
|
||||
}
|
||||
|
||||
public func decodeNil(for key: any CodingKey) throws -> Bool {
|
||||
guard sqliteData.contains(key) else {
|
||||
let info = "No value associated with key \(key)."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.keyNotFound(key, context)
|
||||
}
|
||||
return sqliteData[key] == .null
|
||||
}
|
||||
|
||||
public func decodeDate(for key: any CodingKey) throws -> Date {
|
||||
try dateDecoder.decode(from: self, for: key)
|
||||
}
|
||||
|
||||
public func decode<T: SQLiteRawRepresentable>(
|
||||
_ type: T.Type,
|
||||
for key: any CodingKey
|
||||
) throws -> T {
|
||||
guard let value = sqliteData[key] else {
|
||||
let info = "No value associated with key \(key)."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.keyNotFound(key, context)
|
||||
}
|
||||
|
||||
guard value != .null else {
|
||||
let info = "Cannot get value of type \(T.self), found null value instead."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.valueNotFound(type, context)
|
||||
}
|
||||
|
||||
guard let result = T(value) else {
|
||||
let info = "Expected to decode \(T.self) but found an \(value) instead."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(type, context)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
public func decoder(for key: any CodingKey) throws -> any Decoder {
|
||||
guard let data = sqliteData[key] else {
|
||||
let info = "No value associated with key \(key)."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.keyNotFound(key, context)
|
||||
}
|
||||
return SingleValueDecoder(
|
||||
dateDecoder: dateDecoder,
|
||||
sqliteData: data,
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo
|
||||
)
|
||||
}
|
||||
|
||||
public func container<Key: CodingKey>(
|
||||
keyedBy type: Key.Type
|
||||
) throws -> KeyedDecodingContainer<Key> {
|
||||
let allKeys = sqliteData.compactMap { (column, _) in
|
||||
Key(stringValue: column)
|
||||
}
|
||||
let container = KeyedContainer(
|
||||
decoder: self,
|
||||
codingPath: codingPath,
|
||||
allKeys: allKeys
|
||||
)
|
||||
return KeyedDecodingContainer(container)
|
||||
}
|
||||
|
||||
public func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||
UnkeyedContainer(
|
||||
decoder: self,
|
||||
codingPath: codingPath
|
||||
)
|
||||
}
|
||||
|
||||
public func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||
let info = "Expected a single value container, but found a row value."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
SingleValueDecodingContainer.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
36
Sources/DLCDecoder/Classes/SingleValueContainer.swift
Normal file
36
Sources/DLCDecoder/Classes/SingleValueContainer.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
final class SingleValueContainer<Decoder: ValueDecoder>: Container, SingleValueDecodingContainer {
|
||||
// MARK: - Properties
|
||||
|
||||
let decoder: Decoder
|
||||
let codingPath: [any CodingKey]
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(
|
||||
decoder: Decoder,
|
||||
codingPath: [any CodingKey]
|
||||
) {
|
||||
self.decoder = decoder
|
||||
self.codingPath = codingPath
|
||||
}
|
||||
|
||||
// MARK: - Container Methods
|
||||
|
||||
func decodeNil() -> Bool {
|
||||
decoder.decodeNil()
|
||||
}
|
||||
|
||||
func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
||||
switch type {
|
||||
case is Date.Type:
|
||||
try decoder.decodeDate() as! T
|
||||
case let type as SQLiteRawRepresentable.Type:
|
||||
try decoder.decode(type) as! T
|
||||
default:
|
||||
try T(from: decoder)
|
||||
}
|
||||
}
|
||||
}
|
||||
87
Sources/DLCDecoder/Classes/SingleValueDecoder.swift
Normal file
87
Sources/DLCDecoder/Classes/SingleValueDecoder.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
final class SingleValueDecoder: ValueDecoder {
|
||||
// MARK: - Properties
|
||||
|
||||
let dateDecoder: any DateDecoder
|
||||
let sqliteData: SQLiteRawValue
|
||||
let codingPath: [any CodingKey]
|
||||
let userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(
|
||||
dateDecoder: any DateDecoder,
|
||||
sqliteData: SQLiteRawValue,
|
||||
codingPath: [any CodingKey],
|
||||
userInfo: [CodingUserInfoKey: Any]
|
||||
) {
|
||||
self.dateDecoder = dateDecoder
|
||||
self.sqliteData = sqliteData
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func decodeNil() -> Bool {
|
||||
sqliteData == .null
|
||||
}
|
||||
|
||||
func decodeDate() throws -> Date {
|
||||
try dateDecoder.decode(from: self)
|
||||
}
|
||||
|
||||
func decode<T: SQLiteRawRepresentable>(_ type: T.Type) throws -> T {
|
||||
guard sqliteData != .null else {
|
||||
let info = "Cannot get value of type \(T.self), found null value instead."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.valueNotFound(type, context)
|
||||
}
|
||||
|
||||
guard let result = type.init(sqliteData) else {
|
||||
let info = "Expected to decode \(T.self) but found an \(sqliteData) instead."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(type, context)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func container<Key: CodingKey>(
|
||||
keyedBy type: Key.Type
|
||||
) throws -> KeyedDecodingContainer<Key> {
|
||||
let info = "Expected a keyed container, but found a single value."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
KeyedDecodingContainer<Key>.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||
let info = "Expected a unkeyed container, but found a single value."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
UnkeyedDecodingContainer.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||
SingleValueContainer(decoder: self, codingPath: codingPath)
|
||||
}
|
||||
}
|
||||
121
Sources/DLCDecoder/Classes/UnkeyedContainer.swift
Normal file
121
Sources/DLCDecoder/Classes/UnkeyedContainer.swift
Normal file
@@ -0,0 +1,121 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCCommon
|
||||
|
||||
final class UnkeyedContainer<Decoder: RowDecoder>: Container, UnkeyedDecodingContainer {
|
||||
// MARK: - Properties
|
||||
|
||||
let decoder: Decoder
|
||||
let codingPath: [any CodingKey]
|
||||
|
||||
var count: Int? {
|
||||
decoder.count
|
||||
}
|
||||
|
||||
var isAtEnd: Bool {
|
||||
currentIndex >= count ?? 0
|
||||
}
|
||||
|
||||
private(set) var currentIndex: Int = 0
|
||||
|
||||
private var currentKey: CodingKey {
|
||||
RowCodingKey(intValue: currentIndex)
|
||||
}
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(
|
||||
decoder: Decoder,
|
||||
codingPath: [any CodingKey]
|
||||
) {
|
||||
self.decoder = decoder
|
||||
self.codingPath = codingPath
|
||||
}
|
||||
|
||||
// MARK: - Container Methods
|
||||
|
||||
func decodeNil() throws -> Bool {
|
||||
try checkIsAtEnd(Optional<Any>.self)
|
||||
if try decoder.decodeNil(for: currentKey) {
|
||||
currentIndex += 1
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
||||
try checkIsAtEnd(type)
|
||||
defer { currentIndex += 1 }
|
||||
|
||||
switch type {
|
||||
case is Date.Type:
|
||||
return try decoder.decodeDate(for: currentKey) as! T
|
||||
case let type as SQLiteRawRepresentable.Type:
|
||||
return try decoder.decode(type, for: currentKey) as! T
|
||||
default:
|
||||
return try T(from: decoder.decoder(for: currentKey))
|
||||
}
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy type: NestedKey.Type
|
||||
) throws -> KeyedDecodingContainer<NestedKey> {
|
||||
let info = """
|
||||
Attempted to decode a nested keyed container,
|
||||
but the value cannot be represented as a keyed container.
|
||||
"""
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [currentKey],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
KeyedDecodingContainer<NestedKey>.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||
let info = """
|
||||
Attempted to decode a nested unkeyed container,
|
||||
but the value cannot be represented as an unkeyed container.
|
||||
"""
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [currentKey],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(
|
||||
UnkeyedDecodingContainer.self,
|
||||
context
|
||||
)
|
||||
}
|
||||
|
||||
func superDecoder() throws -> any Swift.Decoder {
|
||||
let info = """
|
||||
Attempted to get a superDecoder,
|
||||
but SQLiteRowDecoder does not support superDecoding.
|
||||
"""
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [currentKey],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.typeMismatch(Swift.Decoder.self, context)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private Methods
|
||||
|
||||
private extension UnkeyedContainer {
|
||||
@inline(__always)
|
||||
func checkIsAtEnd<T>(_ type: T.Type) throws {
|
||||
guard !isAtEnd else {
|
||||
let info = "Unkeyed container is at end."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: codingPath + [currentKey],
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.valueNotFound(type, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
6
Sources/DLCDecoder/Protocols/Container.swift
Normal file
6
Sources/DLCDecoder/Protocols/Container.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
protocol Container {
|
||||
associatedtype Decoder: Swift.Decoder
|
||||
var decoder: Decoder { get }
|
||||
}
|
||||
6
Sources/DLCDecoder/Protocols/DateDecoder.swift
Normal file
6
Sources/DLCDecoder/Protocols/DateDecoder.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
public protocol DateDecoder {
|
||||
func decode(from decoder: any ValueDecoder) throws -> Date
|
||||
func decode(from decoder: any RowDecoder, for key: any CodingKey) throws -> Date
|
||||
}
|
||||
8
Sources/DLCDecoder/Protocols/Decoder.swift
Normal file
8
Sources/DLCDecoder/Protocols/Decoder.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
public protocol Decoder: Swift.Decoder {
|
||||
associatedtype SQLiteData
|
||||
|
||||
var dateDecoder: any DateDecoder { get }
|
||||
var sqliteData: SQLiteData { get }
|
||||
}
|
||||
5
Sources/DLCDecoder/Protocols/KeyCheckingDecoder.swift
Normal file
5
Sources/DLCDecoder/Protocols/KeyCheckingDecoder.swift
Normal file
@@ -0,0 +1,5 @@
|
||||
import Foundation
|
||||
|
||||
public protocol KeyCheckingDecoder: Decoder {
|
||||
func contains(_ key: CodingKey) -> Bool
|
||||
}
|
||||
11
Sources/DLCDecoder/Protocols/RowDecoder.swift
Normal file
11
Sources/DLCDecoder/Protocols/RowDecoder.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
public protocol RowDecoder: Decoder {
|
||||
var count: Int? { get }
|
||||
|
||||
func decodeNil(for key: CodingKey) throws -> Bool
|
||||
func decodeDate(for key: CodingKey) throws -> Date
|
||||
func decode<T: SQLiteRawRepresentable>(_ type: T.Type, for key: CodingKey) throws -> T
|
||||
func decoder(for key: CodingKey) throws -> any Decoder
|
||||
}
|
||||
8
Sources/DLCDecoder/Protocols/ValueDecoder.swift
Normal file
8
Sources/DLCDecoder/Protocols/ValueDecoder.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
public protocol ValueDecoder: Decoder {
|
||||
func decodeNil() -> Bool
|
||||
func decodeDate() throws -> Date
|
||||
func decode<T: SQLiteRawRepresentable>(_ type: T.Type) throws -> T
|
||||
}
|
||||
37
Sources/DLCEncoder/Classes/FailedEncoder.swift
Normal file
37
Sources/DLCEncoder/Classes/FailedEncoder.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
|
||||
private import DLCCommon
|
||||
|
||||
final class FailedEncoder: Swift.Encoder {
|
||||
// MARK: - Properties
|
||||
|
||||
let codingPath: [any CodingKey]
|
||||
let userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(
|
||||
codingPath: [any CodingKey],
|
||||
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||
) {
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func container<Key: CodingKey>(
|
||||
keyedBy type: Key.Type
|
||||
) -> KeyedEncodingContainer<Key> {
|
||||
let container = FailedEncodingContainer<Key>(codingPath: codingPath)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath)
|
||||
}
|
||||
}
|
||||
84
Sources/DLCEncoder/Classes/FailedEncodingContainer.swift
Normal file
84
Sources/DLCEncoder/Classes/FailedEncodingContainer.swift
Normal file
@@ -0,0 +1,84 @@
|
||||
import Foundation
|
||||
private import DLCCommon
|
||||
|
||||
final class FailedEncodingContainer<Key: CodingKey>: SingleValueEncodingContainer, UnkeyedEncodingContainer, KeyedEncodingContainerProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
let codingPath: [any CodingKey]
|
||||
let count: Int = 0
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(codingPath: [any CodingKey]) {
|
||||
self.codingPath = codingPath
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func encodeNil() throws {
|
||||
throw encodingError(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func encodeNil(forKey key: Key) throws {
|
||||
throw encodingError(codingPath: codingPath + [key])
|
||||
}
|
||||
|
||||
func encode<T: Encodable>(_ value: T) throws {
|
||||
throw encodingError(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
|
||||
throw encodingError(codingPath: codingPath + [key])
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy keyType: NestedKey.Type
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
let container = FailedEncodingContainer<NestedKey>(
|
||||
codingPath: codingPath
|
||||
)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy keyType: NestedKey.Type,
|
||||
forKey key: Key
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
let container = FailedEncodingContainer<NestedKey>(
|
||||
codingPath: codingPath + [key]
|
||||
)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath + [key])
|
||||
}
|
||||
|
||||
func superEncoder() -> any Swift.Encoder {
|
||||
FailedEncoder(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func superEncoder(forKey key: Key) -> any Swift.Encoder {
|
||||
FailedEncoder(codingPath: codingPath + [key])
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension FailedEncodingContainer {
|
||||
func encodingError(
|
||||
_ function: String = #function,
|
||||
codingPath: [any CodingKey]
|
||||
) -> Error {
|
||||
let info = "\(function) is not supported for this encoding path."
|
||||
let context = EncodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
return EncodingError.invalidValue((), context)
|
||||
}
|
||||
}
|
||||
201
Sources/DLCEncoder/Classes/KeyedContainer.swift
Normal file
201
Sources/DLCEncoder/Classes/KeyedContainer.swift
Normal file
@@ -0,0 +1,201 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCCommon
|
||||
|
||||
final class KeyedContainer<Encoder: RowEncoder, Key: CodingKey>: Container, KeyedEncodingContainerProtocol {
|
||||
// MARK: - Properties
|
||||
|
||||
let encoder: Encoder
|
||||
let codingPath: [any CodingKey]
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(
|
||||
encoder: Encoder,
|
||||
codingPath: [any CodingKey]
|
||||
) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
}
|
||||
|
||||
// MARK: - Container Methods
|
||||
|
||||
func encodeNil(forKey key: Key) throws {
|
||||
try encoder.encodeNil(for: key)
|
||||
}
|
||||
|
||||
func encode<T: Encodable>(_ value: T, forKey key: Key) throws {
|
||||
switch value {
|
||||
case let value as Date:
|
||||
try encoder.encodeDate(value, for: key)
|
||||
case let value as SQLiteRawBindable:
|
||||
try encoder.encode(value, for: key)
|
||||
default:
|
||||
let valueEncoder = try encoder.encoder(for: key)
|
||||
try value.encode(to: valueEncoder)
|
||||
try encoder.set(valueEncoder.sqliteData, for: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: Bool?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: String?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: Double?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: Float?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: Int?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: Int8?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: Int16?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: Int32?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: Int64?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: UInt?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: UInt8?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: UInt16?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: UInt32?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent(_ value: UInt64?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func encodeIfPresent<T: Encodable>(_ value: T?, forKey key: Key) throws {
|
||||
switch value {
|
||||
case .some(let value):
|
||||
try encode(value, forKey: key)
|
||||
case .none:
|
||||
try encodeNil(forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy keyType: NestedKey.Type,
|
||||
forKey key: Key
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
let container = FailedEncodingContainer<NestedKey>(
|
||||
codingPath: codingPath + [key]
|
||||
)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(
|
||||
forKey key: Key
|
||||
) -> any UnkeyedEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(
|
||||
codingPath: codingPath + [key]
|
||||
)
|
||||
}
|
||||
|
||||
func superEncoder() -> any Swift.Encoder {
|
||||
FailedEncoder(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func superEncoder(forKey key: Key) -> any Swift.Encoder {
|
||||
FailedEncoder(codingPath: codingPath + [key])
|
||||
}
|
||||
}
|
||||
95
Sources/DLCEncoder/Classes/MultiRowEncoder.swift
Normal file
95
Sources/DLCEncoder/Classes/MultiRowEncoder.swift
Normal file
@@ -0,0 +1,95 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCCommon
|
||||
|
||||
public final class MultiRowEncoder: RowEncoder {
|
||||
// MARK: - Properties
|
||||
|
||||
public let dateEncoder: any DateEncoder
|
||||
public let codingPath: [any CodingKey]
|
||||
public let userInfo: [CodingUserInfoKey : Any]
|
||||
|
||||
public private(set) var sqliteData = [SQLiteRow]()
|
||||
|
||||
public var count: Int { sqliteData.count }
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
public init(
|
||||
dateEncoder: any DateEncoder,
|
||||
codingPath: [any CodingKey],
|
||||
userInfo: [CodingUserInfoKey : Any]
|
||||
) {
|
||||
self.dateEncoder = dateEncoder
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
public func set(_ value: Any, for key: any CodingKey) throws {
|
||||
guard let value = value as? SQLiteRow else {
|
||||
let info = "Expected value of type \(SQLiteRow.self)"
|
||||
let context = EncodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw EncodingError.invalidValue(value, context)
|
||||
}
|
||||
sqliteData.append(value)
|
||||
}
|
||||
|
||||
public func encodeNil(for key: any CodingKey) throws {
|
||||
let value = Optional<Any>.none as Any
|
||||
let info = "Attempted to encode nil, but it's not supported."
|
||||
let context = EncodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw EncodingError.invalidValue(value, context)
|
||||
}
|
||||
|
||||
public func encodeDate(_ date: Date, for key: any CodingKey) throws {
|
||||
let info = "Attempted to encode Date, but it's not supported."
|
||||
let context = EncodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw EncodingError.invalidValue(date, context)
|
||||
}
|
||||
|
||||
public func encode<T: SQLiteRawBindable>(_ value: T, for key: any CodingKey) throws {
|
||||
let info = "Attempted to encode \(T.self), but it's not supported."
|
||||
let context = EncodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw EncodingError.invalidValue(value, context)
|
||||
}
|
||||
|
||||
public func encoder(for key: any CodingKey) throws -> any Encoder {
|
||||
SingleRowEncoder(
|
||||
dateEncoder: dateEncoder,
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo
|
||||
)
|
||||
}
|
||||
|
||||
public func container<Key: CodingKey>(
|
||||
keyedBy type: Key.Type
|
||||
) -> KeyedEncodingContainer<Key> {
|
||||
let container = FailedEncodingContainer<Key>(
|
||||
codingPath: codingPath
|
||||
)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
UnkeyedContainer(encoder: self, codingPath: codingPath)
|
||||
}
|
||||
|
||||
public func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath)
|
||||
}
|
||||
}
|
||||
79
Sources/DLCEncoder/Classes/SingleRowEncoder.swift
Normal file
79
Sources/DLCEncoder/Classes/SingleRowEncoder.swift
Normal file
@@ -0,0 +1,79 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCCommon
|
||||
|
||||
public final class SingleRowEncoder: RowEncoder {
|
||||
// MARK: - Properties
|
||||
|
||||
public let dateEncoder: any DateEncoder
|
||||
public let codingPath: [any CodingKey]
|
||||
public let userInfo: [CodingUserInfoKey : Any]
|
||||
|
||||
public private(set) var sqliteData = SQLiteRow()
|
||||
|
||||
public var count: Int { sqliteData.count }
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
public init(
|
||||
dateEncoder: any DateEncoder,
|
||||
codingPath: [any CodingKey],
|
||||
userInfo: [CodingUserInfoKey : Any],
|
||||
) {
|
||||
self.dateEncoder = dateEncoder
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
public func set(_ value: Any, for key: any CodingKey) throws {
|
||||
guard let value = value as? SQLiteRawValue else {
|
||||
let info = "The value does not match \(SQLiteRawValue.self)"
|
||||
let context = EncodingError.Context(
|
||||
codingPath: codingPath + [key],
|
||||
debugDescription: info
|
||||
)
|
||||
throw EncodingError.invalidValue(value, context)
|
||||
}
|
||||
sqliteData[key] = value
|
||||
}
|
||||
|
||||
public func encodeNil(for key: any CodingKey) throws {
|
||||
sqliteData[key] = .null
|
||||
}
|
||||
|
||||
public func encodeDate(_ date: Date, for key: any CodingKey) throws {
|
||||
try dateEncoder.encode(date, for: key, to: self)
|
||||
}
|
||||
|
||||
public func encode<T: SQLiteRawBindable>(_ value: T, for key: any CodingKey) throws {
|
||||
sqliteData[key] = value.sqliteRawValue
|
||||
}
|
||||
|
||||
public func encoder(for key: any CodingKey) throws -> any Encoder {
|
||||
SingleValueEncoder(
|
||||
dateEncoder: dateEncoder,
|
||||
codingPath: codingPath + [key],
|
||||
userInfo: userInfo
|
||||
)
|
||||
}
|
||||
|
||||
public func container<Key: CodingKey>(
|
||||
keyedBy type: Key.Type
|
||||
) -> KeyedEncodingContainer<Key> {
|
||||
let container = KeyedContainer<SingleRowEncoder, Key>(
|
||||
encoder: self, codingPath: codingPath
|
||||
)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
public func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath)
|
||||
}
|
||||
|
||||
public func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath)
|
||||
}
|
||||
}
|
||||
36
Sources/DLCEncoder/Classes/SingleValueContainer.swift
Normal file
36
Sources/DLCEncoder/Classes/SingleValueContainer.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
final class SingleValueContainer<Encoder: ValueEncoder>: Container, SingleValueEncodingContainer {
|
||||
// MARK: - Properties
|
||||
|
||||
let encoder: Encoder
|
||||
let codingPath: [any CodingKey]
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(
|
||||
encoder: Encoder,
|
||||
codingPath: [any CodingKey]
|
||||
) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
}
|
||||
|
||||
// MARK: - Container Methods
|
||||
|
||||
func encodeNil() throws {
|
||||
try encoder.encodeNil()
|
||||
}
|
||||
|
||||
func encode<T: Encodable>(_ value: T) throws {
|
||||
switch value {
|
||||
case let value as Date:
|
||||
try encoder.encodeDate(value)
|
||||
case let value as SQLiteRawBindable:
|
||||
try encoder.encode(value)
|
||||
default:
|
||||
try value.encode(to: encoder)
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Sources/DLCEncoder/Classes/SingleValueEncoder.swift
Normal file
55
Sources/DLCEncoder/Classes/SingleValueEncoder.swift
Normal file
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCCommon
|
||||
|
||||
final class SingleValueEncoder: ValueEncoder {
|
||||
// MARK: - Properties
|
||||
|
||||
let dateEncoder: any DateEncoder
|
||||
let codingPath: [any CodingKey]
|
||||
let userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
private(set) var sqliteData: SQLiteRawValue?
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(
|
||||
dateEncoder: any DateEncoder,
|
||||
codingPath: [any CodingKey],
|
||||
userInfo: [CodingUserInfoKey: Any]
|
||||
) {
|
||||
self.dateEncoder = dateEncoder
|
||||
self.codingPath = codingPath
|
||||
self.userInfo = userInfo
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func encodeNil() throws {
|
||||
sqliteData = .null
|
||||
}
|
||||
|
||||
func encodeDate(_ date: Date) throws {
|
||||
try dateEncoder.encode(date, to: self)
|
||||
}
|
||||
|
||||
func encode<T: SQLiteRawBindable>(_ value: T) throws {
|
||||
sqliteData = value.sqliteRawValue
|
||||
}
|
||||
|
||||
func container<Key: CodingKey>(
|
||||
keyedBy type: Key.Type
|
||||
) -> KeyedEncodingContainer<Key> {
|
||||
let container = FailedEncodingContainer<Key>(codingPath: codingPath)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||
SingleValueContainer(encoder: self, codingPath: codingPath)
|
||||
}
|
||||
}
|
||||
59
Sources/DLCEncoder/Classes/UnkeyedContainer.swift
Normal file
59
Sources/DLCEncoder/Classes/UnkeyedContainer.swift
Normal file
@@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCCommon
|
||||
|
||||
final class UnkeyedContainer<Encoder: RowEncoder>: Container, UnkeyedEncodingContainer {
|
||||
// MARK: - Properties
|
||||
|
||||
let encoder: Encoder
|
||||
let codingPath: [any CodingKey]
|
||||
var count: Int { encoder.count }
|
||||
|
||||
private var currentKey: CodingKey {
|
||||
RowCodingKey(intValue: count)
|
||||
}
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
init(encoder: Encoder, codingPath: [any CodingKey]) {
|
||||
self.encoder = encoder
|
||||
self.codingPath = codingPath
|
||||
}
|
||||
|
||||
// MARK: - Container Methods
|
||||
|
||||
func encodeNil() throws {
|
||||
}
|
||||
|
||||
func encode<T: Encodable>(_ value: T) throws {
|
||||
if let value = value as? Flattenable {
|
||||
if let value = value.flattened() as? Encodable {
|
||||
try encode(value)
|
||||
} else {
|
||||
try encodeNil()
|
||||
}
|
||||
} else {
|
||||
let valueEncoder = try encoder.encoder(for: currentKey)
|
||||
try value.encode(to: valueEncoder)
|
||||
try encoder.set(valueEncoder.sqliteData, for: currentKey)
|
||||
}
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey: CodingKey>(
|
||||
keyedBy keyType: NestedKey.Type
|
||||
) -> KeyedEncodingContainer<NestedKey> {
|
||||
let container = FailedEncodingContainer<NestedKey>(
|
||||
codingPath: codingPath
|
||||
)
|
||||
return KeyedEncodingContainer(container)
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||
FailedEncodingContainer<RowCodingKey>(codingPath: codingPath)
|
||||
}
|
||||
|
||||
func superEncoder() -> any Swift.Encoder {
|
||||
FailedEncoder(codingPath: codingPath)
|
||||
}
|
||||
}
|
||||
6
Sources/DLCEncoder/Protocols/Container.swift
Normal file
6
Sources/DLCEncoder/Protocols/Container.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
protocol Container {
|
||||
associatedtype Encoder: Swift.Encoder
|
||||
var encoder: Encoder { get }
|
||||
}
|
||||
6
Sources/DLCEncoder/Protocols/DateEncoder.swift
Normal file
6
Sources/DLCEncoder/Protocols/DateEncoder.swift
Normal file
@@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
public protocol DateEncoder {
|
||||
func encode(_ date: Date, to encoder: any ValueEncoder) throws
|
||||
func encode(_ date: Date, for key: any CodingKey, to encoder: any RowEncoder) throws
|
||||
}
|
||||
8
Sources/DLCEncoder/Protocols/Encoder.swift
Normal file
8
Sources/DLCEncoder/Protocols/Encoder.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
|
||||
public protocol Encoder: Swift.Encoder {
|
||||
associatedtype SQLiteData
|
||||
|
||||
var dateEncoder: any DateEncoder { get }
|
||||
var sqliteData: SQLiteData { get }
|
||||
}
|
||||
15
Sources/DLCEncoder/Protocols/Flattenable.swift
Normal file
15
Sources/DLCEncoder/Protocols/Flattenable.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
import Foundation
|
||||
|
||||
protocol Flattenable {
|
||||
func flattened() -> Any?
|
||||
}
|
||||
|
||||
extension Optional: Flattenable {
|
||||
func flattened() -> Any? {
|
||||
switch self {
|
||||
case .some(let x as Flattenable): x.flattened()
|
||||
case .some(let x): x
|
||||
case .none: nil
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Sources/DLCEncoder/Protocols/RowEncoder.swift
Normal file
12
Sources/DLCEncoder/Protocols/RowEncoder.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
public protocol RowEncoder: Encoder {
|
||||
var count: Int { get }
|
||||
|
||||
func set(_ value: Any, for key: CodingKey) throws
|
||||
func encodeNil(for key: CodingKey) throws
|
||||
func encodeDate(_ date: Date, for key: CodingKey) throws
|
||||
func encode<T: SQLiteRawBindable>(_ value: T, for key: CodingKey) throws
|
||||
func encoder(for key: CodingKey) throws -> any Encoder
|
||||
}
|
||||
8
Sources/DLCEncoder/Protocols/ValueEncoder.swift
Normal file
8
Sources/DLCEncoder/Protocols/ValueEncoder.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
public protocol ValueEncoder: Encoder {
|
||||
func encodeNil() throws
|
||||
func encodeDate(_ date: Date) throws
|
||||
func encode<T: SQLiteRawBindable>(_ value: T) throws
|
||||
}
|
||||
83
Sources/DataLiteCoder/Classes/DateDecoder.swift
Normal file
83
Sources/DataLiteCoder/Classes/DateDecoder.swift
Normal file
@@ -0,0 +1,83 @@
|
||||
import Foundation
|
||||
internal import DLCDecoder
|
||||
|
||||
extension RowDecoder {
|
||||
class DateDecoder: DLCDecoder.DateDecoder {
|
||||
typealias ValueDecoder = DLCDecoder.ValueDecoder
|
||||
typealias RowDecoder = DLCDecoder.RowDecoder
|
||||
|
||||
let strategy: DateDecodingStrategy
|
||||
|
||||
init(strategy: DateDecodingStrategy) {
|
||||
self.strategy = strategy
|
||||
}
|
||||
|
||||
func decode(
|
||||
from decoder: any ValueDecoder
|
||||
) throws -> Date {
|
||||
try decode(from: decoder) { decoder in
|
||||
try decoder.decode(Date.self)
|
||||
} _: { decoder in
|
||||
try decoder.decode(String.self)
|
||||
} _: { decoder in
|
||||
try decoder.decode(Int64.self)
|
||||
} _: { decoder in
|
||||
try decoder.decode(Double.self)
|
||||
}
|
||||
}
|
||||
|
||||
func decode(
|
||||
from decoder: any RowDecoder,
|
||||
for key: any CodingKey
|
||||
) throws -> Date {
|
||||
try decode(from: decoder) { decoder in
|
||||
try decoder.decode(Date.self, for: key)
|
||||
} _: { decoder in
|
||||
try decoder.decode(String.self, for: key)
|
||||
} _: { decoder in
|
||||
try decoder.decode(Int64.self, for: key)
|
||||
} _: { decoder in
|
||||
try decoder.decode(Double.self, for: key)
|
||||
}
|
||||
}
|
||||
|
||||
private func decode<D: Decoder>(
|
||||
from decoder: D,
|
||||
_ date: (D) throws -> Date,
|
||||
_ string: (D) throws -> String,
|
||||
_ int: (D) throws -> Int64,
|
||||
_ double: (D) throws -> Double
|
||||
) throws -> Date {
|
||||
switch strategy {
|
||||
case .deferredToDate:
|
||||
return try date(decoder)
|
||||
case .formatted(let dateFormatter):
|
||||
guard
|
||||
let date = dateFormatter.date(
|
||||
from: try string(decoder)
|
||||
)
|
||||
else {
|
||||
let info = "Date string does not match format expected by formatter."
|
||||
let context = DecodingError.Context(
|
||||
codingPath: decoder.codingPath,
|
||||
debugDescription: info
|
||||
)
|
||||
throw DecodingError.dataCorrupted(context)
|
||||
}
|
||||
return date
|
||||
case .millisecondsSince1970Int:
|
||||
let milliseconds = Double(try int(decoder))
|
||||
return Date(timeIntervalSince1970: milliseconds / 1000)
|
||||
case .millisecondsSince1970Double:
|
||||
let milliseconds = try double(decoder)
|
||||
return Date(timeIntervalSince1970: milliseconds / 1000)
|
||||
case .secondsSince1970Int:
|
||||
let seconds = Double(try int(decoder))
|
||||
return Date(timeIntervalSince1970: seconds)
|
||||
case .secondsSince1970Double:
|
||||
let seconds = try double(decoder)
|
||||
return Date(timeIntervalSince1970: seconds)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Sources/DataLiteCoder/Classes/DateEncoder.swift
Normal file
51
Sources/DataLiteCoder/Classes/DateEncoder.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
internal import DLCEncoder
|
||||
|
||||
extension RowEncoder {
|
||||
class DateEncoder: DLCEncoder.DateEncoder {
|
||||
typealias ValueEncoder = DLCEncoder.ValueEncoder
|
||||
typealias RowEncoder = DLCEncoder.RowEncoder
|
||||
|
||||
let strategy: DateEncodingStrategy
|
||||
|
||||
init(strategy: DateEncodingStrategy) {
|
||||
self.strategy = strategy
|
||||
}
|
||||
|
||||
func encode(
|
||||
_ date: Date,
|
||||
to encoder: any ValueEncoder
|
||||
) throws {
|
||||
let value = encodeValue(from: date)
|
||||
try encoder.encode(value)
|
||||
}
|
||||
|
||||
func encode(
|
||||
_ date: Date,
|
||||
for key: any CodingKey,
|
||||
to encoder: any RowEncoder
|
||||
) throws {
|
||||
let value = encodeValue(from: date)
|
||||
try encoder.encode(value, for: key)
|
||||
}
|
||||
|
||||
private func encodeValue(from date: Date) -> SQLiteRawBindable {
|
||||
switch strategy {
|
||||
case .deferredToDate:
|
||||
date
|
||||
case .formatted(let dateFormatter):
|
||||
dateFormatter.string(from: date)
|
||||
case .millisecondsSince1970Int:
|
||||
Int64(date.timeIntervalSince1970 * 1000)
|
||||
case .millisecondsSince1970Double:
|
||||
date.timeIntervalSince1970 * 1000
|
||||
case .secondsSince1970Int:
|
||||
Int64(date.timeIntervalSince1970)
|
||||
case .secondsSince1970Double:
|
||||
date.timeIntervalSince1970
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
253
Sources/DataLiteCoder/Classes/RowDecoder.swift
Normal file
253
Sources/DataLiteCoder/Classes/RowDecoder.swift
Normal file
@@ -0,0 +1,253 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCDecoder
|
||||
|
||||
/// Decoder that decodes SQLite values into Swift types conforming to the `Decodable` protocol.
|
||||
///
|
||||
/// ## Overview
|
||||
///
|
||||
/// Use `RowDecoder` to convert `SQLiteRow` or an array of `SQLiteRow` into Swift types that conform
|
||||
/// to the `Decodable` protocol.
|
||||
///
|
||||
/// ### Decode a Single Row
|
||||
///
|
||||
/// Use ``decode(_:from:)->T`` to decode a single `SQLiteRow` into a `Decodable` value.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct User: Decodable {
|
||||
/// var id: Int
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// do {
|
||||
/// var row: SQLiteRow = .init()
|
||||
/// row["id"] = .int(1)
|
||||
/// row["name"] = .text("John Doe")
|
||||
///
|
||||
/// let decoder = RowDecoder()
|
||||
/// let user = try decoder.decode(User.self, from: row)
|
||||
/// } catch {
|
||||
/// print("Decoding error:", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Decode a Row into an Array
|
||||
///
|
||||
/// Use ``decode(_:from:)->T`` to decode a row containing homogeneous values
|
||||
/// into an array of Swift values.
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// var row: SQLiteRow = .init()
|
||||
/// row["a"] = .int(10)
|
||||
/// row["b"] = .int(20)
|
||||
///
|
||||
/// let decoder = RowDecoder()
|
||||
/// let numbers = try decoder.decode([Int].self, from: row)
|
||||
/// } catch {
|
||||
/// print("Decoding error:", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Decode Multiple Rows
|
||||
///
|
||||
/// Use ``decode(_:from:)->[T]`` to decode an array of `SQLiteRow` into an array of `Decodable` values.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct User: Decodable {
|
||||
/// var id: Int
|
||||
/// var name: String
|
||||
/// }
|
||||
///
|
||||
/// do {
|
||||
/// let rows: [SQLiteRow] = fetchRows() // Fetch rows from a database
|
||||
/// let decoder = RowDecoder()
|
||||
/// let users = try decoder.decode([User].self, from: rows)
|
||||
/// } catch {
|
||||
/// print("Decoding error:", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Customize Decoding with User Info
|
||||
///
|
||||
/// Use the ``userInfo`` property to pass context or flags to decoding logic.
|
||||
///
|
||||
/// First, define a custom key:
|
||||
///
|
||||
/// ```swift
|
||||
/// extension CodingUserInfoKey {
|
||||
/// static let isAdmin = CodingUserInfoKey(
|
||||
/// rawValue: "isAdmin"
|
||||
/// )!
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Then access it inside your model:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct User: Decodable {
|
||||
/// enum CodingKeys: String, CodingKey {
|
||||
/// case id, name
|
||||
/// }
|
||||
///
|
||||
/// var id: Int
|
||||
/// var name: String
|
||||
/// var isAdmin: Bool
|
||||
///
|
||||
/// init(from decoder: Decoder) throws {
|
||||
/// let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
/// id = try container.decode(Int.self, forKey: .id)
|
||||
/// name = try container.decode(String.self, forKey: .name)
|
||||
///
|
||||
/// isAdmin = (decoder.userInfo[.isAdmin] as? Bool) ?? false
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Pass the value before decoding:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// var row: SQLiteRow = .init()
|
||||
/// row["id"] = .int(1)
|
||||
/// row["name"] = .text("John Doe")
|
||||
///
|
||||
/// let decoder = RowDecoder()
|
||||
/// decoder.userInfo[.isAdmin] = true
|
||||
///
|
||||
/// let user = try decoder.decode(User.self, from: row)
|
||||
/// } catch {
|
||||
/// print("Decoding error:", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Date Decoding Strategy
|
||||
///
|
||||
/// Use the ``dateDecodingStrategy`` property to customize how `Date` values are decoded.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Log: Decodable {
|
||||
/// let timestamp: Date
|
||||
/// }
|
||||
///
|
||||
/// do {
|
||||
/// var row: SQLiteRow = .init()
|
||||
/// row["timestamp"] = .int(1_700_000_000)
|
||||
///
|
||||
/// let decoder = RowDecoder()
|
||||
/// decoder.dateDecodingStrategy = .secondsSince1970Int
|
||||
///
|
||||
/// let log = try decoder.decode(Log.self, from: row)
|
||||
/// } catch {
|
||||
/// print("Decoding error:", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// For more detailed information, see ``DateDecodingStrategy``.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating a Decoder
|
||||
///
|
||||
/// - ``init(userInfo:dateDecodingStrategy:)``
|
||||
///
|
||||
/// ### Decoding
|
||||
///
|
||||
/// - ``decode(_:from:)->T``
|
||||
/// - ``decode(_:from:)->[T]``
|
||||
///
|
||||
/// ### Customizing Decoding
|
||||
///
|
||||
/// - ``userInfo``
|
||||
///
|
||||
/// ### Decoding Dates
|
||||
///
|
||||
/// - ``dateDecodingStrategy``
|
||||
/// - ``DateDecodingStrategy``
|
||||
public final class RowDecoder {
|
||||
// MARK: - Properties
|
||||
|
||||
/// A dictionary containing user-defined information accessible during decoding.
|
||||
///
|
||||
/// This dictionary allows passing additional context or settings that can influence
|
||||
/// the decoding process. Values stored here are accessible inside custom `Decodable`
|
||||
/// implementations through the `Decoder`'s `userInfo` property.
|
||||
public var userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
/// The strategy used to decode `Date` values from SQLite data.
|
||||
///
|
||||
/// Determines how `Date` values are decoded from SQLite storage, supporting
|
||||
/// different formats such as timestamps or custom representations.
|
||||
public var dateDecodingStrategy: DateDecodingStrategy
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
/// Initializes a new `RowDecoder` instance with optional configuration.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userInfo: A dictionary with user-defined information accessible during decoding.
|
||||
/// - dateDecodingStrategy: The strategy to decode `Date` values. Default is `.deferredToDate`.
|
||||
public init(
|
||||
userInfo: [CodingUserInfoKey: Any] = [:],
|
||||
dateDecodingStrategy: DateDecodingStrategy = .deferredToDate
|
||||
) {
|
||||
self.userInfo = userInfo
|
||||
self.dateDecodingStrategy = dateDecodingStrategy
|
||||
}
|
||||
|
||||
// MARK: - Decoding Methods
|
||||
|
||||
/// Decodes a single `SQLiteRow` into an instance of the specified `Decodable` type.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - type: The target type conforming to `Decodable`.
|
||||
/// - row: The SQLite row to decode.
|
||||
/// - Returns: An instance of the specified type decoded from the row.
|
||||
/// - Throws: An error if decoding fails.
|
||||
public func decode<T: Decodable>(
|
||||
_ type: T.Type,
|
||||
from row: SQLiteRow
|
||||
) throws -> T {
|
||||
let dateDecoder = DateDecoder(strategy: dateDecodingStrategy)
|
||||
let decoder = SingleRowDecoder(
|
||||
dateDecoder: dateDecoder,
|
||||
sqliteData: row,
|
||||
codingPath: [],
|
||||
userInfo: userInfo
|
||||
)
|
||||
return try T(from: decoder)
|
||||
}
|
||||
|
||||
/// Decodes an array of `SQLiteRow` values into an array of the specified `Decodable` type.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - type: The array type containing the element type to decode.
|
||||
/// - rows: The array of SQLite rows to decode.
|
||||
/// - Returns: An array of decoded instances.
|
||||
/// - Throws: An error if decoding any row fails.
|
||||
public func decode<T: Decodable>(
|
||||
_ type: [T].Type,
|
||||
from rows: [SQLiteRow]
|
||||
) throws -> [T] {
|
||||
let dateDecoder = DateDecoder(strategy: dateDecodingStrategy)
|
||||
let decoder = MultiRowDecoder(
|
||||
dateDecoder: dateDecoder,
|
||||
sqliteData: rows,
|
||||
codingPath: [],
|
||||
userInfo: userInfo
|
||||
)
|
||||
return try [T](from: decoder)
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
|
||||
// MARK: - TopLevelDecoder
|
||||
|
||||
extension RowDecoder: TopLevelDecoder {
|
||||
/// The type of input data expected by this decoder when used as a `TopLevelDecoder`.
|
||||
public typealias Input = SQLiteRow
|
||||
}
|
||||
#endif
|
||||
235
Sources/DataLiteCoder/Classes/RowEncoder.swift
Normal file
235
Sources/DataLiteCoder/Classes/RowEncoder.swift
Normal file
@@ -0,0 +1,235 @@
|
||||
import Foundation
|
||||
import DataLiteCore
|
||||
|
||||
private import DLCEncoder
|
||||
|
||||
/// Encoder that encodes instances of `Encodable` types into `SQLiteRow` or an array of `SQLiteRow`.
|
||||
///
|
||||
/// ## Overview
|
||||
///
|
||||
/// Use `RowEncoder` to convert `Encodable` objects into a single `SQLiteRow` or an array of `SQLiteRow`.
|
||||
///
|
||||
/// ### Encode a Single Object
|
||||
///
|
||||
/// Use ``encode(_:)->SQLiteRow`` to encode a single `Encodable` value into a `SQLiteRow`.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct User: Encodable {
|
||||
/// let id: Int
|
||||
/// let name: String
|
||||
/// }
|
||||
///
|
||||
/// do {
|
||||
/// let user = User(id: 1, name: "John Doe")
|
||||
/// let encoder = RowEncoder()
|
||||
/// let row = try encoder.encode(user)
|
||||
/// } catch {
|
||||
/// print("Encoding error: ", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Encode an Array of Objects
|
||||
///
|
||||
/// Use ``encode(_:)->[SQLiteRow]`` to encode an array of `Encodable` values into an array of `SQLiteRow`.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct User: Encodable {
|
||||
/// let id: Int
|
||||
/// let name: String
|
||||
/// }
|
||||
///
|
||||
/// do {
|
||||
/// let users = [
|
||||
/// User(id: 1, name: "John Doe"),
|
||||
/// User(id: 2, name: "Jane Smith")
|
||||
/// ]
|
||||
/// let encoder = RowEncoder()
|
||||
/// let rows = try encoder.encode(users)
|
||||
/// } catch {
|
||||
/// print("Encoding error: ", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Customize Encoding Behavior
|
||||
///
|
||||
/// Use the ``userInfo`` property to pass additional data that can affect encoding logic.
|
||||
///
|
||||
/// First, define a custom key:
|
||||
///
|
||||
/// ```swift
|
||||
/// extension CodingUserInfoKey {
|
||||
/// static let lowercased = CodingUserInfoKey(
|
||||
/// rawValue: "lowercased"
|
||||
/// )!
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Then pass a value using this key:
|
||||
///
|
||||
/// ```swift
|
||||
/// do {
|
||||
/// let user = User(id: 1, name: "John Doe")
|
||||
/// let encoder = RowEncoder()
|
||||
/// encoder.userInfo[.lowercased] = true
|
||||
///
|
||||
/// let row = try encoder.encode(user)
|
||||
/// } catch {
|
||||
/// print("Encoding error: ", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Access this value inside your custom `encode(to:)` method:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct User: Encodable {
|
||||
/// enum CodingKeys: CodingKey {
|
||||
/// case id, name
|
||||
/// }
|
||||
///
|
||||
/// let id: Int
|
||||
/// let name: String
|
||||
///
|
||||
/// func encode(to encoder: any Encoder) throws {
|
||||
/// var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
/// try container.encode(self.id, forKey: .id)
|
||||
///
|
||||
/// if (encoder.userInfo[.lowercased] as? Bool) ?? false {
|
||||
/// try container.encode(self.name.lowercased(), forKey: .name)
|
||||
/// } else {
|
||||
/// try container.encode(self.name.capitalized, forKey: .name)
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// ### Date Encoding Strategy
|
||||
///
|
||||
/// Use the ``dateEncodingStrategy`` property to control how `Date` values are encoded into SQLite.
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Log: Encodable {
|
||||
/// let timestamp: Date
|
||||
/// }
|
||||
///
|
||||
/// do {
|
||||
/// let encoder = RowEncoder(
|
||||
/// dateEncodingStrategy: .iso8601
|
||||
/// )
|
||||
///
|
||||
/// let log = Log(timestamp: Date())
|
||||
/// let row = try encoder.encode(log)
|
||||
/// } catch {
|
||||
/// print("Encoding error: ", error)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// For more detailed information, see ``DateEncodingStrategy``.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Creating an Encoder
|
||||
///
|
||||
/// - ``init(userInfo:dateEncodingStrategy:)``
|
||||
///
|
||||
/// ### Encoding
|
||||
///
|
||||
/// - ``encode(_:)->SQLiteRow``
|
||||
/// - ``encode(_:)->[SQLiteRow]``
|
||||
///
|
||||
/// ### Customizing Encoding
|
||||
///
|
||||
/// - ``userInfo``
|
||||
///
|
||||
/// ### Encoding Dates
|
||||
///
|
||||
/// - ``dateEncodingStrategy``
|
||||
/// - ``DateEncodingStrategy``
|
||||
public final class RowEncoder {
|
||||
// MARK: - Properties
|
||||
|
||||
/// A dictionary you use to customize encoding behavior.
|
||||
///
|
||||
/// Use this dictionary to pass additional contextual information to the encoded type's `encode(to:)`
|
||||
/// implementation. You can define your own keys by extending `CodingUserInfoKey`.
|
||||
///
|
||||
/// This can be useful when encoding needs to consider external state, such as user roles,
|
||||
/// environment settings, or formatting preferences.
|
||||
public var userInfo: [CodingUserInfoKey: Any]
|
||||
|
||||
/// The strategy to use for encoding `Date` values.
|
||||
///
|
||||
/// Use this property to control how `Date` values are encoded into SQLite-compatible types.
|
||||
/// By default, the `.deferredToDate` strategy is used.
|
||||
///
|
||||
/// You can change the strategy to encode dates as timestamps, ISO 8601 strings,
|
||||
/// or using a custom formatter.
|
||||
///
|
||||
/// For details on available strategies, see ``DateEncodingStrategy``.
|
||||
public var dateEncodingStrategy: DateEncodingStrategy
|
||||
|
||||
// MARK: - Inits
|
||||
|
||||
/// Creates a new instance of `RowEncoder`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - userInfo: A dictionary you can use to customize the encoding process by passing
|
||||
/// additional contextual information. The default value is an empty dictionary.
|
||||
/// - dateEncodingStrategy: The strategy to use for encoding `Date` values.
|
||||
/// The default value is `.deferredToDate`.
|
||||
public init(
|
||||
userInfo: [CodingUserInfoKey: Any] = [:],
|
||||
dateEncodingStrategy: DateEncodingStrategy = .deferredToDate
|
||||
) {
|
||||
self.userInfo = userInfo
|
||||
self.dateEncodingStrategy = dateEncodingStrategy
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
/// Encodes a single value of a type that conforms to `Encodable` into a `SQLiteRow`.
|
||||
///
|
||||
/// - Parameter value: The value to encode.
|
||||
/// - Returns: A `SQLiteRow` containing the encoded data of the provided value.
|
||||
/// - Throws: An error if any value throws an error during encoding.
|
||||
public func encode<T: Encodable>(_ value: T) throws -> SQLiteRow {
|
||||
let dateEncoder = DateEncoder(
|
||||
strategy: dateEncodingStrategy
|
||||
)
|
||||
let encoder = SingleRowEncoder(
|
||||
dateEncoder: dateEncoder,
|
||||
codingPath: [],
|
||||
userInfo: userInfo
|
||||
)
|
||||
try value.encode(to: encoder)
|
||||
return encoder.sqliteData
|
||||
}
|
||||
|
||||
/// Encodes an array of values conforming to `Encodable` into an array of `SQLiteRow`.
|
||||
///
|
||||
/// - Parameter value: The array of values to encode.
|
||||
/// - Returns: An array of `SQLiteRow` objects containing the encoded data.
|
||||
/// - Throws: An error if any value throws an error during encoding.
|
||||
public func encode<T: Encodable>(_ value: [T]) throws -> [SQLiteRow] {
|
||||
let dateEncoder = DateEncoder(
|
||||
strategy: dateEncodingStrategy
|
||||
)
|
||||
let encoder = MultiRowEncoder(
|
||||
dateEncoder: dateEncoder,
|
||||
codingPath: [],
|
||||
userInfo: userInfo
|
||||
)
|
||||
try value.encode(to: encoder)
|
||||
return encoder.sqliteData
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
|
||||
// MARK: - TopLevelEncoder
|
||||
|
||||
extension RowEncoder: TopLevelEncoder {
|
||||
/// The output type produced by the encoder.
|
||||
public typealias Output = SQLiteRow
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,9 @@
|
||||
# ``DataLiteCoder``
|
||||
|
||||
**DataLiteCoder** is a Swift library that provides encoding and decoding of models using `Codable` for working with SQLite.
|
||||
|
||||
## Overview
|
||||
|
||||
**DataLiteCoder** acts as a bridge between your Swift models and SQLite by leveraging the `Codable` system. It enables automatic encoding and decoding of model types to and from SQLite rows, including support for custom date formats and user-defined decoding strategies.
|
||||
|
||||
It is designed to be used alongside **DataLiteCore**, which manages low-level interactions with SQLite databases. Together, they provide a clean and extensible toolkit for building type-safe, SQLite-backed applications in Swift.
|
||||
62
Sources/DataLiteCoder/Enums/DateDecodingStrategy.swift
Normal file
62
Sources/DataLiteCoder/Enums/DateDecodingStrategy.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import Foundation
|
||||
|
||||
extension RowDecoder {
|
||||
/// Strategies to use for decoding `Date` values from SQLite data.
|
||||
///
|
||||
/// Use these strategies to specify how `Date` values should be decoded
|
||||
/// from various SQLite-compatible representations. This enum supports
|
||||
/// deferred decoding, standard formats, custom formatters, and epoch timestamps.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Default Formats
|
||||
///
|
||||
/// - ``deferredToDate``
|
||||
///
|
||||
/// ### Standard Formats
|
||||
///
|
||||
/// - ``iso8601``
|
||||
///
|
||||
/// ### Custom Formats
|
||||
///
|
||||
/// - ``formatted(_:)``
|
||||
///
|
||||
/// ### Epoch Formats
|
||||
///
|
||||
/// - ``millisecondsSince1970Int``
|
||||
/// - ``millisecondsSince1970Double``
|
||||
/// - ``secondsSince1970Int``
|
||||
/// - ``secondsSince1970Double``
|
||||
public enum DateDecodingStrategy {
|
||||
/// Decode dates by using the implementation of the `SQLiteRawRepresentable` protocol.
|
||||
///
|
||||
/// This strategy relies on the type’s conformance to `SQLiteRawRepresentable`
|
||||
/// to decode the date value from SQLite data.
|
||||
case deferredToDate
|
||||
|
||||
/// Decode dates using the provided custom formatter.
|
||||
///
|
||||
/// - Parameter formatter: An object conforming to `DateFormatterProtocol`
|
||||
/// used to decode the date string.
|
||||
case formatted(any DateFormatterProtocol)
|
||||
|
||||
/// Decode dates from an integer representing milliseconds since 1970.
|
||||
case millisecondsSince1970Int
|
||||
|
||||
/// Decode dates from a double representing milliseconds since 1970.
|
||||
case millisecondsSince1970Double
|
||||
|
||||
/// Decode dates from an integer representing seconds since 1970.
|
||||
case secondsSince1970Int
|
||||
|
||||
/// Decode dates from a double representing seconds since 1970.
|
||||
case secondsSince1970Double
|
||||
|
||||
/// Decode dates using ISO 8601 format.
|
||||
///
|
||||
/// This strategy uses `ISO8601DateFormatter` internally.
|
||||
public static var iso8601: Self {
|
||||
.formatted(ISO8601DateFormatter())
|
||||
}
|
||||
}
|
||||
}
|
||||
62
Sources/DataLiteCoder/Enums/DateEncodingStrategy.swift
Normal file
62
Sources/DataLiteCoder/Enums/DateEncodingStrategy.swift
Normal file
@@ -0,0 +1,62 @@
|
||||
import Foundation
|
||||
|
||||
extension RowEncoder {
|
||||
/// Strategies to use for encoding `Date` values into SQLite-compatible types.
|
||||
///
|
||||
/// Use these strategies to specify how `Date` values should be encoded
|
||||
/// into SQLite-compatible representations. This enum supports deferred encoding,
|
||||
/// standard formats, custom formatters, and epoch timestamps.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Default Formats
|
||||
///
|
||||
/// - ``deferredToDate``
|
||||
///
|
||||
/// ### Standard Formats
|
||||
///
|
||||
/// - ``iso8601``
|
||||
///
|
||||
/// ### Custom Formats
|
||||
///
|
||||
/// - ``formatted(_:)``
|
||||
///
|
||||
/// ### Epoch Formats
|
||||
///
|
||||
/// - ``millisecondsSince1970Int``
|
||||
/// - ``millisecondsSince1970Double``
|
||||
/// - ``secondsSince1970Int``
|
||||
/// - ``secondsSince1970Double``
|
||||
public enum DateEncodingStrategy {
|
||||
/// Encode dates by using the implementation of the `SQLiteRawRepresentable` protocol.
|
||||
///
|
||||
/// This strategy relies on the type’s conformance to `SQLiteRawRepresentable`
|
||||
/// to encode the date value into a SQLite-compatible representation.
|
||||
case deferredToDate
|
||||
|
||||
/// Encode dates using the provided custom formatter.
|
||||
///
|
||||
/// - Parameter formatter: An object conforming to `DateFormatterProtocol`
|
||||
/// used to encode the date string.
|
||||
case formatted(any DateFormatterProtocol)
|
||||
|
||||
/// Encode dates as an integer representing milliseconds since 1970.
|
||||
case millisecondsSince1970Int
|
||||
|
||||
/// Encode dates as a double representing milliseconds since 1970.
|
||||
case millisecondsSince1970Double
|
||||
|
||||
/// Encode dates as an integer representing seconds since 1970.
|
||||
case secondsSince1970Int
|
||||
|
||||
/// Encode dates as a double representing seconds since 1970.
|
||||
case secondsSince1970Double
|
||||
|
||||
/// Encode dates using ISO 8601 format.
|
||||
///
|
||||
/// This strategy uses `ISO8601DateFormatter` internally.
|
||||
public static var iso8601: Self {
|
||||
.formatted(ISO8601DateFormatter())
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Sources/DataLiteCoder/Protocols/DateFormatterProtocol.swift
Normal file
24
Sources/DataLiteCoder/Protocols/DateFormatterProtocol.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import Foundation
|
||||
|
||||
/// A common interface for formatting and parsing `Date` values.
|
||||
///
|
||||
/// The `DateFormatterProtocol` abstracts the interface of date formatters, allowing
|
||||
/// interchangeable use of `DateFormatter` and `ISO8601DateFormatter` when encoding or decoding
|
||||
/// date values.
|
||||
public protocol DateFormatterProtocol {
|
||||
/// Returns a string representation of the specified date.
|
||||
///
|
||||
/// - Parameter date: The `Date` to format.
|
||||
/// - Returns: A string that represents the formatted date.
|
||||
func string(from date: Date) -> String
|
||||
|
||||
/// Converts the specified string into a `Date` object.
|
||||
///
|
||||
/// - Parameter string: The date string to parse.
|
||||
/// - Returns: A `Date` object if the string could be parsed, or `nil` otherwise.
|
||||
func date(from string: String) -> Date?
|
||||
}
|
||||
|
||||
extension ISO8601DateFormatter: DateFormatterProtocol {}
|
||||
|
||||
extension DateFormatter: DateFormatterProtocol {}
|
||||
Reference in New Issue
Block a user