Skip to content

Commit

Permalink
feat: QueryEncoder and QueryDecoder support for alternate date formats (
Browse files Browse the repository at this point in the history
  • Loading branch information
CameronMcWilliam authored and djones6 committed Sep 20, 2019
1 parent 0e362cb commit bf06108
Show file tree
Hide file tree
Showing 6 changed files with 1,015 additions and 158 deletions.
111 changes: 109 additions & 2 deletions Sources/KituraContracts/CodableQuery/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ extension String {

/**
Converts the given String to a [Date]?.

- Parameter formatter: The designated DateFormatter to convert the string with.
- Returns: The [Date]? object. Some on success / nil on failure.
*/
Expand All @@ -200,6 +200,92 @@ extension String {
return nil
}

/**
Converts the given String to a [Date]? object using the dateDecodingStrategy supplied.

- Parameter formatter: The designated `DateFormatter` to convert the string with.
- Parameter decoderStrategy: The `JSON.dateDecodingStrategy` that should be used to decode the specifed Date. Default is set to .formatted with default dateFormatter.
- Parameter decoder: The `Decoder` parameter is only used for the custom strategy.
- Returns: The [Date]? object. Some on success / nil on failure.
*/
public func dateArray(decoderStrategy: JSONDecoder.DateDecodingStrategy = .formatted(Coder().dateFormatter), decoder: Decoder?=nil) -> [Date]? {

switch decoderStrategy {
case .formatted(let formatter):
let strs: [String] = self.components(separatedBy: ",")
let dates = strs.map { formatter.date(from: $0) }.filter { $0 != nil }.map { $0! }
if dates.count == strs.count {
return dates
}
return nil
case .deferredToDate:
let strs: [String] = self.components(separatedBy: ",")
#if swift(>=4.1)
let dbs = strs.compactMap(Double.init)
#else
let dbs = strs.flatMap(Double.init)
#endif
let dates = dbs.map { Date(timeIntervalSinceReferenceDate: $0) }
if dates.count == dbs.count {
return dates
}
return nil
case .secondsSince1970:
let strs: [String] = self.components(separatedBy: ",")
#if swift(>=4.1)
let dbs = strs.compactMap(Double.init)
#else
let dbs = strs.flatMap(Double.init)
#endif
let dates = dbs.map { Date(timeIntervalSince1970: $0) }
if dates.count == dbs.count {
return dates
}
return nil
case .millisecondsSince1970:
let strs: [String] = self.components(separatedBy: ",")
#if swift(>=4.1)
let dbs = strs.compactMap(Double.init)
#else
let dbs = strs.flatMap(Double.init)
#endif
let dates = dbs.map { Date(timeIntervalSince1970: ($0)/1000) }
if dates.count == dbs.count {
return dates
}
return nil
case .iso8601:
let strs: [String] = self.components(separatedBy: ",")
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
let dates = strs.map { _iso8601Formatter.date(from: $0) }
if dates.count == strs.count {
return dates as? [Date]
}
return nil
} else {
fatalError("ISO8601DateFormatter is unavailable on this platform.")
}
case .custom(let closure):
var dateArray: [Date] = []
guard let decoder = decoder else {return dateArray}
var fieldValueArray = self.split(separator: ",")
for _ in fieldValueArray {
// Call closure to decode value
guard let date = try? closure(decoder) else {
return nil
}
dateArray.append(date)
// Delete from array after use
fieldValueArray.removeFirst()
}
return dateArray
#if swift(>=5)
@unknown default:
Log.error("Decoding strategy not found")
fatalError()
#endif
}
}
/// Helper Method to decode a string to an LosslessStringConvertible array types.
private func decodeArray<T: LosslessStringConvertible>(_ type: T.Type) -> [T]? {
let strs: [String] = self.components(separatedBy: ",")
Expand Down Expand Up @@ -235,7 +321,7 @@ extension String {
}
let key = String(self[..<range.lowerBound])
let value = String(self[range.upperBound...])

let valueReplacingPlus = value.replacingOccurrences(of: "+", with: " ")
let decodedValue = valueReplacingPlus.removingPercentEncoding
if decodedValue == nil {
Expand All @@ -244,3 +330,24 @@ extension String {
return (key: key, value: decodedValue ?? valueReplacingPlus)
}
}

// ISO8601 Formatter used for formatting ISO8601 dates.
@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *)
var _iso8601Formatter: ISO8601DateFormatter = {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = .withInternetDateTime
return formatter
}()

enum DateError: Error {
case unknownStrategy
}

extension DateError: LocalizedError {
public var errorDescription: String? {
switch self {
case .unknownStrategy:
return("Date encoding or decoding strategy not known.")
}
}
}
70 changes: 66 additions & 4 deletions Sources/KituraContracts/CodableQuery/QueryDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,23 @@ public class QueryDecoder: Coder, Decoder, BodyDecoder {
*/
public var dictionary: [String : String]



// A `JSONDecoder.DateDecodingStrategy` date decoder used to determine what strategy
// to use when decoding the specific date.
private var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy
/**
Initializer with an empty dictionary for decoding from Data.
*/
public override init() {
public override init () {
self.dateDecodingStrategy = .formatted(Coder().dateFormatter)
self.dictionary = [:]
super.init()
}
/**
Initializer with a `[String : String]` dictionary.
*/
public init(dictionary: [String : String]) {
self.dateDecodingStrategy = .formatted(Coder().dateFormatter)
self.dictionary = dictionary
super.init()
}
Expand All @@ -94,6 +99,10 @@ public class QueryDecoder: Coder, Decoder, BodyDecoder {
throw RequestError.unprocessableEntity
}
let decoder = QueryDecoder(dictionary: urlString.urlDecodedFieldValuePairs)
decoder.dateDecodingStrategy = dateDecodingStrategy
if let Q = T.self as? QueryParams.Type {
decoder.dateDecodingStrategy = Q.dateDecodingStrategy
}
return try T(from: decoder)
}

Expand All @@ -111,6 +120,9 @@ public class QueryDecoder: Coder, Decoder, BodyDecoder {
````
*/
public func decode<T: Decodable>(_ type: T.Type) throws -> T {
if let Q = T.self as? QueryParams.Type {
dateDecodingStrategy = Q.dateDecodingStrategy
}
let fieldName = Coder.getFieldName(from: codingPath)
let fieldValue = dictionary[fieldName]
Log.verbose("fieldName: \(fieldName), fieldValue: \(String(describing: fieldValue))")
Expand Down Expand Up @@ -175,9 +187,58 @@ public class QueryDecoder: Coder, Decoder, BodyDecoder {
return try decodeType(fieldValue?.doubleArray, to: T.self)
/// Dates
case is Date.Type:
return try decodeType(fieldValue?.date(dateFormatter), to: T.self)
switch dateDecodingStrategy {
case .deferredToDate:
guard let doubleValue = fieldValue?.double else {return try decodeType(fieldValue, to: T.self)}
return try decodeType(Date(timeIntervalSinceReferenceDate: (doubleValue)), to: T.self)
case .secondsSince1970:
guard let doubleValue = fieldValue?.double else {return try decodeType(fieldValue, to: T.self)}
return try decodeType(Date(timeIntervalSince1970: (doubleValue)), to: T.self)
case .millisecondsSince1970:
guard let doubleValue = fieldValue?.double else {return try decodeType(fieldValue, to: T.self)}
return try decodeType(Date(timeIntervalSince1970: (doubleValue)), to: T.self)
case .iso8601:
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
guard let stringValue = fieldValue?.string else {return try decodeType(fieldValue, to: T.self)}
guard let date = _iso8601Formatter.date(from: stringValue) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath, debugDescription: "Expected date string to be ISO8601-formatted."))
}
return try decodeType(date, to: T.self)
} else {
fatalError("ISO8601DateFormatter is unavailable on this platform.")
}
case .formatted(let formatted):
return try decodeType(fieldValue?.date(formatted), to: T.self)
case .custom(let closure):
return try decodeType(closure(self), to: T.self)
#if swift(>=5)
@unknown default:
throw DateError.unknownStrategy
#endif
}
case is [Date].Type:
return try decodeType(fieldValue?.dateArray(dateFormatter), to: T.self)
switch dateDecodingStrategy {
case .deferredToDate:
return try decodeType(fieldValue?.dateArray(decoderStrategy: .deferredToDate), to: T.self)
case .secondsSince1970:
return try decodeType(fieldValue?.dateArray(decoderStrategy: .secondsSince1970), to: T.self)
case .millisecondsSince1970:
return try decodeType(fieldValue?.dateArray(decoderStrategy: .millisecondsSince1970), to: T.self)
case .iso8601:
if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) {
return try decodeType(fieldValue?.dateArray(decoderStrategy: .iso8601), to: T.self)
} else {
fatalError("ISO8601DateFormatter is unavailable on this platform.")
}
case .formatted(let formatter):
return try decodeType(fieldValue?.dateArray(formatter), to: T.self)
case .custom(let closure):
return try decodeType(fieldValue?.dateArray(decoderStrategy: .custom(closure), decoder: self), to: T.self)
#if swift(>=5)
@unknown default:
throw DateError.unknownStrategy
#endif
}
/// Strings
case is String.Type:
return try decodeType(fieldValue?.string, to: T.self)
Expand Down Expand Up @@ -343,4 +404,5 @@ public class QueryDecoder: Coder, Decoder, BodyDecoder {
return decoder
}
}

}
Loading

0 comments on commit bf06108

Please sign in to comment.