diff --git a/Source/Router.swift b/Source/Router.swift index 3e53ff4..22a05ad 100644 --- a/Source/Router.swift +++ b/Source/Router.swift @@ -14,7 +14,7 @@ import Foundation By default, though, the Router will return a 200 status code and `["Content-Type": "application/json"]` header fields when only returning a Serializable object. In order to customize that behavior, check `ResponseFieldsProvider` to provide custom status code and header fields. */ -public typealias RouteHandler = (Request) -> Serializable? +public typealias RouteHandler = (Request) -> ResponseFieldsProvider? /** A Request struct used in `RouteHandlers` to provide valid requests. @@ -43,7 +43,7 @@ public struct Response: ResponseFieldsProvider { public let statusCode: Int /// The Serializable body object - public let body: Serializable + public let body: Data? /// An optional dictionary holding the response header fields public let headerFields: [String : String]? @@ -57,7 +57,7 @@ public struct Response: ResponseFieldsProvider { - returns: A wrapper `Serializable` object that affect http requests. */ - public init(statusCode: Int, body: Serializable, headerFields: [String : String]? = nil) { + public init(statusCode: Int, body: Data?, headerFields: [String : String]? = nil) { self.statusCode = statusCode self.body = body self.headerFields = headerFields @@ -170,10 +170,8 @@ public final class Router { func startLoading(_ server: Server) { guard let requestURL = server.request.url, let client = server.client else { return } - - var statusCode = 200 - var headerFields: [String : String]? = ["Content-Type": "application/json"] - var serializableObject: Serializable? + + var responseFieldsProvider: ResponseFieldsProvider? for (key, handler) in routes where key.method.rawValue == server.request.httpMethod { if let info = matchRoute(baseURL, path: key.path, requestURL: requestURL) { @@ -182,24 +180,28 @@ public final class Router { let dataBody = server.request.httpBody ?? URLProtocol.property(forKey: "kkp_requestHTTPBody", in: server.request) as? Data let request = Request(components: info.components, queryParameters: info.queryParameters, httpBody: dataBody, httpHeaders: server.request.allHTTPHeaderFields) - serializableObject = handler(request) + responseFieldsProvider = handler(request) break } } - - if let serializableObject = serializableObject as? ResponseFieldsProvider { - statusCode = serializableObject.statusCode - headerFields = serializableObject.headerFields - } - - if let response = HTTPURLResponse(url: requestURL, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headerFields) { + + // In case we don't have a statusCode and headerFields + // we fallback on // https://tools.ietf.org/html/rfc2324 + let response = HTTPURLResponse( + url: requestURL, + statusCode: responseFieldsProvider?.statusCode ?? 418, + httpVersion: "HTTP/1.1", + headerFields: responseFieldsProvider?.headerFields ?? ["Content-Type": "application/coffee-pot-command"] + ) + + if let response = response { client.urlProtocol(server, didReceive: response, cacheStoragePolicy: .allowedInMemoryOnly) } - - if let data = serializableObject?.toData() { + + if let data = responseFieldsProvider?.body { client.urlProtocol(server, didLoad: data) } - + let didFinishLoading: (URLProtocol) -> Void = { (server) in client.urlProtocolDidFinishLoading(server) } diff --git a/Source/Serialization/JSONAPIError.swift b/Source/Serialization/JSONAPIError.swift index a052032..ccd4cf9 100644 --- a/Source/Serialization/JSONAPIError.swift +++ b/Source/Serialization/JSONAPIError.swift @@ -9,7 +9,7 @@ import Foundation /// A convenience error object that conform to JSON API -public struct JSONAPIError: ResponseFieldsProvider { +public struct JSONAPIError: CustomSerializable { /// An object containing references to the source of the error, optionally including any of the following members public struct Source: Serializable { @@ -78,29 +78,22 @@ public struct JSONAPIError: ResponseFieldsProvider { public var statusCode: Int { return builder.status } - - /// A `JSONAPIError.Builder` instance contains all the fields. - public var body: Serializable { - return builder - } - - /// The headerFields that will be returned by the HTTP response. - public let headerFields: [String : String]? /** Initialize a `JSONAPIError` and build it with `JSONAPIError.Builder` - parameter statusCode: The status code of the response, will be used also to provide a statusCode for your request - - parameter headerFields: The headerFields that will be returned by the HTTP response. - parameter errorBuilder: A builder that can be used to fill the error objects, it contains all you need to provide an error object confiorming to JSON API (**see `JSONAPIError.Builder`**) - returns: An error that conforms to JSON API specifications and it's ready to be serialized */ - public init(statusCode: Int, headerFields: [String: String]? = nil, errorBuilder: (_ error: Builder) -> Void) { + public init(statusCode: Int, errorBuilder: (_ error: Builder) -> Void) { let builder = Builder(statusCode: statusCode) errorBuilder(builder) self.builder = builder - self.headerFields = headerFields } - + + public func customSerialized(transformingKeys keyTransformer: KeyTransformer?) -> Any? { + return builder.serialized() + } } diff --git a/Source/Serialization/ResponseFieldsProvider.swift b/Source/Serialization/ResponseFieldsProvider.swift index 2fdcbfc..b540938 100644 --- a/Source/Serialization/ResponseFieldsProvider.swift +++ b/Source/Serialization/ResponseFieldsProvider.swift @@ -13,21 +13,13 @@ import Foundation For example you may use `Response` to wrap your `Serializable` object to just achieve the result or directly implement the protocol. For example `JSONAPIError` implement the protocol in order to be able to provide custom status code in the response. */ -public protocol ResponseFieldsProvider: CustomSerializable { +public protocol ResponseFieldsProvider { /// The response status code var statusCode: Int { get } /// The Serializable body object - var body: Serializable { get } + var body: Data? { get } /// An optional dictionary holding the response header fields var headerFields: [String : String]? { get } } - -extension ResponseFieldsProvider { - - /// The default implementation just return the serialized body. - public func customSerialized(transformingKeys keyTransformer: KeyTransformer?) -> Any? { - return body.serialized(transformingKeys: keyTransformer) - } -} diff --git a/Source/Serialization/Serializer.swift b/Source/Serialization/Serializer.swift index 14f135f..b39f678 100644 --- a/Source/Serialization/Serializer.swift +++ b/Source/Serialization/Serializer.swift @@ -11,10 +11,32 @@ import Foundation /** * A protocol to serialize types into JSON representations, the object will be Mirrored to be serialized. Use `CustomReflectable` if you need different behaviors or use `CustomSerializable`if it's not a valid option. */ -public protocol Serializable { +public protocol Serializable: ResponseFieldsProvider { // empty protocol, marks that the object should be Mirrored to be serialized. } +extension Serializable { + public var statusCode: Int { + return 200 + } + + public var headerFields: [String : String]? { + return ["Content-Type": "application/json"] + } + + /** + Serialize a `Serializable` object and convert the serialized object to `Data`. Unless it is nil the return value is representing a JSON. Usually you don't need to use this method directly since `Router` will automatically serialize objects when needed. + */ + public var body: Data? { + guard let object = serialized() else { return nil } + + if !JSONSerialization.isValidJSONObject(object) { + return nil + } + return try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted) + } +} + /** * Conforming to `CustomSerializable` the object won't be Mirrored to be serialized, use it in case `CustomReflectable` is not a viable option. Array for example use this to return an Array with its serialized objects inside. */ @@ -44,20 +66,6 @@ public extension Serializable { return serialize(self, keyTransformer: keyTransformer) } - /** - Serialize a `Serializable` object and convert the serialized object to `Data`. Unless it is nil the return value is representing a JSON. Usually you don't need to use this method directly since `Router` will automatically serialize objects when needed. - - - returns: The serialized object as `Data` - */ - func toData() -> Data? { - guard let object = serialized() else { return nil } - - if !JSONSerialization.isValidJSONObject(object) { - return nil - } - return try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted) - } - fileprivate func serializeObject(_ value: Any, keyTransformer: KeyTransformer?) -> Any? { if let value = value as? Serializable { return value.serialized(transformingKeys: keyTransformer) diff --git a/Tests/JSONAPIErrorTests.swift b/Tests/JSONAPIErrorTests.swift index 12b78a5..9d8fa78 100644 --- a/Tests/JSONAPIErrorTests.swift +++ b/Tests/JSONAPIErrorTests.swift @@ -80,31 +80,9 @@ class JSONAPIErrorsSpec: QuickSpec { let response = response as! HTTPURLResponse statusCode = response.statusCode }.resume() - // still the only test randomly failing for no reasons... - // 99,9 % is not a router problem (otherwise wouldn't be the only one) - // https://pbs.twimg.com/media/CfSQdwUW8AErog1.jpg + expect(statusCode).toEventually(equal(403), timeout: 2) } - - it("should affect the header fields of the response") { - let router = Router.register("http://www.test1234.com") - - router.get("/users") { _ in - return JSONAPIError(statusCode: 404, headerFields: ["foo": "bar"]) { (error) in - error.title = "test" - } - } - - var foo: String? - let url = URL(string: "http://www.test1234.com/users")! - URLSession.shared.dataTask(with: url) { (_, response, _) in - let response = response as! HTTPURLResponse - let headers = response.allHeaderFields as? [String: String] - foo = headers?["foo"] - }.resume() - - expect(foo).toEventually(equal("bar")) - } } } } diff --git a/Tests/RouterTests.swift b/Tests/RouterTests.swift index 4cae74b..7232997 100644 --- a/Tests/RouterTests.swift +++ b/Tests/RouterTests.swift @@ -15,12 +15,16 @@ import Nimble struct CustomResponse: ResponseFieldsProvider { let statusCode: Int - let body: Serializable + let wrapped: Serializable let headerFields: [String : String]? + + var body: Data? { + return wrapped.body + } - init(statusCode: Int, body: Serializable, headerFields: [String : String]? = nil) { + init(statusCode: Int, wrapped: Serializable, headerFields: [String : String]? = nil) { self.statusCode = statusCode - self.body = body + self.wrapped = wrapped self.headerFields = headerFields } } @@ -548,7 +552,8 @@ class RouterTests: QuickSpec { var responseDictionary: [String: AnyObject]? router.get("/users/:id") { request in - return Response(statusCode: 200, body: store.find(User.self, id: request.components["id"]!)!) + let user = store.find(User.self, id: request.components["id"]!) + return Response(statusCode: 200, body: user.body) } URLSession.shared.dataTask(with: URL(string: "http://www.test.com/users/2")!) { (data, response, _) in @@ -568,7 +573,7 @@ class RouterTests: QuickSpec { router.get("/users/:id") { _ in // Optional.some("none") -> not valid JSON object - return Response(statusCode: 400, body: Optional.some("none")) + return Response(statusCode: 400, body: Optional.some("none").body) } URLSession.shared.dataTask(with: URL(string: "http://www.test.com/users/2")!) { (data, response, _) in @@ -585,7 +590,7 @@ class RouterTests: QuickSpec { var allHeaders: [String : String]? = nil router.get("/users/:id") { _ in - let body = ["id": "foo", "type": "User"] + let body = ["id": "foo", "type": "User"].body let headerFields = ["access_token": "094850348502", "user_id": "124"] return Response(statusCode: 400, body: body, headerFields: headerFields) } @@ -606,7 +611,7 @@ class RouterTests: QuickSpec { var statusCode: Int? = nil router.get("/users/:id") { _ in - return CustomResponse(statusCode: 400, body: ["id": 2], headerFields: ["access_token": "094850348502"]) + return CustomResponse(statusCode: 400, wrapped: ["id": 2], headerFields: ["access_token": "094850348502"]) } URLSession.shared.dataTask(with: URL(string: "http://www.test.com/users/2")!) { (data, response, _) in diff --git a/Tests/SerializationTransformerTests.swift b/Tests/SerializationTransformerTests.swift index 9e8fc04..c66caae 100644 --- a/Tests/SerializationTransformerTests.swift +++ b/Tests/SerializationTransformerTests.swift @@ -145,14 +145,6 @@ class SerializationTransformerSpec: QuickSpec { } } - context("ResponseFieldsProvider") { - it("should transform the keys") { - let object = Response(statusCode: 200, body: friend) - let serialized = UppercaseTransformer(wrapped: object).serialized() as! [String: AnyObject] - expect(serialized["FRIENDS"]).toNot(beNil()) - } - } - context("Array") { it("should transform the keys") { let object = [friend] diff --git a/Tests/SerializerTests.swift b/Tests/SerializerTests.swift index 762f17a..1bce9e9 100644 --- a/Tests/SerializerTests.swift +++ b/Tests/SerializerTests.swift @@ -156,7 +156,7 @@ class SerializeSpec: QuickSpec { it("produces nil data and serialized object when nil") { let nilInt: Int? = nil expect(nilInt.serialized()).to(beNil()) - expect(nilInt.toData()).to(beNil()) + expect(nilInt.body).to(beNil()) } it("serialize an optional") {