From 5aec6ea5784dd5bd098bfa98036fbdc362a8931c Mon Sep 17 00:00:00 2001 From: Oleksii Zghurskyi Date: Sun, 27 Apr 2025 12:53:43 +0300 Subject: [PATCH] DataLireCoder swift package --- .gitignore | 17 + .swift-format | 15 + Package.swift | 56 ++ README.md | 59 ++ Sources/DLCCommon/Extensions/SQLiteRow.swift | 29 + .../DLCCommon/Structures/RowCodingKey.swift | 20 + .../DLCDecoder/Classes/KeyedContainer.swift | 102 +++ .../DLCDecoder/Classes/MultiRowDecoder.swift | 104 +++ .../DLCDecoder/Classes/SingleRowDecoder.swift | 135 ++++ .../Classes/SingleValueContainer.swift | 36 + .../Classes/SingleValueDecoder.swift | 87 +++ .../DLCDecoder/Classes/UnkeyedContainer.swift | 121 +++ Sources/DLCDecoder/Protocols/Container.swift | 6 + .../DLCDecoder/Protocols/DateDecoder.swift | 6 + Sources/DLCDecoder/Protocols/Decoder.swift | 8 + .../Protocols/KeyCheckingDecoder.swift | 5 + Sources/DLCDecoder/Protocols/RowDecoder.swift | 11 + .../DLCDecoder/Protocols/ValueDecoder.swift | 8 + .../DLCEncoder/Classes/FailedEncoder.swift | 37 + .../Classes/FailedEncodingContainer.swift | 84 ++ .../DLCEncoder/Classes/KeyedContainer.swift | 201 +++++ .../DLCEncoder/Classes/MultiRowEncoder.swift | 95 +++ .../DLCEncoder/Classes/SingleRowEncoder.swift | 79 ++ .../Classes/SingleValueContainer.swift | 36 + .../Classes/SingleValueEncoder.swift | 55 ++ .../DLCEncoder/Classes/UnkeyedContainer.swift | 59 ++ Sources/DLCEncoder/Protocols/Container.swift | 6 + .../DLCEncoder/Protocols/DateEncoder.swift | 6 + Sources/DLCEncoder/Protocols/Encoder.swift | 8 + .../DLCEncoder/Protocols/Flattenable.swift | 15 + Sources/DLCEncoder/Protocols/RowEncoder.swift | 12 + .../DLCEncoder/Protocols/ValueEncoder.swift | 8 + .../DataLiteCoder/Classes/DateDecoder.swift | 83 ++ .../DataLiteCoder/Classes/DateEncoder.swift | 51 ++ .../DataLiteCoder/Classes/RowDecoder.swift | 253 ++++++ .../DataLiteCoder/Classes/RowEncoder.swift | 235 ++++++ .../Documentation.docc/DataLiteCoder.md | 9 + .../Enums/DateDecodingStrategy.swift | 62 ++ .../Enums/DateEncodingStrategy.swift | 62 ++ .../Protocols/DateFormatterProtocol.swift | 24 + .../Extensions/SQLiteRowTests.swift | 50 ++ .../Structures/RowCodingKeyTests.swift | 18 + .../DLCDecoderTests/KeyedContainerTests.swift | 488 ++++++++++++ .../MultiRowDecoderTests.swift | 193 +++++ .../SingleRowDecoderTests.swift | 255 ++++++ .../SingleValueContainerTests.swift | 285 +++++++ .../SingleValueDecoderTests.swift | 152 ++++ .../UnkeyedContainerTests.swift | 733 ++++++++++++++++++ .../DLCEncoderTests/FailedEncoderTests.swift | 29 + .../FailedEncodingContainerTests.swift | 153 ++++ .../DLCEncoderTests/KeyedContainerTests.swift | 505 ++++++++++++ .../MultiRowEncoderTests.swift | 174 +++++ .../SingleRowEncoderTests.swift | 164 ++++ .../SingleValueContainerTests.swift | 205 +++++ .../SingleValueEncoderTests.swift | 108 +++ .../UnkeyedContainerTests.swift | 296 +++++++ .../DataLiteCoderTests/DateDecoderTests.swift | 241 ++++++ .../DataLiteCoderTests/DateEncoderTests.swift | 222 ++++++ .../DataLiteCoderTests/RowDecoderTests.swift | 337 ++++++++ .../DataLiteCoderTests/RowEncoderTests.swift | 231 ++++++ 60 files changed, 7144 insertions(+) create mode 100644 .gitignore create mode 100644 .swift-format create mode 100644 Package.swift create mode 100644 Sources/DLCCommon/Extensions/SQLiteRow.swift create mode 100644 Sources/DLCCommon/Structures/RowCodingKey.swift create mode 100644 Sources/DLCDecoder/Classes/KeyedContainer.swift create mode 100644 Sources/DLCDecoder/Classes/MultiRowDecoder.swift create mode 100644 Sources/DLCDecoder/Classes/SingleRowDecoder.swift create mode 100644 Sources/DLCDecoder/Classes/SingleValueContainer.swift create mode 100644 Sources/DLCDecoder/Classes/SingleValueDecoder.swift create mode 100644 Sources/DLCDecoder/Classes/UnkeyedContainer.swift create mode 100644 Sources/DLCDecoder/Protocols/Container.swift create mode 100644 Sources/DLCDecoder/Protocols/DateDecoder.swift create mode 100644 Sources/DLCDecoder/Protocols/Decoder.swift create mode 100644 Sources/DLCDecoder/Protocols/KeyCheckingDecoder.swift create mode 100644 Sources/DLCDecoder/Protocols/RowDecoder.swift create mode 100644 Sources/DLCDecoder/Protocols/ValueDecoder.swift create mode 100644 Sources/DLCEncoder/Classes/FailedEncoder.swift create mode 100644 Sources/DLCEncoder/Classes/FailedEncodingContainer.swift create mode 100644 Sources/DLCEncoder/Classes/KeyedContainer.swift create mode 100644 Sources/DLCEncoder/Classes/MultiRowEncoder.swift create mode 100644 Sources/DLCEncoder/Classes/SingleRowEncoder.swift create mode 100644 Sources/DLCEncoder/Classes/SingleValueContainer.swift create mode 100644 Sources/DLCEncoder/Classes/SingleValueEncoder.swift create mode 100644 Sources/DLCEncoder/Classes/UnkeyedContainer.swift create mode 100644 Sources/DLCEncoder/Protocols/Container.swift create mode 100644 Sources/DLCEncoder/Protocols/DateEncoder.swift create mode 100644 Sources/DLCEncoder/Protocols/Encoder.swift create mode 100644 Sources/DLCEncoder/Protocols/Flattenable.swift create mode 100644 Sources/DLCEncoder/Protocols/RowEncoder.swift create mode 100644 Sources/DLCEncoder/Protocols/ValueEncoder.swift create mode 100644 Sources/DataLiteCoder/Classes/DateDecoder.swift create mode 100644 Sources/DataLiteCoder/Classes/DateEncoder.swift create mode 100644 Sources/DataLiteCoder/Classes/RowDecoder.swift create mode 100644 Sources/DataLiteCoder/Classes/RowEncoder.swift create mode 100644 Sources/DataLiteCoder/Documentation.docc/DataLiteCoder.md create mode 100644 Sources/DataLiteCoder/Enums/DateDecodingStrategy.swift create mode 100644 Sources/DataLiteCoder/Enums/DateEncodingStrategy.swift create mode 100644 Sources/DataLiteCoder/Protocols/DateFormatterProtocol.swift create mode 100644 Tests/DLCCommonTests/Extensions/SQLiteRowTests.swift create mode 100644 Tests/DLCCommonTests/Structures/RowCodingKeyTests.swift create mode 100644 Tests/DLCDecoderTests/KeyedContainerTests.swift create mode 100644 Tests/DLCDecoderTests/MultiRowDecoderTests.swift create mode 100644 Tests/DLCDecoderTests/SingleRowDecoderTests.swift create mode 100644 Tests/DLCDecoderTests/SingleValueContainerTests.swift create mode 100644 Tests/DLCDecoderTests/SingleValueDecoderTests.swift create mode 100644 Tests/DLCDecoderTests/UnkeyedContainerTests.swift create mode 100644 Tests/DLCEncoderTests/FailedEncoderTests.swift create mode 100644 Tests/DLCEncoderTests/FailedEncodingContainerTests.swift create mode 100644 Tests/DLCEncoderTests/KeyedContainerTests.swift create mode 100644 Tests/DLCEncoderTests/MultiRowEncoderTests.swift create mode 100644 Tests/DLCEncoderTests/SingleRowEncoderTests.swift create mode 100644 Tests/DLCEncoderTests/SingleValueContainerTests.swift create mode 100644 Tests/DLCEncoderTests/SingleValueEncoderTests.swift create mode 100644 Tests/DLCEncoderTests/UnkeyedContainerTests.swift create mode 100644 Tests/DataLiteCoderTests/DateDecoderTests.swift create mode 100644 Tests/DataLiteCoderTests/DateEncoderTests.swift create mode 100644 Tests/DataLiteCoderTests/RowDecoderTests.swift create mode 100644 Tests/DataLiteCoderTests/RowEncoderTests.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67356a1 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.swift-format b/.swift-format new file mode 100644 index 0000000..b77abe6 --- /dev/null +++ b/.swift-format @@ -0,0 +1,15 @@ +{ + "fileScopedDeclarationPrivacy": { + "accessLevel": "private" + }, + "indentBlankLines": true, + "indentation": { + "spaces": 4 + }, + "lineLength": 9999, + "maximumBlankLines": 1, + "multiElementCollectionTrailingCommas": false, + "rules": { + "FileScopedDeclarationPrivacy": true + } +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..bcf70e5 --- /dev/null +++ b/Package.swift @@ -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"]) + ] +) diff --git a/README.md b/README.md index e5e7b7c..14ae5e8 100644 --- a/README.md +++ b/README.md @@ -1 +1,60 @@ # 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. diff --git a/Sources/DLCCommon/Extensions/SQLiteRow.swift b/Sources/DLCCommon/Extensions/SQLiteRow.swift new file mode 100644 index 0000000..e301be4 --- /dev/null +++ b/Sources/DLCCommon/Extensions/SQLiteRow.swift @@ -0,0 +1,29 @@ +import Foundation +import DataLiteCore + +public extension SQLiteRow { + func contains(_ key: CodingKey) -> Bool { + if let index = key.intValue { + 0.. 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 + } + } + } +} diff --git a/Sources/DLCCommon/Structures/RowCodingKey.swift b/Sources/DLCCommon/Structures/RowCodingKey.swift new file mode 100644 index 0000000..2203d59 --- /dev/null +++ b/Sources/DLCCommon/Structures/RowCodingKey.swift @@ -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 + } +} diff --git a/Sources/DLCDecoder/Classes/KeyedContainer.swift b/Sources/DLCDecoder/Classes/KeyedContainer.swift new file mode 100644 index 0000000..0c55515 --- /dev/null +++ b/Sources/DLCDecoder/Classes/KeyedContainer.swift @@ -0,0 +1,102 @@ +import Foundation +import DataLiteCore + +final class KeyedContainer: 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(_ 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( + keyedBy type: NestedKey.Type, + forKey key: Key + ) throws -> KeyedDecodingContainer { + 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.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) + } +} diff --git a/Sources/DLCDecoder/Classes/MultiRowDecoder.swift b/Sources/DLCDecoder/Classes/MultiRowDecoder.swift new file mode 100644 index 0000000..26fc9d5 --- /dev/null +++ b/Sources/DLCDecoder/Classes/MultiRowDecoder.swift @@ -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( + _ 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( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + let info = "Expected a keyed container, but found an array of rows." + let context = DecodingError.Context( + codingPath: codingPath, + debugDescription: info + ) + throw DecodingError.typeMismatch( + KeyedDecodingContainer.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 + ) + } +} diff --git a/Sources/DLCDecoder/Classes/SingleRowDecoder.swift b/Sources/DLCDecoder/Classes/SingleRowDecoder.swift new file mode 100644 index 0000000..389682e --- /dev/null +++ b/Sources/DLCDecoder/Classes/SingleRowDecoder.swift @@ -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( + _ 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( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + 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 + ) + } +} diff --git a/Sources/DLCDecoder/Classes/SingleValueContainer.swift b/Sources/DLCDecoder/Classes/SingleValueContainer.swift new file mode 100644 index 0000000..2ec39c6 --- /dev/null +++ b/Sources/DLCDecoder/Classes/SingleValueContainer.swift @@ -0,0 +1,36 @@ +import Foundation +import DataLiteCore + +final class SingleValueContainer: 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(_ 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) + } + } +} diff --git a/Sources/DLCDecoder/Classes/SingleValueDecoder.swift b/Sources/DLCDecoder/Classes/SingleValueDecoder.swift new file mode 100644 index 0000000..ae84c04 --- /dev/null +++ b/Sources/DLCDecoder/Classes/SingleValueDecoder.swift @@ -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(_ 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( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + let info = "Expected a keyed container, but found a single value." + let context = DecodingError.Context( + codingPath: codingPath, + debugDescription: info + ) + throw DecodingError.typeMismatch( + KeyedDecodingContainer.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) + } +} diff --git a/Sources/DLCDecoder/Classes/UnkeyedContainer.swift b/Sources/DLCDecoder/Classes/UnkeyedContainer.swift new file mode 100644 index 0000000..c9bb40e --- /dev/null +++ b/Sources/DLCDecoder/Classes/UnkeyedContainer.swift @@ -0,0 +1,121 @@ +import Foundation +import DataLiteCore + +private import DLCCommon + +final class UnkeyedContainer: 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.self) + if try decoder.decodeNil(for: currentKey) { + currentIndex += 1 + return true + } else { + return false + } + } + + func decode(_ 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( + keyedBy type: NestedKey.Type + ) throws -> KeyedDecodingContainer { + 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.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(_ 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) + } + } +} diff --git a/Sources/DLCDecoder/Protocols/Container.swift b/Sources/DLCDecoder/Protocols/Container.swift new file mode 100644 index 0000000..f4aa30d --- /dev/null +++ b/Sources/DLCDecoder/Protocols/Container.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol Container { + associatedtype Decoder: Swift.Decoder + var decoder: Decoder { get } +} diff --git a/Sources/DLCDecoder/Protocols/DateDecoder.swift b/Sources/DLCDecoder/Protocols/DateDecoder.swift new file mode 100644 index 0000000..57820ba --- /dev/null +++ b/Sources/DLCDecoder/Protocols/DateDecoder.swift @@ -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 +} diff --git a/Sources/DLCDecoder/Protocols/Decoder.swift b/Sources/DLCDecoder/Protocols/Decoder.swift new file mode 100644 index 0000000..1a6c772 --- /dev/null +++ b/Sources/DLCDecoder/Protocols/Decoder.swift @@ -0,0 +1,8 @@ +import Foundation + +public protocol Decoder: Swift.Decoder { + associatedtype SQLiteData + + var dateDecoder: any DateDecoder { get } + var sqliteData: SQLiteData { get } +} diff --git a/Sources/DLCDecoder/Protocols/KeyCheckingDecoder.swift b/Sources/DLCDecoder/Protocols/KeyCheckingDecoder.swift new file mode 100644 index 0000000..aeb4ffd --- /dev/null +++ b/Sources/DLCDecoder/Protocols/KeyCheckingDecoder.swift @@ -0,0 +1,5 @@ +import Foundation + +public protocol KeyCheckingDecoder: Decoder { + func contains(_ key: CodingKey) -> Bool +} diff --git a/Sources/DLCDecoder/Protocols/RowDecoder.swift b/Sources/DLCDecoder/Protocols/RowDecoder.swift new file mode 100644 index 0000000..f6b4c07 --- /dev/null +++ b/Sources/DLCDecoder/Protocols/RowDecoder.swift @@ -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(_ type: T.Type, for key: CodingKey) throws -> T + func decoder(for key: CodingKey) throws -> any Decoder +} diff --git a/Sources/DLCDecoder/Protocols/ValueDecoder.swift b/Sources/DLCDecoder/Protocols/ValueDecoder.swift new file mode 100644 index 0000000..211d362 --- /dev/null +++ b/Sources/DLCDecoder/Protocols/ValueDecoder.swift @@ -0,0 +1,8 @@ +import Foundation +import DataLiteCore + +public protocol ValueDecoder: Decoder { + func decodeNil() -> Bool + func decodeDate() throws -> Date + func decode(_ type: T.Type) throws -> T +} diff --git a/Sources/DLCEncoder/Classes/FailedEncoder.swift b/Sources/DLCEncoder/Classes/FailedEncoder.swift new file mode 100644 index 0000000..e6203cc --- /dev/null +++ b/Sources/DLCEncoder/Classes/FailedEncoder.swift @@ -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( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + let container = FailedEncodingContainer(codingPath: codingPath) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + FailedEncodingContainer(codingPath: codingPath) + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + FailedEncodingContainer(codingPath: codingPath) + } +} diff --git a/Sources/DLCEncoder/Classes/FailedEncodingContainer.swift b/Sources/DLCEncoder/Classes/FailedEncodingContainer.swift new file mode 100644 index 0000000..55ec638 --- /dev/null +++ b/Sources/DLCEncoder/Classes/FailedEncodingContainer.swift @@ -0,0 +1,84 @@ +import Foundation +private import DLCCommon + +final class FailedEncodingContainer: 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(_ value: T) throws { + throw encodingError(codingPath: codingPath) + } + + func encode(_ value: T, forKey key: Key) throws { + throw encodingError(codingPath: codingPath + [key]) + } + + func nestedContainer( + keyedBy keyType: NestedKey.Type + ) -> KeyedEncodingContainer { + let container = FailedEncodingContainer( + codingPath: codingPath + ) + return KeyedEncodingContainer(container) + } + + func nestedContainer( + keyedBy keyType: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + let container = FailedEncodingContainer( + codingPath: codingPath + [key] + ) + return KeyedEncodingContainer(container) + } + + func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { + FailedEncodingContainer(codingPath: codingPath) + } + + func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer { + FailedEncodingContainer(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) + } +} diff --git a/Sources/DLCEncoder/Classes/KeyedContainer.swift b/Sources/DLCEncoder/Classes/KeyedContainer.swift new file mode 100644 index 0000000..5ae0f04 --- /dev/null +++ b/Sources/DLCEncoder/Classes/KeyedContainer.swift @@ -0,0 +1,201 @@ +import Foundation +import DataLiteCore + +private import DLCCommon + +final class KeyedContainer: 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(_ 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(_ 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( + keyedBy keyType: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + let container = FailedEncodingContainer( + codingPath: codingPath + [key] + ) + return KeyedEncodingContainer(container) + } + + func nestedUnkeyedContainer( + forKey key: Key + ) -> any UnkeyedEncodingContainer { + FailedEncodingContainer( + codingPath: codingPath + [key] + ) + } + + func superEncoder() -> any Swift.Encoder { + FailedEncoder(codingPath: codingPath) + } + + func superEncoder(forKey key: Key) -> any Swift.Encoder { + FailedEncoder(codingPath: codingPath + [key]) + } +} diff --git a/Sources/DLCEncoder/Classes/MultiRowEncoder.swift b/Sources/DLCEncoder/Classes/MultiRowEncoder.swift new file mode 100644 index 0000000..6ec6c65 --- /dev/null +++ b/Sources/DLCEncoder/Classes/MultiRowEncoder.swift @@ -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.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(_ 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( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + let container = FailedEncodingContainer( + codingPath: codingPath + ) + return KeyedEncodingContainer(container) + } + + public func unkeyedContainer() -> any UnkeyedEncodingContainer { + UnkeyedContainer(encoder: self, codingPath: codingPath) + } + + public func singleValueContainer() -> any SingleValueEncodingContainer { + FailedEncodingContainer(codingPath: codingPath) + } +} diff --git a/Sources/DLCEncoder/Classes/SingleRowEncoder.swift b/Sources/DLCEncoder/Classes/SingleRowEncoder.swift new file mode 100644 index 0000000..5efe985 --- /dev/null +++ b/Sources/DLCEncoder/Classes/SingleRowEncoder.swift @@ -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(_ 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( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + let container = KeyedContainer( + encoder: self, codingPath: codingPath + ) + return KeyedEncodingContainer(container) + } + + public func unkeyedContainer() -> any UnkeyedEncodingContainer { + FailedEncodingContainer(codingPath: codingPath) + } + + public func singleValueContainer() -> any SingleValueEncodingContainer { + FailedEncodingContainer(codingPath: codingPath) + } +} diff --git a/Sources/DLCEncoder/Classes/SingleValueContainer.swift b/Sources/DLCEncoder/Classes/SingleValueContainer.swift new file mode 100644 index 0000000..3ea44d8 --- /dev/null +++ b/Sources/DLCEncoder/Classes/SingleValueContainer.swift @@ -0,0 +1,36 @@ +import Foundation +import DataLiteCore + +final class SingleValueContainer: 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(_ 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) + } + } +} diff --git a/Sources/DLCEncoder/Classes/SingleValueEncoder.swift b/Sources/DLCEncoder/Classes/SingleValueEncoder.swift new file mode 100644 index 0000000..eb56373 --- /dev/null +++ b/Sources/DLCEncoder/Classes/SingleValueEncoder.swift @@ -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(_ value: T) throws { + sqliteData = value.sqliteRawValue + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + let container = FailedEncodingContainer(codingPath: codingPath) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + FailedEncodingContainer(codingPath: codingPath) + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + SingleValueContainer(encoder: self, codingPath: codingPath) + } +} diff --git a/Sources/DLCEncoder/Classes/UnkeyedContainer.swift b/Sources/DLCEncoder/Classes/UnkeyedContainer.swift new file mode 100644 index 0000000..473184f --- /dev/null +++ b/Sources/DLCEncoder/Classes/UnkeyedContainer.swift @@ -0,0 +1,59 @@ +import Foundation +import DataLiteCore + +private import DLCCommon + +final class UnkeyedContainer: 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(_ 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( + keyedBy keyType: NestedKey.Type + ) -> KeyedEncodingContainer { + let container = FailedEncodingContainer( + codingPath: codingPath + ) + return KeyedEncodingContainer(container) + } + + func nestedUnkeyedContainer() -> any UnkeyedEncodingContainer { + FailedEncodingContainer(codingPath: codingPath) + } + + func superEncoder() -> any Swift.Encoder { + FailedEncoder(codingPath: codingPath) + } +} diff --git a/Sources/DLCEncoder/Protocols/Container.swift b/Sources/DLCEncoder/Protocols/Container.swift new file mode 100644 index 0000000..ca66fc8 --- /dev/null +++ b/Sources/DLCEncoder/Protocols/Container.swift @@ -0,0 +1,6 @@ +import Foundation + +protocol Container { + associatedtype Encoder: Swift.Encoder + var encoder: Encoder { get } +} diff --git a/Sources/DLCEncoder/Protocols/DateEncoder.swift b/Sources/DLCEncoder/Protocols/DateEncoder.swift new file mode 100644 index 0000000..95bed11 --- /dev/null +++ b/Sources/DLCEncoder/Protocols/DateEncoder.swift @@ -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 +} diff --git a/Sources/DLCEncoder/Protocols/Encoder.swift b/Sources/DLCEncoder/Protocols/Encoder.swift new file mode 100644 index 0000000..db3963b --- /dev/null +++ b/Sources/DLCEncoder/Protocols/Encoder.swift @@ -0,0 +1,8 @@ +import Foundation + +public protocol Encoder: Swift.Encoder { + associatedtype SQLiteData + + var dateEncoder: any DateEncoder { get } + var sqliteData: SQLiteData { get } +} diff --git a/Sources/DLCEncoder/Protocols/Flattenable.swift b/Sources/DLCEncoder/Protocols/Flattenable.swift new file mode 100644 index 0000000..75f1e46 --- /dev/null +++ b/Sources/DLCEncoder/Protocols/Flattenable.swift @@ -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 + } + } +} diff --git a/Sources/DLCEncoder/Protocols/RowEncoder.swift b/Sources/DLCEncoder/Protocols/RowEncoder.swift new file mode 100644 index 0000000..f1eb550 --- /dev/null +++ b/Sources/DLCEncoder/Protocols/RowEncoder.swift @@ -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(_ value: T, for key: CodingKey) throws + func encoder(for key: CodingKey) throws -> any Encoder +} diff --git a/Sources/DLCEncoder/Protocols/ValueEncoder.swift b/Sources/DLCEncoder/Protocols/ValueEncoder.swift new file mode 100644 index 0000000..d09fdbc --- /dev/null +++ b/Sources/DLCEncoder/Protocols/ValueEncoder.swift @@ -0,0 +1,8 @@ +import Foundation +import DataLiteCore + +public protocol ValueEncoder: Encoder { + func encodeNil() throws + func encodeDate(_ date: Date) throws + func encode(_ value: T) throws +} diff --git a/Sources/DataLiteCoder/Classes/DateDecoder.swift b/Sources/DataLiteCoder/Classes/DateDecoder.swift new file mode 100644 index 0000000..f090134 --- /dev/null +++ b/Sources/DataLiteCoder/Classes/DateDecoder.swift @@ -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( + 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) + } + } + } +} diff --git a/Sources/DataLiteCoder/Classes/DateEncoder.swift b/Sources/DataLiteCoder/Classes/DateEncoder.swift new file mode 100644 index 0000000..0f99b5d --- /dev/null +++ b/Sources/DataLiteCoder/Classes/DateEncoder.swift @@ -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 + } + } + } +} diff --git a/Sources/DataLiteCoder/Classes/RowDecoder.swift b/Sources/DataLiteCoder/Classes/RowDecoder.swift new file mode 100644 index 0000000..dda49a3 --- /dev/null +++ b/Sources/DataLiteCoder/Classes/RowDecoder.swift @@ -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( + _ 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( + _ 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 diff --git a/Sources/DataLiteCoder/Classes/RowEncoder.swift b/Sources/DataLiteCoder/Classes/RowEncoder.swift new file mode 100644 index 0000000..1a0bb3b --- /dev/null +++ b/Sources/DataLiteCoder/Classes/RowEncoder.swift @@ -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(_ 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(_ 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 diff --git a/Sources/DataLiteCoder/Documentation.docc/DataLiteCoder.md b/Sources/DataLiteCoder/Documentation.docc/DataLiteCoder.md new file mode 100644 index 0000000..a1a3ddc --- /dev/null +++ b/Sources/DataLiteCoder/Documentation.docc/DataLiteCoder.md @@ -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. diff --git a/Sources/DataLiteCoder/Enums/DateDecodingStrategy.swift b/Sources/DataLiteCoder/Enums/DateDecodingStrategy.swift new file mode 100644 index 0000000..420f1b2 --- /dev/null +++ b/Sources/DataLiteCoder/Enums/DateDecodingStrategy.swift @@ -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()) + } + } +} diff --git a/Sources/DataLiteCoder/Enums/DateEncodingStrategy.swift b/Sources/DataLiteCoder/Enums/DateEncodingStrategy.swift new file mode 100644 index 0000000..ff55d01 --- /dev/null +++ b/Sources/DataLiteCoder/Enums/DateEncodingStrategy.swift @@ -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()) + } + } +} diff --git a/Sources/DataLiteCoder/Protocols/DateFormatterProtocol.swift b/Sources/DataLiteCoder/Protocols/DateFormatterProtocol.swift new file mode 100644 index 0000000..547de68 --- /dev/null +++ b/Sources/DataLiteCoder/Protocols/DateFormatterProtocol.swift @@ -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 {} diff --git a/Tests/DLCCommonTests/Extensions/SQLiteRowTests.swift b/Tests/DLCCommonTests/Extensions/SQLiteRowTests.swift new file mode 100644 index 0000000..580a191 --- /dev/null +++ b/Tests/DLCCommonTests/Extensions/SQLiteRowTests.swift @@ -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 + } + } +} diff --git a/Tests/DLCCommonTests/Structures/RowCodingKeyTests.swift b/Tests/DLCCommonTests/Structures/RowCodingKeyTests.swift new file mode 100644 index 0000000..da57820 --- /dev/null +++ b/Tests/DLCCommonTests/Structures/RowCodingKeyTests.swift @@ -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.") + } +} diff --git a/Tests/DLCDecoderTests/KeyedContainerTests.swift b/Tests/DLCDecoderTests/KeyedContainerTests.swift new file mode 100644 index 0000000..d6cbfb6 --- /dev/null +++ b/Tests/DLCDecoderTests/KeyedContainerTests.swift @@ -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.self, + "Mismatched type in decoding error. Expected \(KeyedDecodingContainer.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( + _ 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( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + 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( + _ type: T.Type + ) throws -> T { + fatalError() + } + + func container( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + 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(_ type: T.Type) throws -> T { + fatalError() + } + } +} diff --git a/Tests/DLCDecoderTests/MultiRowDecoderTests.swift b/Tests/DLCDecoderTests/MultiRowDecoderTests.swift new file mode 100644 index 0000000..bcfc93c --- /dev/null +++ b/Tests/DLCDecoderTests/MultiRowDecoderTests.swift @@ -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.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 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() + } + } +} diff --git a/Tests/DLCDecoderTests/SingleRowDecoderTests.swift b/Tests/DLCDecoderTests/SingleRowDecoderTests.swift new file mode 100644 index 0000000..812a690 --- /dev/null +++ b/Tests/DLCDecoderTests/SingleRowDecoderTests.swift @@ -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 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 + } + } +} diff --git a/Tests/DLCDecoderTests/SingleValueContainerTests.swift b/Tests/DLCDecoderTests/SingleValueContainerTests.swift new file mode 100644 index 0000000..11f7185 --- /dev/null +++ b/Tests/DLCDecoderTests/SingleValueContainerTests.swift @@ -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( + _ type: T.Type + ) throws -> T { + type.init(sqliteData)! + } + + func container( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + 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(_ type: T.Type) throws -> T { + fatalError() + } + } +} diff --git a/Tests/DLCDecoderTests/SingleValueDecoderTests.swift b/Tests/DLCDecoderTests/SingleValueDecoderTests.swift new file mode 100644 index 0000000..0932520 --- /dev/null +++ b/Tests/DLCDecoderTests/SingleValueDecoderTests.swift @@ -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.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 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() + } + } +} diff --git a/Tests/DLCDecoderTests/UnkeyedContainerTests.swift b/Tests/DLCDecoderTests/UnkeyedContainerTests.swift new file mode 100644 index 0000000..188f06f --- /dev/null +++ b/Tests/DLCDecoderTests/UnkeyedContainerTests.swift @@ -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.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.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 { + var row = SQLiteRow() + row["key"] = data + return container( + withData: row, + codingPath: codingPath + ) + } + + func container( + withData data: SQLiteRow = .init(), + codingPath: [any CodingKey] = [] + ) -> UnkeyedContainer { + 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( + _ 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( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + 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( + _ type: T.Type + ) throws -> T { + fatalError() + } + + func container( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + 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(_ type: T.Type) throws -> T { + fatalError() + } + } +} diff --git a/Tests/DLCEncoderTests/FailedEncoderTests.swift b/Tests/DLCEncoderTests/FailedEncoderTests.swift new file mode 100644 index 0000000..9ce73cf --- /dev/null +++ b/Tests/DLCEncoderTests/FailedEncoderTests.swift @@ -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) + 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) + XCTAssertEqual(container.codingPath as? [RowCodingKey], path) + } +} diff --git a/Tests/DLCEncoderTests/FailedEncodingContainerTests.swift b/Tests/DLCEncoderTests/FailedEncodingContainerTests.swift new file mode 100644 index 0000000..ed8243a --- /dev/null +++ b/Tests/DLCEncoderTests/FailedEncodingContainerTests.swift @@ -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( + 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( + 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( + 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( + 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( + 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( + 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( + codingPath: path + ) + let nestedContainer = container.nestedUnkeyedContainer() + XCTAssertTrue(nestedContainer is FailedEncodingContainer) + XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path) + } + + func testNestedUnkeyedContainerForKey() { + let path = [RowCodingKey(intValue: 1)] + let key = RowCodingKey(intValue: 2) + let container = FailedEncodingContainer( + codingPath: path + ) + let nestedContainer = container.nestedUnkeyedContainer(forKey: key) + XCTAssertTrue(nestedContainer is FailedEncodingContainer) + XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path + [key]) + } + + func testSuperEncoder() { + let path = [RowCodingKey(intValue: 1)] + let container = FailedEncodingContainer( + 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( + codingPath: path + ) + let encoder = container.superEncoder(forKey: key) + XCTAssertTrue(encoder is FailedEncoder) + XCTAssertEqual(encoder.codingPath as? [RowCodingKey], path + [key]) + } +} diff --git a/Tests/DLCEncoderTests/KeyedContainerTests.swift b/Tests/DLCEncoderTests/KeyedContainerTests.swift new file mode 100644 index 0000000..f41ee75 --- /dev/null +++ b/Tests/DLCEncoderTests/KeyedContainerTests.swift @@ -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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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( + encoder: encoder, codingPath: [CodingKeys.key1] + ) + ) + let nestedContainer = container.nestedUnkeyedContainer(forKey: .key3) + XCTAssertTrue(nestedContainer is FailedEncodingContainer) + XCTAssertEqual(nestedContainer.codingPath as? [CodingKeys], [.key1, .key3]) + } + + func testSuperEncoder() { + let encoder = MockSingleRowEncoder() + var container = KeyedEncodingContainer( + KeyedContainer( + 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( + 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(_ value: T, for key: any CodingKey) throws { + sqliteData[key.stringValue] = value.sqliteRawValue + } + + func encoder(for key: any CodingKey) throws -> any Encoder { + MockSingleValueEncoder() + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + 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(_ value: T) throws { + sqliteData = value.sqliteRawValue + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + fatalError() + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + fatalError() + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + MockSingleValueContainer(encoder: self, codingPath: []) + } + } + + final class MockSingleValueContainer: 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(_ 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) + } + } + } +} diff --git a/Tests/DLCEncoderTests/MultiRowEncoderTests.swift b/Tests/DLCEncoderTests/MultiRowEncoderTests.swift new file mode 100644 index 0000000..725f49c --- /dev/null +++ b/Tests/DLCEncoderTests/MultiRowEncoderTests.swift @@ -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) + 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) + 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() + } + } +} diff --git a/Tests/DLCEncoderTests/SingleRowEncoderTests.swift b/Tests/DLCEncoderTests/SingleRowEncoderTests.swift new file mode 100644 index 0000000..fc88cd6 --- /dev/null +++ b/Tests/DLCEncoderTests/SingleRowEncoderTests.swift @@ -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) + 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) + 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) + } + } +} diff --git a/Tests/DLCEncoderTests/SingleValueContainerTests.swift b/Tests/DLCEncoderTests/SingleValueContainerTests.swift new file mode 100644 index 0000000..5a6f8c0 --- /dev/null +++ b/Tests/DLCEncoderTests/SingleValueContainerTests.swift @@ -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(_ value: T) throws { + sqliteData = value.sqliteRawValue + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + fatalError() + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + fatalError() + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + SingleValueContainer(encoder: self, codingPath: codingPath) + } + } +} diff --git a/Tests/DLCEncoderTests/SingleValueEncoderTests.swift b/Tests/DLCEncoderTests/SingleValueEncoderTests.swift new file mode 100644 index 0000000..3352888 --- /dev/null +++ b/Tests/DLCEncoderTests/SingleValueEncoderTests.swift @@ -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) + 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) + 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() + } + } +} diff --git a/Tests/DLCEncoderTests/UnkeyedContainerTests.swift b/Tests/DLCEncoderTests/UnkeyedContainerTests.swift new file mode 100644 index 0000000..e593217 --- /dev/null +++ b/Tests/DLCEncoderTests/UnkeyedContainerTests.swift @@ -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( + encoder: encoder, + codingPath: [] + ) + try container.encodeNil() + XCTAssertTrue(encoder.sqliteData.isEmpty) + } + + func testEncodeModel() throws { + let encoder = MockMultiRowEncoder() + let container = UnkeyedContainer( + 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( + 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( + 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( + 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( + encoder: encoder, + codingPath: path + ) + let nestedContainer = container.nestedUnkeyedContainer() + XCTAssertTrue(nestedContainer is FailedEncodingContainer) + XCTAssertEqual(nestedContainer.codingPath as? [RowCodingKey], path) + } + + func testSuperEncoder() { + let path = [RowCodingKey(intValue: 123)] + let encoder = MockMultiRowEncoder() + let container = UnkeyedContainer( + 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(_ value: T, for key: any CodingKey) throws { + fatalError() + } + + func encoder(for key: any CodingKey) throws -> any Encoder { + MockSingleRowEncoder() + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + 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(_ value: T, for key: any CodingKey) throws { + sqliteData[key.stringValue] = value.sqliteRawValue + } + + func encoder(for key: any CodingKey) throws -> any Encoder { + fatalError() + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + let container = MockKeyedContainer( + encoder: self, codingPath: [] + ) + return KeyedEncodingContainer(container) + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + fatalError() + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + fatalError() + } + } + + final class MockKeyedContainer: 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(_ 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(_ 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( + keyedBy keyType: NestedKey.Type, + forKey key: Key + ) -> KeyedEncodingContainer { + fatalError() + } + + func nestedUnkeyedContainer( + forKey key: Key + ) -> any UnkeyedEncodingContainer { + fatalError() + } + + func superEncoder() -> any Swift.Encoder { + fatalError() + } + + func superEncoder(forKey key: Key) -> any Swift.Encoder { + fatalError() + } + } +} diff --git a/Tests/DataLiteCoderTests/DateDecoderTests.swift b/Tests/DataLiteCoderTests/DateDecoderTests.swift new file mode 100644 index 0000000..66ca297 --- /dev/null +++ b/Tests/DataLiteCoderTests/DateDecoderTests.swift @@ -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(_ type: T.Type) throws -> T { + type.init(sqliteData)! + } + + func container( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + 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( + _ 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( + keyedBy type: Key.Type + ) throws -> KeyedDecodingContainer { + fatalError() + } + + func unkeyedContainer() throws -> any UnkeyedDecodingContainer { + fatalError() + } + + func singleValueContainer() throws -> any SingleValueDecodingContainer { + fatalError() + } + } +} diff --git a/Tests/DataLiteCoderTests/DateEncoderTests.swift b/Tests/DataLiteCoderTests/DateEncoderTests.swift new file mode 100644 index 0000000..2485b7f --- /dev/null +++ b/Tests/DataLiteCoderTests/DateEncoderTests.swift @@ -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(_ value: T) throws { + sqliteData = value.sqliteRawValue + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + 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(_ value: T, for key: any CodingKey) throws { + sqliteData[key] = value.sqliteRawValue + } + + func encoder(for key: any CodingKey) throws -> any Encoder { + fatalError() + } + + func container( + keyedBy type: Key.Type + ) -> KeyedEncodingContainer { + fatalError() + } + + func unkeyedContainer() -> any UnkeyedEncodingContainer { + fatalError() + } + + func singleValueContainer() -> any SingleValueEncodingContainer { + fatalError() + } + } +} diff --git a/Tests/DataLiteCoderTests/RowDecoderTests.swift b/Tests/DataLiteCoderTests/RowDecoderTests.swift new file mode 100644 index 0000000..d5680c9 --- /dev/null +++ b/Tests/DataLiteCoderTests/RowDecoderTests.swift @@ -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 + } +} diff --git a/Tests/DataLiteCoderTests/RowEncoderTests.swift b/Tests/DataLiteCoderTests/RowEncoderTests.swift new file mode 100644 index 0000000..2e4ce2d --- /dev/null +++ b/Tests/DataLiteCoderTests/RowEncoderTests.swift @@ -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) + } + } +}