Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 20 additions & 18 deletions Source/Router.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]?
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down
19 changes: 6 additions & 13 deletions Source/Serialization/JSONAPIError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
}
}
12 changes: 2 additions & 10 deletions Source/Serialization/ResponseFieldsProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
38 changes: 23 additions & 15 deletions Source/Serialization/Serializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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)
Expand Down
24 changes: 1 addition & 23 deletions Tests/JSONAPIErrorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
}
}
}
Expand Down
19 changes: 12 additions & 7 deletions Tests/RouterTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}
Expand All @@ -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
Expand Down
8 changes: 0 additions & 8 deletions Tests/SerializationTransformerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion Tests/SerializerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down