diff --git a/Sources/KituraContracts/CodableQuery/Extensions.swift b/Sources/KituraContracts/CodableQuery/Extensions.swift index 1c63f28..8652129 100644 --- a/Sources/KituraContracts/CodableQuery/Extensions.swift +++ b/Sources/KituraContracts/CodableQuery/Extensions.swift @@ -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. */ @@ -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(_ type: T.Type) -> [T]? { let strs: [String] = self.components(separatedBy: ",") @@ -235,7 +321,7 @@ extension String { } let key = String(self[..(_ 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))") @@ -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) @@ -343,4 +404,5 @@ public class QueryDecoder: Coder, Decoder, BodyDecoder { return decoder } } + } diff --git a/Sources/KituraContracts/CodableQuery/QueryEncoder.swift b/Sources/KituraContracts/CodableQuery/QueryEncoder.swift index c6702e0..34ad00b 100644 --- a/Sources/KituraContracts/CodableQuery/QueryEncoder.swift +++ b/Sources/KituraContracts/CodableQuery/QueryEncoder.swift @@ -61,10 +61,15 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { */ public var userInfo: [CodingUserInfoKey: Any] = [:] + // A `JSONDecoder.DateEncodingStrategy` date encoder used to determine what strategy + // to use when encoding the specific date. + private var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy + /** Initializer for the dictionary, which initializes an empty `[String: String]` dictionary. */ public override init() { + self.dateEncodingStrategy = .formatted(Coder().dateFormatter) self.dictionary = [:] self.anyDictionary = [:] super.init() @@ -129,6 +134,9 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { ```` */ public func encode(_ value: T) throws -> [URLQueryItem] { + if let Q = T.self as? QueryParams.Type { + dateEncodingStrategy = Q.dateEncodingStrategy + } let dict: [String : String] = try encode(value) return dict.reduce([URLQueryItem]()) { array, element in var array = array @@ -152,6 +160,10 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { */ public func encode(_ value: T) throws -> [String : String] { let encoder = QueryEncoder() + encoder.dateEncodingStrategy = self.dateEncodingStrategy + if let Q = T.self as? QueryParams.Type { + encoder.dateEncodingStrategy = Q.dateEncodingStrategy + } try value.encode(to: encoder) return encoder.dictionary } @@ -161,6 +173,10 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { /// - Parameter _ value: The Encodable object to encode to its [String: String] representation public func encode(_ value: T) throws -> [String : Any] { let encoder = QueryEncoder() + encoder.dateEncodingStrategy = self.dateEncodingStrategy + if let Q = T.self as? QueryParams.Type { + encoder.dateEncodingStrategy = Q.dateEncodingStrategy + } try value.encode(to: encoder) return encoder.anyDictionary } @@ -187,7 +203,7 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { ```` */ public func unkeyedContainer() -> UnkeyedEncodingContainer { - return UnkeyedContanier(encoder: self) + return UnkeyedContainer(encoder: self) } /** @@ -199,7 +215,233 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { ```` */ public func singleValueContainer() -> SingleValueEncodingContainer { - return UnkeyedContanier(encoder: self) + return UnkeyedContainer(encoder: self) + } + + /// Decode a value for the current field, determined by this encoder's state (codingPath). Some + /// paths through this function are recursive (for handling custom Date encodings). + /// + /// Both the keyed and unkeyed containers call this function. The keyed container first sets the + /// encoder's codingPath, which determines the field name we encode. + /// + /// If a custom encoding is defined for Date, the custom closure will call this encoder back. It + /// is expected that any such custom encoding produces a single value, calling back via the + /// unkeyed container. + /// + /// When custom encoding Date arrays, this function will be invoked multiple times for the same + /// key. The =+= operator is used to build a comma-separated list of values for a key. + internal func _encode(value: T) throws { + let encoder = self + let fieldName = Coder.getFieldName(from: encoder.codingPath) + + switch value { + /// Ints + case let fieldValue as Int: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as Int8: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as Int16: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as Int32: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as Int64: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + /// Int Arrays + case let fieldValue as [Int]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [Int8]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [Int16]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [Int32]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [Int64]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + /// UInts + case let fieldValue as UInt: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + /// Int Arrays + case let fieldValue as UInt8: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + /// Int Arrays + case let fieldValue as UInt16: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + /// Int Arrays + case let fieldValue as UInt32: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + /// Int Arrays + case let fieldValue as UInt64: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + /// Int Arrays + /// UInt Arrays + case let fieldValue as [UInt]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + /// Int Arrays + case let fieldValue as [UInt8]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [UInt16]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [UInt32]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [UInt64]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + /// Floats + case let fieldValue as Float: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [Float]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + /// Doubles + case let fieldValue as Double: + encoder.dictionary[fieldName] =+= String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [Double]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + /// Bools + case let fieldValue as Bool: + encoder.dictionary[fieldName] = String(fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [Bool]: + let strs: [String] = fieldValue.map { String($0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + /// Strings + case let fieldValue as String: + encoder.dictionary[fieldName] =+= fieldValue + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as [String]: + encoder.dictionary[fieldName] = fieldValue.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + /// Dates + case let fieldValue as Date: + switch encoder.dateEncodingStrategy { + case .formatted(let formatter): + encoder.dictionary[fieldName] = formatter.string(from: fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + case .deferredToDate: + let date = NSNumber(value: fieldValue.timeIntervalSinceReferenceDate) + encoder.dictionary[fieldName] = date.stringValue + encoder.anyDictionary[fieldName] = fieldValue + case .secondsSince1970: + let date = NSNumber(value: fieldValue.timeIntervalSince1970) + encoder.dictionary[fieldName] = date.stringValue + encoder.anyDictionary[fieldName] = fieldValue + case .millisecondsSince1970: + let date = NSNumber(value: 1000 * fieldValue.timeIntervalSince1970) + encoder.dictionary[fieldName] = date.stringValue + encoder.anyDictionary[fieldName] = fieldValue + case .iso8601: + if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { + encoder.dictionary[fieldName] = _iso8601Formatter.string(from: fieldValue) + encoder.anyDictionary[fieldName] = fieldValue + } else { + fatalError("ISO8601DateFormatter is unavailable on this platform.") + } + case .custom(let closure): + try closure(fieldValue, encoder) + #if swift(>=5) + @unknown default: + throw DateError.unknownStrategy + #endif + } + case let fieldValue as [Date]: + switch encoder.dateEncodingStrategy { + case .deferredToDate: + let dbs: [NSNumber] = fieldValue.map { NSNumber(value: $0.timeIntervalSinceReferenceDate) } + let strs: [String] = dbs.map { ($0).stringValue} + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case .secondsSince1970: + let dbs: [NSNumber] = fieldValue.map { NSNumber(value: $0.timeIntervalSince1970) } + let strs: [String] = dbs.map { ($0).stringValue} + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case .millisecondsSince1970: + let dbs: [NSNumber] = fieldValue.map { NSNumber(value: ($0.timeIntervalSince1970)/1000) } + let strs: [String] = dbs.map { ($0).stringValue} + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + case .iso8601: + if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { + let strs: [String] = fieldValue.map { _iso8601Formatter.string(from: $0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + } else { + fatalError("ISO8601DateFormatter is unavailable on this platform.") + } + case .formatted(let formatter): + let strs: [String] = fieldValue.map { formatter.string(from: $0) } + encoder.dictionary[fieldName] = strs.joined(separator: ",") + encoder.anyDictionary[fieldName] = fieldValue + // This calls us back with each serialized element individually, with the same fieldName key, + // which builds a comma-separated list using the '=+=' operator. + case .custom(let closure): + for element in fieldValue { + try closure(element, encoder) + } + #if swift(>=5) + @unknown default: + throw DateError.unknownStrategy + #endif + } + case let fieldValue as Operation: + encoder.dictionary[fieldName] = fieldValue.getStringValue() + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as Ordering: + encoder.dictionary[fieldName] = fieldValue.getStringValue() + encoder.anyDictionary[fieldName] = fieldValue + case let fieldValue as Pagination: + encoder.dictionary[fieldName] = fieldValue.getStringValue() + encoder.anyDictionary[fieldName] = fieldValue + default: + if fieldName.isEmpty { + encoder.dictionary = [:] // Make encoder instance reusable + encoder.anyDictionary = [:] // Make encoder instance reusable + try value.encode(to: encoder) + } else { + do { + let jsonData = try JSONEncoder().encode(value) + encoder.dictionary[fieldName] = String(data: jsonData, encoding: .utf8) + encoder.anyDictionary[fieldName] = jsonData + } catch let error { + throw encoder.encodingError(value, underlyingError: error) + } + } + } } internal func encodingError(_ value: Any, underlyingError: Swift.Error?) -> EncodingError { @@ -210,158 +452,14 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { private struct KeyedContainer: KeyedEncodingContainerProtocol { var encoder: QueryEncoder - var codingPath: [CodingKey] { return [] } + /// The typical path for encoding a QueryParams (keyed) type. This encode will be called + /// for each field in turn. func encode(_ value: T, forKey key: Key) throws where T : Encodable { self.encoder.codingPath.append(key) defer { self.encoder.codingPath.removeLast() } - let fieldName = Coder.getFieldName(from: self.encoder.codingPath) - - switch value { - /// Ints - case let fieldValue as Int: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as Int8: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as Int16: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as Int32: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as Int64: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - /// Int Arrays - case let fieldValue as [Int]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [Int8]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [Int16]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [Int32]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [Int64]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - /// UInts - case let fieldValue as UInt: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - /// Int Arrays - case let fieldValue as UInt8: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - /// Int Arrays - case let fieldValue as UInt16: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - /// Int Arrays - case let fieldValue as UInt32: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - /// Int Arrays - case let fieldValue as UInt64: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - /// Int Arrays - /// UInt Arrays - case let fieldValue as [UInt]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - /// Int Arrays - case let fieldValue as [UInt8]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [UInt16]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [UInt32]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [UInt64]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - /// Floats - case let fieldValue as Float: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [Float]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - /// Doubles - case let fieldValue as Double: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [Double]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - /// Bools - case let fieldValue as Bool: - encoder.dictionary[fieldName] = String(fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [Bool]: - let strs: [String] = fieldValue.map { String($0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - /// Strings - case let fieldValue as String: - encoder.dictionary[fieldName] = fieldValue - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [String]: - encoder.dictionary[fieldName] = fieldValue.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - /// Dates - case let fieldValue as Date: - encoder.dictionary[fieldName] = encoder.dateFormatter.string(from: fieldValue) - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as [Date]: - let strs: [String] = fieldValue.map { encoder.dateFormatter.string(from: $0) } - encoder.dictionary[fieldName] = strs.joined(separator: ",") - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as Operation: - encoder.dictionary[fieldName] = fieldValue.getStringValue() - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as Ordering: - encoder.dictionary[fieldName] = fieldValue.getStringValue() - encoder.anyDictionary[fieldName] = fieldValue - case let fieldValue as Pagination: - encoder.dictionary[fieldName] = fieldValue.getStringValue() - encoder.anyDictionary[fieldName] = fieldValue - default: - if fieldName.isEmpty { - encoder.dictionary = [:] // Make encoder instance reusable - encoder.anyDictionary = [:] // Make encoder instance reusable - try value.encode(to: encoder) - } else { - do { - let jsonData = try JSONEncoder().encode(value) - encoder.dictionary[fieldName] = String(data: jsonData, encoding: .utf8) - encoder.anyDictionary[fieldName] = jsonData - } catch let error { - throw encoder.encodingError(value, underlyingError: error) - } - } - } + try encoder._encode(value: value) } func encodeNil(forKey: Key) throws {} @@ -383,7 +481,7 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { } } - private struct UnkeyedContanier: UnkeyedEncodingContainer, SingleValueEncodingContainer { + private struct UnkeyedContainer: UnkeyedEncodingContainer, SingleValueEncodingContainer { var encoder: QueryEncoder var codingPath: [CodingKey] { return [] } @@ -404,8 +502,21 @@ public class QueryEncoder: Coder, Encoder, BodyEncoder { func encodeNil() throws {} + /// This unkeyed encode will be called by a custom Date encoder. The correct key (field + /// name) will already have been set by a call to the KeyedEncodingContainer. func encode(_ value: T) throws where T : Encodable { - let _: [String : String] = try encoder.encode(value) + try encoder._encode(value: value) } } } + +// The '=+=' operator builds a comma-separated list of values for a given fieldName when encoding a [Date] that uses a custom formatting. +infix operator =+= + func =+= (lhs: inout String?, rhs: String) { + if let lhsValue = lhs { + lhs = lhsValue + "," + rhs + } else { + lhs = rhs + } + } + diff --git a/Sources/KituraContracts/Contracts.swift b/Sources/KituraContracts/Contracts.swift index aa87448..2881030 100644 --- a/Sources/KituraContracts/Contracts.swift +++ b/Sources/KituraContracts/Contracts.swift @@ -523,6 +523,54 @@ extension RequestError { - All other non-optional types throw a decoding error */ public protocol QueryParams: Codable { + + /** + The decoding strategy for Dates. + The variable can be defined within your QueryParams object and tells the `QueryDecoder` how dates should be decoded. The enum used for the DateDecodingStrategy is the same one found in the `JSONDecoder`. + ### Usage Example: ### + ```swift + struct MyQuery: QueryParams { + let date: Date + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .iso8601 + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .iso8601 + } + + let queryParams = ["date": "2019-09-06T10:14:41+0000"] + + let query = try QueryDecoder(dictionary: queryParams).decode(MyQuery.self) + ``` + */ + static var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy { get } + + /** + The encoding strategy for Dates. + The variable would be defined within your QueryParams object and tells the `QueryEncoder` how dates should be encoded. The enum used for the DateEncodingStrategy is the same one found in the `JSONEncoder`. + ### Usage Example: ### + ```swift + struct MyQuery: QueryParams { + let date: Date + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .iso8601 + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .iso8601 + } + + let query = MyQuery(date: Date(timeIntervalSinceNow: 0)) + + let myQueryDict: [String: String] = try QueryEncoder().encode(query) + ``` + */ + static var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy { get } +} + +extension QueryParams { + + static var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy { + return .formatted(Coder().dateFormatter) + } + + static var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy { + return .formatted(Coder().dateFormatter) + } + } /** diff --git a/Tests/KituraContractsTests/QueryCoderTests.swift b/Tests/KituraContractsTests/QueryCoderTests.swift index fe40c47..fc88453 100644 --- a/Tests/KituraContractsTests/QueryCoderTests.swift +++ b/Tests/KituraContractsTests/QueryCoderTests.swift @@ -18,14 +18,25 @@ import XCTest @testable import KituraContracts +@available(OSX 10.12, *) class QueryCoderTests: XCTestCase { static var allTests: [(String, (QueryCoderTests) -> () throws -> Void)] { return [ ("testQueryDecoder", testQueryDecoder), ("testQueryEncoder", testQueryEncoder), + ("test1970Decode", test1970Decode), + ("test1970Encode", test1970Encode), + ("testISODecode", testISODecode), + ("testISOEncode", testISOEncode), + ("testCustomDecode", testCustomDecode), + ("testCustomEncode", testCustomEncode), + ("testFormattedDecode", testFormattedDecode), + ("testFormattedEncode", testFormattedEncode), + ("testCustomArrayDecode", testCustomArrayDecode), + ("testCustomArrayEncode", testCustomArrayEncode), ("testCycle", testCycle), - ("testIllegalInt", testIllegalInt) + ("testIllegalInt", testIllegalInt), ] } @@ -145,6 +156,112 @@ class QueryCoderTests: XCTestCase { } } + struct Query1970: QueryParams, Equatable { + public let dateField: Date + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .secondsSince1970 + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .secondsSince1970 + public static func ==(lhs: Query1970, rhs: Query1970) -> Bool { + return lhs.dateField == rhs.dateField + } + } + + struct QueryISO: QueryParams, Equatable { + public let dateField: Date + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .iso8601 + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .iso8601 + public static func ==(lhs: QueryISO, rhs: QueryISO) -> Bool { + return lhs.dateField == rhs.dateField + } + } + + struct QueryCustom: QueryParams, Equatable { + public let dateField: Date + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .custom { decoder in + // pull out the number of days from Codable + let container = try decoder.singleValueContainer() + let numberOfDays = try container.decode(Int.self) + + // create a start date of Jan 1st 1970, then a DateComponents instance for our JSON days + let startDate = Date(timeIntervalSince1970: 0) + var components = DateComponents() + components.day = numberOfDays + + // create a Calendar and use it to measure the difference between the two + let calendar = Calendar(identifier: .gregorian) + return calendar.date(byAdding: components, to: startDate) ?? Date() + } + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .custom { (date, encoder) in + + let calendar = Calendar(identifier: .gregorian) + let startDate = Date(timeIntervalSince1970: 0) + let endDate = date + let components = calendar.dateComponents([.day], from: startDate, to: endDate) + let days = components.day + let intData = Int(days!) + var container = encoder.singleValueContainer() + try container.encode(intData) + + } + public static func ==(lhs: QueryCustom, rhs: QueryCustom) -> Bool { + return lhs.dateField == rhs.dateField + } + + } + + class AlternateFormatter { + public let altFormatter: DateFormatter + + public init() { + self.altFormatter = DateFormatter() + self.altFormatter.timeZone = TimeZone(identifier: "UTC") + self.altFormatter.dateFormat = "yyyy-MM-dd" + } + } + + struct QueryFormatted: QueryParams, Equatable { + + public let dateField: Date + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .formatted(AlternateFormatter().altFormatter) + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .formatted(AlternateFormatter().altFormatter) + public static func ==(lhs: QueryFormatted, rhs: QueryFormatted) -> Bool { + return lhs.dateField == rhs.dateField + } + } + + struct QueryCustomArray: QueryParams, Equatable { + + public let dateField: [Date] + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .custom { decoder in + // pull out the number of days from Codable + let container = try decoder.singleValueContainer() + let numberOfDaysArray = try container.decode([Int].self) + let numberOfDays = numberOfDaysArray[0] + + // create a start date of Jan 1st 1970, then a DateComponents instance for our JSON days + let startDate = Date(timeIntervalSince1970: 0) + var components = DateComponents() + components.day = numberOfDays + + // create a Calendar and use it to measure the difference between the two + let calendar = Calendar(identifier: .gregorian) + return calendar.date(byAdding: components, to: startDate) ?? Date() + } + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .custom { (date, encoder) in + + let calendar = Calendar(identifier: .gregorian) + let startDate = Date(timeIntervalSince1970: 0) + let endDate = date + let components = calendar.dateComponents([.day], from: startDate, to: endDate) + let days = components.day + let intData = Int(days!) + var container = encoder.singleValueContainer() + try container.encode(intData) + + } + public static func ==(lhs: QueryCustomArray, rhs: QueryCustomArray) -> Bool { + return lhs.dateField == rhs.dateField + } + } let expectedDict = ["boolField": "true", "intField": "23", "stringField": "a string", "emptyStringField": "", "optionalStringField": "", "intArray": "1,2,3", "dateField": "2017-10-31T16:15:56+0000", "optionalDateField": "", "nested": "{\"nestedIntField\":333,\"nestedStringField\":\"nested string\"}" ] @@ -183,6 +300,70 @@ class QueryCoderTests: XCTestCase { pagination: Pagination(start: 8, size: 14) ) + let expected1970Dict = ["dateField": "1567684372.1"] + let expected1970String = "?dateField=1567684372.1" + var expectedData1970: Data { + let droppedQuestionMark = String(expected1970String.dropFirst()) + return droppedQuestionMark.data(using: .utf8)! + } + let expected1970DateStr = "1567684372.1" + let expected1970Date = Date(timeIntervalSince1970: 1567684372.1) + let expectedQuery1970 = Query1970(dateField: Date(timeIntervalSince1970: 1567684372.1)) + + let expectedISODict = ["dateField": "2019-09-06T10:14:41+0000"] + let expectedISOString = "?dateField=2019-09-06T10:14:41%2B0000" + var expectedDataISO: Data { + let droppedQuestionMark = String(expectedISOString.dropFirst()) + return droppedQuestionMark.data(using: .utf8)! + } + let expectedISODateStr = "2019-09-06T10:14:41+0000" + let expectedISODate = _iso8601Formatter.date(from: "2019-09-06T10:14:41+0000") + let expectedQueryISO = QueryISO(dateField: _iso8601Formatter.date(from: "2019-09-06T10:14:41+0000")!) + + let expectedCustomDict = ["dateField": "10650"] + let expectedCustomString = "?dateField=10650" + var expectedDataCustom: Data { + let droppedQuestionMark = String(expectedCustomString.dropFirst()) + return droppedQuestionMark.data(using: .utf8)! + } + let expectedCustomDateStr = "10650" + var expectedCustomDate: Date { + let numberOfDays = 10650 + let startDate = Date(timeIntervalSince1970: 0) + var components = DateComponents() + components.day = numberOfDays + // create a Calendar and use it to measure the difference between the two + let calendar = Calendar(identifier: .gregorian) + return calendar.date(byAdding: components, to: startDate) ?? Date() + } + + let expectedFormattedDict = ["dateField": "2017-10-31"] + let expectedFormattedString = "?dateField=2017-10-31" + var expectedDataFormatted: Data { + let droppedQuestionMark = String(expectedFormattedString.dropFirst()) + return droppedQuestionMark.data(using: .utf8)! + } + let expectedFormattedDateStr = "2017-10-31" + let expectedFormattedDate = AlternateFormatter().altFormatter.date(from: "2017-10-31")! + let expectedQueryFormatted = QueryFormatted(dateField: AlternateFormatter().altFormatter.date(from: "2017-10-31")!) + + let expectedCustomArrayDict = ["dateField": "10650,10650,10650"] + let expectedCustomArrayString = "?dateField=10650%2C10650%2C10650" + var expectedDataCustomArray: Data { + let droppedQuestionMark = String(expectedCustomArrayString.dropFirst()) + return droppedQuestionMark.data(using: .utf8)! + } + let expectedCustomArrayDateStr = "10650" + var expectedCustomArrayDate: Date { + let numberOfDays = 10650 + let startDate = Date(timeIntervalSince1970: 0) + var components = DateComponents() + components.day = numberOfDays + // create a Calendar and use it to measure the difference between the two + let calendar = Calendar(identifier: .gregorian) + return calendar.date(byAdding: components, to: startDate) ?? Date() + } + func testQueryDecoder() { guard let query = try? QueryDecoder(dictionary: expectedDict).decode(MyQuery.self) else { XCTFail("Failed to decode query to MyQuery Object") @@ -315,6 +496,335 @@ class QueryCoderTests: XCTestCase { } + func test1970Decode() { + + guard let query = try? QueryDecoder(dictionary: expected1970Dict).decode(Query1970.self) else { + XCTFail("Failed to decode query to Query1970 Object") + return + } + + XCTAssertEqual(query, expectedQuery1970) + + guard let dataQuery = try? QueryDecoder().decode(Query1970.self, from: expectedData1970.self) else { + XCTFail("Failed to decode query to Query1970 Object") + return + } + + XCTAssertEqual(dataQuery, expectedQuery1970) + + } + + func test1970Encode() { + + let query = Query1970(dateField: Date(timeIntervalSince1970: 1567684372.1)) + + guard let myQueryDict: [String: String] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to [String: String]") + return + } + + XCTAssertEqual(myQueryDict["dateField"], "1567684372.1") + + guard let myQueryStr: String = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + func createDict(_ str: String) -> [String: String] { + return myQueryStr.components(separatedBy: "&").reduce([String: String]()) { acc, val in + var acc = acc + let split = val.components(separatedBy: "=") + acc[split[0]] = split[1] + return acc + } + } + + let myQueryStrSplit1: [String: String] = createDict(myQueryStr) + let myQueryStrSplit2: [String: String] = createDict(expected1970String) + + XCTAssertEqual(myQueryStrSplit1["dateField"], myQueryStrSplit2["dateField"]) + + guard let myURLQueryItems: [URLQueryItem] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + let queryItems = [ URLQueryItem(name: "dateField", value: "1567684372.1")] + XCTAssertEqual(queryItems, myURLQueryItems) + + } + + func testISODecode() { + + guard let query = try? QueryDecoder(dictionary: expectedISODict).decode(QueryISO.self) else { + XCTFail("Failed to decode query to QueryISO Object") + return + } + + XCTAssertEqual(query, expectedQueryISO) + + guard let dataQuery = try? QueryDecoder().decode(QueryISO.self, from: expectedDataISO.self) else { + XCTFail("Failed to decode query to QueryISO Object") + return + } + + XCTAssertEqual(dataQuery, expectedQueryISO) + + } + + func testISOEncode() { + + guard let dateString = _iso8601Formatter.date(from: "2019-09-06T10:14:41+0000") else { + return XCTFail("Date could not be formatted") + } + let query = QueryISO(dateField: dateString) + + guard let myQueryDict: [String: String] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to [String: String]") + return + } + + XCTAssertEqual(myQueryDict["dateField"], "2019-09-06T10:14:41Z") + + guard let myQueryStr: String = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + func createDict(_ str: String) -> [String: String] { + return myQueryStr.components(separatedBy: "&").reduce([String: String]()) { acc, val in + var acc = acc + let split = val.components(separatedBy: "=") + acc[split[0]] = split[1] + return acc + } + } + + let myQueryStrSplit1: [String: String] = createDict(myQueryStr) + let myQueryStrSplit2: [String: String] = createDict(expectedISOString) + + XCTAssertEqual(myQueryStrSplit1["dateField"], myQueryStrSplit2["dateField"]) + + guard let myURLQueryItems: [URLQueryItem] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + let queryItems = [ URLQueryItem(name: "dateField", value: "2019-09-06T10:14:41Z")] + XCTAssertEqual(queryItems, myURLQueryItems) + + } + + func testCustomDecode() { + + let expectedQueryCustom = QueryCustom(dateField: expectedCustomDate) + + guard let query = try? QueryDecoder(dictionary: expectedCustomDict).decode(QueryCustom.self) else { + XCTFail("Failed to decode query to QueryCustom Object") + return + } + + XCTAssertEqual(query, expectedQueryCustom) + + guard let dataQuery = try? QueryDecoder().decode(QueryCustom.self, from: expectedDataCustom.self) else { + XCTFail("Failed to decode query to QueryCustom Object") + return + } + + XCTAssertEqual(dataQuery, expectedQueryCustom) + } + + func testCustomEncode() { + + let query = QueryCustom(dateField: expectedCustomDate) + + guard let customQueryDict: [String: String] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to [String: String]") + return + } + + XCTAssertEqual(customQueryDict["dateField"], "10650") + + guard let customQueryStr: String = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + func createDict(_ str: String) -> [String: String] { + return customQueryStr.components(separatedBy: "&").reduce([String: String]()) { acc, val in + var acc = acc + let split = val.components(separatedBy: "=") + acc[split[0]] = split[1] + return acc + } + } + + let customQueryStrSplit1: [String: String] = createDict(customQueryStr) + let customQueryStrSplit2: [String: String] = createDict(expectedCustomString) + + XCTAssertEqual(customQueryStrSplit1["dateField"], customQueryStrSplit2["dateField"]) + + guard let customURLQueryItems: [URLQueryItem] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + let queryItems = [ URLQueryItem(name: "dateField", value: "10650")] + XCTAssertEqual(queryItems, customURLQueryItems) + + } + + func testFormattedDecode() { + + let expectedQueryFormatted = QueryFormatted(dateField: expectedFormattedDate) + + guard let query = try? QueryDecoder(dictionary: expectedFormattedDict).decode(QueryFormatted.self) else { + XCTFail("Failed to decode query to QueryFormatted Object") + return + } + + XCTAssertEqual(query, expectedQueryFormatted) + + guard let dataQuery = try? QueryDecoder().decode(QueryFormatted.self, from: expectedDataFormatted.self) else { + XCTFail("Failed to decode query to QueryFormatted Object") + return + } + + XCTAssertEqual(dataQuery, expectedQueryFormatted) + } + + func testFormattedEncode() { + + let query = QueryFormatted(dateField: expectedFormattedDate) + + guard let formattedQueryDict: [String: String] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to [String: String]") + return + } + + XCTAssertEqual(formattedQueryDict["dateField"], "2017-10-31") + + guard let formattedQueryStr: String = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + func createDict(_ str: String) -> [String: String] { + return formattedQueryStr.components(separatedBy: "&").reduce([String: String]()) { acc, val in + var acc = acc + let split = val.components(separatedBy: "=") + acc[split[0]] = split[1] + return acc + } + } + + let formattedQueryStrSplit1: [String: String] = createDict(formattedQueryStr) + let formattedQueryStrSplit2: [String: String] = createDict(expectedFormattedString) + + XCTAssertEqual(formattedQueryStrSplit1["dateField"], formattedQueryStrSplit2["dateField"]) + + guard let formattedURLQueryItems: [URLQueryItem] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + let queryItems = [ URLQueryItem(name: "dateField", value: "2017-10-31")] + XCTAssertEqual(queryItems, formattedURLQueryItems) + + } + + func testCustomArrayDecode() { + + let expectedQueryCustomArray = QueryCustomArray(dateField: [expectedCustomArrayDate,expectedCustomArrayDate,expectedCustomArrayDate]) + + guard let query = try? QueryDecoder(dictionary: expectedCustomArrayDict).decode(QueryCustomArray.self) else { + XCTFail("Failed to decode query to QueryCustomArray Object") + return + } + + XCTAssertEqual(query, expectedQueryCustomArray) + + guard let dataQuery = try? QueryDecoder().decode(QueryCustomArray.self, from: expectedDataCustomArray.self) else { + XCTFail("Failed to decode query to QueryCustomArray Object") + return + } + + XCTAssertEqual(dataQuery, expectedQueryCustomArray) + } + + func testCustomArrayEncode() { + + let query = QueryCustomArray(dateField: [expectedCustomArrayDate,expectedCustomArrayDate,expectedCustomArrayDate]) + + guard let customArrayQueryDict: [String: String] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to [String: String]") + return + } + + XCTAssertEqual(customArrayQueryDict["dateField"], "10650,10650,10650") + + guard let customArrayQueryStr: String = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + func createDict(_ str: String) -> [String: String] { + return customArrayQueryStr.components(separatedBy: "&").reduce([String: String]()) { acc, val in + var acc = acc + let split = val.components(separatedBy: "=") + acc[split[0]] = split[1] + return acc + } + } + + let customArrayQueryStrSplit1: [String: String] = createDict(customArrayQueryStr) + let customArrayQueryStrSplit2: [String: String] = createDict(expectedCustomArrayString) + + XCTAssertEqual(customArrayQueryStrSplit1["dateField"], customArrayQueryStrSplit2["dateField"]) + + guard let customURLQueryItems: [URLQueryItem] = try? QueryEncoder().encode(query) else { + XCTFail("Failed to encode query to String") + return + } + + let queryItems = [ URLQueryItem(name: "dateField", value: "10650,10650,10650")] + XCTAssertEqual(queryItems, customURLQueryItems) + + } + + //This tests the first code example in the QueryParams struct + func testExample1() { + struct MyQuery: QueryParams { + let date: Date + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .iso8601 + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .iso8601 + } + + let queryParams = ["date": "2019-09-06T10:14:41+0000"] + + XCTAssertNoThrow(try QueryDecoder(dictionary: queryParams).decode(MyQuery.self)) + } + + //This tests the second code example in the QueryParams struct + func testExample2() { + do { + struct MyQuery: QueryParams { + let date: Date + static let dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .iso8601 + static let dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .iso8601 + } + + let query = MyQuery(date: Date(timeIntervalSinceNow: 0)) + + let myQueryDict: [String: String] = try QueryEncoder().encode(query) + XCTAssertNotNil(myQueryDict["date"]) + } catch { + XCTFail("\(error)") + } + + } + + func testCycle() { let myInts = MyInts(intField: 1, int8Field: 2, int16Field: 3, int32Field: 4, int64Field: 5, uintField: 6, uint8Field: 7, uint16Field: 8, uint32Field: 9, uint64Field: 10) let myIntArrays = MyIntArrays(intField: [1,2,3], @@ -385,4 +895,5 @@ class QueryCoderTests: XCTestCase { XCTAssertEqual(myQuery2, obj) } + } diff --git a/Tests/KituraContractsTests/StringExtensionTests.swift b/Tests/KituraContractsTests/StringExtensionTests.swift index 9397d66..43c9138 100644 --- a/Tests/KituraContractsTests/StringExtensionTests.swift +++ b/Tests/KituraContractsTests/StringExtensionTests.swift @@ -36,6 +36,18 @@ class StringExtensionsTests: XCTestCase { let d2 = fm.string(from: Date()) let d3 = fm.string(from: Date()) + let dDeferred1 = 589714729.0 + let dDeferred2 = 589714729.0 + let dDeferred3 = 589714729.0 + + let dSeventy1 = 1567684372.0 + let dSeventy2 = 1567684372.0 + let dSeventy3 = 1567684372.0 + + let dISO1 = "2019-09-06T10:14:41+0000" + let dISO2 = "2019-09-06T10:14:41+0000" + let dISO3 = "2019-09-06T10:14:41+0000" + /// Assert object string -> T conversion XCTAssertEqual("string".string, "string") XCTAssertEqual("1".int, Int(1)) @@ -73,5 +85,11 @@ class StringExtensionsTests: XCTestCase { XCTAssertEqual(pointIntArray.floatArray!, [Float(1.0), Float(2.0), Float(3.0)]) XCTAssertEqual("true,false,true".booleanArray!, [true, false, true]) XCTAssertEqual("\(d1),\(d2),\(d3)".dateArray(fm)!, [fm.date(from: d1)!, fm.date(from: d2)!, fm.date(from: d3)!]) + XCTAssertEqual("\(dSeventy1),\(dSeventy2),\(dSeventy3)".dateArray(decoderStrategy: .secondsSince1970)!, [Date(timeIntervalSince1970: dSeventy1), Date(timeIntervalSince1970: dSeventy2), Date(timeIntervalSince1970: dSeventy3)]) + XCTAssertEqual("\(dSeventy1),\(dSeventy2),\(dSeventy3)".dateArray(decoderStrategy: .millisecondsSince1970)!, [Date(timeIntervalSince1970: dSeventy1/1000), Date(timeIntervalSince1970: dSeventy2/1000), Date(timeIntervalSince1970: dSeventy3/1000)]) + if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { + XCTAssertEqual("\(dISO1),\(dISO2),\(dISO3)".dateArray(decoderStrategy: .iso8601)!, [_iso8601Formatter.date(from: dISO1)!, _iso8601Formatter.date(from: dISO2)!, _iso8601Formatter.date(from: dISO3)!]) + } + XCTAssertEqual("\(dDeferred1),\(dDeferred2),\(dDeferred3)".dateArray(decoderStrategy: .deferredToDate)!, [Date(timeIntervalSinceReferenceDate: dDeferred1), Date(timeIntervalSinceReferenceDate: dDeferred2), Date(timeIntervalSinceReferenceDate: dDeferred3)]) } }