Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add property to set encoding format for Date properties. #110

Merged
merged 10 commits into from
May 30, 2019
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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”

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ” at the end of this line looks like a typo?


```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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ import KituraContracts
import Foundation
import Dispatch

/// Defines the supported formats for persisiting properties of type `Date`.
public enum DateEncodingFormat {
// time - Corresponds to the `time` column type

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we intend to have these comments (describing each case) to be a part of the API docs, they'd need to start with ///

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I think we must document these in the API docs :)

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
Original file line number Diff line number Diff line change
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