Files
data-lite-coder/Sources/DataLiteCoder/Classes/RowDecoder.swift

254 lines
7.0 KiB
Swift

import Foundation
import DataLiteCore
private import DLCDecoder
/// Decoder that decodes SQLite values into Swift types conforming to the `Decodable` protocol.
///
/// ## Overview
///
/// Use `RowDecoder` to convert `SQLiteRow` or an array of `SQLiteRow` into Swift types that conform
/// to the `Decodable` protocol.
///
/// ### Decode a Single Row
///
/// Use ``decode(_:from:)->T`` to decode a single `SQLiteRow` into a `Decodable` value.
///
/// ```swift
/// struct User: Decodable {
/// var id: Int
/// var name: String
/// }
///
/// do {
/// var row: SQLiteRow = .init()
/// row["id"] = .int(1)
/// row["name"] = .text("John Doe")
///
/// let decoder = RowDecoder()
/// let user = try decoder.decode(User.self, from: row)
/// } catch {
/// print("Decoding error:", error)
/// }
/// ```
///
/// ### Decode a Row into an Array
///
/// Use ``decode(_:from:)->T`` to decode a row containing homogeneous values
/// into an array of Swift values.
///
/// ```swift
/// do {
/// var row: SQLiteRow = .init()
/// row["a"] = .int(10)
/// row["b"] = .int(20)
///
/// let decoder = RowDecoder()
/// let numbers = try decoder.decode([Int].self, from: row)
/// } catch {
/// print("Decoding error:", error)
/// }
/// ```
///
/// ### Decode Multiple Rows
///
/// Use ``decode(_:from:)->[T]`` to decode an array of `SQLiteRow` into an array of `Decodable` values.
///
/// ```swift
/// struct User: Decodable {
/// var id: Int
/// var name: String
/// }
///
/// do {
/// let rows: [SQLiteRow] = fetchRows() // Fetch rows from a database
/// let decoder = RowDecoder()
/// let users = try decoder.decode([User].self, from: rows)
/// } catch {
/// print("Decoding error:", error)
/// }
/// ```
///
/// ### Customize Decoding with User Info
///
/// Use the ``userInfo`` property to pass context or flags to decoding logic.
///
/// First, define a custom key:
///
/// ```swift
/// extension CodingUserInfoKey {
/// static let isAdmin = CodingUserInfoKey(
/// rawValue: "isAdmin"
/// )!
/// }
/// ```
///
/// Then access it inside your model:
///
/// ```swift
/// struct User: Decodable {
/// enum CodingKeys: String, CodingKey {
/// case id, name
/// }
///
/// var id: Int
/// var name: String
/// var isAdmin: Bool
///
/// init(from decoder: Decoder) throws {
/// let container = try decoder.container(keyedBy: CodingKeys.self)
/// id = try container.decode(Int.self, forKey: .id)
/// name = try container.decode(String.self, forKey: .name)
///
/// isAdmin = (decoder.userInfo[.isAdmin] as? Bool) ?? false
/// }
/// }
/// ```
///
/// Pass the value before decoding:
///
/// ```swift
/// do {
/// var row: SQLiteRow = .init()
/// row["id"] = .int(1)
/// row["name"] = .text("John Doe")
///
/// let decoder = RowDecoder()
/// decoder.userInfo[.isAdmin] = true
///
/// let user = try decoder.decode(User.self, from: row)
/// } catch {
/// print("Decoding error:", error)
/// }
/// ```
///
/// ### Date Decoding Strategy
///
/// Use the ``dateDecodingStrategy`` property to customize how `Date` values are decoded.
///
/// ```swift
/// struct Log: Decodable {
/// let timestamp: Date
/// }
///
/// do {
/// var row: SQLiteRow = .init()
/// row["timestamp"] = .int(1_700_000_000)
///
/// let decoder = RowDecoder()
/// decoder.dateDecodingStrategy = .secondsSince1970Int
///
/// let log = try decoder.decode(Log.self, from: row)
/// } catch {
/// print("Decoding error:", error)
/// }
/// ```
///
/// For more detailed information, see ``DateDecodingStrategy``.
///
/// ## Topics
///
/// ### Creating a Decoder
///
/// - ``init(userInfo:dateDecodingStrategy:)``
///
/// ### Decoding
///
/// - ``decode(_:from:)->T``
/// - ``decode(_:from:)->[T]``
///
/// ### Customizing Decoding
///
/// - ``userInfo``
///
/// ### Decoding Dates
///
/// - ``dateDecodingStrategy``
/// - ``DateDecodingStrategy``
public final class RowDecoder {
// MARK: - Properties
/// A dictionary containing user-defined information accessible during decoding.
///
/// This dictionary allows passing additional context or settings that can influence
/// the decoding process. Values stored here are accessible inside custom `Decodable`
/// implementations through the `Decoder`'s `userInfo` property.
public var userInfo: [CodingUserInfoKey: Any]
/// The strategy used to decode `Date` values from SQLite data.
///
/// Determines how `Date` values are decoded from SQLite storage, supporting
/// different formats such as timestamps or custom representations.
public var dateDecodingStrategy: DateDecodingStrategy
// MARK: - Initialization
/// Initializes a new `RowDecoder` instance with optional configuration.
///
/// - Parameters:
/// - userInfo: A dictionary with user-defined information accessible during decoding.
/// - dateDecodingStrategy: The strategy to decode `Date` values. Default is `.deferredToDate`.
public init(
userInfo: [CodingUserInfoKey: Any] = [:],
dateDecodingStrategy: DateDecodingStrategy = .deferredToDate
) {
self.userInfo = userInfo
self.dateDecodingStrategy = dateDecodingStrategy
}
// MARK: - Decoding Methods
/// Decodes a single `SQLiteRow` into an instance of the specified `Decodable` type.
///
/// - Parameters:
/// - type: The target type conforming to `Decodable`.
/// - row: The SQLite row to decode.
/// - Returns: An instance of the specified type decoded from the row.
/// - Throws: An error if decoding fails.
public func decode<T: Decodable>(
_ type: T.Type,
from row: SQLiteRow
) throws -> T {
let dateDecoder = DateDecoder(strategy: dateDecodingStrategy)
let decoder = SingleRowDecoder(
dateDecoder: dateDecoder,
sqliteData: row,
codingPath: [],
userInfo: userInfo
)
return try T(from: decoder)
}
/// Decodes an array of `SQLiteRow` values into an array of the specified `Decodable` type.
///
/// - Parameters:
/// - type: The array type containing the element type to decode.
/// - rows: The array of SQLite rows to decode.
/// - Returns: An array of decoded instances.
/// - Throws: An error if decoding any row fails.
public func decode<T: Decodable>(
_ type: [T].Type,
from rows: [SQLiteRow]
) throws -> [T] {
let dateDecoder = DateDecoder(strategy: dateDecodingStrategy)
let decoder = MultiRowDecoder(
dateDecoder: dateDecoder,
sqliteData: rows,
codingPath: [],
userInfo: userInfo
)
return try [T](from: decoder)
}
}
#if canImport(Combine)
import Combine
// MARK: - TopLevelDecoder
extension RowDecoder: TopLevelDecoder {
/// The type of input data expected by this decoder when used as a `TopLevelDecoder`.
public typealias Input = SQLiteRow
}
#endif