DataLireCoder swift package

This commit is contained in:
2025-04-27 12:53:43 +03:00
parent 2cca986016
commit 5aec6ea578
60 changed files with 7144 additions and 0 deletions

View 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
}
}
}
}

View 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
}
}

View 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)
}
}

View 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
)
}
}

View 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
)
}
}

View 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)
}
}
}

View 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)
}
}

View 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)
}
}
}

View File

@@ -0,0 +1,6 @@
import Foundation
protocol Container {
associatedtype Decoder: Swift.Decoder
var decoder: Decoder { get }
}

View 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
}

View File

@@ -0,0 +1,8 @@
import Foundation
public protocol Decoder: Swift.Decoder {
associatedtype SQLiteData
var dateDecoder: any DateDecoder { get }
var sqliteData: SQLiteData { get }
}

View File

@@ -0,0 +1,5 @@
import Foundation
public protocol KeyCheckingDecoder: Decoder {
func contains(_ key: CodingKey) -> Bool
}

View 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
}

View 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
}

View 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)
}
}

View 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)
}
}

View 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])
}
}

View 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)
}
}

View 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)
}
}

View 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)
}
}
}

View 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)
}
}

View 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)
}
}

View File

@@ -0,0 +1,6 @@
import Foundation
protocol Container {
associatedtype Encoder: Swift.Encoder
var encoder: Encoder { get }
}

View 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
}

View File

@@ -0,0 +1,8 @@
import Foundation
public protocol Encoder: Swift.Encoder {
associatedtype SQLiteData
var dateEncoder: any DateEncoder { get }
var sqliteData: SQLiteData { get }
}

View 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
}
}
}

View 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
}

View 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
}

View 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)
}
}
}
}

View 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
}
}
}
}

View 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

View 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

View File

@@ -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.

View 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 types 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())
}
}
}

View 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 types 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())
}
}
}

View 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 {}