feat: Add codable identifier arrays
MODIFY
Sources/Kitura/CodableRouter.swift
Sources/Kitura/CodableRouter.swift
@@ -85,6 +85,25 @@ extension Router {
getSafely(route, handler: handler)
}
+ /**
+ Setup a IdentifierCodableArrayClosure on the provided route which will be invoked when a request comes to the server.
+ ### Usage Example: ###
+ ````
+ //User is a struct object that conforms to Codable
+ router.get("/users") { (respondWith: ([(Int, User)]?, RequestError?) -> Void) in
+
+ ...
+
+ respondWith([(Int, User)], nil)
+ }
+ ````
+ - Parameter route: A String specifying the pattern that needs to be matched, in order for the handler to be invoked.
+ - Parameter handler: A IdentifierCodableArrayClosure that gets invoked when a request comes to the server.
+ */
+ public func get<Id: Identifier, O: Codable>(_ route: String, handler: @escaping IdentifierCodableArrayClosure<Id, O>) {
+ getSafely(route, handler: handler)
+ }
+
/**
Setup a (QueryParams, CodableArrayResultClosure) -> Void on the provided route which will be invoked when a request comes to the server.
@@ -322,6 +341,14 @@ extension Router {
}
}
+ // Get array of (Id, Codable) tuples
+ fileprivate func getSafely<Id: Identifier, O: Codable>(_ route: String, handler: @escaping IdentifierCodableArrayClosure<Id, O>) {
+ get(route) { request, response, next in
+ Log.verbose("Received GET (plural with identifier) type-safe request")
+ handler(CodableHelpers.constructTupleArrayOutResultHandler(response: response, completion: next))
+ }
+ }
+
// Get w/Query Parameters
fileprivate func getSafely<Q: QueryParams, O: Codable>(_ route: String, handler: @escaping (Q, @escaping CodableArrayResultClosure<O>) -> Void) {
get(route) { request, response, next in
@@ -464,7 +491,7 @@ public struct CodableHelpers {
*
* - Parameter response: The `RouterResponse` to which the codable response error and
* status code will be written
- * - Parameter completion: The completion to be called after the when the returned
+ * - Parameter completion: The completion to be called after the returned
* closure completes execution.
* - Returns: The closure to pass to the codable route handler. The closure takes one argument
* `(RequestError?)`.
@@ -474,7 +501,7 @@ public struct CodableHelpers {
* If successful, the HTTP status code will be set to `HTTPStatusCode.noContent` and no
* body will be sent.
*
- * If failed, the HTTP status code used for the response wll be set to either the
+ * If failed, the HTTP status code used for the response will be set to either the
* `httpCode` of the `RequestError`, if that is a valid HTTP status code, or
* `HTTPStatusCode.unknown` otherwise. If the `RequestError` has a codable `body` then
* it will be encoded and sent as the body of the response.
@@ -510,18 +537,18 @@ public struct CodableHelpers {
* - Parameter successStatus: The `HTTPStatusCode` to use for a successful response (see below)
* - Parameter response: The `RouterResponse` to which the codable response body (or codable
* error) and status code will be written
- * - Parameter completion: The completion to be called after the when the returned
+ * - Parameter completion: The completion to be called after the returned
* closure completes execution.
* - Returns: The closure to pass to the codable route handler. The closure takes two arguments
* `(OutputType?, RequestError?)`.
* If the second (error) argument is `nil` then the first (body) argument should be non-`nil`
* and the response will be considered successful. If the second (error) argument is non-`nil`
- * then the first argument is ignore and the response is considered failed.
+ * then the first argument is ignored and the response is considered failed.
*
- * If successful, the HTTP status code will be set to `successStatus` and the first argument
- * will be encoded and sent as the body of the response.
+ * If successful, the HTTP status code will be set to `successStatus` and the `CodableResultClosure` output
+ * will be JSON encoded and sent as the body of the response.
*
- * If failed, the HTTP status code used for the response wll be set to either the
+ * If failed, the HTTP status code used for the response will be set to either the
* `httpCode` of the `RequestError`, if that is a valid HTTP status code, or
* `HTTPStatusCode.unknown` otherwise. If the `RequestError` has a codable `body` then
* it will be encoded and sent as the body of the response.
@@ -554,6 +581,62 @@ public struct CodableHelpers {
}
}
+ /**
+ * Create a closure that can be called by a codable route handler that
+ * provides an array of tuples of (Identifier, Codable) and an optional `RequestError`
+ *
+ * - Note: This function is intended for use by the codable router or extensions
+ * thereof. It will create a closure that can be passed to the registered
+ * route handler.
+ *
+ * - Parameter successStatus: The `HTTPStatusCode` to use for a successful response (see below)
+ * - Parameter response: The `RouterResponse` to which the codable response body (or codable
+ * error) and status code will be written
+ * - Parameter completion: The completion to be called after the returned
+ * closure completes execution.
+ * - Returns: The closure to pass to the codable route handler. The closure takes two arguments
+ * `([(Id, OutputType)]?, RequestError?)`.
+ * If the second (error) argument is `nil` then the first argument (body) should be non-`nil`
+ * and the response will be considered successful. If the second (error) argument is non-`nil`
+ * then the first argument is ignored and the response is considered failed.
+ *
+ * If successful, the HTTP status code will be set to `successStatus` and the `IdentifierCodableArrayResultClosure` output
+ * will be JSON encoded as an array of dictionaries, which is then sent as the body of the response.
+ *
+ * If failed, the HTTP status code used for the response will be set to either the
+ * `httpCode` of the `RequestError`, if that is a valid HTTP status code, or
+ * `HTTPStatusCode.unknown` otherwise. If the `RequestError` has a codable `body` then
+ * it will be encoded and sent as the body of the response.
+ */
+ public static func constructTupleArrayOutResultHandler<Id: Identifier, OutputType: Codable>(successStatus: HTTPStatusCode = .OK, response: RouterResponse, completion: @escaping () -> Void) -> IdentifierCodableArrayResultClosure<Id, OutputType> {
+ return { codableOutput, error in
+ if let error = error {
+ response.status(httpStatusCode(from: error))
+ do {
+ if let bodyData = try error.encodeBody(.json) {
+ response.headers.setType("json")
+ response.send(data: bodyData)
+ }
+ } catch {
+ Log.error("Could not encode error: \(error)")
+ response.status(.internalServerError)
+ }
+ } else {
+ do {
+ let entries = codableOutput?.map({ [$0.value: $1] })
+ let encoded = try JSONEncoder().encode(entries)
+ response.headers.setType("json")
+ response.send(data: encoded)
+ response.status(successStatus)
+ } catch {
+ Log.error("Could not encode result: \(error)")
+ response.status(.internalServerError)
+ }
+ }
+ completion()
+ }
+ }
+
/**
* Create a closure that can be called by a codable route handler that
* provides an optional `Identifier` id, optional `Codable` body and an optional `RequestError`
@@ -565,7 +648,7 @@ public struct CodableHelpers {
* - Parameter successStatus: The `HTTPStatusCode` to use for a successful response (see below)
* - Parameter response: The `RouterResponse` to which the id, codable response body (or codable
* error) and status code will be written
- * - Parameter completion: The completion to be called after the when the returned
+ * - Parameter completion: The completion to be called after the returned
* closure completes execution.
* - Returns: The closure to pass to the codable route handler. The closure takes three arguments
* `(IdType?, OutputType?, RequestError?)`.
@@ -574,11 +657,11 @@ public struct CodableHelpers {
* (error) argument is non-`nil` then the first and second arguments are ignored and the
* response is considered failed.
*
- * If successful, the HTTP status code will be set to `successStatus`, the first argument
- * will be encoded and sent as the body of the response, and the `Location` header of the
+ * If successful, the HTTP status code will be set to `successStatus`, the `IdentifierCodableResultClosure` output
+ * will be JSON encoded and sent as the body of the response, and the `Location` header of the
* response will be set to the id (by converting it to a `String` using its `value` property).
*
- * If failed, the HTTP status code used for the response wll be set to either the
+ * If failed, the HTTP status code used for the response will be set to either the
* `httpCode` of the `RequestError`, if that is a valid HTTP status code, or
* `HTTPStatusCode.unknown` otherwise. If the `RequestError` has a codable `body` then
* it will be encoded and sent as the body of the response.

MODIFY
Tests/KituraTests/KituraTestBuilder.swift
Tests/KituraTests/KituraTestBuilder.swift
@@ -57,6 +57,7 @@ protocol AssertionTestBuilder: RequestTestBuilder {
func hasData(_ expected: String) -> Self
func hasData<T: Decodable & Equatable>(_ expected: [T]) -> Self
func hasData<T: Decodable & Equatable>(_ expected: T) -> Self
+ func hasData<T: Decodable & Equatable>(_ expected: [[String: T]]) -> Self
}
// A builder object for constructing tests made up of one or more
@@ -238,6 +239,23 @@ class ServerTestBuilder: RequestTestBuilder, AssertionTestBuilder {
}
}
+ func hasData<T: Decodable & Equatable>(_ expected: [[String : T]]) -> Self {
+ return has { response in
+ guard let (_, data) = self.readDataOrFail(from: response) else { return }
+ do {
+ let actual = try JSONDecoder().decode([[String : T]].self, from: data)
+ for (index, tuple) in actual.enumerated() {
+ let tupleKey = Array(tuple.keys)[0]
+ let expectedKey = Array(expected[index].keys)[0]
+ XCTAssertEqual(tupleKey, expectedKey, "Response data does not match expected key:\nexpected: \(tupleKey)\nactual: \(expectedKey)")
+ XCTAssertEqual(tuple[tupleKey], expected[index][expectedKey], "Response data does not match expected value:\nexpected: \(String(describing: tuple[tupleKey]))\nactual: \(String(describing: expected[index][expectedKey]))")
+ }
+ } catch {
+ XCTFail("Failed to decode response data into type \(T.self): \(error)")
+ }
+ }
+ }
+
public func run() {
// Construct a list of async tasks that will perform each
// request in turn and test their associated assertions

MODIFY
Tests/KituraTests/TestCodableRouter.swift
Tests/KituraTests/TestCodableRouter.swift
@@ -28,6 +28,7 @@ class TestCodableRouter: KituraTest {
("testBasicGetSingleton", testBasicGetSingleton),
("testBasicGetArray", testBasicGetArray),
("testBasicGetSingle", testBasicGetSingle),
+ ("testBasicGetIdentifiersArray", testBasicGetIdentifiersArray),
("testBasicDelete", testBasicDelete),
("testBasicDeleteSingle", testBasicDeleteSingle),
("testBasicPut", testBasicPut),
@@ -287,6 +288,48 @@ class TestCodableRouter: KituraTest {
.run()
}
+ func testBasicGetIdentifiersArray() {
+ var intTuple = [(Int, User)]()
+ self.userStore.forEach { intTuple.append(($0.0, $0.1)) }
+ let expectedIntData: [[String: User]] = intTuple.map({ [$0.value: $1] })
+
+ var stringTuple = [(String, User)]()
+ self.userStore.forEach { stringTuple.append((String($0.0), $0.1)) }
+ let expectedStringData: [[String: User]] = stringTuple.map({ [$0.value: $1] })
+
+ router.get("/int/users") { (respondWith: ([(Int, User)]?, RequestError?) -> Void) in
+ print("GET on /int/users")
+ respondWith(intTuple, nil)
+ }
+
+ router.get("/string/users") { (respondWith: ([(String, User)]?, RequestError?) -> Void) in
+ print("GET on /string/users")
+ respondWith(stringTuple, nil)
+ }
+
+ router.get("/error/users") { (respondWith: ([(String, User)]?, RequestError?) -> Void) in
+ print("GET on /error/users")
+ respondWith(nil, .serviceUnavailable)
+ }
+
+ buildServerTest(router, timeout: 30)
+ .request("get", path: "/int/users")
+ .hasStatus(.OK)
+ .hasContentType(withPrefix: "application/json")
+ .hasData(expectedIntData)
+
+ .request("get", path: "/string/users")
+ .hasStatus(.OK)
+ .hasContentType(withPrefix: "application/json")
+ .hasData(expectedStringData)
+
+ .request("get", path: "/error/users")
+ .hasStatus(.serviceUnavailable)
+ .hasNoData()
+
+ .run()
+ }
+
func testBasicGetSingle() {
router.get("/users") { (id: Int, respondWith: (User?, RequestError?) -> Void) in
print("GET on /users/\(id)")

feat: Add codable identifier arrays (#1223)