DataLireCoder swift package
This commit is contained in:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
## General
|
||||||
|
.DS_Store
|
||||||
|
.swiftpm
|
||||||
|
.build/
|
||||||
|
|
||||||
|
## Various settings
|
||||||
|
Package.resolved
|
||||||
|
*.pbxuser
|
||||||
|
!default.pbxuser
|
||||||
|
*.mode1v3
|
||||||
|
!default.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
!default.mode2v3
|
||||||
|
*.perspectivev3
|
||||||
|
!default.perspectivev3
|
||||||
|
xcuserdata/
|
||||||
|
*.xcuserdatad/
|
||||||
15
.swift-format
Normal file
15
.swift-format
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"fileScopedDeclarationPrivacy": {
|
||||||
|
"accessLevel": "private"
|
||||||
|
},
|
||||||
|
"indentBlankLines": true,
|
||||||
|
"indentation": {
|
||||||
|
"spaces": 4
|
||||||
|
},
|
||||||
|
"lineLength": 9999,
|
||||||
|
"maximumBlankLines": 1,
|
||||||
|
"multiElementCollectionTrailingCommas": false,
|
||||||
|
"rules": {
|
||||||
|
"FileScopedDeclarationPrivacy": true
|
||||||
|
}
|
||||||
|
}
|
||||||
56
Package.swift
Normal file
56
Package.swift
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// swift-tools-version: 6.0
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "DataLiteCoder",
|
||||||
|
platforms: [
|
||||||
|
.macOS(.v10_14),
|
||||||
|
.iOS(.v12)
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(name: "DataLiteCoder", targets: ["DataLiteCoder"])
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(
|
||||||
|
url: "https://github.com/angd-dev/data-lite-core.git",
|
||||||
|
revision: "5c6942bd0b9636b5ac3e550453c07aac843e8416"
|
||||||
|
),
|
||||||
|
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "DataLiteCoder",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "DataLiteCore", package: "data-lite-core"),
|
||||||
|
"DLCDecoder",
|
||||||
|
"DLCEncoder"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "DLCCommon",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "DataLiteCore", package: "data-lite-core")
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "DLCDecoder",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "DataLiteCore", package: "data-lite-core"),
|
||||||
|
"DLCCommon"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.target(
|
||||||
|
name: "DLCEncoder",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "DataLiteCore", package: "data-lite-core"),
|
||||||
|
"DLCCommon"
|
||||||
|
]
|
||||||
|
),
|
||||||
|
.testTarget(name: "DataLiteCoderTests", dependencies: ["DataLiteCoder"]),
|
||||||
|
.testTarget(name: "DLCCommonTests", dependencies: ["DLCCommon"]),
|
||||||
|
.testTarget(name: "DLCDecoderTests", dependencies: ["DLCDecoder"]),
|
||||||
|
.testTarget(name: "DLCEncoderTests", dependencies: ["DLCEncoder"])
|
||||||
|
]
|
||||||
|
)
|
||||||
59
README.md
59
README.md
@@ -1 +1,60 @@
|
|||||||
# DataLiteCoder
|
# DataLiteCoder
|
||||||
|
|
||||||
|
DataLiteCoder is a Swift library that provides encoding and decoding of models using `Codable` for working with SQLite, designed for integration with the [DataLiteCore](https://github.com/angd-dev/data-lite-core) library.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- **Swift**: 6.0 or later
|
||||||
|
- **Platforms**: macOS 10.14+, iOS 12.0+, Linux
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To add DataLiteCoder to your project, use Swift Package Manager (SPM).
|
||||||
|
|
||||||
|
> **Important:** The API of `DataLiteCoder` is currently unstable and may change without notice. It is **strongly recommended** to pin the dependency to a specific commit to ensure compatibility and avoid unexpected breakage when the API evolves.
|
||||||
|
|
||||||
|
### Adding to an Xcode Project
|
||||||
|
|
||||||
|
1. Open your project in Xcode.
|
||||||
|
2. Navigate to the `File` menu and select `Add Package Dependencies`.
|
||||||
|
3. Enter the repository URL: `https://github.com/angd-dev/data-lite-coder.git`
|
||||||
|
4. Choose the version to install.
|
||||||
|
5. Add the library to your target module.
|
||||||
|
|
||||||
|
### Adding to Package.swift
|
||||||
|
|
||||||
|
If you are using Swift Package Manager with a `Package.swift` file, add the dependency like this:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// swift-tools-version: 6.0
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "YourProject",
|
||||||
|
dependencies: [
|
||||||
|
.package(url: "https://github.com/angd-dev/data-lite-coder.git", branch: "develop")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "YourTarget",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "DataLiteCoder", package: "data-lite-coder")
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
For more information and usage examples, see the [documentation](https://docs.angd.dev/?package=data-lite-coder&version=develop).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License. See the `LICENSE` file for details.
|
||||||
|
|||||||
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 {}
|
||||||
50
Tests/DLCCommonTests/Extensions/SQLiteRowTests.swift
Normal file
50
Tests/DLCCommonTests/Extensions/SQLiteRowTests.swift
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
final class SQLiteRowTests: XCTestCase {
|
||||||
|
func testContainsKey() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["key1"] = .text("value1")
|
||||||
|
row["key2"] = .int(42)
|
||||||
|
|
||||||
|
XCTAssertTrue(row.contains(DummyKey(stringValue: "key1")))
|
||||||
|
XCTAssertFalse(row.contains(DummyKey(stringValue: "key3")))
|
||||||
|
|
||||||
|
XCTAssertTrue(row.contains(DummyKey(intValue: 0)))
|
||||||
|
XCTAssertFalse(row.contains(DummyKey(intValue: 2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSubscriptWithKey() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["key1"] = .text("value")
|
||||||
|
|
||||||
|
XCTAssertEqual(row[DummyKey(stringValue: "key1")], .text("value"))
|
||||||
|
XCTAssertNil(row[DummyKey(stringValue: "key2")])
|
||||||
|
|
||||||
|
XCTAssertEqual(row[DummyKey(intValue: 0)], .text("value"))
|
||||||
|
|
||||||
|
row[DummyKey(stringValue: "key1")] = .int(42)
|
||||||
|
XCTAssertEqual(row[DummyKey(stringValue: "key1")], .int(42))
|
||||||
|
|
||||||
|
row[DummyKey(intValue: 0)] = .real(3.14)
|
||||||
|
XCTAssertEqual(row[DummyKey(intValue: 0)], .real(3.14))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SQLiteRowTests {
|
||||||
|
struct DummyKey: CodingKey, Equatable {
|
||||||
|
var stringValue: String
|
||||||
|
var intValue: Int?
|
||||||
|
|
||||||
|
init(stringValue: String) {
|
||||||
|
self.stringValue = stringValue
|
||||||
|
self.intValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init(intValue: Int) {
|
||||||
|
self.stringValue = "\(intValue)"
|
||||||
|
self.intValue = intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Tests/DLCCommonTests/Structures/RowCodingKeyTests.swift
Normal file
18
Tests/DLCCommonTests/Structures/RowCodingKeyTests.swift
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import XCTest
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
final class RowCodingKeyTests: XCTestCase {
|
||||||
|
func testInitWithStringValue() {
|
||||||
|
let key = RowCodingKey(stringValue: "testKey")
|
||||||
|
|
||||||
|
XCTAssertEqual(key.stringValue, "testKey", "String value does not match the expected value.")
|
||||||
|
XCTAssertNil(key.intValue, "Integer value should be nil when initialized with a string.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testInitWithIntValue() {
|
||||||
|
let key = RowCodingKey(intValue: 5)
|
||||||
|
|
||||||
|
XCTAssertEqual(key.stringValue, "Index 5", "String value does not match the expected format.")
|
||||||
|
XCTAssertEqual(key.intValue, 5, "Integer value does not match the expected value.")
|
||||||
|
}
|
||||||
|
}
|
||||||
488
Tests/DLCDecoderTests/KeyedContainerTests.swift
Normal file
488
Tests/DLCDecoderTests/KeyedContainerTests.swift
Normal file
@@ -0,0 +1,488 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
|
||||||
|
@testable import DLCDecoder
|
||||||
|
|
||||||
|
final class KeyedContainerTests: XCTestCase {
|
||||||
|
func testContains() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .int(0)
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertTrue(container.contains(.key1))
|
||||||
|
XCTAssertFalse(container.contains(.key2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeNil() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .int(0)
|
||||||
|
row[CodingKeys.key2.stringValue] = .null
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1, .key2]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertFalse(try container.decodeNil(forKey: .key1))
|
||||||
|
XCTAssertTrue(try container.decodeNil(forKey: .key2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeBool() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .int(0)
|
||||||
|
row[CodingKeys.key2.stringValue] = .int(1)
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1, .key2]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertFalse(try container.decode(Bool.self, forKey: .key1))
|
||||||
|
XCTAssertTrue(try container.decode(Bool.self, forKey: .key2))
|
||||||
|
XCTAssertNil(try container.decodeIfPresent(Bool.self, forKey: .key3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeString() {
|
||||||
|
let string = "Test string"
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .text(string)
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(try container.decode(String.self, forKey: .key1), string)
|
||||||
|
XCTAssertNil(try container.decodeIfPresent(String.self, forKey: .key2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRealNumber() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .real(3.14)
|
||||||
|
row[CodingKeys.key2.stringValue] = .real(2.55)
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1, .key2]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(try container.decode(Double.self, forKey: .key1), 3.14)
|
||||||
|
XCTAssertEqual(try container.decode(Float.self, forKey: .key2), 2.55)
|
||||||
|
XCTAssertNil(try container.decodeIfPresent(Double.self, forKey: .key3))
|
||||||
|
XCTAssertNil(try container.decodeIfPresent(Float.self, forKey: .key3))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeIntNumber() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .int(42)
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(try container.decode(Int.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(Int8.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(Int16.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(Int32.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(Int64.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(UInt.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(UInt8.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(UInt16.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(UInt32.self, forKey: .key1), 42)
|
||||||
|
XCTAssertEqual(try container.decode(UInt64.self, forKey: .key1), 42)
|
||||||
|
XCTAssertNil(try container.decodeIfPresent(Int.self, forKey: .key2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDate() {
|
||||||
|
let date = Date(timeIntervalSince1970: 123456789)
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .real(date.timeIntervalSince1970)
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(try container.decode(Date.self, forKey: .key1), date)
|
||||||
|
XCTAssertNil(try container.decodeIfPresent(Date.self, forKey: .key2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRawRepresentable() {
|
||||||
|
let `case` = RawRepresentableEnum.test
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .text(`case`.rawValue)
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(try container.decode(RawRepresentableEnum.self, forKey: .key1), `case`)
|
||||||
|
XCTAssertNil(try container.decodeIfPresent(RawRepresentableEnum.self, forKey: .key2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDecodable() {
|
||||||
|
let `case` = DecodableEnum.test
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key1.stringValue] = .text(`case`.rawValue)
|
||||||
|
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: row),
|
||||||
|
codingPath: [],
|
||||||
|
allKeys: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(try container.decode(DecodableEnum.self, forKey: .key1), `case`)
|
||||||
|
XCTAssertNil(try container.decodeIfPresent(DecodableEnum.self, forKey: .key2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedContainerKeyedBy() {
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: .init()),
|
||||||
|
codingPath: [CodingKeys.key1],
|
||||||
|
allKeys: [CodingKeys]()
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container.nestedContainer(
|
||||||
|
keyedBy: CodingKeys.self,
|
||||||
|
forKey: .key2
|
||||||
|
)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
XCTFail("Expected DecodingError.typeMismatch, but got: \(error).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertTrue(
|
||||||
|
type == KeyedDecodingContainer<CodingKeys>.self,
|
||||||
|
"Mismatched type in decoding error. Expected \(KeyedDecodingContainer<CodingKeys>.self)."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.codingPath as? [CodingKeys], [.key1, .key2],
|
||||||
|
"Incorrect coding path in decoding error."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"""
|
||||||
|
Attempted to decode a nested keyed container for key '\(CodingKeys.key2.stringValue)',
|
||||||
|
but the value cannot be represented as a keyed container.
|
||||||
|
""",
|
||||||
|
"Unexpected debug description in decoding error."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedUnkeyedContainer() {
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: .init()),
|
||||||
|
codingPath: [CodingKeys.key1],
|
||||||
|
allKeys: [CodingKeys]()
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container.nestedUnkeyedContainer(forKey: .key2)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
XCTFail("Expected DecodingError.typeMismatch, but got: \(error).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertTrue(
|
||||||
|
type == UnkeyedDecodingContainer.self,
|
||||||
|
"Mismatched type in decoding error. Expected \(UnkeyedDecodingContainer.self)."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.codingPath as? [CodingKeys], [.key1, .key2],
|
||||||
|
"Incorrect coding path in decoding error."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"""
|
||||||
|
Attempted to decode a nested unkeyed container for key '\(CodingKeys.key2.stringValue)',
|
||||||
|
but the value cannot be represented as an unkeyed container.
|
||||||
|
""",
|
||||||
|
"Unexpected debug description in decoding error."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuperDecoder() {
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: .init()),
|
||||||
|
codingPath: [CodingKeys.key1],
|
||||||
|
allKeys: [CodingKeys]()
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container.superDecoder()
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
XCTFail("Expected DecodingError.typeMismatch, but got: \(error).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertTrue(
|
||||||
|
type == Swift.Decoder.self,
|
||||||
|
"Mismatched type in decoding error. Expected \(Swift.Decoder.self)."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.codingPath as? [CodingKeys], [.key1],
|
||||||
|
"Incorrect coding path in decoding error."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"""
|
||||||
|
Attempted to get a superDecoder,
|
||||||
|
but SQLiteRowDecoder does not support superDecoding.
|
||||||
|
""",
|
||||||
|
"Unexpected debug description in decoding error."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuperDecoderForKey() {
|
||||||
|
let container = KeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: .init()),
|
||||||
|
codingPath: [CodingKeys.key1],
|
||||||
|
allKeys: [CodingKeys]()
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container.superDecoder(forKey: .key2)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
XCTFail("Expected DecodingError.typeMismatch, but got: \(error).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
XCTAssertTrue(
|
||||||
|
type == Swift.Decoder.self,
|
||||||
|
"Unexpected type in decoding error. Expected \(Swift.Decoder.self)."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.codingPath as? [CodingKeys], [.key1, .key2],
|
||||||
|
"Incorrect coding path in decoding error."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"""
|
||||||
|
Attempted to get a superDecoder for key '\(CodingKeys.key2.stringValue)',
|
||||||
|
but SQLiteRowDecoder does not support nested structures.
|
||||||
|
""",
|
||||||
|
"Unexpected debug description in decoding error."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension KeyedContainerTests {
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case key1
|
||||||
|
case key2
|
||||||
|
case key3
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RawRepresentableEnum: String, Decodable, SQLiteRawRepresentable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DecodableEnum: String, Decodable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateDecoder: DateDecoder {
|
||||||
|
func decode(
|
||||||
|
from decoder: any ValueDecoder
|
||||||
|
) throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(
|
||||||
|
from decoder: any RowDecoder,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> Date {
|
||||||
|
try decoder.decode(Date.self, for: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockKeyedDecoder: RowDecoder, KeyCheckingDecoder {
|
||||||
|
typealias SQLiteData = SQLiteRow
|
||||||
|
|
||||||
|
let sqliteData: SQLiteData
|
||||||
|
let dateDecoder: DateDecoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
var count: Int? { sqliteData.count }
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteData,
|
||||||
|
dateDecoder: DateDecoder = MockDateDecoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateDecoder = dateDecoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(_ key: any CodingKey) -> Bool {
|
||||||
|
sqliteData.contains(key.stringValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil(
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> Bool {
|
||||||
|
sqliteData[key.stringValue] == .null
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDate(for key: any CodingKey) throws -> Date {
|
||||||
|
try dateDecoder.decode(from: self, for: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: SQLiteRawRepresentable>(
|
||||||
|
_ type: T.Type,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> T {
|
||||||
|
type.init(sqliteData[key.stringValue]!)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func decoder(for key: any CodingKey) -> any Decoder {
|
||||||
|
MockValueDecoder(sqliteData: sqliteData[key.stringValue]!)
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) throws -> KeyedDecodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockValueDecoder: ValueDecoder, SingleValueDecodingContainer {
|
||||||
|
typealias SQLiteData = SQLiteRawValue
|
||||||
|
|
||||||
|
let sqliteData: SQLiteData
|
||||||
|
var dateDecoder: DateDecoder
|
||||||
|
var codingPath: [any CodingKey]
|
||||||
|
var userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteData,
|
||||||
|
dateDecoder: DateDecoder = MockDateDecoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateDecoder = dateDecoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil() -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDate() throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: SQLiteRawRepresentable>(
|
||||||
|
_ type: T.Type
|
||||||
|
) throws -> T {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) throws -> KeyedDecodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Bool.Type) throws -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: String.Type) throws -> String {
|
||||||
|
type.init(sqliteData)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Double.Type) throws -> Double {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Float.Type) throws -> Float {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int.Type) throws -> Int {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int8.Type) throws -> Int8 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int16.Type) throws -> Int16 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int32.Type) throws -> Int32 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int64.Type) throws -> Int64 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt.Type) throws -> UInt {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt8.Type) throws -> UInt8 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt16.Type) throws -> UInt16 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt32.Type) throws -> UInt32 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt64.Type) throws -> UInt64 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
193
Tests/DLCDecoderTests/MultiRowDecoderTests.swift
Normal file
193
Tests/DLCDecoderTests/MultiRowDecoderTests.swift
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
|
||||||
|
@testable import DLCDecoder
|
||||||
|
|
||||||
|
final class MultiRowDecoderTests: XCTestCase {
|
||||||
|
func testCount() {
|
||||||
|
let rows = [SQLiteRow](repeating: .init(), count: 5)
|
||||||
|
let decoder = decoder(sqliteData: rows)
|
||||||
|
XCTAssertEqual(decoder.count, rows.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeNil() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let testKey = DummyKey(intValue: 0)
|
||||||
|
let decoder = decoder(sqliteData: [], codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decodeNil(for: testKey)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.dataCorrupted(context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.dataCorrupted, but got: \(error).")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [testKey])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Attempted to decode nil, but it's not supported for an array of rows.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDate() {
|
||||||
|
let path = [DummyKey(stringValue: "root")]
|
||||||
|
let key = DummyKey(stringValue: "date")
|
||||||
|
let decoder = decoder(sqliteData: [], codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decodeDate(for: key)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssert(type == Date.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [key])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected a type of \(Date.self), but found an array of rows.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeForKey() {
|
||||||
|
let path = [DummyKey(stringValue: "user")]
|
||||||
|
let key = DummyKey(stringValue: "id")
|
||||||
|
let decoder = decoder(sqliteData: [], codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode(String.self, for: key)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(type == String.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [key])
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"Expected a type of \(String.self), but found an array of rows."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoderForKey() throws {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let key = DummyKey(intValue: 0)
|
||||||
|
let rows = [SQLiteRow()]
|
||||||
|
let decoder = decoder(sqliteData: rows, codingPath: path)
|
||||||
|
let nestedDecoder = try decoder.decoder(for: key)
|
||||||
|
|
||||||
|
guard let rowDecoder = nestedDecoder as? SingleRowDecoder else {
|
||||||
|
return XCTFail("Expected SingleRowDecoder, but got: \(type(of: nestedDecoder)).")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(rowDecoder.dateDecoder as? MockDateDecoder === decoder.dateDecoder as? MockDateDecoder)
|
||||||
|
XCTAssertEqual(rowDecoder.codingPath as? [DummyKey], path + [key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoderWithInvalidKey() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let testKey = DummyKey(stringValue: "invalidKey")
|
||||||
|
let decoder = decoder(sqliteData: [], codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decoder(for: testKey)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.keyNotFound(key, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.keyNotFound, but got: \(error).")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(key as? DummyKey, testKey)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [testKey])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected an integer key, but found a non-integer key.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyedContainer() {
|
||||||
|
let path: [DummyKey] = [DummyKey(stringValue: "root")]
|
||||||
|
let decoder = decoder(sqliteData: [], codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try decoder.container(keyedBy: DummyKey.self)) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(expectedType, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(expectedType == KeyedDecodingContainer<DummyKey>.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path)
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected a keyed container, but found an array of rows.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnkeyedContainer() throws {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let decoder = decoder(sqliteData: [SQLiteRow()], codingPath: path)
|
||||||
|
|
||||||
|
let container = try decoder.unkeyedContainer()
|
||||||
|
|
||||||
|
guard let unkeyed = container as? UnkeyedContainer<MultiRowDecoder> else {
|
||||||
|
return XCTFail("Expected UnkeyedContainer, got: \(type(of: container))")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(unkeyed.decoder === decoder)
|
||||||
|
XCTAssertEqual(unkeyed.codingPath as? [DummyKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleValueContainer() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let decoder = decoder(sqliteData: [], codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.singleValueContainer()
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error).")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(type == SingleValueDecodingContainer.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path)
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected a single value container, but found an array of rows.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension MultiRowDecoderTests {
|
||||||
|
func decoder(
|
||||||
|
sqliteData: [SQLiteRow],
|
||||||
|
codingPath: [any CodingKey] = []
|
||||||
|
) -> MultiRowDecoder {
|
||||||
|
MultiRowDecoder(
|
||||||
|
dateDecoder: MockDateDecoder(),
|
||||||
|
sqliteData: sqliteData,
|
||||||
|
codingPath: codingPath,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension MultiRowDecoderTests {
|
||||||
|
struct DummyKey: CodingKey, Equatable {
|
||||||
|
var stringValue: String
|
||||||
|
var intValue: Int?
|
||||||
|
|
||||||
|
init(stringValue: String) {
|
||||||
|
self.stringValue = stringValue
|
||||||
|
self.intValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init(intValue: Int) {
|
||||||
|
self.stringValue = "\(intValue)"
|
||||||
|
self.intValue = intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateDecoder: DateDecoder {
|
||||||
|
func decode(
|
||||||
|
from decoder: any ValueDecoder
|
||||||
|
) throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(
|
||||||
|
from decoder: any RowDecoder,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
255
Tests/DLCDecoderTests/SingleRowDecoderTests.swift
Normal file
255
Tests/DLCDecoderTests/SingleRowDecoderTests.swift
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
|
||||||
|
@testable import DLCDecoder
|
||||||
|
|
||||||
|
final class SingleRowDecoderTests: XCTestCase {
|
||||||
|
func testCount() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["key1"] = .null
|
||||||
|
row["key2"] = .int(1)
|
||||||
|
row["key3"] = .text("")
|
||||||
|
let decoder = decoder(sqliteData: row)
|
||||||
|
XCTAssertEqual(decoder.count, row.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testContains() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["key1"] = .int(123)
|
||||||
|
let decoder = decoder(sqliteData: row)
|
||||||
|
XCTAssertTrue(decoder.contains(DummyKey(stringValue: "key1")))
|
||||||
|
XCTAssertFalse(decoder.contains(DummyKey(stringValue: "key2")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeNil() {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["key1"] = .null
|
||||||
|
row["key2"] = .int(0)
|
||||||
|
let decoder = decoder(sqliteData: row)
|
||||||
|
XCTAssertTrue(try decoder.decodeNil(for: DummyKey(stringValue: "key1")))
|
||||||
|
XCTAssertFalse(try decoder.decodeNil(for: DummyKey(stringValue: "key2")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeNilKeyNotFound() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let decoder = decoder(sqliteData: SQLiteRow(), codingPath: path)
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decodeNil(for: DummyKey(stringValue: "key"))
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.keyNotFound(key, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.keyNotFound, but got: \(error).")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(key as? DummyKey, DummyKey(stringValue: "key"))
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [DummyKey(stringValue: "key")])
|
||||||
|
XCTAssertEqual(context.debugDescription, "No value associated with key \(DummyKey(stringValue: "key")).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDate() {
|
||||||
|
let expected = Date()
|
||||||
|
let dateDecoder = MockDateDecoder(expectedDate: expected)
|
||||||
|
let decoder = decoder(dateDecoder: dateDecoder, sqliteData: SQLiteRow())
|
||||||
|
|
||||||
|
XCTAssertEqual(try decoder.decodeDate(for: DummyKey(stringValue: "key")), expected)
|
||||||
|
XCTAssertTrue(dateDecoder.didCallDecode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeKeyNotFound() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let testKey = DummyKey(stringValue: "testKey")
|
||||||
|
let decoder = decoder(sqliteData: SQLiteRow(), codingPath: path)
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode(Int.self, for: testKey)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.keyNotFound(key, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.keyNotFound, but got: \(error).")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(key as? DummyKey, testKey)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [testKey])
|
||||||
|
XCTAssertEqual(context.debugDescription, "No value associated with key \(testKey).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeWithNullValue() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let testKey = DummyKey(stringValue: "testKey")
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[testKey.stringValue] = .null
|
||||||
|
let decoder = decoder(sqliteData: row, codingPath: path)
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode(Int.self, for: testKey)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.valueNotFound(type, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.valueNotFound, but got: \(error).")
|
||||||
|
}
|
||||||
|
XCTAssertTrue(type == Int.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [testKey])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Cannot get value of type Int, found null value instead.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeWithTypeMismatch() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let testKey = DummyKey(stringValue: "testKey")
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[testKey.stringValue] = .int(0)
|
||||||
|
let decoder = decoder(sqliteData: row, codingPath: path)
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode(Data.self, for: testKey)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error).")
|
||||||
|
}
|
||||||
|
XCTAssertTrue(type == Data.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [testKey])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected to decode Data but found an \(SQLiteRawValue.int(0)) instead.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeWithCorrectValue() {
|
||||||
|
let testKey = DummyKey(stringValue: "testKey")
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[testKey.stringValue] = .text("test")
|
||||||
|
let decoder = decoder(sqliteData: row)
|
||||||
|
XCTAssertEqual(try decoder.decode(String.self, for: testKey), "test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoderKeyNotFound() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let testKey = DummyKey(stringValue: "testKey")
|
||||||
|
let decoder = decoder(sqliteData: SQLiteRow(), codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decoder(for: testKey)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.keyNotFound(key, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.keyNotFound, but got: \(error).")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(key as? DummyKey, testKey)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path + [testKey])
|
||||||
|
XCTAssertEqual(context.debugDescription, "No value associated with key \(testKey).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecoderWithValidData() throws {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
let testKey = DummyKey(stringValue: "testKey")
|
||||||
|
row[testKey.stringValue] = .int(42)
|
||||||
|
|
||||||
|
let decoder = decoder(sqliteData: row)
|
||||||
|
let valueDecoder = try decoder.decoder(for: testKey)
|
||||||
|
|
||||||
|
guard let singleValueDecoder = valueDecoder as? SingleValueDecoder else {
|
||||||
|
return XCTFail("Expected SingleValueDecoder, but got: \(type(of: valueDecoder)).")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertEqual(singleValueDecoder.sqliteData, .int(42))
|
||||||
|
XCTAssertEqual(singleValueDecoder.codingPath as? [DummyKey], [testKey])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyedContainer() throws {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let expectedKeys = [
|
||||||
|
DummyKey(stringValue: "key1"),
|
||||||
|
DummyKey(stringValue: "key2"),
|
||||||
|
DummyKey(stringValue: "key3")
|
||||||
|
]
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[expectedKeys[0].stringValue] = .int(123)
|
||||||
|
row[expectedKeys[1].stringValue] = .text("str")
|
||||||
|
row[expectedKeys[2].stringValue] = .null
|
||||||
|
|
||||||
|
let decoder = decoder(sqliteData: row, codingPath: path)
|
||||||
|
let container = try decoder.container(keyedBy: DummyKey.self)
|
||||||
|
|
||||||
|
XCTAssertEqual(container.codingPath as? [DummyKey], decoder.codingPath as? [DummyKey])
|
||||||
|
XCTAssertEqual(container.allKeys, expectedKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnkeyedContainer() throws {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let decoder = decoder(sqliteData: SQLiteRow(), codingPath: path)
|
||||||
|
|
||||||
|
let container = try decoder.unkeyedContainer()
|
||||||
|
|
||||||
|
guard let unkeyed = container as? UnkeyedContainer<SingleRowDecoder> else {
|
||||||
|
return XCTFail("Expected UnkeyedContainer, got: \(type(of: container))")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(unkeyed.decoder === decoder)
|
||||||
|
XCTAssertEqual(unkeyed.codingPath as? [DummyKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleValueContainer() {
|
||||||
|
let path = [DummyKey(stringValue: "rootKey")]
|
||||||
|
let decoder = decoder(sqliteData: SQLiteRow(), codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.singleValueContainer()
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error).")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(type == SingleValueDecodingContainer.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path)
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected a single value container, but found a row value.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SingleRowDecoderTests {
|
||||||
|
func decoder(
|
||||||
|
dateDecoder: DateDecoder = MockDateDecoder(),
|
||||||
|
sqliteData: SQLiteRow,
|
||||||
|
codingPath: [any CodingKey] = []
|
||||||
|
) -> SingleRowDecoder {
|
||||||
|
SingleRowDecoder(
|
||||||
|
dateDecoder: dateDecoder,
|
||||||
|
sqliteData: sqliteData,
|
||||||
|
codingPath: codingPath,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SingleRowDecoderTests {
|
||||||
|
struct DummyKey: CodingKey, Equatable {
|
||||||
|
var stringValue: String
|
||||||
|
var intValue: Int?
|
||||||
|
|
||||||
|
init(stringValue: String) {
|
||||||
|
self.stringValue = stringValue
|
||||||
|
self.intValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init(intValue: Int) {
|
||||||
|
self.stringValue = "\(intValue)"
|
||||||
|
self.intValue = intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateDecoder: DateDecoder {
|
||||||
|
let expectedDate: Date
|
||||||
|
private(set) var didCallDecode = false
|
||||||
|
|
||||||
|
init(expectedDate: Date = Date()) {
|
||||||
|
self.expectedDate = expectedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(
|
||||||
|
from decoder: any ValueDecoder
|
||||||
|
) throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(
|
||||||
|
from decoder: any RowDecoder,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> Date {
|
||||||
|
didCallDecode = true
|
||||||
|
return expectedDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
285
Tests/DLCDecoderTests/SingleValueContainerTests.swift
Normal file
285
Tests/DLCDecoderTests/SingleValueContainerTests.swift
Normal file
@@ -0,0 +1,285 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
|
||||||
|
@testable import DLCDecoder
|
||||||
|
|
||||||
|
final class SingleValueContainerTests: XCTestCase {
|
||||||
|
func testDecodeNil() {
|
||||||
|
let nullContainer = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .null),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertTrue(
|
||||||
|
nullContainer.decodeNil(),
|
||||||
|
"Expected decodeNil to return true for null value."
|
||||||
|
)
|
||||||
|
|
||||||
|
let nonNullContainer = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .int(42)),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertFalse(
|
||||||
|
nonNullContainer.decodeNil(),
|
||||||
|
"Expected decodeNil to return false for non-null value."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeBool() {
|
||||||
|
let container = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .int(1)),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertTrue(
|
||||||
|
try container.decode(Bool.self),
|
||||||
|
"Expected decoded Bool to be true."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeString() {
|
||||||
|
let container = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .text("Hello")),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(String.self), "Hello",
|
||||||
|
"Decoded String does not match expected value."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDouble() {
|
||||||
|
let container = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .real(3.14)),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Double.self), 3.14,
|
||||||
|
"Decoded Double does not match expected value."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeFloat() {
|
||||||
|
let container = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .real(2.5)),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Float.self), 2.5,
|
||||||
|
"Decoded Float does not match expected value."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeInt() {
|
||||||
|
let container = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .int(42)),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int.self), 42,
|
||||||
|
"Decoded Int does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int8.self), 42,
|
||||||
|
"Decoded Int8 does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int16.self), 42,
|
||||||
|
"Decoded Int16 does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int32.self), 42,
|
||||||
|
"Decoded Int32 does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int64.self), 42,
|
||||||
|
"Decoded Int64 does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt.self), 42,
|
||||||
|
"Decoded UInt does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt8.self), 42,
|
||||||
|
"Decoded UInt8 does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt16.self), 42,
|
||||||
|
"Decoded UInt16 does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt32.self), 42,
|
||||||
|
"Decoded UInt32 does not match expected value."
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt64.self), 42,
|
||||||
|
"Decoded UInt64 does not match expected value."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDate() {
|
||||||
|
let date = Date(timeIntervalSince1970: 123456789)
|
||||||
|
let container = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .real(date.timeIntervalSince1970)),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Date.self), date,
|
||||||
|
"Decoded Date does not match expected value."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRawRepresentable() {
|
||||||
|
let `case` = RawRepresentableEnum.test
|
||||||
|
let container = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .text(`case`.rawValue)),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(RawRepresentableEnum.self), `case`,
|
||||||
|
"Decoded RawRepresentableEnum does not match expected value."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDecodable() {
|
||||||
|
let `case` = DecodableEnum.test
|
||||||
|
let container = SingleValueContainer(
|
||||||
|
decoder: MockSingleValueDecoder(sqliteData: .text(`case`.rawValue)),
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(DecodableEnum.self), `case`,
|
||||||
|
"Decoded DecodableEnum does not match expected value."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SingleValueContainerTests {
|
||||||
|
enum RawRepresentableEnum: String, Decodable, SQLiteRawRepresentable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DecodableEnum: String, Decodable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateDecoder: DateDecoder {
|
||||||
|
func decode(
|
||||||
|
from decoder: any ValueDecoder
|
||||||
|
) throws -> Date {
|
||||||
|
try decoder.decode(Date.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(
|
||||||
|
from decoder: any RowDecoder,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockSingleValueDecoder: ValueDecoder, SingleValueDecodingContainer {
|
||||||
|
let sqliteData: SQLiteRawValue
|
||||||
|
let dateDecoder: DateDecoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteRawValue,
|
||||||
|
dateDecoder: DateDecoder = MockDateDecoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateDecoder = dateDecoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil() -> Bool {
|
||||||
|
sqliteData == .null
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDate() throws -> Date {
|
||||||
|
try dateDecoder.decode(from: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: SQLiteRawRepresentable>(
|
||||||
|
_ type: T.Type
|
||||||
|
) throws -> T {
|
||||||
|
type.init(sqliteData)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) throws -> KeyedDecodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Bool.Type) throws -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: String.Type) throws -> String {
|
||||||
|
String(sqliteData)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Double.Type) throws -> Double {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Float.Type) throws -> Float {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int.Type) throws -> Int {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int8.Type) throws -> Int8 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int16.Type) throws -> Int16 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int32.Type) throws -> Int32 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int64.Type) throws -> Int64 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt.Type) throws -> UInt {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt8.Type) throws -> UInt8 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt16.Type) throws -> UInt16 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt32.Type) throws -> UInt32 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt64.Type) throws -> UInt64 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
152
Tests/DLCDecoderTests/SingleValueDecoderTests.swift
Normal file
152
Tests/DLCDecoderTests/SingleValueDecoderTests.swift
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
|
||||||
|
@testable import DLCDecoder
|
||||||
|
|
||||||
|
final class SingleValueDecoderTests: XCTestCase {
|
||||||
|
func testDecodeNil() {
|
||||||
|
XCTAssertTrue(decoder(sqliteData: .null).decodeNil())
|
||||||
|
XCTAssertFalse(decoder(sqliteData: .int(0)).decodeNil())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDate() {
|
||||||
|
let expected = Date()
|
||||||
|
let dateDecoder = MockDateDecoder(expectedDate: expected)
|
||||||
|
let decoder = decoder(dateDecoder: dateDecoder, sqliteData: .int(0))
|
||||||
|
|
||||||
|
XCTAssertEqual(try decoder.decodeDate(), expected)
|
||||||
|
XCTAssertTrue(dateDecoder.didCallDecode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeWithNullValue() {
|
||||||
|
let decoder = decoder(sqliteData: .null, codingPath: [DummyKey(stringValue: "test_key")])
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode(Int.self)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.valueNotFound(type, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.valueNotFound, but got: \(error).")
|
||||||
|
}
|
||||||
|
XCTAssertTrue(type == Int.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], decoder.codingPath as? [DummyKey])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Cannot get value of type Int, found null value instead.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeWithTypeMismatch() {
|
||||||
|
let decoder = decoder(sqliteData: .int(0), codingPath: [DummyKey(intValue: 1)])
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode(Data.self)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error).")
|
||||||
|
}
|
||||||
|
XCTAssertTrue(type == Data.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], decoder.codingPath as? [DummyKey])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected to decode Data but found an \(SQLiteRawValue.int(0)) instead.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeWithCorrectValue() {
|
||||||
|
let decoder = decoder(sqliteData: .text("test"))
|
||||||
|
XCTAssertEqual(try decoder.decode(String.self), "test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyedContainer() {
|
||||||
|
let path: [DummyKey] = [DummyKey(stringValue: "root")]
|
||||||
|
let decoder = decoder(sqliteData: .text("value"), codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try decoder.container(keyedBy: DummyKey.self)) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(expectedType, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(expectedType == KeyedDecodingContainer<DummyKey>.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path)
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected a keyed container, but found a single value.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnkeyedContainer() {
|
||||||
|
let path: [DummyKey] = [DummyKey(stringValue: "array_key")]
|
||||||
|
let decoder = decoder(sqliteData: .int(42), codingPath: path)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try decoder.unkeyedContainer()) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(expectedType, context) = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(expectedType == UnkeyedDecodingContainer.self)
|
||||||
|
XCTAssertEqual(context.codingPath as? [DummyKey], path)
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected a unkeyed container, but found a single value.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleValueContainer() throws {
|
||||||
|
let path: [DummyKey] = [DummyKey(stringValue: "test_key")]
|
||||||
|
let decoder = decoder(sqliteData: .int(0), codingPath: path)
|
||||||
|
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
|
||||||
|
guard let singleContainer = container as? SingleValueContainer<SingleValueDecoder> else {
|
||||||
|
return XCTFail("Expected SingleValueContainer")
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(singleContainer.decoder === decoder)
|
||||||
|
XCTAssertEqual(singleContainer.codingPath as? [DummyKey], path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SingleValueDecoderTests {
|
||||||
|
func decoder(
|
||||||
|
dateDecoder: DateDecoder = MockDateDecoder(),
|
||||||
|
sqliteData: SQLiteRawValue,
|
||||||
|
codingPath: [any CodingKey] = []
|
||||||
|
) -> SingleValueDecoder {
|
||||||
|
SingleValueDecoder(
|
||||||
|
dateDecoder: dateDecoder,
|
||||||
|
sqliteData: sqliteData,
|
||||||
|
codingPath: codingPath,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SingleValueDecoderTests {
|
||||||
|
struct DummyKey: CodingKey, Equatable {
|
||||||
|
var stringValue: String
|
||||||
|
var intValue: Int?
|
||||||
|
|
||||||
|
init(stringValue: String) {
|
||||||
|
self.stringValue = stringValue
|
||||||
|
self.intValue = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
init(intValue: Int) {
|
||||||
|
self.stringValue = "\(intValue)"
|
||||||
|
self.intValue = intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateDecoder: DateDecoder {
|
||||||
|
let expectedDate: Date
|
||||||
|
private(set) var didCallDecode = false
|
||||||
|
|
||||||
|
init(expectedDate: Date = Date()) {
|
||||||
|
self.expectedDate = expectedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(
|
||||||
|
from decoder: any ValueDecoder
|
||||||
|
) throws -> Date {
|
||||||
|
didCallDecode = true
|
||||||
|
return expectedDate
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(
|
||||||
|
from decoder: any RowDecoder,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
733
Tests/DLCDecoderTests/UnkeyedContainerTests.swift
Normal file
733
Tests/DLCDecoderTests/UnkeyedContainerTests.swift
Normal file
@@ -0,0 +1,733 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DLCDecoder
|
||||||
|
|
||||||
|
final class UnkeyedContainerTests: XCTestCase {
|
||||||
|
func testDecodeNil() {
|
||||||
|
XCTAssertThrowsError(try container().decodeNil()) {
|
||||||
|
checkIsAtEnd($0, Optional<Any>.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let nilContainer = container(withData: .null)
|
||||||
|
XCTAssertEqual(
|
||||||
|
nilContainer.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding nil"
|
||||||
|
)
|
||||||
|
XCTAssertTrue(
|
||||||
|
try nilContainer.decodeNil(),
|
||||||
|
"Expected decodeNil() to return true"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
nilContainer.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding nil"
|
||||||
|
)
|
||||||
|
|
||||||
|
let notNilContainer = container(withData: .text("value"))
|
||||||
|
XCTAssertFalse(
|
||||||
|
try notNilContainer.decodeNil(),
|
||||||
|
"Expected decodeNil() to return false for non-nil value"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
notNilContainer.currentIndex, 0,
|
||||||
|
"Expected index to remain unchanged for non-nil value"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeBool() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Bool.self)) {
|
||||||
|
checkIsAtEnd($0, Bool.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let trueContainer = container(withData: .int(1))
|
||||||
|
XCTAssertEqual(
|
||||||
|
trueContainer.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding true"
|
||||||
|
)
|
||||||
|
XCTAssertTrue(
|
||||||
|
try trueContainer.decode(Bool.self),
|
||||||
|
"Expected decode(Bool.self) to return true"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
trueContainer.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding true"
|
||||||
|
)
|
||||||
|
|
||||||
|
let falseContainer = container(withData: .int(0))
|
||||||
|
XCTAssertEqual(
|
||||||
|
falseContainer.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding false"
|
||||||
|
)
|
||||||
|
XCTAssertFalse(
|
||||||
|
try falseContainer.decode(Bool.self),
|
||||||
|
"Expected decode(Bool.self) to return false"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
falseContainer.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding false"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeString() {
|
||||||
|
XCTAssertThrowsError(try container().decode(String.self)) {
|
||||||
|
checkIsAtEnd($0, String.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .text("Hello, SQLite!"))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding string"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(String.self), "Hello, SQLite!",
|
||||||
|
"Expected decoded value to match original string"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding string"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDouble() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Double.self)) {
|
||||||
|
checkIsAtEnd($0, Double.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .real(3.14))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding double"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Double.self), 3.14,
|
||||||
|
"Expected decoded value to match original double"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding double"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeFloat() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Float.self)) {
|
||||||
|
checkIsAtEnd($0, Float.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .real(2.71))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding float"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Float.self), 2.71,
|
||||||
|
"Expected decoded value to match original float"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding float"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeInt() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Int.self)) {
|
||||||
|
checkIsAtEnd($0, Int.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(42))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding Int"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int.self), 42,
|
||||||
|
"Expected decoded value to match Int"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding Int"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeInt8() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Int8.self)) {
|
||||||
|
checkIsAtEnd($0, Int8.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(127))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding Int8"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int8.self), 127,
|
||||||
|
"Expected decoded value to match Int8"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding Int8"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeInt16() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Int16.self)) {
|
||||||
|
checkIsAtEnd($0, Int16.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(32_000))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding Int16"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int16.self), 32_000,
|
||||||
|
"Expected decoded value to match Int16"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding Int16"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeInt32() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Int32.self)) {
|
||||||
|
checkIsAtEnd($0, Int32.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(2_147_483_647))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding Int32"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int32.self), 2_147_483_647,
|
||||||
|
"Expected decoded value to match Int32"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding Int32"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeInt64() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Int64.self)) {
|
||||||
|
checkIsAtEnd($0, Int64.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(9_223_372_036_854_775_807))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding Int64"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Int64.self), 9_223_372_036_854_775_807,
|
||||||
|
"Expected decoded value to match Int64"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding Int64"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeUInt() {
|
||||||
|
XCTAssertThrowsError(try container().decode(UInt.self)) {
|
||||||
|
checkIsAtEnd($0, UInt.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(42))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding UInt"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt.self), 42,
|
||||||
|
"Expected decoded value to match UInt"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding UInt"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeUInt8() {
|
||||||
|
XCTAssertThrowsError(try container().decode(UInt8.self)) {
|
||||||
|
checkIsAtEnd($0, UInt8.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(255))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding UInt8"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt8.self), 255,
|
||||||
|
"Expected decoded value to match UInt8"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding UInt8"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeUInt16() {
|
||||||
|
XCTAssertThrowsError(try container().decode(UInt16.self)) {
|
||||||
|
checkIsAtEnd($0, UInt16.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(32_000))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding UInt16"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt16.self), 32_000,
|
||||||
|
"Expected decoded value to match UInt16"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding UInt16"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeUInt32() {
|
||||||
|
XCTAssertThrowsError(try container().decode(UInt32.self)) {
|
||||||
|
checkIsAtEnd($0, UInt32.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(4_294_967_295))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding UInt32"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt32.self), 4_294_967_295,
|
||||||
|
"Expected decoded value to match UInt32"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding UInt32"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeUInt64() {
|
||||||
|
XCTAssertThrowsError(try container().decode(UInt64.self)) {
|
||||||
|
checkIsAtEnd($0, UInt64.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = container(withData: .int(9_223_372_036_854_775_807))
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding UInt64"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(UInt64.self), 9_223_372_036_854_775_807,
|
||||||
|
"Expected decoded value to match UInt64"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding UInt64"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDate() {
|
||||||
|
XCTAssertThrowsError(try container().decode(Date.self)) {
|
||||||
|
checkIsAtEnd($0, Date.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let date = Date(timeIntervalSince1970: 12345)
|
||||||
|
let container = container(withData: .real(date.timeIntervalSince1970))
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding Date"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(Date.self), date,
|
||||||
|
"Expected decoded value to match Date"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding Date"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRawRepresentable() {
|
||||||
|
XCTAssertThrowsError(try container().decode(RawRepresentableEnum.self)) {
|
||||||
|
checkIsAtEnd($0, RawRepresentableEnum.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let `case` = RawRepresentableEnum.test
|
||||||
|
let container = container(withData: .text(`case`.rawValue))
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding RawRepresentableEnum"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(RawRepresentableEnum.self), `case`,
|
||||||
|
"Expected decoded value to match RawRepresentableEnum"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding RawRepresentableEnum"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeDecodable() {
|
||||||
|
XCTAssertThrowsError(try container().decode(DecodableEnum.self)) {
|
||||||
|
checkIsAtEnd($0, DecodableEnum.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
let `case` = DecodableEnum.test
|
||||||
|
let container = container(withData: .text(`case`.rawValue))
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 0,
|
||||||
|
"Expected index to be 0 before decoding DecodableEnum"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
try container.decode(DecodableEnum.self), `case`,
|
||||||
|
"Expected decoded value to match DecodableEnum"
|
||||||
|
)
|
||||||
|
XCTAssertEqual(
|
||||||
|
container.currentIndex, 1,
|
||||||
|
"Expected index to increment after decoding DecodableEnum"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedContainerKeyedBy() {
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container().nestedContainer(keyedBy: RowCodingKey.self)
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
XCTFail("Expected .typeMismatch, but got: \(error).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
type == KeyedDecodingContainer<RowCodingKey>.self,
|
||||||
|
"Expected KeyedDecodingContainer type."
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.codingPath as? [RowCodingKey], [.init(intValue: 0)],
|
||||||
|
"Incorrect coding path in decoding error."
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"""
|
||||||
|
Attempted to decode a nested keyed container,
|
||||||
|
but the value cannot be represented as a keyed container.
|
||||||
|
""",
|
||||||
|
"Unexpected debug description in decoding error."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedUnkeyedContainer() {
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container().nestedUnkeyedContainer()
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
XCTFail("Expected .typeMismatch, but got: \(error).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
type == UnkeyedDecodingContainer.self,
|
||||||
|
"Expected UnkeyedDecodingContainer type."
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.codingPath as? [RowCodingKey], [.init(intValue: 0)],
|
||||||
|
"Incorrect coding path in decoding error."
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"""
|
||||||
|
Attempted to decode a nested unkeyed container,
|
||||||
|
but the value cannot be represented as an unkeyed container.
|
||||||
|
""",
|
||||||
|
"Unexpected debug description in decoding error."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuperDecoder() {
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container().superDecoder()
|
||||||
|
) { error in
|
||||||
|
guard case let DecodingError.typeMismatch(type, context) = error else {
|
||||||
|
XCTFail("Expected .typeMismatch, but got: \(error).")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
type == Swift.Decoder.self,
|
||||||
|
"Expected Swift.Decoder type."
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.codingPath as? [RowCodingKey], [.init(intValue: 0)],
|
||||||
|
"Incorrect coding path in decoding error."
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"""
|
||||||
|
Attempted to get a superDecoder,
|
||||||
|
but SQLiteRowDecoder does not support superDecoding.
|
||||||
|
""",
|
||||||
|
"Unexpected debug description in decoding error."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UnkeyedContainerTests {
|
||||||
|
func container(
|
||||||
|
withData data: SQLiteRawValue,
|
||||||
|
codingPath: [any CodingKey] = []
|
||||||
|
) -> UnkeyedContainer<MockKeyedDecoder> {
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["key"] = data
|
||||||
|
return container(
|
||||||
|
withData: row,
|
||||||
|
codingPath: codingPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func container(
|
||||||
|
withData data: SQLiteRow = .init(),
|
||||||
|
codingPath: [any CodingKey] = []
|
||||||
|
) -> UnkeyedContainer<MockKeyedDecoder> {
|
||||||
|
UnkeyedContainer(
|
||||||
|
decoder: MockKeyedDecoder(sqliteData: data),
|
||||||
|
codingPath: codingPath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkIsAtEnd(_ error: any Error, _ expectedType: Any.Type) {
|
||||||
|
guard case let DecodingError.valueNotFound(type, context) = error else {
|
||||||
|
XCTFail("Expected .valueNotFound, got: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
XCTAssertTrue(
|
||||||
|
type == expectedType,
|
||||||
|
"Expected type: \(expectedType), got: \(type)"
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.codingPath as? [RowCodingKey],
|
||||||
|
[RowCodingKey(intValue: 0)],
|
||||||
|
"Invalid codingPath: \(context.codingPath)"
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"Unkeyed container is at end.",
|
||||||
|
"Invalid debugDescription: \(context.debugDescription)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UnkeyedContainerTests {
|
||||||
|
enum RawRepresentableEnum: String, Decodable, SQLiteRawRepresentable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DecodableEnum: String, Decodable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateDecoder: DateDecoder {
|
||||||
|
func decode(
|
||||||
|
from decoder: any ValueDecoder
|
||||||
|
) throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(
|
||||||
|
from decoder: any RowDecoder,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> Date {
|
||||||
|
try decoder.decode(Date.self, for: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockKeyedDecoder: RowDecoder {
|
||||||
|
typealias SQLiteData = SQLiteRow
|
||||||
|
|
||||||
|
let sqliteData: SQLiteData
|
||||||
|
let dateDecoder: DateDecoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
var count: Int? {
|
||||||
|
sqliteData.isEmpty ? nil : sqliteData.count
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteData,
|
||||||
|
dateDecoder: DateDecoder = MockDateDecoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateDecoder = dateDecoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(_ key: any CodingKey) -> Bool {
|
||||||
|
sqliteData.contains(key.stringValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil(
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> Bool {
|
||||||
|
sqliteData[key.intValue!].value == .null
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDate(for key: any CodingKey) throws -> Date {
|
||||||
|
try dateDecoder.decode(from: self, for: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: SQLiteRawRepresentable>(
|
||||||
|
_ type: T.Type,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> T {
|
||||||
|
type.init(sqliteData[key.intValue!].value)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func decoder(for key: any CodingKey) -> any Decoder {
|
||||||
|
MockValueDecoder(sqliteData: sqliteData[key.intValue!].value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) throws -> KeyedDecodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockValueDecoder: ValueDecoder, SingleValueDecodingContainer {
|
||||||
|
typealias SQLiteData = SQLiteRawValue
|
||||||
|
|
||||||
|
let sqliteData: SQLiteData
|
||||||
|
var dateDecoder: DateDecoder
|
||||||
|
var codingPath: [any CodingKey]
|
||||||
|
var userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteData,
|
||||||
|
dateDecoder: DateDecoder = MockDateDecoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateDecoder = dateDecoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil() -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDate() throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: SQLiteRawRepresentable>(
|
||||||
|
_ type: T.Type
|
||||||
|
) throws -> T {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) throws -> KeyedDecodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Bool.Type) throws -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: String.Type) throws -> String {
|
||||||
|
type.init(sqliteData)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Double.Type) throws -> Double {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Float.Type) throws -> Float {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int.Type) throws -> Int {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int8.Type) throws -> Int8 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int16.Type) throws -> Int16 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int32.Type) throws -> Int32 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int64.Type) throws -> Int64 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt.Type) throws -> UInt {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt8.Type) throws -> UInt8 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt16.Type) throws -> UInt16 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt32.Type) throws -> UInt32 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt64.Type) throws -> UInt64 {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: Decodable>(_ type: T.Type) throws -> T {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Tests/DLCEncoderTests/FailedEncoderTests.swift
Normal file
29
Tests/DLCEncoderTests/FailedEncoderTests.swift
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import XCTest
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DLCEncoder
|
||||||
|
|
||||||
|
final class FailedEncoderTests: XCTestCase {
|
||||||
|
func testKeyedContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 1), RowCodingKey(intValue: 2)]
|
||||||
|
let encoder = FailedEncoder(codingPath: path)
|
||||||
|
let container = encoder.container(keyedBy: RowCodingKey.self)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnkeyedContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 1), RowCodingKey(intValue: 2)]
|
||||||
|
let encoder = FailedEncoder(codingPath: path)
|
||||||
|
let container = encoder.unkeyedContainer()
|
||||||
|
XCTAssertTrue(container is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleValueContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 1), RowCodingKey(intValue: 2)]
|
||||||
|
let encoder = FailedEncoder(codingPath: path)
|
||||||
|
let container = encoder.singleValueContainer()
|
||||||
|
XCTAssertTrue(container is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
}
|
||||||
153
Tests/DLCEncoderTests/FailedEncodingContainerTests.swift
Normal file
153
Tests/DLCEncoderTests/FailedEncodingContainerTests.swift
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import XCTest
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DLCEncoder
|
||||||
|
|
||||||
|
final class FailedEncodingContainerTests: XCTestCase {
|
||||||
|
func testEncodeNil() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container.encodeNil()
|
||||||
|
) { error in
|
||||||
|
guard case let EncodingError.invalidValue(_, context) = error else {
|
||||||
|
return XCTFail()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.codingPath as? [RowCodingKey], path)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"encodeNil() is not supported for this encoding path."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeNilForKey() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let key = RowCodingKey(intValue: 2)
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container.encodeNil(forKey: key)
|
||||||
|
) { error in
|
||||||
|
guard case let EncodingError.invalidValue(_, context) = error else {
|
||||||
|
return XCTFail()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"encodeNil(forKey:) is not supported for this encoding path."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeValue() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container.encode(123)
|
||||||
|
) { error in
|
||||||
|
guard case let EncodingError.invalidValue(_, context) = error else {
|
||||||
|
return XCTFail()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.codingPath as? [RowCodingKey], path)
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"encode(_:) is not supported for this encoding path."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeValueForKey() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let key = RowCodingKey(intValue: 2)
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try container.encode(123, forKey: key)
|
||||||
|
) { error in
|
||||||
|
guard case let EncodingError.invalidValue(_, context) = error else {
|
||||||
|
return XCTFail()
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"encode(_:forKey:) is not supported for this encoding path."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedKeyedContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let nestedContainer = container.nestedContainer(
|
||||||
|
keyedBy: RowCodingKey.self
|
||||||
|
)
|
||||||
|
XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedKeyedContainerForKey() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let key = RowCodingKey(intValue: 2)
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let nestedContainer = container.nestedContainer(
|
||||||
|
keyedBy: RowCodingKey.self, forKey: key
|
||||||
|
)
|
||||||
|
XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedUnkeyedContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let nestedContainer = container.nestedUnkeyedContainer()
|
||||||
|
XCTAssertTrue(nestedContainer is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedUnkeyedContainerForKey() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let key = RowCodingKey(intValue: 2)
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let nestedContainer = container.nestedUnkeyedContainer(forKey: key)
|
||||||
|
XCTAssertTrue(nestedContainer is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuperEncoder() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let encoder = container.superEncoder()
|
||||||
|
XCTAssertTrue(encoder is FailedEncoder)
|
||||||
|
XCTAssertEqual(encoder.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuperEncoderForKey() {
|
||||||
|
let path = [RowCodingKey(intValue: 1)]
|
||||||
|
let key = RowCodingKey(intValue: 2)
|
||||||
|
let container = FailedEncodingContainer<RowCodingKey>(
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let encoder = container.superEncoder(forKey: key)
|
||||||
|
XCTAssertTrue(encoder is FailedEncoder)
|
||||||
|
XCTAssertEqual(encoder.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
}
|
||||||
|
}
|
||||||
505
Tests/DLCEncoderTests/KeyedContainerTests.swift
Normal file
505
Tests/DLCEncoderTests/KeyedContainerTests.swift
Normal file
@@ -0,0 +1,505 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DLCEncoder
|
||||||
|
|
||||||
|
final class KeyedContainerTests: XCTestCase {
|
||||||
|
func testEncodeNil() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeNil(forKey: .key1)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 1)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeBool() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(true, forKey: .key1)
|
||||||
|
try container.encodeIfPresent(false, forKey: .key2)
|
||||||
|
try container.encodeIfPresent(nil as Bool?, forKey: .key3)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 3)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(1))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .int(0))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key3.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeString() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent("test", forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as String?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .text("test"))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDouble() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(Double(3.14), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as Double?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .real(3.14))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeFloat() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(Float(3.14), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as Float?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .real(Double(Float(3.14))))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(Int(42), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as Int?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(42))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt8() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(Int8(8), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as Int8?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(Int64(8)))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt16() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(Int16(16), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as Int16?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(Int64(16)))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt32() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(Int32(32), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as Int32?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(Int64(32)))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt64() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(Int64(64), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as Int64?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(64))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(UInt(42), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as UInt?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(42))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt8() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(UInt8(8), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as UInt8?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(8))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt16() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(UInt16(16), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as UInt16?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(16))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt32() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(UInt32(32), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as UInt32?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(32))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt64() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(UInt64(64), forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as UInt64?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .int(64))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDate() throws {
|
||||||
|
let date = Date()
|
||||||
|
let dateString = ISO8601DateFormatter().string(from: date)
|
||||||
|
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(date, forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as Date?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .text(dateString))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeRawRepresentable() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(RawRepresentableModel.test, forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as RawRepresentableModel?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .text(RawRepresentableModel.test.rawValue))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeEncodable() throws {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: []
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encodeIfPresent(EncodableModel.test, forKey: .key1)
|
||||||
|
try container.encodeIfPresent(nil as EncodableModel?, forKey: .key2)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1.stringValue], .text(EncodableModel.test.rawValue))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2.stringValue], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedKeyedContainer() {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let nestedContainer = container.nestedContainer(
|
||||||
|
keyedBy: CodingKeys.self, forKey: .key3
|
||||||
|
)
|
||||||
|
XCTAssertEqual(nestedContainer.codingPath as? [CodingKeys], [.key1, .key3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedUnkeyedContainer() {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let nestedContainer = container.nestedUnkeyedContainer(forKey: .key3)
|
||||||
|
XCTAssertTrue(nestedContainer is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(nestedContainer.codingPath as? [CodingKeys], [.key1, .key3])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuperEncoder() {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let superEncoder = container.superEncoder()
|
||||||
|
XCTAssertTrue(superEncoder is FailedEncoder)
|
||||||
|
XCTAssertEqual(superEncoder.codingPath as? [CodingKeys], [.key1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuperEncoderForKey() {
|
||||||
|
let encoder = MockSingleRowEncoder()
|
||||||
|
var container = KeyedEncodingContainer(
|
||||||
|
KeyedContainer<MockSingleRowEncoder, CodingKeys>(
|
||||||
|
encoder: encoder, codingPath: [CodingKeys.key1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
let superEncoder = container.superEncoder(forKey: .key3)
|
||||||
|
XCTAssertTrue(superEncoder is FailedEncoder)
|
||||||
|
XCTAssertEqual(superEncoder.codingPath as? [CodingKeys], [.key1, .key3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension KeyedContainerTests {
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case key1
|
||||||
|
case key2
|
||||||
|
case key3
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RawRepresentableModel: String, Encodable, SQLiteRawRepresentable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EncodableModel: String, Encodable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateEncoder: DateEncoder {
|
||||||
|
func encode(_ date: Date, to encoder: any ValueEncoder) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ date: Date, for key: any CodingKey, to encoder: any RowEncoder) throws {
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
let dateString = formatter.string(from: date)
|
||||||
|
try encoder.encode(dateString, for: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockSingleRowEncoder: RowEncoder {
|
||||||
|
private(set) var sqliteData: SQLiteRow
|
||||||
|
let dateEncoder: any DateEncoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
var count: Int { sqliteData.count }
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteRow = SQLiteRow(),
|
||||||
|
dateEncoder: any DateEncoder = MockDateEncoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateEncoder = dateEncoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(_ value: Any, for key: any CodingKey) throws {
|
||||||
|
guard let value = value as? SQLiteRawValue else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
sqliteData[key.stringValue] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNil(for key: any CodingKey) throws {
|
||||||
|
sqliteData[key.stringValue] = .null
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeDate(_ date: Date, for key: any CodingKey) throws {
|
||||||
|
try dateEncoder.encode(date, for: key, to: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T: SQLiteRawBindable>(_ value: T, for key: any CodingKey) throws {
|
||||||
|
sqliteData[key.stringValue] = value.sqliteRawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func encoder(for key: any CodingKey) throws -> any Encoder {
|
||||||
|
MockSingleValueEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) -> KeyedEncodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockSingleValueEncoder: ValueEncoder {
|
||||||
|
private(set) var sqliteData: SQLiteRawValue?
|
||||||
|
let dateEncoder: any DateEncoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteRawValue? = nil,
|
||||||
|
dateEncoder: any DateEncoder = MockDateEncoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateEncoder = dateEncoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
MockSingleValueContainer(encoder: self, codingPath: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockSingleValueContainer<Encoder: ValueEncoder>: Container, SingleValueEncodingContainer {
|
||||||
|
let encoder: Encoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
|
||||||
|
init(
|
||||||
|
encoder: Encoder,
|
||||||
|
codingPath: [any CodingKey]
|
||||||
|
) {
|
||||||
|
self.encoder = encoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
}
|
||||||
|
|
||||||
|
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 SQLiteRawRepresentable:
|
||||||
|
try encoder.encode(value)
|
||||||
|
default:
|
||||||
|
try value.encode(to: encoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
174
Tests/DLCEncoderTests/MultiRowEncoderTests.swift
Normal file
174
Tests/DLCEncoderTests/MultiRowEncoderTests.swift
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DLCEncoder
|
||||||
|
|
||||||
|
final class MultiRowEncoderTests: XCTestCase {
|
||||||
|
func testSetValueForKey() throws {
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: [],
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
try encoder.set(SQLiteRow(), for: RowCodingKey(intValue: 0))
|
||||||
|
try encoder.set(SQLiteRow(), for: RowCodingKey(intValue: 1))
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 2)
|
||||||
|
XCTAssertEqual(encoder.count, encoder.sqliteData.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSetInvalidValueForKey() {
|
||||||
|
let value = "Test Value"
|
||||||
|
let path = [RowCodingKey(intValue: 0)]
|
||||||
|
let key = RowCodingKey(intValue: 1)
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try encoder.set(value, for: key)
|
||||||
|
) { error in
|
||||||
|
guard case let EncodingError.invalidValue(thrownValue, context) = error else {
|
||||||
|
return XCTFail("Expected EncodingError.invalidValue, got \(error)")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(thrownValue as? String, value)
|
||||||
|
XCTAssertEqual(context.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Expected value of type SQLiteRow")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeNilThrows() {
|
||||||
|
let key = RowCodingKey(intValue: 1)
|
||||||
|
let path = [RowCodingKey(intValue: 0)]
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try encoder.encodeNil(for: key)) { error in
|
||||||
|
guard case let EncodingError.invalidValue(_, context) = error else {
|
||||||
|
return XCTFail("Expected EncodingError.invalidValue, got \(error)")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(context.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Attempted to encode nil, but it's not supported.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDateThrows() {
|
||||||
|
let key = RowCodingKey(intValue: 2)
|
||||||
|
let path = [RowCodingKey(intValue: 0)]
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let date = Date()
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try encoder.encodeDate(date, for: key)) { error in
|
||||||
|
guard case let EncodingError.invalidValue(thrownValue, context) = error else {
|
||||||
|
return XCTFail("Expected EncodingError.invalidValue, got \(error)")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(thrownValue as? Date, date)
|
||||||
|
XCTAssertEqual(context.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
XCTAssertEqual(context.debugDescription, "Attempted to encode Date, but it's not supported.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeRawBindableThrows() {
|
||||||
|
let value = "Test Value"
|
||||||
|
let path = [RowCodingKey(intValue: 0)]
|
||||||
|
let key = RowCodingKey(intValue: 1)
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertThrowsError(try encoder.encode(value, for: key)) { error in
|
||||||
|
guard case let EncodingError.invalidValue(thrownValue, context) = error else {
|
||||||
|
return XCTFail("Expected EncodingError.invalidValue, got \(error)")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(thrownValue as? String, value)
|
||||||
|
XCTAssertEqual(context.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
XCTAssertEqual(
|
||||||
|
context.debugDescription,
|
||||||
|
"Attempted to encode \(type(of: value)), but it's not supported."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncoderForKey() throws {
|
||||||
|
let path = [RowCodingKey(intValue: 0)]
|
||||||
|
let key = RowCodingKey(intValue: 1)
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
let nestedEncoder = try encoder.encoder(for: key)
|
||||||
|
|
||||||
|
XCTAssertTrue(nestedEncoder is SingleRowEncoder)
|
||||||
|
XCTAssertEqual(nestedEncoder.codingPath as? [RowCodingKey], path + [key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyedContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 0)]
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.container(keyedBy: RowCodingKey.self)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnkeyedContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 0)]
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.unkeyedContainer()
|
||||||
|
|
||||||
|
XCTAssertTrue(container is UnkeyedContainer<MultiRowEncoder>)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleValueContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 0)]
|
||||||
|
let encoder = MultiRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.singleValueContainer()
|
||||||
|
|
||||||
|
XCTAssertTrue(container is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension MultiRowEncoderTests {
|
||||||
|
final class MockDateEncoder: DateEncoder {
|
||||||
|
private(set) var didCallEncode = false
|
||||||
|
|
||||||
|
func encode(
|
||||||
|
_ date: Date,
|
||||||
|
to encoder: any ValueEncoder
|
||||||
|
) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(
|
||||||
|
_ date: Date,
|
||||||
|
for key: any CodingKey,
|
||||||
|
to encoder: any RowEncoder
|
||||||
|
) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
164
Tests/DLCEncoderTests/SingleRowEncoderTests.swift
Normal file
164
Tests/DLCEncoderTests/SingleRowEncoderTests.swift
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DLCEncoder
|
||||||
|
|
||||||
|
final class SingleRowEncoderTests: XCTestCase {
|
||||||
|
func testSetValueForKey() throws {
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: [],
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
try encoder.set(SQLiteRawValue.int(42), for: CodingKeys.key1)
|
||||||
|
try encoder.set(SQLiteRawValue.real(3.14), for: CodingKeys.key2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1], .int(42))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2], .real(3.14))
|
||||||
|
XCTAssertEqual(encoder.count, encoder.sqliteData.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSetInvalidValueForKey() {
|
||||||
|
let value = "Test Value"
|
||||||
|
let path = [CodingKeys.key1]
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try encoder.set(value, for: CodingKeys.key2)
|
||||||
|
) { error in
|
||||||
|
guard case let EncodingError.invalidValue(thrownValue, context) = error else {
|
||||||
|
return XCTFail("Expected EncodingError.invalidValue, got \(error)")
|
||||||
|
}
|
||||||
|
XCTAssertEqual(thrownValue as? String, value)
|
||||||
|
XCTAssertEqual(context.codingPath as? [CodingKeys], path + [.key2])
|
||||||
|
XCTAssertEqual(context.debugDescription, "The value does not match SQLiteRawValue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeNilForKey() throws {
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: [],
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
try encoder.encodeNil(for: CodingKeys.key1)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1], .null)
|
||||||
|
XCTAssertEqual(encoder.count, encoder.sqliteData.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDateForKey() throws {
|
||||||
|
let date = Date()
|
||||||
|
let mockDateEncoder = MockDateEncoder()
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: mockDateEncoder,
|
||||||
|
codingPath: [],
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
try encoder.encodeDate(date, for: CodingKeys.key1)
|
||||||
|
|
||||||
|
XCTAssertTrue(mockDateEncoder.didCallEncode)
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1], date.sqliteRawValue)
|
||||||
|
XCTAssertEqual(encoder.count, encoder.sqliteData.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeValueForKey() throws {
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: [],
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
try encoder.encode(123, for: CodingKeys.key1)
|
||||||
|
try encoder.encode(3.14, for: CodingKeys.key2)
|
||||||
|
try encoder.encode("Hello", for: CodingKeys.key3)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key1], .int(123))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key2], .real(3.14))
|
||||||
|
XCTAssertEqual(encoder.sqliteData[CodingKeys.key3], .text("Hello"))
|
||||||
|
XCTAssertEqual(encoder.count, encoder.sqliteData.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncoderForKey() throws {
|
||||||
|
let path = [CodingKeys.key1]
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
|
||||||
|
let key = CodingKeys.key2
|
||||||
|
let nestedEncoder = try encoder.encoder(for: key)
|
||||||
|
|
||||||
|
XCTAssertTrue(nestedEncoder is SingleValueEncoder)
|
||||||
|
XCTAssertEqual(nestedEncoder.codingPath as? [CodingKeys], path + [key])
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyedContainer() {
|
||||||
|
let path = [CodingKeys.key1]
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
XCTAssertEqual(container.codingPath as? [CodingKeys], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnkeyedContainer() {
|
||||||
|
let path = [CodingKeys.key1]
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.unkeyedContainer()
|
||||||
|
|
||||||
|
XCTAssertTrue(container is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(container.codingPath as? [CodingKeys], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleValueContainer() {
|
||||||
|
let path = [CodingKeys.key1]
|
||||||
|
let encoder = SingleRowEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.singleValueContainer()
|
||||||
|
|
||||||
|
XCTAssertTrue(container is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(container.codingPath as? [CodingKeys], path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SingleRowEncoderTests {
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case key1
|
||||||
|
case key2
|
||||||
|
case key3
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateEncoder: DateEncoder {
|
||||||
|
private(set) var didCallEncode = false
|
||||||
|
|
||||||
|
func encode(
|
||||||
|
_ date: Date,
|
||||||
|
to encoder: any ValueEncoder
|
||||||
|
) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(
|
||||||
|
_ date: Date,
|
||||||
|
for key: any CodingKey,
|
||||||
|
to encoder: any RowEncoder
|
||||||
|
) throws {
|
||||||
|
didCallEncode = true
|
||||||
|
try encoder.encode(date, for: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
205
Tests/DLCEncoderTests/SingleValueContainerTests.swift
Normal file
205
Tests/DLCEncoderTests/SingleValueContainerTests.swift
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
|
||||||
|
@testable import DLCEncoder
|
||||||
|
|
||||||
|
final class SingleValueContainerTests: XCTestCase {
|
||||||
|
func testEncodeNil() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encodeNil()
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeBool() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
|
||||||
|
try container.encode(true)
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(1))
|
||||||
|
|
||||||
|
try container.encode(false)
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeString() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode("test string")
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .text("test string"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDouble() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(Double(3.14))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .real(3.14))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeFloat() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(Float(3.14))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .real(Double(Float(3.14))))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(Int(42))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(42))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt8() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(Int8(8))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(Int64(8)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt16() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(Int16(16))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(Int64(16)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt32() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(Int32(32))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(Int64(32)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeInt64() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(Int64(64))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(UInt(42))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(42))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt8() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(UInt8(8))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(8))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt16() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(UInt16(16))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(16))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt32() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(UInt32(32))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(32))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeUInt64() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(UInt64(64))
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .int(64))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDate() throws {
|
||||||
|
let date = Date()
|
||||||
|
let dateString = ISO8601DateFormatter().string(from: date)
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(date)
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .text(dateString))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeRawRepresentable() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(RawRepresentableModel.test)
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .text(RawRepresentableModel.test.rawValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeEncodable() throws {
|
||||||
|
let encoder = MockSingleValueEncoder()
|
||||||
|
let container = SingleValueContainer(encoder: encoder, codingPath: [])
|
||||||
|
try container.encode(EncodableModel.test)
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .text(EncodableModel.test.rawValue))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SingleValueContainerTests {
|
||||||
|
enum RawRepresentableModel: String, Encodable, SQLiteRawRepresentable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EncodableModel: String, Encodable {
|
||||||
|
case test
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateEncoder: DateEncoder {
|
||||||
|
func encode(_ date: Date, to encoder: any ValueEncoder) throws {
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
let dateString = formatter.string(from: date)
|
||||||
|
try encoder.encode(dateString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ date: Date, for key: any CodingKey, to encoder: any RowEncoder) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockSingleValueEncoder: ValueEncoder {
|
||||||
|
private(set) var sqliteData: SQLiteRawValue?
|
||||||
|
let dateEncoder: any DateEncoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteRawValue? = nil,
|
||||||
|
dateEncoder: any DateEncoder = MockDateEncoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateEncoder = dateEncoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
SingleValueContainer(encoder: self, codingPath: codingPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
108
Tests/DLCEncoderTests/SingleValueEncoderTests.swift
Normal file
108
Tests/DLCEncoderTests/SingleValueEncoderTests.swift
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DLCEncoder
|
||||||
|
|
||||||
|
final class SingleValueEncoderTests: XCTestCase {
|
||||||
|
func testEncodeNil() throws {
|
||||||
|
let encoder = SingleValueEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: [],
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
try encoder.encodeNil()
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeDate() throws {
|
||||||
|
let date = Date()
|
||||||
|
let dateEncoder = MockDateEncoder()
|
||||||
|
let encoder = SingleValueEncoder(
|
||||||
|
dateEncoder: dateEncoder,
|
||||||
|
codingPath: [],
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
try encoder.encodeDate(date)
|
||||||
|
XCTAssertEqual(encoder.sqliteData, date.sqliteRawValue)
|
||||||
|
XCTAssertTrue(dateEncoder.didCallEncode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeValue() throws {
|
||||||
|
let encoder = SingleValueEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: [],
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
try encoder.encode("Test String")
|
||||||
|
XCTAssertEqual(encoder.sqliteData, .text("Test String"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testKeyedContainer() {
|
||||||
|
let path = [
|
||||||
|
RowCodingKey(intValue: 1),
|
||||||
|
RowCodingKey(intValue: 2)
|
||||||
|
]
|
||||||
|
let encoder = SingleValueEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.container(
|
||||||
|
keyedBy: RowCodingKey.self
|
||||||
|
)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUnkeyedContainer() {
|
||||||
|
let path = [
|
||||||
|
RowCodingKey(intValue: 1),
|
||||||
|
RowCodingKey(intValue: 2)
|
||||||
|
]
|
||||||
|
let encoder = SingleValueEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.unkeyedContainer()
|
||||||
|
XCTAssertTrue(container is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleValueContainer() {
|
||||||
|
let path = [
|
||||||
|
RowCodingKey(intValue: 1),
|
||||||
|
RowCodingKey(intValue: 2)
|
||||||
|
]
|
||||||
|
let encoder = SingleValueEncoder(
|
||||||
|
dateEncoder: MockDateEncoder(),
|
||||||
|
codingPath: path,
|
||||||
|
userInfo: [:]
|
||||||
|
)
|
||||||
|
let container = encoder.singleValueContainer()
|
||||||
|
XCTAssertTrue(container is SingleValueContainer<SingleValueEncoder>)
|
||||||
|
XCTAssertEqual(container.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension SingleValueEncoderTests {
|
||||||
|
final class MockDateEncoder: DateEncoder {
|
||||||
|
private(set) var didCallEncode = false
|
||||||
|
|
||||||
|
func encode(
|
||||||
|
_ date: Date,
|
||||||
|
to encoder: any ValueEncoder
|
||||||
|
) throws {
|
||||||
|
didCallEncode = true
|
||||||
|
try encoder.encode(date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(
|
||||||
|
_ date: Date,
|
||||||
|
for key: any CodingKey,
|
||||||
|
to encoder: any RowEncoder
|
||||||
|
) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
296
Tests/DLCEncoderTests/UnkeyedContainerTests.swift
Normal file
296
Tests/DLCEncoderTests/UnkeyedContainerTests.swift
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DLCEncoder
|
||||||
|
|
||||||
|
final class UnkeyedContainerTests: XCTestCase {
|
||||||
|
func testEncodeNil() throws {
|
||||||
|
let encoder = MockMultiRowEncoder()
|
||||||
|
let container = UnkeyedContainer<MockMultiRowEncoder>(
|
||||||
|
encoder: encoder,
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
try container.encodeNil()
|
||||||
|
XCTAssertTrue(encoder.sqliteData.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeModel() throws {
|
||||||
|
let encoder = MockMultiRowEncoder()
|
||||||
|
let container = UnkeyedContainer<MockMultiRowEncoder>(
|
||||||
|
encoder: encoder,
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encode(TestModel(id: 1, name: "John"))
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 1)
|
||||||
|
XCTAssertEqual(encoder.sqliteData.first?.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData.first?["id"], .int(1))
|
||||||
|
XCTAssertEqual(encoder.sqliteData.first?["name"], .text("John"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeOptionalModel() throws {
|
||||||
|
let encoder = MockMultiRowEncoder()
|
||||||
|
let container = UnkeyedContainer<MockMultiRowEncoder>(
|
||||||
|
encoder: encoder,
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encode(TestModel(id: 1, name: "John") as TestModel?)
|
||||||
|
|
||||||
|
XCTAssertEqual(encoder.sqliteData.count, 1)
|
||||||
|
XCTAssertEqual(encoder.sqliteData.first?.count, 2)
|
||||||
|
XCTAssertEqual(encoder.sqliteData.first?["id"], .int(1))
|
||||||
|
XCTAssertEqual(encoder.sqliteData.first?["name"], .text("John"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeNilModel() throws {
|
||||||
|
let encoder = MockMultiRowEncoder()
|
||||||
|
let container = UnkeyedContainer<MockMultiRowEncoder>(
|
||||||
|
encoder: encoder,
|
||||||
|
codingPath: []
|
||||||
|
)
|
||||||
|
|
||||||
|
try container.encode(nil as TestModel?)
|
||||||
|
|
||||||
|
XCTAssertTrue(encoder.sqliteData.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedKeyedContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 123)]
|
||||||
|
let encoder = MockMultiRowEncoder()
|
||||||
|
let container = UnkeyedContainer<MockMultiRowEncoder>(
|
||||||
|
encoder: encoder,
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let nestedContainer = container.nestedContainer(
|
||||||
|
keyedBy: RowCodingKey.self
|
||||||
|
)
|
||||||
|
XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNestedUnkeyedContainer() {
|
||||||
|
let path = [RowCodingKey(intValue: 123)]
|
||||||
|
let encoder = MockMultiRowEncoder()
|
||||||
|
let container = UnkeyedContainer<MockMultiRowEncoder>(
|
||||||
|
encoder: encoder,
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let nestedContainer = container.nestedUnkeyedContainer()
|
||||||
|
XCTAssertTrue(nestedContainer is FailedEncodingContainer<RowCodingKey>)
|
||||||
|
XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSuperEncoder() {
|
||||||
|
let path = [RowCodingKey(intValue: 123)]
|
||||||
|
let encoder = MockMultiRowEncoder()
|
||||||
|
let container = UnkeyedContainer<MockMultiRowEncoder>(
|
||||||
|
encoder: encoder,
|
||||||
|
codingPath: path
|
||||||
|
)
|
||||||
|
let superEncoder = container.superEncoder()
|
||||||
|
XCTAssertTrue(superEncoder is FailedEncoder)
|
||||||
|
XCTAssertEqual(superEncoder.codingPath as? [RowCodingKey], path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension UnkeyedContainerTests {
|
||||||
|
struct TestModel: Encodable {
|
||||||
|
let id: Int
|
||||||
|
let name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockDateEncoder: DateEncoder {
|
||||||
|
func encode(_ date: Date, to encoder: any ValueEncoder) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(_ date: Date, for key: any CodingKey, to encoder: any RowEncoder) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockMultiRowEncoder: RowEncoder {
|
||||||
|
private(set) var sqliteData: [SQLiteRow]
|
||||||
|
let dateEncoder: any DateEncoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
var count: Int { sqliteData.count }
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: [SQLiteRow] = [],
|
||||||
|
dateEncoder: any DateEncoder = MockDateEncoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateEncoder = dateEncoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(_ value: Any, for key: any CodingKey) throws {
|
||||||
|
guard let value = value as? SQLiteRow else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
sqliteData.append(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNil(for key: any CodingKey) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeDate(_ date: Date, for key: any CodingKey) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T: SQLiteRawBindable>(_ value: T, for key: any CodingKey) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encoder(for key: any CodingKey) throws -> any Encoder {
|
||||||
|
MockSingleRowEncoder()
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) -> KeyedEncodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockSingleRowEncoder: RowEncoder {
|
||||||
|
private(set) var sqliteData: SQLiteRow
|
||||||
|
let dateEncoder: any DateEncoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
var count: Int { sqliteData.count }
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteRow = SQLiteRow(),
|
||||||
|
dateEncoder: any DateEncoder = MockDateEncoder(),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateEncoder = dateEncoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(_ value: Any, for key: any CodingKey) throws {
|
||||||
|
guard let value = value as? SQLiteRawValue else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
sqliteData[key.stringValue] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNil(for key: any CodingKey) throws {
|
||||||
|
sqliteData[key.stringValue] = .null
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeDate(_ date: Date, for key: any CodingKey) throws {
|
||||||
|
try dateEncoder.encode(date, for: key, to: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T: SQLiteRawBindable>(_ value: T, for key: any CodingKey) throws {
|
||||||
|
sqliteData[key.stringValue] = value.sqliteRawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func encoder(for key: any CodingKey) throws -> any Encoder {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) -> KeyedEncodingContainer<Key> {
|
||||||
|
let container = MockKeyedContainer<MockSingleRowEncoder, Key>(
|
||||||
|
encoder: self, codingPath: []
|
||||||
|
)
|
||||||
|
return KeyedEncodingContainer(container)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MockKeyedContainer<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 SQLiteRawRepresentable:
|
||||||
|
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<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> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer(
|
||||||
|
forKey key: Key
|
||||||
|
) -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func superEncoder() -> any Swift.Encoder {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func superEncoder(forKey key: Key) -> any Swift.Encoder {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
241
Tests/DataLiteCoderTests/DateDecoderTests.swift
Normal file
241
Tests/DataLiteCoderTests/DateDecoderTests.swift
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCDecoder
|
||||||
|
|
||||||
|
@testable import DataLiteCoder
|
||||||
|
|
||||||
|
final class DateDecoderTests: XCTestCase {
|
||||||
|
func testDeferredToDate() {
|
||||||
|
let date = Date(timeIntervalSince1970: 123456789)
|
||||||
|
let dateDecoder = RowDecoder.DateDecoder(strategy: .deferredToDate)
|
||||||
|
|
||||||
|
let singleDecoder = SingleValueDecoder(
|
||||||
|
sqliteData: .real(date.timeIntervalSince1970)
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: singleDecoder), date)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key.stringValue] = .real(date.timeIntervalSince1970)
|
||||||
|
let keyedDecoder = KeyedDecoder(sqliteData: row)
|
||||||
|
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: keyedDecoder, for: CodingKeys.key), date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testISO8601() throws {
|
||||||
|
let dateDecoder = RowDecoder.DateDecoder(strategy: .iso8601)
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
let string = "2024-04-18T13:45:00Z"
|
||||||
|
let date = formatter.date(from: string)!
|
||||||
|
|
||||||
|
let single = SingleValueDecoder(sqliteData: .text(string))
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: single), date)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key.stringValue] = .text(string)
|
||||||
|
let keyed = KeyedDecoder(sqliteData: row)
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: keyed, for: CodingKeys.key), date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFormatted() throws {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||||
|
let string = "2023-10-15 16:30:00"
|
||||||
|
let date = formatter.date(from: string)!
|
||||||
|
|
||||||
|
let dateDecoder = RowDecoder.DateDecoder(strategy: .formatted(formatter))
|
||||||
|
|
||||||
|
let single = SingleValueDecoder(sqliteData: .text(string))
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: single), date)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key.stringValue] = .text(string)
|
||||||
|
let keyed = KeyedDecoder(sqliteData: row)
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: keyed, for: CodingKeys.key), date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFormattedInvalidDate() {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||||
|
let invalidString = "not a date"
|
||||||
|
|
||||||
|
let dateDecoder = RowDecoder.DateDecoder(strategy: .formatted(formatter))
|
||||||
|
|
||||||
|
let single = SingleValueDecoder(sqliteData: .text(invalidString))
|
||||||
|
XCTAssertThrowsError(try dateDecoder.decode(from: single))
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key.stringValue] = .text(invalidString)
|
||||||
|
let keyed = KeyedDecoder(sqliteData: row)
|
||||||
|
XCTAssertThrowsError(try dateDecoder.decode(from: keyed, for: CodingKeys.key))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMillisecondsSince1970Int() throws {
|
||||||
|
let millis: Int64 = 1_600_000_000_000
|
||||||
|
let date = Date(timeIntervalSince1970: Double(millis) / 1000)
|
||||||
|
|
||||||
|
let dateDecoder = RowDecoder.DateDecoder(strategy: .millisecondsSince1970Int)
|
||||||
|
|
||||||
|
let single = SingleValueDecoder(sqliteData: .int(millis))
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: single), date)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key.stringValue] = .int(millis)
|
||||||
|
let keyed = KeyedDecoder(sqliteData: row)
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: keyed, for: CodingKeys.key), date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMillisecondsSince1970Double() throws {
|
||||||
|
let millis: Double = 1_600_000_000_000.0
|
||||||
|
let date = Date(timeIntervalSince1970: millis / 1000)
|
||||||
|
|
||||||
|
let dateDecoder = RowDecoder.DateDecoder(strategy: .millisecondsSince1970Double)
|
||||||
|
|
||||||
|
let single = SingleValueDecoder(sqliteData: .real(millis))
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: single), date)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key.stringValue] = .real(millis)
|
||||||
|
let keyed = KeyedDecoder(sqliteData: row)
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: keyed, for: CodingKeys.key), date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSecondsSince1970Int() throws {
|
||||||
|
let seconds: Int64 = 1_600_000_000
|
||||||
|
let date = Date(timeIntervalSince1970: Double(seconds))
|
||||||
|
|
||||||
|
let dateDecoder = RowDecoder.DateDecoder(strategy: .secondsSince1970Int)
|
||||||
|
|
||||||
|
let single = SingleValueDecoder(sqliteData: .int(seconds))
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: single), date)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key.stringValue] = .int(seconds)
|
||||||
|
let keyed = KeyedDecoder(sqliteData: row)
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: keyed, for: CodingKeys.key), date)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSecondsSince1970Double() throws {
|
||||||
|
let seconds: Double = 1_600_000_000.789
|
||||||
|
let date = Date(timeIntervalSince1970: seconds)
|
||||||
|
|
||||||
|
let dateDecoder = RowDecoder.DateDecoder(strategy: .secondsSince1970Double)
|
||||||
|
|
||||||
|
let single = SingleValueDecoder(sqliteData: .real(seconds))
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: single), date)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row[CodingKeys.key.stringValue] = .real(seconds)
|
||||||
|
let keyed = KeyedDecoder(sqliteData: row)
|
||||||
|
XCTAssertEqual(try dateDecoder.decode(from: keyed, for: CodingKeys.key), date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DateDecoderTests {
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case key
|
||||||
|
}
|
||||||
|
|
||||||
|
final class SingleValueDecoder: DLCDecoder.ValueDecoder {
|
||||||
|
let sqliteData: SQLiteRawValue
|
||||||
|
let dateDecoder: DLCDecoder.DateDecoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteRawValue,
|
||||||
|
dateDecoder: DLCDecoder.DateDecoder = RowDecoder.DateDecoder(strategy: .deferredToDate),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateDecoder = dateDecoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil() -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDate() throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: SQLiteRawRepresentable>(_ type: T.Type) throws -> T {
|
||||||
|
type.init(sqliteData)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) throws -> KeyedDecodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class KeyedDecoder: DLCDecoder.RowDecoder {
|
||||||
|
let sqliteData: SQLiteRow
|
||||||
|
let dateDecoder: DLCDecoder.DateDecoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey: Any]
|
||||||
|
|
||||||
|
var count: Int? { sqliteData.count }
|
||||||
|
|
||||||
|
init(
|
||||||
|
sqliteData: SQLiteRow,
|
||||||
|
dateDecoder: DLCDecoder.DateDecoder = RowDecoder.DateDecoder(strategy: .deferredToDate),
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey: Any] = [:]
|
||||||
|
) {
|
||||||
|
self.sqliteData = sqliteData
|
||||||
|
self.dateDecoder = dateDecoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func contains(_ key: any CodingKey) -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil(for key: any CodingKey) throws -> Bool {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeDate(for key: any CodingKey) throws -> Date {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T: SQLiteRawRepresentable>(
|
||||||
|
_ type: T.Type,
|
||||||
|
for key: any CodingKey
|
||||||
|
) throws -> T {
|
||||||
|
type.init(sqliteData[key.stringValue]!)!
|
||||||
|
}
|
||||||
|
|
||||||
|
func decoder(for key: any CodingKey) -> any Decoder {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) throws -> KeyedDecodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> any UnkeyedDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> any SingleValueDecodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
222
Tests/DataLiteCoderTests/DateEncoderTests.swift
Normal file
222
Tests/DataLiteCoderTests/DateEncoderTests.swift
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DLCEncoder
|
||||||
|
import DLCCommon
|
||||||
|
|
||||||
|
@testable import DataLiteCoder
|
||||||
|
|
||||||
|
final class DateEncoderTests: XCTestCase {
|
||||||
|
func testDeferredToDate() throws {
|
||||||
|
let date = Date(timeIntervalSince1970: 1234567890)
|
||||||
|
|
||||||
|
let dateEncoder = RowEncoder.DateEncoder(strategy: .deferredToDate)
|
||||||
|
let singleEncoder = SingleValueEncoder(dateEncoder: dateEncoder)
|
||||||
|
let keyedEncoder = KeyedEncoder(dateEncoder: dateEncoder)
|
||||||
|
let key = RowCodingKey(stringValue: "key1")
|
||||||
|
|
||||||
|
try dateEncoder.encode(date, to: singleEncoder)
|
||||||
|
try dateEncoder.encode(date, for: key, to: keyedEncoder)
|
||||||
|
|
||||||
|
XCTAssertEqual(singleEncoder.sqliteData, date.sqliteRawValue)
|
||||||
|
XCTAssertEqual(keyedEncoder.sqliteData[key], date.sqliteRawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testISO8601() throws {
|
||||||
|
let formatter = ISO8601DateFormatter()
|
||||||
|
let string = "2024-04-18T13:45:00Z"
|
||||||
|
let date = formatter.date(from: string)!
|
||||||
|
|
||||||
|
let dateEncoder = RowEncoder.DateEncoder(strategy: .iso8601)
|
||||||
|
let singleEncoder = SingleValueEncoder(dateEncoder: dateEncoder)
|
||||||
|
let keyedEncoder = KeyedEncoder(dateEncoder: dateEncoder)
|
||||||
|
let key = RowCodingKey(stringValue: "key1")
|
||||||
|
|
||||||
|
try dateEncoder.encode(date, to: singleEncoder)
|
||||||
|
try dateEncoder.encode(date, for: key, to: keyedEncoder)
|
||||||
|
|
||||||
|
XCTAssertEqual(singleEncoder.sqliteData, .text(string))
|
||||||
|
XCTAssertEqual(keyedEncoder.sqliteData[key], .text(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFormatted() throws {
|
||||||
|
let formatter = DateFormatter()
|
||||||
|
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
|
||||||
|
let string = "2023-10-15 16:30:00"
|
||||||
|
let date = formatter.date(from: string)!
|
||||||
|
|
||||||
|
let dateEncoder = RowEncoder.DateEncoder(strategy: .formatted(formatter))
|
||||||
|
let singleEncoder = SingleValueEncoder(dateEncoder: dateEncoder)
|
||||||
|
let keyedEncoder = KeyedEncoder(dateEncoder: dateEncoder)
|
||||||
|
let key = RowCodingKey(stringValue: "key1")
|
||||||
|
|
||||||
|
try dateEncoder.encode(date, to: singleEncoder)
|
||||||
|
try dateEncoder.encode(date, for: key, to: keyedEncoder)
|
||||||
|
|
||||||
|
XCTAssertEqual(singleEncoder.sqliteData, .text(string))
|
||||||
|
XCTAssertEqual(keyedEncoder.sqliteData[key], .text(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMillisecondsSince1970Int() throws {
|
||||||
|
let millis: Int64 = 1_600_000_000_000
|
||||||
|
let date = Date(timeIntervalSince1970: Double(millis) / 1000)
|
||||||
|
|
||||||
|
let dateEncoder = RowEncoder.DateEncoder(strategy: .millisecondsSince1970Int)
|
||||||
|
let singleEncoder = SingleValueEncoder(dateEncoder: dateEncoder)
|
||||||
|
let keyedEncoder = KeyedEncoder(dateEncoder: dateEncoder)
|
||||||
|
let key = RowCodingKey(stringValue: "key1")
|
||||||
|
|
||||||
|
try dateEncoder.encode(date, to: singleEncoder)
|
||||||
|
try dateEncoder.encode(date, for: key, to: keyedEncoder)
|
||||||
|
|
||||||
|
XCTAssertEqual(singleEncoder.sqliteData, .int(millis))
|
||||||
|
XCTAssertEqual(keyedEncoder.sqliteData[key], .int(millis))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testMillisecondsSince1970Double() throws {
|
||||||
|
let millis: Double = 1_600_000_000_000
|
||||||
|
let date = Date(timeIntervalSince1970: millis / 1000)
|
||||||
|
|
||||||
|
let dateEncoder = RowEncoder.DateEncoder(strategy: .millisecondsSince1970Double)
|
||||||
|
let singleEncoder = SingleValueEncoder(dateEncoder: dateEncoder)
|
||||||
|
let keyedEncoder = KeyedEncoder(dateEncoder: dateEncoder)
|
||||||
|
let key = RowCodingKey(stringValue: "key1")
|
||||||
|
|
||||||
|
try dateEncoder.encode(date, to: singleEncoder)
|
||||||
|
try dateEncoder.encode(date, for: key, to: keyedEncoder)
|
||||||
|
|
||||||
|
XCTAssertEqual(singleEncoder.sqliteData, .real(millis))
|
||||||
|
XCTAssertEqual(keyedEncoder.sqliteData[key], .real(millis))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSecondsSince1970Int() throws {
|
||||||
|
let seconds: Int64 = 1_600_000_000
|
||||||
|
let date = Date(timeIntervalSince1970: Double(seconds))
|
||||||
|
|
||||||
|
let dateEncoder = RowEncoder.DateEncoder(strategy: .secondsSince1970Int)
|
||||||
|
let singleEncoder = SingleValueEncoder(dateEncoder: dateEncoder)
|
||||||
|
let keyedEncoder = KeyedEncoder(dateEncoder: dateEncoder)
|
||||||
|
let key = RowCodingKey(stringValue: "key1")
|
||||||
|
|
||||||
|
try dateEncoder.encode(date, to: singleEncoder)
|
||||||
|
try dateEncoder.encode(date, for: key, to: keyedEncoder)
|
||||||
|
|
||||||
|
XCTAssertEqual(singleEncoder.sqliteData, .int(seconds))
|
||||||
|
XCTAssertEqual(keyedEncoder.sqliteData[key], .int(seconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSecondsSince1970Double() throws {
|
||||||
|
let seconds: Double = 1_600_000_000
|
||||||
|
let date = Date(timeIntervalSince1970: seconds)
|
||||||
|
|
||||||
|
let dateEncoder = RowEncoder.DateEncoder(strategy: .secondsSince1970Double)
|
||||||
|
let singleEncoder = SingleValueEncoder(dateEncoder: dateEncoder)
|
||||||
|
let keyedEncoder = KeyedEncoder(dateEncoder: dateEncoder)
|
||||||
|
let key = RowCodingKey(stringValue: "key1")
|
||||||
|
|
||||||
|
try dateEncoder.encode(date, to: singleEncoder)
|
||||||
|
try dateEncoder.encode(date, for: key, to: keyedEncoder)
|
||||||
|
|
||||||
|
XCTAssertEqual(singleEncoder.sqliteData, .real(seconds))
|
||||||
|
XCTAssertEqual(keyedEncoder.sqliteData[key], .real(seconds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension DateEncoderTests {
|
||||||
|
final class SingleValueEncoder: DLCEncoder.ValueEncoder {
|
||||||
|
let dateEncoder: any DateEncoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey : Any]
|
||||||
|
|
||||||
|
private(set) var sqliteData: SQLiteRawValue?
|
||||||
|
|
||||||
|
init(
|
||||||
|
dateEncoder: any DateEncoder,
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey : Any] = [:]
|
||||||
|
) {
|
||||||
|
self.dateEncoder = dateEncoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNil() throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeDate(_ date: Date) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T: SQLiteRawBindable>(_ value: T) throws {
|
||||||
|
sqliteData = value.sqliteRawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) -> KeyedEncodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class KeyedEncoder: DLCEncoder.RowEncoder {
|
||||||
|
let dateEncoder: any DateEncoder
|
||||||
|
let codingPath: [any CodingKey]
|
||||||
|
let userInfo: [CodingUserInfoKey : Any]
|
||||||
|
|
||||||
|
private(set) var sqliteData = SQLiteRow()
|
||||||
|
|
||||||
|
var count: Int { fatalError() }
|
||||||
|
|
||||||
|
init(
|
||||||
|
dateEncoder: any DateEncoder,
|
||||||
|
codingPath: [any CodingKey] = [],
|
||||||
|
userInfo: [CodingUserInfoKey : Any] = [:]
|
||||||
|
) {
|
||||||
|
self.dateEncoder = dateEncoder
|
||||||
|
self.codingPath = codingPath
|
||||||
|
self.userInfo = userInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func set(_ value: Any, for key: any CodingKey) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeNil(for key: any CodingKey) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeDate(_ date: Date, for key: any CodingKey) throws {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode<T: SQLiteRawBindable>(_ value: T, for key: any CodingKey) throws {
|
||||||
|
sqliteData[key] = value.sqliteRawValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func encoder(for key: any CodingKey) throws -> any Encoder {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func container<Key: CodingKey>(
|
||||||
|
keyedBy type: Key.Type
|
||||||
|
) -> KeyedEncodingContainer<Key> {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() -> any UnkeyedEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() -> any SingleValueEncodingContainer {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
337
Tests/DataLiteCoderTests/RowDecoderTests.swift
Normal file
337
Tests/DataLiteCoderTests/RowDecoderTests.swift
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DataLiteCoder
|
||||||
|
|
||||||
|
final class RowDecoderTests: XCTestCase {
|
||||||
|
// MARK: - Decode SQLiteRow
|
||||||
|
|
||||||
|
func testDecodeRowWithAllTypes() throws {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
let model = StandardModel(
|
||||||
|
id: 123,
|
||||||
|
type: .simple,
|
||||||
|
name: "John Doe",
|
||||||
|
age: 34,
|
||||||
|
isActive: true,
|
||||||
|
score: 3.1415,
|
||||||
|
createdAt: Date(timeIntervalSince1970: 12345),
|
||||||
|
payload: "payload".data(using: .utf8)!
|
||||||
|
)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["id"] = model.id.sqliteRawValue
|
||||||
|
row["type"] = model.type.rawValue.sqliteRawValue
|
||||||
|
row["name"] = model.name.sqliteRawValue
|
||||||
|
row["age"] = model.age.sqliteRawValue
|
||||||
|
row["isActive"] = model.isActive.sqliteRawValue
|
||||||
|
row["score"] = model.score.sqliteRawValue
|
||||||
|
row["createdAt"] = model.createdAt.sqliteRawValue
|
||||||
|
row["payload"] = model.payload.sqliteRawValue
|
||||||
|
|
||||||
|
let decoded = try decoder.decode(
|
||||||
|
StandardModel.self,
|
||||||
|
from: row
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, model)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRowWithOptionalValues() throws {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
let model = OptionalModel(
|
||||||
|
id: 123,
|
||||||
|
type: .multiple,
|
||||||
|
name: "Jane Doe",
|
||||||
|
createdAt: Date(timeIntervalSince1970: 11000),
|
||||||
|
payload: "payload".data(using: .utf8)!
|
||||||
|
)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["id"] = model.id!.sqliteRawValue
|
||||||
|
row["type"] = model.type!.rawValue.sqliteRawValue
|
||||||
|
row["name"] = model.name!.sqliteRawValue
|
||||||
|
row["createdAt"] = model.createdAt!.sqliteRawValue
|
||||||
|
row["payload"] = model.payload!.sqliteRawValue
|
||||||
|
|
||||||
|
let decoded = try decoder.decode(
|
||||||
|
OptionalModel.self,
|
||||||
|
from: row
|
||||||
|
)
|
||||||
|
|
||||||
|
let empty = try decoder.decode(
|
||||||
|
OptionalModel.self,
|
||||||
|
from: SQLiteRow()
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, model)
|
||||||
|
XCTAssertEqual(empty, OptionalModel())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRowAsArray() throws {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
let dates = [
|
||||||
|
Date(timeIntervalSince1970: 1234),
|
||||||
|
Date(timeIntervalSince1970: 31415),
|
||||||
|
Date(timeIntervalSince1970: 123456789)
|
||||||
|
]
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["key0"] = dates[0].sqliteRawValue
|
||||||
|
row["key1"] = dates[1].sqliteRawValue
|
||||||
|
row["key2"] = dates[2].sqliteRawValue
|
||||||
|
|
||||||
|
let decoded = try decoder.decode([Date].self, from: row)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, dates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRowMissingRequiredField() {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["id"] = 1.sqliteRawValue
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode(SimpleModel.self, from: row)
|
||||||
|
) { error in
|
||||||
|
guard case DecodingError.keyNotFound = error else {
|
||||||
|
return XCTFail("Expected DecodingError.keyNotFound, but got: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRowWrongType() {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["id"] = "not an int".sqliteRawValue
|
||||||
|
row["name"] = "test".sqliteRawValue
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode(SimpleModel.self, from: row)
|
||||||
|
) { error in
|
||||||
|
guard case DecodingError.typeMismatch = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Decode array of SQLiteRow
|
||||||
|
|
||||||
|
func testDecodeRowArrayWithAllTypes() throws {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
let models = [
|
||||||
|
StandardModel(
|
||||||
|
id: 123,
|
||||||
|
type: .simple,
|
||||||
|
name: "John Doe",
|
||||||
|
age: 34,
|
||||||
|
isActive: true,
|
||||||
|
score: 3.1415,
|
||||||
|
createdAt: Date(timeIntervalSince1970: 12345),
|
||||||
|
payload: "payload".data(using: .utf8)!
|
||||||
|
),
|
||||||
|
StandardModel(
|
||||||
|
id: 456,
|
||||||
|
type: .multiple,
|
||||||
|
name: "Jane Doe",
|
||||||
|
age: 28,
|
||||||
|
isActive: false,
|
||||||
|
score: 2.7182,
|
||||||
|
createdAt: Date(timeIntervalSince1970: 67890),
|
||||||
|
payload: "another payload".data(using: .utf8)!
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
let rows: [SQLiteRow] = models.map { model in
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["id"] = model.id.sqliteRawValue
|
||||||
|
row["type"] = model.type.rawValue.sqliteRawValue
|
||||||
|
row["name"] = model.name.sqliteRawValue
|
||||||
|
row["age"] = model.age.sqliteRawValue
|
||||||
|
row["isActive"] = model.isActive.sqliteRawValue
|
||||||
|
row["score"] = model.score.sqliteRawValue
|
||||||
|
row["createdAt"] = model.createdAt.sqliteRawValue
|
||||||
|
row["payload"] = model.payload.sqliteRawValue
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = try decoder.decode([StandardModel].self, from: rows)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, models)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRowArrayWithOptionalValues() throws {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
let models = [
|
||||||
|
OptionalModel(
|
||||||
|
id: 123,
|
||||||
|
type: .multiple,
|
||||||
|
name: "Jane Doe",
|
||||||
|
createdAt: Date(timeIntervalSince1970: 11000),
|
||||||
|
payload: "payload".data(using: .utf8)!
|
||||||
|
),
|
||||||
|
OptionalModel(
|
||||||
|
id: nil,
|
||||||
|
type: nil,
|
||||||
|
name: "John Doe",
|
||||||
|
createdAt: nil,
|
||||||
|
payload: nil
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
let rows: [SQLiteRow] = models.map { model in
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["id"] = model.id?.sqliteRawValue
|
||||||
|
row["type"] = model.type?.rawValue.sqliteRawValue
|
||||||
|
row["name"] = model.name?.sqliteRawValue
|
||||||
|
row["createdAt"] = model.createdAt?.sqliteRawValue
|
||||||
|
row["payload"] = model.payload?.sqliteRawValue
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = try decoder.decode([OptionalModel].self, from: rows)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, models)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRowArrayAsDates() throws {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
let dates = [
|
||||||
|
[
|
||||||
|
Date(timeIntervalSince1970: 1234),
|
||||||
|
Date(timeIntervalSince1970: 31415),
|
||||||
|
Date(timeIntervalSince1970: 12345679)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
Date(timeIntervalSince1970: 1234),
|
||||||
|
Date(timeIntervalSince1970: 31415),
|
||||||
|
Date(timeIntervalSince1970: 12345679)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let rows: [SQLiteRow] = dates.map { dates in
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["key0"] = dates[0].sqliteRawValue
|
||||||
|
row["key1"] = dates[1].sqliteRawValue
|
||||||
|
row["key2"] = dates[2].sqliteRawValue
|
||||||
|
return row
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoded = try decoder.decode([[Date]].self, from: rows)
|
||||||
|
|
||||||
|
XCTAssertEqual(decoded, dates)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRowArrayMissingRequiredField() {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["id"] = 1.sqliteRawValue
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode([SimpleModel].self, from: [row])
|
||||||
|
) { error in
|
||||||
|
guard case DecodingError.keyNotFound = error else {
|
||||||
|
return XCTFail("Expected DecodingError.keyNotFound, but got: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDecodeRowArrayWrongType() {
|
||||||
|
let decoder = RowDecoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateDecodingStrategy: .deferredToDate
|
||||||
|
)
|
||||||
|
|
||||||
|
var row = SQLiteRow()
|
||||||
|
row["id"] = "not an int".sqliteRawValue
|
||||||
|
row["name"] = "test".sqliteRawValue
|
||||||
|
|
||||||
|
XCTAssertThrowsError(
|
||||||
|
try decoder.decode([SimpleModel].self, from: [row])
|
||||||
|
) { error in
|
||||||
|
guard case DecodingError.typeMismatch = error else {
|
||||||
|
return XCTFail("Expected DecodingError.typeMismatch, but got: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension RowDecoderTests {
|
||||||
|
enum `Type`: String, Decodable, Equatable {
|
||||||
|
case simple
|
||||||
|
case multiple
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StandardModel: Decodable, Equatable {
|
||||||
|
let id: Int
|
||||||
|
let type: `Type`
|
||||||
|
let name: String
|
||||||
|
let age: Int
|
||||||
|
let isActive: Bool
|
||||||
|
let score: Double
|
||||||
|
let createdAt: Date
|
||||||
|
let payload: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OptionalModel: Decodable, Equatable {
|
||||||
|
let id: Int?
|
||||||
|
let type: `Type`?
|
||||||
|
let name: String?
|
||||||
|
let createdAt: Date?
|
||||||
|
let payload: Data?
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: Int? = nil,
|
||||||
|
type: `Type`? = nil,
|
||||||
|
name: String? = nil,
|
||||||
|
createdAt: Date? = nil,
|
||||||
|
payload: Data? = nil
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.type = type
|
||||||
|
self.name = name
|
||||||
|
self.createdAt = createdAt
|
||||||
|
self.payload = payload
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SimpleModel: Decodable, Equatable {
|
||||||
|
let id: Int
|
||||||
|
let name: String
|
||||||
|
}
|
||||||
|
}
|
||||||
231
Tests/DataLiteCoderTests/RowEncoderTests.swift
Normal file
231
Tests/DataLiteCoderTests/RowEncoderTests.swift
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
import XCTest
|
||||||
|
import DataLiteCore
|
||||||
|
import DataLiteCoder
|
||||||
|
|
||||||
|
final class RowEncoderTests: XCTestCase {
|
||||||
|
// MARK: - Encode to SQLiteRow
|
||||||
|
|
||||||
|
func testEncodeToRowWithAllTypes() throws {
|
||||||
|
let createdAt = Date(timeIntervalSince1970: 12345)
|
||||||
|
let payload = "payload".data(using: .utf8)!
|
||||||
|
|
||||||
|
let model = StandardModel(
|
||||||
|
id: 123456,
|
||||||
|
type: .simple,
|
||||||
|
name: "John Doe",
|
||||||
|
age: 34,
|
||||||
|
isActive: true,
|
||||||
|
score: 3.1415,
|
||||||
|
createdAt: createdAt,
|
||||||
|
payload: payload
|
||||||
|
)
|
||||||
|
let row = try RowEncoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateEncodingStrategy: .deferredToDate
|
||||||
|
).encode(model)
|
||||||
|
|
||||||
|
XCTAssertEqual(row.count, 8)
|
||||||
|
XCTAssertEqual(row["id"], .int(123456))
|
||||||
|
XCTAssertEqual(row["type"], .text("simple"))
|
||||||
|
XCTAssertEqual(row["name"], .text("John Doe"))
|
||||||
|
XCTAssertEqual(row["age"], .int(34))
|
||||||
|
XCTAssertEqual(row["isActive"], .int(1))
|
||||||
|
XCTAssertEqual(row["score"], .real(3.1415))
|
||||||
|
XCTAssertEqual(row["createdAt"], createdAt.sqliteRawValue)
|
||||||
|
XCTAssertEqual(row["payload"], .blob(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeToRowWithOptionalValues() throws {
|
||||||
|
let createdAt = Date(timeIntervalSince1970: 12345)
|
||||||
|
let payload = "payload".data(using: .utf8)!
|
||||||
|
|
||||||
|
let model = OptionalModel(
|
||||||
|
id: 123456,
|
||||||
|
type: .multiple,
|
||||||
|
name: "Jane Doe",
|
||||||
|
createdAt: createdAt,
|
||||||
|
payload: payload
|
||||||
|
)
|
||||||
|
let row = try RowEncoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateEncodingStrategy: .deferredToDate
|
||||||
|
).encode(model)
|
||||||
|
|
||||||
|
XCTAssertEqual(row.count, 5)
|
||||||
|
XCTAssertEqual(row["id"], .int(123456))
|
||||||
|
XCTAssertEqual(row["type"], .text("multiple"))
|
||||||
|
XCTAssertEqual(row["name"], .text("Jane Doe"))
|
||||||
|
XCTAssertEqual(row["createdAt"], createdAt.sqliteRawValue)
|
||||||
|
XCTAssertEqual(row["payload"], .blob(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeToRowWithOptionalNilValues() throws {
|
||||||
|
let row = try RowEncoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateEncodingStrategy: .deferredToDate
|
||||||
|
).encode(OptionalModel())
|
||||||
|
|
||||||
|
XCTAssertEqual(row.count, 5)
|
||||||
|
XCTAssertEqual(row["id"], .null)
|
||||||
|
XCTAssertEqual(row["type"], .null)
|
||||||
|
XCTAssertEqual(row["name"], .null)
|
||||||
|
XCTAssertEqual(row["createdAt"], .null)
|
||||||
|
XCTAssertEqual(row["payload"], .null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Encode to Array of SQLiteRow
|
||||||
|
|
||||||
|
func testEncodeToRowArrayWithAllTypes() throws {
|
||||||
|
let createdAt = Date(timeIntervalSince1970: 12345)
|
||||||
|
let payload = "payload".data(using: .utf8)!
|
||||||
|
|
||||||
|
let models = [
|
||||||
|
StandardModel(
|
||||||
|
id: 123456,
|
||||||
|
type: .simple,
|
||||||
|
name: "John Doe",
|
||||||
|
age: 34,
|
||||||
|
isActive: true,
|
||||||
|
score: 3.1415,
|
||||||
|
createdAt: createdAt,
|
||||||
|
payload: payload
|
||||||
|
),
|
||||||
|
StandardModel(
|
||||||
|
id: 456,
|
||||||
|
type: .multiple,
|
||||||
|
name: "Jane Doe",
|
||||||
|
age: 28,
|
||||||
|
isActive: false,
|
||||||
|
score: 2.7182,
|
||||||
|
createdAt: createdAt,
|
||||||
|
payload: payload
|
||||||
|
)
|
||||||
|
]
|
||||||
|
let rows = try RowEncoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateEncodingStrategy: .deferredToDate
|
||||||
|
).encode(models)
|
||||||
|
|
||||||
|
XCTAssertEqual(rows.count, 2)
|
||||||
|
XCTAssertEqual(rows[0].count, 8)
|
||||||
|
XCTAssertEqual(rows[1].count, 8)
|
||||||
|
|
||||||
|
XCTAssertEqual(rows[0]["id"], .int(123456))
|
||||||
|
XCTAssertEqual(rows[0]["type"], .text("simple"))
|
||||||
|
XCTAssertEqual(rows[0]["name"], .text("John Doe"))
|
||||||
|
XCTAssertEqual(rows[0]["age"], .int(34))
|
||||||
|
XCTAssertEqual(rows[0]["isActive"], .int(1))
|
||||||
|
XCTAssertEqual(rows[0]["score"], .real(3.1415))
|
||||||
|
XCTAssertEqual(rows[0]["createdAt"], createdAt.sqliteRawValue)
|
||||||
|
XCTAssertEqual(rows[0]["payload"], .blob(payload))
|
||||||
|
|
||||||
|
XCTAssertEqual(rows[1]["id"], .int(456))
|
||||||
|
XCTAssertEqual(rows[1]["type"], .text("multiple"))
|
||||||
|
XCTAssertEqual(rows[1]["name"], .text("Jane Doe"))
|
||||||
|
XCTAssertEqual(rows[1]["age"], .int(28))
|
||||||
|
XCTAssertEqual(rows[1]["isActive"], .int(0))
|
||||||
|
XCTAssertEqual(rows[1]["score"], .real(2.7182))
|
||||||
|
XCTAssertEqual(rows[1]["createdAt"], createdAt.sqliteRawValue)
|
||||||
|
XCTAssertEqual(rows[1]["payload"], .blob(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEncodeToRowArrayWithOptionalValues() throws {
|
||||||
|
let createdAt = Date(timeIntervalSince1970: 12345)
|
||||||
|
let payload = "payload".data(using: .utf8)!
|
||||||
|
|
||||||
|
let models = [
|
||||||
|
OptionalModel(
|
||||||
|
id: 123,
|
||||||
|
type: .multiple,
|
||||||
|
name: "Jane Doe",
|
||||||
|
createdAt: createdAt,
|
||||||
|
payload: payload
|
||||||
|
),
|
||||||
|
OptionalModel(
|
||||||
|
id: nil,
|
||||||
|
type: nil,
|
||||||
|
name: "John Doe",
|
||||||
|
createdAt: nil,
|
||||||
|
payload: nil
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
let rows = try RowEncoder(
|
||||||
|
userInfo: [:],
|
||||||
|
dateEncodingStrategy: .deferredToDate
|
||||||
|
).encode(models)
|
||||||
|
|
||||||
|
XCTAssertEqual(rows.count, 2)
|
||||||
|
XCTAssertEqual(rows[0].count, 5)
|
||||||
|
XCTAssertEqual(rows[1].count, 5)
|
||||||
|
|
||||||
|
XCTAssertEqual(rows[0]["id"], .int(123))
|
||||||
|
XCTAssertEqual(rows[0]["type"], .text("multiple"))
|
||||||
|
XCTAssertEqual(rows[0]["name"], .text("Jane Doe"))
|
||||||
|
XCTAssertEqual(rows[0]["createdAt"], createdAt.sqliteRawValue)
|
||||||
|
XCTAssertEqual(rows[0]["payload"], .blob(payload))
|
||||||
|
|
||||||
|
XCTAssertEqual(rows[1]["id"], .null)
|
||||||
|
XCTAssertEqual(rows[1]["type"], .null)
|
||||||
|
XCTAssertEqual(rows[1]["name"], .text("John Doe"))
|
||||||
|
XCTAssertEqual(rows[1]["createdAt"], .null)
|
||||||
|
XCTAssertEqual(rows[1]["payload"], .null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension RowEncoderTests {
|
||||||
|
enum `Type`: String, Encodable, Equatable {
|
||||||
|
case simple
|
||||||
|
case multiple
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StandardModel: Encodable, Equatable {
|
||||||
|
let id: Int
|
||||||
|
let type: `Type`
|
||||||
|
let name: String
|
||||||
|
let age: Int
|
||||||
|
let isActive: Bool
|
||||||
|
let score: Double
|
||||||
|
let createdAt: Date
|
||||||
|
let payload: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OptionalModel: Encodable, Equatable {
|
||||||
|
let id: Int?
|
||||||
|
let type: `Type`?
|
||||||
|
let name: String?
|
||||||
|
let createdAt: Date?
|
||||||
|
let payload: Data?
|
||||||
|
|
||||||
|
init(
|
||||||
|
id: Int? = nil,
|
||||||
|
type: `Type`? = nil,
|
||||||
|
name: String? = nil,
|
||||||
|
createdAt: Date? = nil,
|
||||||
|
payload: Data? = nil
|
||||||
|
) {
|
||||||
|
self.id = id
|
||||||
|
self.type = type
|
||||||
|
self.name = name
|
||||||
|
self.createdAt = createdAt
|
||||||
|
self.payload = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CodingKeys: CodingKey {
|
||||||
|
case id
|
||||||
|
case type
|
||||||
|
case name
|
||||||
|
case createdAt
|
||||||
|
case payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(to encoder: any Encoder) throws {
|
||||||
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
try container.encodeIfPresent(self.id, forKey: .id)
|
||||||
|
try container.encodeIfPresent(self.type, forKey: .type)
|
||||||
|
try container.encodeIfPresent(self.name, forKey: .name)
|
||||||
|
try container.encodeIfPresent(self.createdAt, forKey: .createdAt)
|
||||||
|
try container.encodeIfPresent(self.payload, forKey: .payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user