feat: add route handling for urlencoded forms
MODIFY
Sources/Kitura/CodableRouter.swift
Sources/Kitura/CodableRouter.swift
@@ -420,12 +420,27 @@ public struct CodableHelpers {
* - Returns: True if the content type of the request is application/json, false otherwise
*/
public static func isContentTypeJSON(_ request: RouterRequest) -> Bool {
+ // FIXME: This should be a simple lookup of content type cached on the RouterRequest
guard let contentType = request.headers["Content-Type"] else {
return false
}
return contentType.hasPrefix("application/json")
}
+ /**
+ * Check if the given request has content type x-www-form-urlencoded
+ *
+ * - Parameter request: The RouterRequest to check
+ * - Returns: True if the content type of the request is application/x-www-form-urlencoded, false otherwise
+ */
+ public static func isContentTypeURLEncoded(_ request: RouterRequest) -> Bool {
+ // FIXME: This should be a simple lookup of content type cached on the RouterRequest
+ guard let contentType = request.headers["Content-Type"] else {
+ return false
+ }
+ return contentType.hasPrefix("application/x-www-form-urlencoded")
+ }
+
/**
* Get the HTTPStatusCode corresponding to the provided RequestError
*
@@ -616,7 +631,7 @@ public struct CodableHelpers {
* - Returns: An instance of `InputType` representing the decoded body data.
*/
public static func readCodableOrSetResponseStatus<InputType: Codable>(_ inputCodableType: InputType.Type, from request: RouterRequest, response: RouterResponse) -> InputType? {
- guard CodableHelpers.isContentTypeJSON(request) else {
+ guard CodableHelpers.isContentTypeJSON(request) || CodableHelpers.isContentTypeURLEncoded(request) else {
response.status(.unsupportedMediaType)
return nil
}
@@ -633,6 +648,7 @@ public struct CodableHelpers {
return nil
}
}
+
/**
* Read an id from the request URL, setting an error status on the given response in the case of failure.
*

MODIFY
Sources/Kitura/RouterRequest.swift
Sources/Kitura/RouterRequest.swift
@@ -19,6 +19,7 @@ import Socket
import LoggerAPI
import Foundation
+import KituraContracts
// MARK: RouterRequest
@@ -172,14 +173,23 @@ public class RouterRequest {
return try serverRequest.read(into: &data)
}
- /// Read the body of the request as a Codable object.
- ///
- /// - Parameter type: Codable object to which the body of the request will be converted.
+ /// Read the body of the request as a Codable object. It can decode JSON or URLEncoded forms.
+ /// It chooses the decoder by looking up the Content-Type header.
+ /// If there is no header it defaults to JSONDecoder.
+ /// - Parameter as: Codable object to which the body of the request will be converted.
/// - Throws: Socket.Error if an error occurred while reading from a socket.
/// - Throws: `DecodingError.dataCorrupted` if values requested from the payload are corrupted, or if the given data is not valid JSON.
/// - Throws: An error if any value throws an error during decoding.
/// - Returns: The instantiated Codable object
public func read<T: Decodable>(as type: T.Type) throws -> T {
+ // FIXME: RouterRequest should cache the content type, so that this is just a lookup
+ if CodableHelpers.isContentTypeURLEncoded(self) {
+ let body = try self.readString()
+ guard let urlKeyValuePairs = body?.urlDecodedFieldValuePairs else {
+ throw Error.failedToParseRequestBody(body: body ?? "Failed to read body as String")
+ }
+ return try QueryDecoder(dictionary: urlKeyValuePairs).decode(type)
+ }
var data = Data()
_ = try serverRequest.read(into: &data)
return try JSONDecoder().decode(type, from: data)

MODIFY
Tests/KituraTests/KituraTestBuilder.swift
Tests/KituraTests/KituraTestBuilder.swift
@@ -18,6 +18,7 @@ import XCTest
import Kitura
@testable import KituraNet
+@testable import KituraContracts
import Foundation
import Dispatch
@@ -42,6 +43,7 @@ import Dispatch
protocol RequestTestBuilder {
func request(_ method: String, path: String) -> AssertionTestBuilder
func request<T: Encodable>(_ method: String, path: String, data: T) -> AssertionTestBuilder
+ func request(_ method: String, path: String, urlEncodedString: String) -> AssertionTestBuilder
func run()
}
@@ -84,6 +86,16 @@ class ServerTestBuilder: RequestTestBuilder, AssertionTestBuilder {
})
}
}
+
+ init(_ test: KituraTest, _ method: String, _ path: String, _ urlEncodedString: String) {
+ self.test = test
+ self.invoker = { callback in
+ test.performRequest(method, path: path, callback: callback, requestModifier: { request in
+ request.headers["Content-Type"] = "application/x-www-form-urlencoded"
+ request.write(from: urlEncodedString)
+ })
+ }
+ }
}
let test: KituraTest
let router: ServerDelegate
@@ -111,6 +123,11 @@ class ServerTestBuilder: RequestTestBuilder, AssertionTestBuilder {
return self
}
+ public func request(_ method: String, path: String, urlEncodedString: String) -> AssertionTestBuilder {
+ requests.append(Request(test, method, path, urlEncodedString))
+ return self
+ }
+
public func has(callback: @escaping (ClientResponse) -> Void) -> Self {
currentRequest?.assertions.append(callback)
return self

MODIFY
Tests/KituraTests/TestCodableRouter.swift
Tests/KituraTests/TestCodableRouter.swift
@@ -19,7 +19,6 @@ import Foundation
import KituraContracts
@testable import Kitura
-@testable import KituraNet
class TestCodableRouter: KituraTest {
static var allTests: [(String, (TestCodableRouter) -> () throws -> Void)] {
@@ -146,6 +145,10 @@ class TestCodableRouter: KituraTest {
print("POST on /bodyerror/users for user \(user)")
respondWith(nil, RequestError(.conflict, body: Conflict(on: "id")))
}
+ router.post("/urlencoded") { (user: User, respondWith: (User?, RequestError?) -> Void) in
+ print("POST on /urlencoded for user \(user)")
+ respondWith(user, nil)
+ }
let user = User(id: 4, name: "David")
buildServerTest(router, timeout: 30)
@@ -163,6 +166,26 @@ class TestCodableRouter: KituraTest {
.hasContentType(withPrefix: "application/json")
.hasData(Conflict(on: "id"))
+ .request("post", path: "/urlencoded", urlEncodedString: "id=4&name=David")
+ .hasStatus(.created)
+ .hasContentType(withPrefix: "application/json")
+ .hasData(user)
+
+ .request("post", path: "/urlencoded", urlEncodedString: "id=4&name=David&extra=yes")
+ .hasStatus(.created)
+ .hasContentType(withPrefix: "application/json")
+ .hasData(user)
+
+ .request("post", path: "/urlencoded", urlEncodedString: "encoding=valid&failed=match")
+ .hasStatus(.unprocessableEntity)
+ .hasNoData()
+
+ .request("post", path: "/urlencoded", urlEncodedString: "invalidEncoding")
+ .hasStatus(.unprocessableEntity)
+ .hasNoData()
+
+
+
.run()
}

feat: add route handling for urlencoded forms (#1228)