add swiftUI code
This commit is contained in:
@@ -0,0 +1,725 @@
|
||||
import Foundation // For JSONEncoder
|
||||
|
||||
/// A type that can encode itself in a database row.
|
||||
///
|
||||
/// To conform to `EncodableRecord`, provide an implementation for the
|
||||
/// ``encode(to:)-k9pf`` method. This implementation is ready-made for
|
||||
/// `Encodable` types.
|
||||
///
|
||||
/// Most of the time, your record types will get `EncodableRecord` conformance
|
||||
/// through the ``MutablePersistableRecord`` or ``PersistableRecord`` protocols,
|
||||
/// which provide persistence methods.
|
||||
///
|
||||
/// ## Topics
|
||||
///
|
||||
/// ### Encoding a Database Row
|
||||
///
|
||||
/// - ``encode(to:)-k9pf``
|
||||
/// - ``PersistenceContainer``
|
||||
///
|
||||
/// ### Configuring Persistence for the Standard Encodable Protocol
|
||||
///
|
||||
/// - ``databaseColumnEncodingStrategy-5sx4v``
|
||||
/// - ``databaseDataEncodingStrategy-9y0c7``
|
||||
/// - ``databaseDateEncodingStrategy-2gtc1``
|
||||
/// - ``databaseEncodingUserInfo-8upii``
|
||||
/// - ``databaseJSONEncoder(for:)-6x62c``
|
||||
/// - ``databaseUUIDEncodingStrategy-2t96q``
|
||||
/// - ``DatabaseColumnEncodingStrategy``
|
||||
/// - ``DatabaseDataEncodingStrategy``
|
||||
/// - ``DatabaseDateEncodingStrategy``
|
||||
/// - ``DatabaseUUIDEncodingStrategy``
|
||||
///
|
||||
/// ### Converting a Record to a Dictionary
|
||||
///
|
||||
/// - ``databaseDictionary``
|
||||
///
|
||||
/// ### Comparing Records
|
||||
///
|
||||
/// - ``databaseChanges(from:)``
|
||||
/// - ``databaseChanges(modify:)``
|
||||
/// - ``databaseEquals(_:)``
|
||||
public protocol EncodableRecord {
|
||||
/// Encodes the record into the provided persistence container.
|
||||
///
|
||||
/// In your implementation of this method, store in the `container` argument
|
||||
/// all values that should be stored in database columns.
|
||||
///
|
||||
/// Primary key columns, if any, must be included.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord {
|
||||
/// var id: Int64?
|
||||
/// var name: String?
|
||||
///
|
||||
/// func encode(to container: inout PersistenceContainer) {
|
||||
/// container["id"] = id
|
||||
/// container["name"] = name
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// It is undefined behavior to set different values for the same column.
|
||||
/// Column names are case insensitive, so defining both "name" and "NAME"
|
||||
/// is considered undefined behavior.
|
||||
///
|
||||
/// - throws: An error is thrown if the record can't be encoded to its
|
||||
/// database representation.
|
||||
func encode(to container: inout PersistenceContainer) throws
|
||||
|
||||
// MARK: - Customizing the Format of Database Columns
|
||||
|
||||
/// Contextual information made available to the
|
||||
/// `Encodable.encode(to:)` method.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// The returned dictionary is returned by `Encoder.userInfo` when the
|
||||
/// record is encoded.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// // A key that holds a encoder's name
|
||||
/// let encoderName = CodingUserInfoKey(rawValue: "encoderName")!
|
||||
///
|
||||
/// struct Player: PersistableRecord, Encodable {
|
||||
/// // Customize the encoder name when encoding a database row
|
||||
/// static let databaseEncodingUserInfo: [CodingUserInfoKey: Any] = [encoderName: "Database"]
|
||||
///
|
||||
/// func encode(to encoder: Encoder) throws {
|
||||
/// // Print the encoder name
|
||||
/// print(encoder.userInfo[encoderName])
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let player = Player(...)
|
||||
///
|
||||
/// // prints "Database"
|
||||
/// try player.insert(db)
|
||||
///
|
||||
/// // prints "JSON"
|
||||
/// let encoder = JSONEncoder()
|
||||
/// encoder.userInfo = [encoderName: "JSON"]
|
||||
/// let data = try encoder.encode(player)
|
||||
/// ```
|
||||
static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get }
|
||||
|
||||
/// Returns the `JSONEncoder` that encodes the value for a given column.
|
||||
///
|
||||
/// This method is dedicated to ``EncodableRecord`` types that also conform
|
||||
/// to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
static func databaseJSONEncoder(for column: String) -> JSONEncoder
|
||||
|
||||
/// The strategy for encoding `Data` columns.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text
|
||||
///
|
||||
/// // Encoded as SQL text. Data must contain valid UTF8 bytes.
|
||||
/// var jsonData: Data
|
||||
/// }
|
||||
/// ```
|
||||
static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy { get }
|
||||
|
||||
/// The strategy for encoding `Date` columns.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970
|
||||
///
|
||||
/// // Encoded as an epoch timestamp
|
||||
/// var creationDate: Date
|
||||
/// }
|
||||
/// ```
|
||||
static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get }
|
||||
|
||||
/// The strategy for encoding `UUID` columns.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString
|
||||
///
|
||||
/// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
|
||||
/// var uuid: UUID
|
||||
/// }
|
||||
/// ```
|
||||
static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get }
|
||||
|
||||
/// The strategy for converting coding keys to column names.
|
||||
///
|
||||
/// This property is dedicated to ``EncodableRecord`` types that also
|
||||
/// conform to the standard `Encodable` protocol and use the default
|
||||
/// ``encode(to:)-1mrt`` implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableProtocol, Encodable {
|
||||
/// static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
|
||||
///
|
||||
/// // Encoded in the 'player_id' column
|
||||
/// var playerID: String
|
||||
/// }
|
||||
/// ```
|
||||
static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy { get }
|
||||
}
|
||||
|
||||
extension EncodableRecord {
|
||||
/// Contextual information made available to the
|
||||
/// `Encodable.encode(to:)` method.
|
||||
///
|
||||
/// The default implementation returns an empty dictionary.
|
||||
public static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] {
|
||||
[:]
|
||||
}
|
||||
|
||||
/// Returns the `JSONEncoder` that encodes the value for a given column.
|
||||
///
|
||||
/// The default implementation returns a `JSONEncoder` with the
|
||||
/// following properties:
|
||||
///
|
||||
/// - `dataEncodingStrategy`: `.base64`
|
||||
/// - `dateEncodingStrategy`: `.millisecondsSince1970`
|
||||
/// - `nonConformingFloatEncodingStrategy`: `.throw`
|
||||
/// - `outputFormatting`: `.sortedKeys`
|
||||
public static func databaseJSONEncoder(for column: String) -> JSONEncoder {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.dataEncodingStrategy = .base64
|
||||
encoder.dateEncodingStrategy = .millisecondsSince1970
|
||||
encoder.nonConformingFloatEncodingStrategy = .throw
|
||||
// guarantee some stability in order to ease record comparison
|
||||
encoder.outputFormatting = .sortedKeys
|
||||
encoder.userInfo = databaseEncodingUserInfo
|
||||
return encoder
|
||||
}
|
||||
|
||||
/// Returns the default strategy for encoding `Data` columns:
|
||||
/// ``DatabaseDataEncodingStrategy/deferredToData``.
|
||||
public static var databaseDataEncodingStrategy: DatabaseDataEncodingStrategy {
|
||||
.deferredToData
|
||||
}
|
||||
|
||||
/// Returns the default strategy for encoding `Date` columns:
|
||||
/// ``DatabaseDateEncodingStrategy/deferredToDate``.
|
||||
public static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy {
|
||||
.deferredToDate
|
||||
}
|
||||
|
||||
/// Returns the default strategy for encoding `UUID` columns:
|
||||
/// ``DatabaseUUIDEncodingStrategy/deferredToUUID``.
|
||||
public static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy {
|
||||
.deferredToUUID
|
||||
}
|
||||
|
||||
/// Returns the default strategy for converting coding keys to column names:
|
||||
/// ``DatabaseColumnEncodingStrategy/useDefaultKeys``.
|
||||
public static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy {
|
||||
.useDefaultKeys
|
||||
}
|
||||
}
|
||||
|
||||
extension EncodableRecord {
|
||||
/// A dictionary whose keys are the columns encoded in the
|
||||
/// <doc:/documentation/GRDB/EncodableRecord/encode(to:)-k9pf> method.
|
||||
///
|
||||
/// - throws: An error is thrown if the record can't be encoded to its
|
||||
/// database representation.
|
||||
public var databaseDictionary: [String: DatabaseValue] {
|
||||
get throws {
|
||||
try Dictionary(PersistenceContainer(self).storage).mapValues { $0?.databaseValue ?? .null }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension EncodableRecord {
|
||||
|
||||
// MARK: - Record Comparison
|
||||
|
||||
/// Returns a boolean indicating whether this record and the other record
|
||||
/// have the same database representation.
|
||||
public func databaseEquals(_ record: Self) -> Bool {
|
||||
do {
|
||||
return try PersistenceContainer(self).changesIterator(from: PersistenceContainer(record)).next() == nil
|
||||
} catch {
|
||||
// one record can't be encoded: they can't be identical in the database
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a dictionary of values changed from the other record.
|
||||
///
|
||||
/// The keys of the dictionary are the column names for which record do not
|
||||
/// share the same value. Values are the database values from the
|
||||
/// `other` record.
|
||||
///
|
||||
/// Note that the `other` record does not have to have the same type of the
|
||||
/// receiver record. When the two records don't define the same set of
|
||||
/// columns in their <doc:/documentation/GRDB/EncodableRecord/encode(to:)-k9pf>
|
||||
/// method, only the columns defined by the receiver are considered.
|
||||
///
|
||||
/// - throws: An error is thrown if one record can't be encoded to its
|
||||
/// database representation.
|
||||
public func databaseChanges(from record: some EncodableRecord)
|
||||
throws -> [String: DatabaseValue]
|
||||
{
|
||||
let changes = try PersistenceContainer(self).changesIterator(from: PersistenceContainer(record))
|
||||
return Dictionary(uniqueKeysWithValues: changes)
|
||||
}
|
||||
|
||||
/// Modifies the record according to the provided `modify` closure, and
|
||||
/// returns a dictionary of changed values.
|
||||
///
|
||||
/// The keys of the dictionary are the changed column names. Values are
|
||||
/// the database values from the initial version record.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// var player = Player(id: 1, score: 1000, hasAward: false)
|
||||
/// let changes = try player.databaseChanges {
|
||||
/// $0.score = 1000
|
||||
/// $0.hasAward = true
|
||||
/// }
|
||||
///
|
||||
/// player.hasAward // true (changed)
|
||||
///
|
||||
/// changes["score"] // nil (not changed)
|
||||
/// changes["hasAward"] // false (old value)
|
||||
/// ```
|
||||
///
|
||||
/// - parameter modify: A closure that modifies the record.
|
||||
public mutating func databaseChanges(modify: (inout Self) throws -> Void)
|
||||
throws -> [String: DatabaseValue]
|
||||
{
|
||||
let container = try PersistenceContainer(self)
|
||||
try modify(&self)
|
||||
let changes = try PersistenceContainer(self).changesIterator(from: container)
|
||||
return Dictionary(uniqueKeysWithValues: changes)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PersistenceContainer
|
||||
|
||||
/// A container for database values to store in a database row.
|
||||
///
|
||||
/// `PersistenceContainer` is the argument of the
|
||||
/// ``EncodableRecord/encode(to:)-k9pf`` method.
|
||||
public struct PersistenceContainer {
|
||||
// fileprivate for Row(_:PersistenceContainer)
|
||||
// The ordering of the OrderedDictionary helps generating always the same
|
||||
// SQL queries, and hit the statement cache.
|
||||
fileprivate var storage: OrderedDictionary<String, (any DatabaseValueConvertible)?>
|
||||
|
||||
/// The value associated with the given column.
|
||||
public subscript(_ column: String) -> (any DatabaseValueConvertible)? {
|
||||
get { self[caseInsensitive: column] }
|
||||
set { storage.updateValue(newValue, forKey: column) }
|
||||
}
|
||||
|
||||
/// The value associated with the given column.
|
||||
public subscript(_ column: some ColumnExpression) -> (any DatabaseValueConvertible)? {
|
||||
get { self[column.name] }
|
||||
set { self[column.name] = newValue }
|
||||
}
|
||||
|
||||
init() {
|
||||
storage = OrderedDictionary()
|
||||
}
|
||||
|
||||
init(minimumCapacity: Int) {
|
||||
storage = OrderedDictionary(minimumCapacity: minimumCapacity)
|
||||
}
|
||||
|
||||
/// Convenience initializer from a record
|
||||
init<Record: EncodableRecord>(_ record: Record) throws {
|
||||
self.init()
|
||||
try record.encode(to: &self)
|
||||
}
|
||||
|
||||
/// Convenience initializer from a database connection and a record
|
||||
@usableFromInline
|
||||
init(_ db: Database, _ record: some EncodableRecord & TableRecord) throws {
|
||||
let databaseTableName = type(of: record).databaseTableName
|
||||
let columnCount = try db.columns(in: databaseTableName).count
|
||||
self.init(minimumCapacity: columnCount) // Optimization
|
||||
try record.encode(to: &self)
|
||||
}
|
||||
|
||||
/// Columns stored in the container, ordered like values.
|
||||
var columns: [String] { Array(storage.keys) }
|
||||
|
||||
/// Values stored in the container, ordered like columns.
|
||||
var values: [(any DatabaseValueConvertible)?] { Array(storage.values) }
|
||||
|
||||
/// Accesses the value associated with the given column, in a
|
||||
/// case-insensitive fashion.
|
||||
subscript(caseInsensitive column: String) -> (any DatabaseValueConvertible)? {
|
||||
get {
|
||||
if let value = storage[column] {
|
||||
return value
|
||||
}
|
||||
let lowercaseColumn = column.lowercased()
|
||||
for (key, value) in storage where key.lowercased() == lowercaseColumn {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
set {
|
||||
if storage[column] != nil {
|
||||
storage[column] = newValue
|
||||
return
|
||||
}
|
||||
let lowercaseColumn = column.lowercased()
|
||||
for key in storage.keys where key.lowercased() == lowercaseColumn {
|
||||
storage[key] = newValue
|
||||
return
|
||||
}
|
||||
|
||||
storage[column] = newValue
|
||||
}
|
||||
}
|
||||
|
||||
// Returns nil if column is not defined
|
||||
func value(forCaseInsensitiveColumn column: String) -> DatabaseValue? {
|
||||
let lowercaseColumn = column.lowercased()
|
||||
for (key, value) in storage where key.lowercased() == lowercaseColumn {
|
||||
return value?.databaseValue ?? .null
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var isEmpty: Bool { storage.isEmpty }
|
||||
|
||||
/// An iterator over the (column, value) pairs
|
||||
func makeIterator() -> IndexingIterator<OrderedDictionary<String, (any DatabaseValueConvertible)?>> {
|
||||
storage.makeIterator()
|
||||
}
|
||||
|
||||
@usableFromInline
|
||||
func changesIterator(from container: PersistenceContainer) -> AnyIterator<(String, DatabaseValue)> {
|
||||
var newValueIterator = makeIterator()
|
||||
return AnyIterator {
|
||||
// Loop until we find a change, or exhaust columns:
|
||||
while let (column, newValue) = newValueIterator.next() {
|
||||
let oldValue = container[caseInsensitive: column]
|
||||
let oldDbValue = oldValue?.databaseValue ?? .null
|
||||
let newDbValue = newValue?.databaseValue ?? .null
|
||||
if newDbValue != oldDbValue {
|
||||
return (column, oldDbValue)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Row {
|
||||
convenience init<Record: EncodableRecord>(_ record: Record) throws {
|
||||
try self.init(PersistenceContainer(record))
|
||||
}
|
||||
|
||||
convenience init(_ container: PersistenceContainer) {
|
||||
self.init(Dictionary(container.storage))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DatabaseDataEncodingStrategy
|
||||
|
||||
/// `DatabaseDataEncodingStrategy` specifies how `EncodableRecord` types that
|
||||
/// also adopt the standard `Encodable` protocol encode their `Data` properties
|
||||
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
|
||||
/// implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseDataEncodingStrategy = DatabaseDataEncodingStrategy.text
|
||||
///
|
||||
/// // Encoded as SQL text. Data must contain valid UTF8 bytes.
|
||||
/// var jsonData: Data
|
||||
/// }
|
||||
/// ```
|
||||
public enum DatabaseDataEncodingStrategy {
|
||||
/// Encodes `Data` columns as SQL blob.
|
||||
case deferredToData
|
||||
|
||||
/// Encodes `Data` columns as SQL text. Data must contain valid UTF8 bytes.
|
||||
case text
|
||||
|
||||
/// Encodes `Data` column as the result of the user-provided function.
|
||||
case custom((Data) -> (any DatabaseValueConvertible)?)
|
||||
|
||||
func encode(_ data: Data) -> DatabaseValue {
|
||||
switch self {
|
||||
case .deferredToData:
|
||||
return data.databaseValue
|
||||
case .text:
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
fatalError("Invalid UTF8 data")
|
||||
}
|
||||
return string.databaseValue
|
||||
case .custom(let format):
|
||||
return format(data)?.databaseValue ?? .null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DatabaseDateEncodingStrategy
|
||||
|
||||
/// `DatabaseDateEncodingStrategy` specifies how `EncodableRecord` types that
|
||||
/// also adopt the standard `Encodable` protocol encode their `Date` properties
|
||||
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
|
||||
/// implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseDateEncodingStrategy = DatabaseDateEncodingStrategy.timeIntervalSince1970
|
||||
///
|
||||
/// // Encoded as an epoch timestamp
|
||||
/// var creationDate: Date
|
||||
/// }
|
||||
/// ```
|
||||
public enum DatabaseDateEncodingStrategy {
|
||||
/// The strategy that uses formatting from the Date structure.
|
||||
///
|
||||
/// It encodes dates using the format "YYYY-MM-DD HH:MM:SS.SSS" in the
|
||||
/// UTC time zone.
|
||||
case deferredToDate
|
||||
|
||||
/// Encodes a Double: the number of seconds between the date and
|
||||
/// midnight UTC on 1 January 2001
|
||||
case timeIntervalSinceReferenceDate
|
||||
|
||||
/// Encodes a Double: the number of seconds between the date and
|
||||
/// midnight UTC on 1 January 1970
|
||||
case timeIntervalSince1970
|
||||
|
||||
/// Encodes an Int64: the number of seconds between the date and
|
||||
/// midnight UTC on 1 January 1970
|
||||
case secondsSince1970
|
||||
|
||||
/// Encodes an Int64: the number of milliseconds between the date and
|
||||
/// midnight UTC on 1 January 1970
|
||||
case millisecondsSince1970
|
||||
|
||||
/// Encodes dates according to the ISO 8601 and RFC 3339 standards
|
||||
case iso8601
|
||||
|
||||
/// Encodes a String, according to the provided formatter
|
||||
case formatted(DateFormatter)
|
||||
|
||||
/// Encodes the result of the user-provided function
|
||||
case custom((Date) -> (any DatabaseValueConvertible)?)
|
||||
|
||||
private static let iso8601Formatter: ISO8601DateFormatter = {
|
||||
let formatter = ISO8601DateFormatter()
|
||||
formatter.formatOptions = .withInternetDateTime
|
||||
return formatter
|
||||
}()
|
||||
|
||||
func encode(_ date: Date) -> DatabaseValue {
|
||||
switch self {
|
||||
case .deferredToDate:
|
||||
return date.databaseValue
|
||||
case .timeIntervalSinceReferenceDate:
|
||||
return date.timeIntervalSinceReferenceDate.databaseValue
|
||||
case .timeIntervalSince1970:
|
||||
return date.timeIntervalSince1970.databaseValue
|
||||
case .millisecondsSince1970:
|
||||
return Int64(floor(1000.0 * date.timeIntervalSince1970)).databaseValue
|
||||
case .secondsSince1970:
|
||||
return Int64(floor(date.timeIntervalSince1970)).databaseValue
|
||||
case .iso8601:
|
||||
return Self.iso8601Formatter.string(from: date).databaseValue
|
||||
case .formatted(let formatter):
|
||||
return formatter.string(from: date).databaseValue
|
||||
case .custom(let format):
|
||||
return format(date)?.databaseValue ?? .null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DatabaseUUIDEncodingStrategy
|
||||
|
||||
/// `DatabaseUUIDEncodingStrategy` specifies how `EncodableRecord` types that
|
||||
/// also adopt the standard `Encodable` protocol encode their `UUID` properties
|
||||
/// in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
|
||||
/// implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableRecord, Encodable {
|
||||
/// static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString
|
||||
///
|
||||
/// // Encoded in a string like "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
|
||||
/// var uuid: UUID
|
||||
/// }
|
||||
/// ```
|
||||
public enum DatabaseUUIDEncodingStrategy: Sendable {
|
||||
/// The strategy that uses formatting from the UUID type.
|
||||
///
|
||||
/// It encodes UUIDs as 16-bytes data blobs.
|
||||
case deferredToUUID
|
||||
|
||||
/// Encodes UUIDs as uppercased strings such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"
|
||||
case uppercaseString
|
||||
|
||||
/// Encodes UUIDs as lowercased strings such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f"
|
||||
case lowercaseString
|
||||
|
||||
func encode(_ uuid: UUID) -> DatabaseValue {
|
||||
switch self {
|
||||
case .deferredToUUID:
|
||||
return uuid.databaseValue
|
||||
case .uppercaseString:
|
||||
return uuid.uuidString.databaseValue
|
||||
case .lowercaseString:
|
||||
return uuid.uuidString.lowercased().databaseValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DatabaseColumnEncodingStrategy
|
||||
|
||||
/// `DatabaseColumnEncodingStrategy` specifies how `EncodableRecord` types that
|
||||
/// also adopt the standard `Encodable` protocol encode their coding keys into
|
||||
/// database columns in the default <doc:/documentation/GRDB/EncodableRecord/encode(to:)-1mrt>
|
||||
/// implementation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// ```swift
|
||||
/// struct Player: EncodableProtocol, Encodable {
|
||||
/// static let databaseColumnEncodingStrategy = DatabaseColumnEncodingStrategy.convertToSnakeCase
|
||||
///
|
||||
/// // Encoded in the 'player_id' column
|
||||
/// var playerID: String
|
||||
/// }
|
||||
/// ```
|
||||
public enum DatabaseColumnEncodingStrategy {
|
||||
/// A key encoding strategy that doesn’t change key names during encoding.
|
||||
case useDefaultKeys
|
||||
|
||||
/// A key encoding strategy that converts camel-case keys to snake-case keys.
|
||||
case convertToSnakeCase
|
||||
|
||||
/// A key encoding strategy defined by the closure you supply.
|
||||
case custom((any CodingKey) -> String)
|
||||
|
||||
func column(forKey key: some CodingKey) -> String {
|
||||
switch self {
|
||||
case .useDefaultKeys:
|
||||
return key.stringValue
|
||||
case .convertToSnakeCase:
|
||||
return Self._convertToSnakeCase(key.stringValue)
|
||||
case let .custom(column):
|
||||
return column(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Copied straight from
|
||||
// https://github.com/apple/swift-corelibs-foundation/blob/8d6398d76eaf886a214e0bb2bd7549d968f7b40e/Sources/Foundation/JSONEncoder.swift#L127
|
||||
static func _convertToSnakeCase(_ stringKey: String) -> String {
|
||||
//===----------------------------------------------------------------------===//
|
||||
//
|
||||
// This function is part of the Swift.org open source project
|
||||
//
|
||||
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
|
||||
// Licensed under Apache License v2.0 with Runtime Library Exception
|
||||
//
|
||||
// See https://swift.org/LICENSE.txt for license information
|
||||
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
|
||||
//
|
||||
//===----------------------------------------------------------------------===//
|
||||
guard !stringKey.isEmpty else { return stringKey }
|
||||
|
||||
var words: [Range<String.Index>] = []
|
||||
// The general idea of this algorithm is to split words on transition
|
||||
// from lower to upper case, then on transition of >1 upper case
|
||||
// characters to lowercase
|
||||
//
|
||||
// myProperty -> my_property
|
||||
// myURLProperty -> my_url_property
|
||||
//
|
||||
// We assume, per Swift naming conventions, that the first character of
|
||||
// the key is lowercase.
|
||||
var wordStart = stringKey.startIndex
|
||||
var searchRange = stringKey.index(after: wordStart)..<stringKey.endIndex
|
||||
|
||||
// Find next uppercase character
|
||||
while let upperCaseRange = stringKey.rangeOfCharacter(
|
||||
from: CharacterSet.uppercaseLetters,
|
||||
options: [], range: searchRange)
|
||||
{
|
||||
let untilUpperCase = wordStart..<upperCaseRange.lowerBound
|
||||
words.append(untilUpperCase)
|
||||
|
||||
// Find next lowercase character
|
||||
searchRange = upperCaseRange.lowerBound..<searchRange.upperBound
|
||||
guard let lowerCaseRange = stringKey.rangeOfCharacter(
|
||||
from: CharacterSet.lowercaseLetters,
|
||||
options: [],
|
||||
range: searchRange)
|
||||
else {
|
||||
// There are no more lower case letters. Just end here.
|
||||
wordStart = searchRange.lowerBound
|
||||
break
|
||||
}
|
||||
|
||||
// Is the next lowercase letter more than 1 after the uppercase? If
|
||||
// so, we encountered a group of uppercase letters that we should
|
||||
// treat as its own word
|
||||
let nextCharacterAfterCapital = stringKey.index(after: upperCaseRange.lowerBound)
|
||||
if lowerCaseRange.lowerBound == nextCharacterAfterCapital {
|
||||
// The next character after capital is a lower case character
|
||||
// and therefore not a word boundary.
|
||||
// Continue searching for the next upper case for the boundary.
|
||||
wordStart = upperCaseRange.lowerBound
|
||||
} else {
|
||||
// There was a range of >1 capital letters. Turn those into a
|
||||
// word, stopping at the capital before the lower case character.
|
||||
let beforeLowerIndex = stringKey.index(before: lowerCaseRange.lowerBound)
|
||||
words.append(upperCaseRange.lowerBound..<beforeLowerIndex)
|
||||
|
||||
// Next word starts at the capital before the lowercase we just found
|
||||
wordStart = beforeLowerIndex
|
||||
}
|
||||
searchRange = lowerCaseRange.upperBound..<searchRange.upperBound
|
||||
}
|
||||
words.append(wordStart..<searchRange.upperBound)
|
||||
let result = words
|
||||
.map { (range) in stringKey[range].lowercased() }
|
||||
.joined(separator: "_")
|
||||
return result
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user