Skip to content

Commit

Permalink
Add property to set encoding format for Date properties. (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
kilnerm committed May 30, 2019
1 parent fd7a7ab commit c909e0e
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 19 deletions.
21 changes: 21 additions & 0 deletions README.md
Expand Up @@ -394,6 +394,27 @@ specificPerson.save() { (savedPerson, error) in

**NOTE** - When using manual or optional ID properties, you should be prepared to handle violation of unique identifier constraints. These can occur if you attempt to save a model with an ID that already exists, or in the case of Postgres, if the auto-incremented value collides with an ID that was previously inserted explicitly.

## Alternative encoding for `Date` properties

By default any property on your Model that is declared as a `Date` will be encoded and decoded as a `Double`.

You can change this behaviour by overriding the default value of the property `dateEncodingStrategy`. The dateEncodingStrategy will apply to all Date properties on your Model.

The example below defines a model which will have its Date properties encoded and decoded as a timestamp:

```swift

struct Person: Model {

static var dateEncodingStrategy: DateEncodingFormat = .timestamp

var firstname: String
var surname: String
var age: Int
var dob: Date
}
```

## List of plugins

* [PostgreSQL](https://github.com/IBM-Swift/Swift-Kuery-PostgreSQL)
Expand Down
44 changes: 40 additions & 4 deletions Sources/SwiftKueryORM/DatabaseDecoder.swift
Expand Up @@ -23,7 +23,8 @@ open class DatabaseDecoder {
fileprivate let decoder = _DatabaseDecoder()

/// Decode from a dictionary [String: Any] to a Decodable type
open func decode<T : Decodable>(_ type: T.Type, _ values: [String : Any?]) throws -> T {
open func decode<T : Decodable>(_ type: T.Type, _ values: [String : Any?], dateEncodingStrategy: DateEncodingFormat) throws -> T {
decoder.dateEncodingStrategy = dateEncodingStrategy
decoder.values = values
return try T(from: decoder)
}
Expand All @@ -33,6 +34,8 @@ open class DatabaseDecoder {
public var userInfo: [CodingUserInfoKey:Any] = [:]
public var values = [String:Any?]()

public var dateEncodingStrategy: DateEncodingFormat = .double

fileprivate init(at codingPath: [CodingKey] = []){
self.codingPath = codingPath
}
Expand Down Expand Up @@ -192,9 +195,42 @@ open class DatabaseDecoder {
let uuid = UUID(uuidString: castValue)
return try castedValue(uuid, type, key)
} else if type is Date.Type && value != nil {
let castValue = try castedValue(value, Double.self, key)
let date = Date(timeIntervalSinceReferenceDate: castValue)
return try castedValue(date, type, key)
switch decoder.dateEncodingStrategy {
case .double:
let castValue = try castedValue(value, Double.self, key)
let date = Date(timeIntervalSinceReferenceDate: castValue)
return try castedValue(date, type, key)
case .timestamp:
if let dateValue = value as? Date {
return try castedValue(dateValue, type.self, key)
} else {
let castValue = try castedValue(value, String.self, key)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let date = dateFormatter.date(from: castValue)
return try castedValue(date, type.self, key)
}
case .date:
if let dateValue = value as? Date {
return try castedValue(dateValue, type.self, key)
} else {
let castValue = try castedValue(value, String.self, key)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let date = dateFormatter.date(from: castValue)
return try castedValue(date, type.self, key)
}
case .time:
if let dateValue = value as? Date {
return try castedValue(dateValue, type.self, key)
} else {
let castValue = try castedValue(value, String.self, key)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
let date = dateFormatter.date(from: castValue)
return try castedValue(date, type.self, key)
}
}
} else {
throw RequestError(.ormDatabaseDecodingError, reason: "Unsupported type: \(String(describing: type)) for value: \(String(describing: value))")
}
Expand Down
22 changes: 20 additions & 2 deletions Sources/SwiftKueryORM/DatabaseEncoder.swift
Expand Up @@ -23,7 +23,8 @@ open class DatabaseEncoder {
private var databaseEncoder = _DatabaseEncoder()

/// Encode a Encodable type to a dictionary [String: Any]
open func encode<T: Encodable>(_ value: T) throws -> [String: Any] {
open func encode<T: Encodable>(_ value: T, dateEncodingStrategy: DateEncodingFormat) throws -> [String: Any] {
databaseEncoder.dateEncodingStrategy = dateEncodingStrategy
try value.encode(to: databaseEncoder)
return databaseEncoder.values
}
Expand All @@ -34,6 +35,8 @@ fileprivate class _DatabaseEncoder: Encoder {

public var values: [String: Any] = [:]

public var dateEncodingStrategy: DateEncodingFormat = .double

public var userInfo: [CodingUserInfoKey: Any] = [:]
public func container<Key>(keyedBy: Key.Type) -> KeyedEncodingContainer<Key> {
let container = _DatabaseKeyedEncodingContainer<Key>(encoder: self, codingPath: codingPath)
Expand Down Expand Up @@ -65,7 +68,22 @@ fileprivate struct _DatabaseKeyedEncodingContainer<K: CodingKey> : KeyedEncoding
} else if let uuidValue = value as? UUID {
encoder.values[key.stringValue] = uuidValue.uuidString
} else if let dateValue = value as? Date {
encoder.values[key.stringValue] = dateValue.timeIntervalSinceReferenceDate
switch encoder.dateEncodingStrategy {
case .double:
encoder.values[key.stringValue] = dateValue.timeIntervalSinceReferenceDate
case .timestamp:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
encoder.values[key.stringValue] = dateFormatter.string(from: dateValue)
case .date:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
encoder.values[key.stringValue] = dateFormatter.string(from: dateValue)
case .time:
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "HH:mm:ss"
encoder.values[key.stringValue] = dateFormatter.string(from: dateValue)
}
} else if value is [Any] {
throw RequestError(.ormDatabaseEncodingError, reason: "Encoding an array is not currently supported")
} else if value is [AnyHashable: Any] {
Expand Down
33 changes: 25 additions & 8 deletions Sources/SwiftKueryORM/Model.swift
Expand Up @@ -19,6 +19,18 @@ import KituraContracts
import Foundation
import Dispatch

/// The DateEncodingFormat enumeration defines the supported formats for persisiting properties of type `Date`.
public enum DateEncodingFormat {
/// time - Corresponds to the `time` column type
case time
/// date - Corresponds to the `date` column type
case date
/// timestamp - Corresponds to the `timestamp` column type.
case timestamp
/// double - This is the default encoding type and corresponds to Swifts encoding of `Date`.
case double
}

/// Protocol Model conforming to Codable defining the available operations
public protocol Model: Codable {
/// Defines the tableName in the Database
Expand All @@ -34,6 +46,9 @@ public protocol Model: Codable {
/// Defines the keypath to the Models id field
static var idKeypath: IDKeyPath {get}

/// Defines the format in which `Date` properties of the `Model` will be written to the Database. Defaults to .double
static var dateEncodingFormat: DateEncodingFormat { get }

/// Call to create the table in the database synchronously
static func createTableSync(using db: Database?) throws -> Bool

Expand Down Expand Up @@ -120,6 +135,8 @@ public extension Model {

static var idKeypath: IDKeyPath { return nil }

static var dateEncodingFormat: DateEncodingFormat { return .double }

private static func executeTask(using db: Database? = nil, task: @escaping ((Connection?, QueryError?) -> ())) {
guard let database = db ?? Database.default else {

Expand Down Expand Up @@ -233,7 +250,7 @@ public extension Model {
var values: [String : Any]
do {
table = try Self.getTable()
values = try DatabaseEncoder().encode(self)
values = try DatabaseEncoder().encode(self, dateEncodingStrategy: Self.dateEncodingFormat)
} catch let error {
onCompletion(nil, Self.convertError(error))
return
Expand All @@ -252,7 +269,7 @@ public extension Model {
var values: [String : Any]
do {
table = try Self.getTable()
values = try DatabaseEncoder().encode(self)
values = try DatabaseEncoder().encode(self, dateEncodingStrategy: Self.dateEncodingFormat)
} catch let error {
onCompletion(nil, nil, Self.convertError(error))
return
Expand All @@ -270,7 +287,7 @@ public extension Model {
var values: [String: Any]
do {
table = try Self.getTable()
values = try DatabaseEncoder().encode(self)
values = try DatabaseEncoder().encode(self, dateEncodingStrategy: Self.dateEncodingFormat)
} catch let error {
onCompletion(nil, Self.convertError(error))
return
Expand Down Expand Up @@ -500,7 +517,7 @@ public extension Model {

var decodedModel: Self
do {
decodedModel = try DatabaseDecoder().decode(Self.self, dictionaryTitleToValue)
decodedModel = try DatabaseDecoder().decode(Self.self, dictionaryTitleToValue, dateEncodingStrategy: Self.dateEncodingFormat)
} catch {
onCompletion(nil, Self.convertError(error))
return
Expand Down Expand Up @@ -563,7 +580,7 @@ public extension Model {

var decodedModel: Self
do {
decodedModel = try DatabaseDecoder().decode(Self.self, dictionaryTitleToValue)
decodedModel = try DatabaseDecoder().decode(Self.self, dictionaryTitleToValue, dateEncodingStrategy: Self.dateEncodingFormat)
} catch {
onCompletion(nil, nil, Self.convertError(error))
return
Expand Down Expand Up @@ -619,7 +636,7 @@ public extension Model {
for dictionary in dictionariesTitleToValue {
var decodedModel: Self
do {
decodedModel = try DatabaseDecoder().decode(Self.self, dictionary)
decodedModel = try DatabaseDecoder().decode(Self.self, dictionary, dateEncodingStrategy: Self.dateEncodingFormat)
} catch {
onCompletion(nil, Self.convertError(error))
return
Expand Down Expand Up @@ -684,7 +701,7 @@ public extension Model {
for dictionary in dictionariesTitleToValue {
var decodedModel: Self
do {
decodedModel = try DatabaseDecoder().decode(Self.self, dictionary)
decodedModel = try DatabaseDecoder().decode(Self.self, dictionary, dateEncodingStrategy: Self.dateEncodingFormat)
} catch let error {
onCompletion(nil, Self.convertError(error))
return
Expand Down Expand Up @@ -754,7 +771,7 @@ public extension Model {

static func getTable() throws -> Table {
let idKeyPathSet: Bool = Self.idKeypath != nil
return try Database.tableInfo.getTable((Self.idColumnName, Self.idColumnType, idKeyPathSet), Self.tableName, for: Self.self)
return try Database.tableInfo.getTable((Self.idColumnName, Self.idColumnType, idKeyPathSet), Self.tableName, for: Self.self, with: Self.dateEncodingFormat)
}

/**
Expand Down
21 changes: 16 additions & 5 deletions Sources/SwiftKueryORM/TableInfo.swift
Expand Up @@ -29,11 +29,11 @@ public class TableInfo {
private var codableMapQueue = DispatchQueue(label: "codableMap.queue", attributes: .concurrent)

/// Get the table for a model
func getTable<T: Decodable>(_ idColumn: (name: String, type: SQLDataType.Type, idKeyPathSet: Bool), _ tableName: String, for type: T.Type) throws -> Table {
return try getInfo(idColumn, tableName, type).table
func getTable<T: Decodable>(_ idColumn: (name: String, type: SQLDataType.Type, idKeyPathSet: Bool), _ tableName: String, for type: T.Type, with dateEncodingFormat: DateEncodingFormat) throws -> Table {
return try getInfo(idColumn, tableName, type, dateEncodingFormat).table
}

func getInfo<T: Decodable>(_ idColumn: (name: String, type: SQLDataType.Type, idKeyPathSet: Bool), _ tableName: String, _ type: T.Type) throws -> (info: TypeInfo, table: Table) {
func getInfo<T: Decodable>(_ idColumn: (name: String, type: SQLDataType.Type, idKeyPathSet: Bool), _ tableName: String, _ type: T.Type, _ dateEncodingFormat: DateEncodingFormat) throws -> (info: TypeInfo, table: Table) {
let typeString = "\(type)"
var result: (TypeInfo, Table)? = nil
// Read from codableMap when no concurrent write is occurring
Expand All @@ -46,7 +46,7 @@ public class TableInfo {

try codableMapQueue.sync(flags: .barrier) {
let typeInfo = try TypeDecoder.decode(type)
result = (info: typeInfo, table: try constructTable(idColumn, tableName, typeInfo))
result = (info: typeInfo, table: try constructTable(idColumn, tableName, typeInfo, dateEncodingFormat))
codableMap[typeString] = result
}

Expand All @@ -57,7 +57,7 @@ public class TableInfo {
}

/// Construct the table for a Model
func constructTable(_ idColumn: (name: String, type: SQLDataType.Type, idKeyPathSet: Bool), _ tableName: String, _ typeInfo: TypeInfo) throws -> Table {
func constructTable(_ idColumn: (name: String, type: SQLDataType.Type, idKeyPathSet: Bool), _ tableName: String, _ typeInfo: TypeInfo, _ dateEncodingFormat: DateEncodingFormat) throws -> Table {
var columns: [Column] = []
var idColumnIsSet = false
switch typeInfo {
Expand All @@ -73,6 +73,17 @@ public class TableInfo {
switch keyedTypeInfo {
case .single(_ as UUID.Type, _):
valueType = UUID.self
case .single(_ as Date.Type, _):
switch dateEncodingFormat {
case .double:
valueType = Double.self
case .timestamp:
valueType = Timestamp.self
case .date:
valueType = SQLDate.self
case .time:
valueType = Time.self
}
case .single(_, let singleType):
valueType = singleType
if valueType is Int.Type {
Expand Down

0 comments on commit c909e0e

Please sign in to comment.