diff --git a/Sources/KituraContracts/CodableQuery/QueryDecoder.swift b/Sources/KituraContracts/CodableQuery/QueryDecoder.swift index 1666adc..f234d66 100644 --- a/Sources/KituraContracts/CodableQuery/QueryDecoder.swift +++ b/Sources/KituraContracts/CodableQuery/QueryDecoder.swift @@ -146,6 +146,33 @@ public class QueryDecoder: Coder, Decoder { return try decodeType(fieldValue?.string, to: T.self) case is [String].Type: return try decodeType(fieldValue?.stringArray, to: T.self) + case is Operation.Type: + if let oType = type as? Operation.Type, + let value = fieldValue?.string { + let result = try oType.init(string: value) + if let castedValue = result as? T { + return castedValue + } + } + return try decodeType(fieldValue?.decodable(T.self), to: T.self) + case is Ordering.Type: + if let oType = type as? Ordering.Type, + let value = fieldValue?.string { + let result = try oType.init(string: value) + if let castedValue = result as? T { + return castedValue + } + } + return try decodeType(fieldValue?.decodable(T.self), to: T.self) + case is Pagination.Type: + if let oType = type as? Pagination.Type, + let value = fieldValue?.string { + let result = try oType.init(string: value) + if let castedValue = result as? T { + return castedValue + } + } + return try decodeType(fieldValue?.decodable(T.self), to: T.self) default: Log.verbose("Decoding Custom Type: \(T.Type.self)") if fieldName.isEmpty { @@ -219,7 +246,7 @@ public class QueryDecoder: Coder, Decoder { func contains(_ key: Key) -> Bool { return decoder.dictionary[key.stringValue] != nil } - + func decode(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable { self.decoder.codingPath.append(key) defer { self.decoder.codingPath.removeLast() } diff --git a/Sources/KituraContracts/CodableQuery/QueryEncoder.swift b/Sources/KituraContracts/CodableQuery/QueryEncoder.swift index 4a111a2..4d4c4c1 100644 --- a/Sources/KituraContracts/CodableQuery/QueryEncoder.swift +++ b/Sources/KituraContracts/CodableQuery/QueryEncoder.swift @@ -42,7 +42,9 @@ public class QueryEncoder: Coder, Encoder { /** A `[String: String]` dictionary. */ - private var dictionary: [String: String] + internal var dictionary: [String: String] + + internal var anyDictionary: [String: Any] /** The coding key path. @@ -64,6 +66,7 @@ public class QueryEncoder: Coder, Encoder { */ public override init() { self.dictionary = [:] + self.anyDictionary = [:] super.init() } @@ -124,110 +127,18 @@ public class QueryEncoder: Coder, Encoder { ```` */ public func encode(_ value: T) throws -> [String : String] { - let fieldName = Coder.getFieldName(from: codingPath) - - Log.verbose("fieldName: \(fieldName), fieldValue: \(value)") - - switch value { - /// Ints - case let fieldValue as Int: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as Int8: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as Int16: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as Int32: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as Int64: - self.dictionary[fieldName] = String(fieldValue) - /// Int Arrays - case let fieldValue as [Int]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - case let fieldValue as [Int8]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - case let fieldValue as [Int16]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - case let fieldValue as [Int32]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - case let fieldValue as [Int64]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - /// UInts - case let fieldValue as UInt: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as UInt8: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as UInt16: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as UInt32: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as UInt64: - self.dictionary[fieldName] = String(fieldValue) - /// UInt Arrays - case let fieldValue as [UInt]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - case let fieldValue as [UInt8]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - case let fieldValue as [UInt16]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - case let fieldValue as [UInt32]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - case let fieldValue as [UInt64]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - /// Floats - case let fieldValue as Float: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as [Float]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - /// Doubles - case let fieldValue as Double: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as [Double]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - /// Bools - case let fieldValue as Bool: - self.dictionary[fieldName] = String(fieldValue) - case let fieldValue as [Bool]: - let strs: [String] = fieldValue.map { String($0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - /// Strings - case let fieldValue as String: - self.dictionary[fieldName] = fieldValue - case let fieldValue as [String]: - self.dictionary[fieldName] = fieldValue.joined(separator: ",") - /// Dates - case let fieldValue as Date: - self.dictionary[fieldName] = dateFormatter.string(from: fieldValue) - case let fieldValue as [Date]: - let strs: [String] = fieldValue.map { dateFormatter.string(from: $0) } - self.dictionary[fieldName] = strs.joined(separator: ",") - default: - if fieldName.isEmpty { - self.dictionary = [:] // Make encoder instance reusable - try value.encode(to: self) - } else { - do { - let jsonData = try JSONEncoder().encode(value) - self.dictionary[fieldName] = String(data: jsonData, encoding: .utf8) - } catch let error { - throw encodingError(value, underlyingError: error) - } - } - } + try value.encode(to: self) return self.dictionary } + /// Encodes an Encodable object to a String -> String dictionary + /// + /// - Parameter _ value: The Encodable object to encode to its [String: String] representation + public func encode(_ value: T) throws -> [String : Any] { + try value.encode(to: self) + return self.anyDictionary + } + /** Returns a keyed encoding container based on the key type. @@ -236,6 +147,7 @@ public class QueryEncoder: Coder, Encoder { encoder.container(keyedBy: keyType) ```` */ + public func container(keyedBy type: Key.Type) -> KeyedEncodingContainer where Key : CodingKey { return KeyedEncodingContainer(KeyedContainer(encoder: self)) } @@ -264,7 +176,7 @@ public class QueryEncoder: Coder, Encoder { return UnkeyedContanier(encoder: self) } - private func encodingError(_ value: Any, underlyingError: Swift.Error?) -> EncodingError { + internal func encodingError(_ value: Any, underlyingError: Swift.Error?) -> EncodingError { let fieldName = Coder.getFieldName(from: codingPath) let errorCtx = EncodingError.Context(codingPath: codingPath, debugDescription: "Could not process field named '\(fieldName)'.", underlyingError: underlyingError) return EncodingError.invalidValue(value, errorCtx) @@ -278,13 +190,158 @@ public class QueryEncoder: Coder, Encoder { func encode(_ value: T, forKey key: Key) throws where T : Encodable { self.encoder.codingPath.append(key) defer { self.encoder.codingPath.removeLast() } - let _: [String : String] = try encoder.encode(value) + 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) + } + } + } } - func encodeNil(forKey key: Key) throws { } + func encodeNil(forKey: Key) throws {} func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer where NestedKey : CodingKey { - return encoder.container(keyedBy: keyType) + return encoder.container(keyedBy: keyType) } func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { diff --git a/Sources/KituraContracts/Contracts.swift b/Sources/KituraContracts/Contracts.swift index 10db790..24f1f5e 100644 --- a/Sources/KituraContracts/Contracts.swift +++ b/Sources/KituraContracts/Contracts.swift @@ -24,7 +24,7 @@ and server side (e.g. Kitura) of the request (typically a HTTP REST request). ### Usage Example: ### - + In this example, the `RequestError` is used in a Kitura server Codable route handler to indicate the request has failed because the requested record was not found. ````swift @@ -57,7 +57,7 @@ public struct RequestError: RawRepresentable, Equatable, Hashable, Comparable, E // MARK: Creating a RequestError from a numeric code /** Creates an error representing the given error code. - + - parameter rawValue: An Int indicating an error code representing the type of error that has occurred. */ public init(rawValue: Int) { @@ -67,7 +67,7 @@ public struct RequestError: RawRepresentable, Equatable, Hashable, Comparable, E /** Creates an error representing the given error code and reason string. - + - parameter rawValue: An Int indicating an error code representing the type of error that has occurred. - parameter reason: A human-readable description of the error code. */ @@ -75,11 +75,11 @@ public struct RequestError: RawRepresentable, Equatable, Hashable, Comparable, E self.rawValue = rawValue self.reason = reason } - + /** Creates an error representing the given base error, with a custom response body given as a Codable. - + - parameter base: A `RequestError` object. - parameter body: A representation of the error body - an object representing further details of the failure. */ @@ -98,7 +98,7 @@ public struct RequestError: RawRepresentable, Equatable, Hashable, Comparable, E /** Creates an error respresenting the given base error, with a custom response body given as Data and a BodyFormat. - + - parameter base: A `RequestError` object. - parameter bodyData: A `Data` object. - parameter format: A `BodyFormat` object used to check whether it is legal JSON. @@ -114,7 +114,7 @@ public struct RequestError: RawRepresentable, Equatable, Hashable, Comparable, E default: throw UnsupportedBodyFormatError(format) } } - + // MARK: Accessing information about the error /** @@ -502,13 +502,30 @@ public extension RequestError { /** An identifier for a query parameter object. */ -public protocol QueryParams: Codable {} +public protocol QueryParams: Codable { +} + +/** + An error representing a failure to create an `Identifier`. + +### Usage Example: ### + + An `QueryParamsError.invalidValue` may be thrown if the given type cannot be constructed from the given string. + ````swift + throw QueryParamsError.invalidValue + ```` + */ +public enum QueryParamsError: Error { + /// Represents a failure to create a given filtering type from a given `String` representation. + case invalidValue + +} /** An error representing a failure to create an `Identifier`. ### Usage Example: ### - + An `IdentifierError.invalidValue` may be thrown if the given string cannot be converted to an integer when using an `Identifier`. ````swift throw IdentifierError.invalidValue @@ -528,7 +545,7 @@ public enum IdentifierError: Error { public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void ```` */ -public protocol Identifier { +public protocol Identifier: Codable { /// Creates an identifier from a given string value. /// - Throws: An IdentifierError.invalidValue if the given string is not a valid representation. init(value: String) throws @@ -539,7 +556,7 @@ public protocol Identifier { /** Extends `String` to comply to the `Identifier` protocol. - + ### Usage Example: ### ````swift // The Identifier used in the Id field could be a `String`. @@ -560,7 +577,7 @@ extension String: Identifier { /** Extends `Int` to comply to the `Identifier` protocol. - + ### Usage Example: ### ````swift // The Identifier used in the Id field could be an `Int`. @@ -584,6 +601,766 @@ extension Int: Identifier { } } +/** + Extends `Int8` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `Int8`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension Int8: Identifier { + /// Creates an integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an integer. + public init(value: String) throws { + if let id = Int8(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + Extends `Int16` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `Int16`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension Int16: Identifier { + /// Creates an integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an integer. + public init(value: String) throws { + if let id = Int16(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + Extends `Int32` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `Int32`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension Int32: Identifier { + /// Creates an integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an integer. + public init(value: String) throws { + if let id = Int32(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + Extends `Int64` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `Int64`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension Int64: Identifier { + /// Creates an integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an integer. + public init(value: String) throws { + if let id = Int64(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + Extends `UInt` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `UInt`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension UInt: Identifier { + /// Creates an unsigned integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an unsigned integer. + public init(value: String) throws { + if let id = UInt(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + Extends `UInt8` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `UInt8`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension UInt8: Identifier { + /// Creates an unsigned integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an unsigned integer. + public init(value: String) throws { + if let id = UInt8(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + Extends `UInt16` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `UInt16`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension UInt16: Identifier { + /// Creates an unsigned integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an unsigned integer. + public init(value: String) throws { + if let id = UInt16(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + Extends `UInt32` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `UInt32`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension UInt32: Identifier { + /// Creates an unsigned integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an unsigned integer. + public init(value: String) throws { + if let id = UInt32(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + Extends `UInt64` to comply to the `Identifier` protocol. + +### Usage Example: ### + ````swift + // The Identifier used in the Id field could be an `UInt64`. + public typealias IdentifierCodableClosure = (Id, I, @escaping CodableResultClosure) -> Void + ```` + */ +extension UInt64: Identifier { + /// Creates an unsigned integer identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to an unsigned integer. + public init(value: String) throws { + if let id = UInt64(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +extension Double: Identifier { + /// Creates a double identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to a Double. + public init(value: String) throws { + if let id = Double(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +extension Float: Identifier { + /// Creates a float identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to a Float. + public init(value: String) throws { + if let id = Float(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +extension Bool: Identifier { + /// Creates a bool identifier from a given string representation. + /// - Throws: An `IdentifierError.invalidValue` if the given string cannot be converted to a Bool. + public init(value: String) throws { + if let id = Bool(value) { + self = id + } else { + throw IdentifierError.invalidValue + } + } + + /// The string representation of the identifier. + public var value: String { + return String(describing: self) + } +} + +/** + An enum containing the ordering information + ### Usage Example: ### + To order ascending by name, we would write: + ```swift + Order.asc("name") + ``` +*/ + +public enum Order: Codable { + + /// Represents an ascending order with an associated String value + case asc(String) + /// Represents a descending order with an associated String value + case desc(String) + + // Coding Keys for encoding and decoding + enum CodingKeys: CodingKey { + case asc + case desc + } + + // Function to encode enum case + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .asc(let value): + try container.encode(value, forKey: .asc) + case .desc(let value): + try container.encode(value, forKey: .desc) + } + } + + // Function to decode enum case + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + do { + let ascValue = try container.decode(String.self, forKey: .asc) + self = .asc(ascValue) + } catch { + let descValue = try container.decode(String.self, forKey: .desc) + self = .desc(descValue) + } + } + + /// Description of the enum case + public var description: String { + switch self { + case let .asc(value): + return "asc(\(value))" + case let .desc(value): + return "desc(\(value))" + } + } + + /// Associated value of the enum case + public var value: String { + switch self { + case let .asc(value): + return value + case let .desc(value): + return value + } + } +} + +/** + A codable struct containing the ordering information + ### Usage Example: ### + To order ascending by name and descending by age, we would write: + ```swift + Ordering(by: .asc("name"), .desc("age")) + ``` +*/ +public struct Ordering: Codable { + /// Array of Orders + var order: [Order]! + + /// Creates an Ordering instance from one or more Orders + public init(by order: Order...) { + self.order = order + } + + /// Creates an Ordering instance from a given array of Orders. + public init(by order: [Order]) { + self.order = order + } + + /// Creates an Ordering instance from a given string value. + internal init(string value: String) throws { + if !value.contains(",") { + let extractedValue = try extractValue(value) + if value.contains("asc") { + self.order = [.asc(extractedValue)] + } else if value.contains("desc") { + self.order = [.desc(extractedValue)] + } else { + throw QueryParamsError.invalidValue + } + } else { + self.order = try value.split(separator: ",").map { String($0) }.map { + let extractedValue = try extractValue($0) + if $0.contains("asc") { + return .asc(extractedValue) + } else if $0.contains("desc") { + return .desc(extractedValue) + } else { + throw QueryParamsError.invalidValue + } + } + } + } + + // Function to extract the String value from the Order enum case + private func extractValue(_ value: String) throws -> String { + guard var startIndex = value.index(of: "("), + let endIndex = value.index(of: ")") else { + throw QueryParamsError.invalidValue + } + startIndex = value.index(startIndex, offsetBy: 1) + let extractedValue = value[startIndex.. String { + return self.order.map{ $0.description } .joined(separator: ",") + } + + /// Returns an array of Orders + public func getValues() -> [Order] { + return self.order + } +} + + +/** + A codable struct containing the pagination information + ### Usage Example: ### + To get only the first 10 values, we would write: + ```swift + Pagination(size: 10) + ``` + To get the 11th to 20th values, we would write: + ```swift + Pagination(start: 10, size: 10) + ``` +*/ +public struct Pagination: Codable { + private var start: Int + private var size: Int + + /// Creates a Pagination instance from start and size Int values + public init(start: Int = 0, size: Int) { + self.start = start + self.size = size + } + + internal init(string value: String) throws { + var array = value.split(separator: ",") + if array.count != 2 { + throw QueryParamsError.invalidValue + } + self.start = try Int(value: String(array[0])) + self.size = try Int(value: String(array[1])) + } + + internal func getStringValue() -> String { + return "\(start),\(size)" + } + + /// Returns a tuple containing the start and size Int values + public func getValues() -> (start: Int, size: Int) { + return (start, size) + } +} + +/** + An enum defining the available logical operators + ### Usage Example: ### + To use the OR Operator, we would write: + ```swift + Operator.or + ``` +*/ +public enum Operator: String, Codable { + /// OR Operator + case or + /// Equal Operator + case equal + /// LowerThan Operator + case lowerThan + /// LowerThanOrEqual Operator + case lowerThanOrEqual + /// GreaterThan Operator + case greaterThan + /// GreaterThanOrEqual Operator + case greaterThanOrEqual + /// ExclusiveRange Operator + case exclusiveRange + /// InclusiveRange Operator + case inclusiveRange +} + + +/** + An identifier for an operation object. +*/ +public protocol Operation: Codable { + init(string: String) throws + func getStringValue() -> String + func getOperator() -> Operator +} + + +/** + A codable struct enabling greater than filtering + ### Usage Example: ### + To filter with greaterThan on age which is an Integer, we would write: + ```swift + struct MyQuery: QueryParams { + let age: GreaterThan + } + let query = MyQuery(age: GreaterThan(value: 8)) + ``` +*/ +public struct GreaterThan: Operation { + private var value: I + private var `operator`: Operator = .greaterThan + + /// Creates a GreaterThan instance from a given Identifier value + public init(value: I) { + self.value = value + } + + /// Creates a GreaterThan instance from a given String value + public init(string value: String) throws { + self.value = try I(value: value) + } + + /// Returns the stored value + public func getValue() -> I { + return self.value + } + + /// Returns the stored value as a String + public func getStringValue() -> String { + return self.value.value + } + + /// Returns the Operator + public func getOperator() -> Operator { + return self.`operator` + } +} + +/** + A codable struct enabling greater than or equal filtering + ### Usage Example: ### + To filter with greater than or equal on age which is an Integer, we would write: + ```swift + struct MyQuery: QueryParams { + let age: GreaterThanOrEqual + } + let query = MyQuery(age: GreaterThanOrEqual(value: 8)) + ``` +*/ +public struct GreaterThanOrEqual: Operation { + private var value: I + private var `operator`: Operator = .greaterThanOrEqual + + /// Creates a GreaterThanOrEqual instance from a given Identifier value + public init(value: I) { + self.value = value + } + + /// Creates a GreaterThanOrEqual instance from a given String value + public init(string value: String) throws { + self.value = try I(value: value) + } + + /// Returns the stored value + public func getValue() -> I { + return self.value + } + + /// Returns the stored value as a String + public func getStringValue() -> String { + return self.value.value + } + + /// Returns the Operator + public func getOperator() -> Operator { + return self.`operator` + } +} + +/** + A codable struct enabling lower than filtering + ### Usage Example: ### + To filter with lower than on age, we would write: + ```swift + struct MyQuery: QueryParams { + let age: LowerThan + } + let query = MyQuery(age: LowerThan(value: 8)) + ``` +*/ +public struct LowerThan: Operation { + private var value: I + private var `operator`: Operator = .lowerThan + + /// Creates a LowerThan instance from a given Identifier value + public init(value: I) { + self.value = value + } + + /// Creates a LowerThan instance from a given String value + public init(string value: String) throws { + self.value = try I(value: value) + } + + /// Returns the stored value + public func getValue() -> I { + return self.value + } + + /// Returns the stored value as a String + public func getStringValue() -> String { + return String(describing: value) + } + + /// Returns the Operator + public func getOperator() -> Operator { + return self.`operator` + } +} + +/** + A codable struct enabling lower than or equal filtering + ### Usage Example: ### + To filter with lower than or equal on age, we would write: + ```swift + struct MyQuery: QueryParams { + let age: LowerThanOrEqual + } + let query = MyQuery(age: LowerThanOrEqual(value: 8)) + ``` +*/ +public struct LowerThanOrEqual: Operation { + private var value: I + private var `operator`: Operator = .lowerThanOrEqual + + /// Creates a LowerThan instance from a given Identifier value + public init(value: I) { + self.value = value + } + + /// Creates a LowerThan instance from a given String value + public init(string value: String) throws { + self.value = try I(value: value) + } + + /// Returns the stored value + public func getValue() -> I { + return self.value + } + + /// Returns the stored value as a String + public func getStringValue() -> String { + return String(describing: value) + } + + /// Returns the Operator + public func getOperator() -> Operator { + return self.`operator` + } +} + +/** + A codable struct enabling to filter with an inclusive range + ### Usage Example: ### + To filter on age using an inclusive range, we would write: + ```swift + struct MyQuery: QueryParams { + let age: InclusiveRange + } + let query = MyQuery(age: InclusiveRange(value: 8)) + ``` +*/ +public struct InclusiveRange: Operation { + private var start: I + private var end: I + private var `operator`: Operator = .inclusiveRange + + /// Creates a InclusiveRange instance from given start and end values + public init(start: I, end: I) { + self.start = start + self.end = end + } + + /// Creates a InclusiveRange instance from a given String value + public init(string value: String) throws { + var array = value.split(separator: ",") + if array.count != 2 { + throw QueryParamsError.invalidValue + } + self.start = try I(value: String(array[0])) + self.end = try I(value: String(array[1])) + } + + /// Returns the stored values as a tuple + public func getValue() -> (start: I, end: I) { + return (start: self.start, end: self.end) + } + + /// Returns the stored value as a String + public func getStringValue() -> String { + return "\(start),\(end)" + } + + /// Returns the Operator + public func getOperator() -> Operator { + return self.`operator` + } +} + +/** + A codable struct enabling to filter with an exlusive range + ### Usage Example: ### + To filter on age using an exclusive range, we would write: + ```swift + struct MyQuery: QueryParams { + let age: ExclusiveRange + } + let query = MyQuery(age: ExclusiveRange(value: 8)) + ``` +*/ +public struct ExclusiveRange: Operation { + private var start: I + private var end: I + private var `operator`: Operator = .exclusiveRange + + /// Creates a ExclusiveRange instance from given start and end values + public init(start: I, end: I) { + self.start = start + self.end = end + } + + /// Creates a ExclusiveRange instance from a given String value + public init(string value: String) throws { + var array = value.split(separator: ",") + if array.count != 2 { + throw QueryParamsError.invalidValue + } + self.start = try I(value: String(array[0])) + self.end = try I(value: String(array[1])) + } + + /// Returns the stored values as a tuple + public func getValue() -> (start: I, end: I) { + return (start: self.start, end: self.end) + } + + /// Returns the stored value as a String + public func getStringValue() -> String { + return "\(start),\(end)" + } + + /// Returns the Operator + public func getOperator() -> Operator { + return self.`operator` + } +} + //public protocol Persistable: Codable { // // Related types // associatedtype Id: Identifier diff --git a/Tests/KituraContractsTests/QueryCoderTests.swift b/Tests/KituraContractsTests/QueryCoderTests.swift index 4106790..2b3954e 100644 --- a/Tests/KituraContractsTests/QueryCoderTests.swift +++ b/Tests/KituraContractsTests/QueryCoderTests.swift @@ -119,6 +119,29 @@ class QueryCoderTests: XCTestCase { } } + struct MyFilters: QueryParams, Equatable { + public let greaterThan: GreaterThan + public let greaterThanOrEqual: GreaterThanOrEqual + public let lowerThan: LowerThan + public let lowerThanOrEqual: LowerThanOrEqual + public let inclusiveRange: InclusiveRange + public let exclusiveRange: ExclusiveRange + public let ordering: Ordering + public let pagination: Pagination + + public static func ==(lhs: MyFilters, rhs: MyFilters) -> Bool { + return lhs.greaterThan.getValue() == rhs.greaterThan.getValue() && + lhs.greaterThanOrEqual.getValue() == rhs.greaterThanOrEqual.getValue() && + lhs.lowerThan.getValue() == rhs.lowerThan.getValue() && + lhs.lowerThanOrEqual.getValue() == rhs.lowerThanOrEqual.getValue() && + lhs.inclusiveRange.getValue() == rhs.inclusiveRange.getValue() && + lhs.exclusiveRange.getValue() == rhs.exclusiveRange.getValue() && + lhs.ordering.getStringValue() == rhs.ordering.getStringValue() && + lhs.pagination.getStringValue() == rhs.pagination.getStringValue() + } + } + + let expectedDict = ["boolField": "true", "intField": "23", "stringField": "a string", "intArray": "1,2,3", "dateField": "2017-10-31T16:15:56+0000", "optionalDateField": "2017-10-31T16:15:56+0000", "nested": "{\"nestedIntField\":333,\"nestedStringField\":\"nested string\"}" ] let expectedQueryString = "?boolField=true&intArray=1%2C2%2C3&stringField=a%20string&intField=23&dateField=2017-12-07T21:42:06%2B0000&nested=%7B\"nestedStringField\":\"nested%20string\"%2C\"nestedIntField\":333%7D" @@ -135,6 +158,20 @@ class QueryCoderTests: XCTestCase { optionalDateField: Coder().dateFormatter.date(from: "2017-10-31T16:15:56+0000")!, nested: Nested(nestedIntField: 333, nestedStringField: "nested string")) + let expectedFiltersDict = ["greaterThan": "8", "greaterThanOrEqual": "10", "lowerThan": "7.0", "lowerThanOrEqual": "12.0", "inclusiveRange": "0,5", "exclusiveRange": "4,15", "ordering": "asc(name),desc(age)", "pagination": "8,14"] + let expectedQueryFiltersString = "?greaterThan=8&greaterThanOrEqual=10&lowerThan=7.0&lowerThanOrEqual=12.0&inclusiveRange=0,5&exclusiveRange=4,15&ordering=asc(name),desc(age)&pagination=8,14" + + let expectedFilterQuery = MyFilters( + greaterThan: GreaterThan(value: 8), + greaterThanOrEqual: GreaterThanOrEqual(value: 10), + lowerThan: LowerThan(value: 7.0), + lowerThanOrEqual: LowerThanOrEqual(value: 12.0), + inclusiveRange: InclusiveRange(start: 0, end: 5), + exclusiveRange: ExclusiveRange(start: 4, end: 15), + ordering: Ordering(by: .asc("name"), .desc("age")), + pagination: Pagination(start: 8, size: 14) + ) + func testQueryDecoder() { guard let query = try? QueryDecoder(dictionary: expectedDict).decode(MyQuery.self) else { XCTFail("Failed to decode query to MyQuery Object") @@ -143,6 +180,12 @@ class QueryCoderTests: XCTestCase { XCTAssertEqual(query, expectedMyQuery) + guard let filterQuery = try? QueryDecoder(dictionary: expectedFiltersDict).decode(MyFilters.self) else { + XCTFail("Failed to decode query to MyQuery Object") + return + } + + XCTAssertEqual(filterQuery, expectedFilterQuery) } func testQueryEncoder() { @@ -202,6 +245,48 @@ class QueryCoderTests: XCTestCase { XCTAssertEqual(myQueryStrSplit1["optionalDateField"], myQueryStrSplit2["optionalDateField"]) XCTAssertEqual(myQueryStrSplit1["nested"], myQueryStrSplit2["nested"]) + let filterQuery = MyFilters( + greaterThan: GreaterThan(value: 8), + greaterThanOrEqual: GreaterThanOrEqual(value: 10), + lowerThan: LowerThan(value: 7.0), + lowerThanOrEqual: LowerThanOrEqual(value: 12.0), + inclusiveRange: InclusiveRange(start: 0, end: 5), + exclusiveRange: ExclusiveRange(start: 4, end: 15), + ordering: Ordering(by: .asc("name"), .desc("age")), + pagination: Pagination(start: 8, size: 14) + ) + + guard let myFilterQueryDict: [String: String] = try? QueryEncoder().encode(filterQuery) else { + XCTFail("Failed to encode query to [String: String]") + return + } + + guard let myFilterQueryStr: String = try? QueryEncoder().encode(filterQuery) else { + XCTFail("Failed to encode query to String") + return + } + + XCTAssertEqual(myFilterQueryDict["greaterThan"], "8") + XCTAssertEqual(myFilterQueryDict["greaterThanOrEqual"], "10") + XCTAssertEqual(myFilterQueryDict["lowerThan"], "7.0") + XCTAssertEqual(myFilterQueryDict["lowerThanOrEqual"], "12.0") + XCTAssertEqual(myFilterQueryDict["inclusiveRange"], "0,5") + XCTAssertEqual(myFilterQueryDict["exclusiveRange"], "4,15") + XCTAssertEqual(myFilterQueryDict["ordering"], "asc(name),desc(age)") + XCTAssertEqual(myFilterQueryDict["pagination"], "8,14") + + let myFilterQueryStrSplit1: [String: String] = createDict(myFilterQueryStr) + let myFilterQueryStrSplit2: [String: String] = createDict(expectedQueryFiltersString) + + XCTAssertEqual(myFilterQueryStrSplit1["greaterThan"], myFilterQueryStrSplit2["greaterThan"]) + XCTAssertEqual(myFilterQueryStrSplit1["greaterThanOrEqual"], myFilterQueryStrSplit2["greaterThanOrEqual"]) + XCTAssertEqual(myFilterQueryStrSplit1["lowerThan"], myFilterQueryStrSplit2["lowerThan"]) + XCTAssertEqual(myFilterQueryStrSplit1["lowerThanOrEqual"], myFilterQueryStrSplit2["lowerThanOrEqual"]) + XCTAssertEqual(myFilterQueryStrSplit1["inclusiveRange"], myFilterQueryStrSplit2["inclusiveRange"]) + XCTAssertEqual(myFilterQueryStrSplit1["exlusiveRange"], myFilterQueryStrSplit2["exlusiveRange"]) + XCTAssertEqual(myFilterQueryStrSplit1["ordering"], myFilterQueryStrSplit2["ordering"]) + XCTAssertEqual(myFilterQueryStrSplit1["pagination"], myFilterQueryStrSplit2["pagination"]) + } func testCycle() {