Skip to content

Commit d7771dc

Browse files
committed
Refactor Kakapo to not be json centric
This is a draft PR to address #153, I want some feedback before proceeding with updating the documentation, README, tests The idea is that Router now doesn’t work anymore w/ `Serializable` objects and `ResponseFieldsProvider` is not anymore a special `Serializable`. Router now only accept `ResponseFieldsProvider`s an object that provides: statusCode (Int) headerfields (dict) body (data) `Serializable` became a `ResponseFieldsProvider` that by default has a status code == 200 and “Content-Type” == “application/json”. It should have been like that since ever, so we can use Kakapo with images, xml or whatever we want by creating new `ResponseFieldsProvider ` The change is a breaking change but not a big one for users of Kakapo (only objects that were confirming to ResponseFieldsProvider will be broken)
1 parent 393a9e5 commit d7771dc

8 files changed

+65
-95
lines changed

Source/Router.swift

+20-18
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import Foundation
1414
By default, though, the Router will return a 200 status code and `["Content-Type": "application/json"]` header fields when only returning a Serializable object.
1515
In order to customize that behavior, check `ResponseFieldsProvider` to provide custom status code and header fields.
1616
*/
17-
public typealias RouteHandler = (Request) -> Serializable?
17+
public typealias RouteHandler = (Request) -> ResponseFieldsProvider?
1818

1919
/**
2020
A Request struct used in `RouteHandlers` to provide valid requests.
@@ -43,7 +43,7 @@ public struct Response: ResponseFieldsProvider {
4343
public let statusCode: Int
4444

4545
/// The Serializable body object
46-
public let body: Serializable
46+
public let body: Data?
4747

4848
/// An optional dictionary holding the response header fields
4949
public let headerFields: [String : String]?
@@ -57,7 +57,7 @@ public struct Response: ResponseFieldsProvider {
5757

5858
- returns: A wrapper `Serializable` object that affect http requests.
5959
*/
60-
public init(statusCode: Int, body: Serializable, headerFields: [String : String]? = nil) {
60+
public init(statusCode: Int, body: Data?, headerFields: [String : String]? = nil) {
6161
self.statusCode = statusCode
6262
self.body = body
6363
self.headerFields = headerFields
@@ -170,10 +170,8 @@ public final class Router {
170170
func startLoading(_ server: Server) {
171171
guard let requestURL = server.request.url,
172172
let client = server.client else { return }
173-
174-
var statusCode = 200
175-
var headerFields: [String : String]? = ["Content-Type": "application/json"]
176-
var serializableObject: Serializable?
173+
174+
var responseFieldsProvider: ResponseFieldsProvider?
177175

178176
for (key, handler) in routes where key.method.rawValue == server.request.httpMethod {
179177
if let info = matchRoute(baseURL, path: key.path, requestURL: requestURL) {
@@ -182,24 +180,28 @@ public final class Router {
182180
let dataBody = server.request.httpBody ?? URLProtocol.property(forKey: "kkp_requestHTTPBody", in: server.request) as? Data
183181

184182
let request = Request(components: info.components, queryParameters: info.queryParameters, httpBody: dataBody, httpHeaders: server.request.allHTTPHeaderFields)
185-
serializableObject = handler(request)
183+
responseFieldsProvider = handler(request)
186184
break
187185
}
188186
}
189-
190-
if let serializableObject = serializableObject as? ResponseFieldsProvider {
191-
statusCode = serializableObject.statusCode
192-
headerFields = serializableObject.headerFields
193-
}
194-
195-
if let response = HTTPURLResponse(url: requestURL, statusCode: statusCode, httpVersion: "HTTP/1.1", headerFields: headerFields) {
187+
188+
// In case we don't have a statusCode and headerFields
189+
// we fallback on // https://tools.ietf.org/html/rfc2324
190+
let response = HTTPURLResponse(
191+
url: requestURL,
192+
statusCode: responseFieldsProvider?.statusCode ?? 418,
193+
httpVersion: "HTTP/1.1",
194+
headerFields: responseFieldsProvider?.headerFields ?? ["Content-Type": "application/coffee-pot-command"]
195+
)
196+
197+
if let response = response {
196198
client.urlProtocol(server, didReceive: response, cacheStoragePolicy: .allowedInMemoryOnly)
197199
}
198-
199-
if let data = serializableObject?.toData() {
200+
201+
if let data = responseFieldsProvider?.body {
200202
client.urlProtocol(server, didLoad: data)
201203
}
202-
204+
203205
let didFinishLoading: (URLProtocol) -> Void = { (server) in
204206
client.urlProtocolDidFinishLoading(server)
205207
}

Source/Serialization/JSONAPIError.swift

+6-13
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import Foundation
1010

1111
/// A convenience error object that conform to JSON API
12-
public struct JSONAPIError: ResponseFieldsProvider {
12+
public struct JSONAPIError: CustomSerializable {
1313

1414
/// An object containing references to the source of the error, optionally including any of the following members
1515
public struct Source: Serializable {
@@ -78,29 +78,22 @@ public struct JSONAPIError: ResponseFieldsProvider {
7878
public var statusCode: Int {
7979
return builder.status
8080
}
81-
82-
/// A `JSONAPIError.Builder` instance contains all the fields.
83-
public var body: Serializable {
84-
return builder
85-
}
86-
87-
/// The headerFields that will be returned by the HTTP response.
88-
public let headerFields: [String : String]?
8981

9082
/**
9183
Initialize a `JSONAPIError` and build it with `JSONAPIError.Builder`
9284

9385
- parameter statusCode: The status code of the response, will be used also to provide a statusCode for your request
94-
- parameter headerFields: The headerFields that will be returned by the HTTP response.
9586
- 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`**)
9687

9788
- returns: An error that conforms to JSON API specifications and it's ready to be serialized
9889
*/
99-
public init(statusCode: Int, headerFields: [String: String]? = nil, errorBuilder: (_ error: Builder) -> Void) {
90+
public init(statusCode: Int, errorBuilder: (_ error: Builder) -> Void) {
10091
let builder = Builder(statusCode: statusCode)
10192
errorBuilder(builder)
10293
self.builder = builder
103-
self.headerFields = headerFields
10494
}
105-
95+
96+
public func customSerialized(transformingKeys keyTransformer: KeyTransformer?) -> Any? {
97+
return builder.serialized()
98+
}
10699
}

Source/Serialization/ResponseFieldsProvider.swift

+2-10
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,13 @@ import Foundation
1313
For example you may use `Response` to wrap your `Serializable` object to just achieve the result or directly implement the protocol.
1414
For example `JSONAPIError` implement the protocol in order to be able to provide custom status code in the response.
1515
*/
16-
public protocol ResponseFieldsProvider: CustomSerializable {
16+
public protocol ResponseFieldsProvider {
1717
/// The response status code
1818
var statusCode: Int { get }
1919

2020
/// The Serializable body object
21-
var body: Serializable { get }
21+
var body: Data? { get }
2222

2323
/// An optional dictionary holding the response header fields
2424
var headerFields: [String : String]? { get }
2525
}
26-
27-
extension ResponseFieldsProvider {
28-
29-
/// The default implementation just return the serialized body.
30-
public func customSerialized(transformingKeys keyTransformer: KeyTransformer?) -> Any? {
31-
return body.serialized(transformingKeys: keyTransformer)
32-
}
33-
}

Source/Serialization/Serializer.swift

+23-15
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,32 @@ import Foundation
1111
/**
1212
* 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.
1313
*/
14-
public protocol Serializable {
14+
public protocol Serializable: ResponseFieldsProvider {
1515
// empty protocol, marks that the object should be Mirrored to be serialized.
1616
}
1717

18+
extension Serializable {
19+
public var statusCode: Int {
20+
return 200
21+
}
22+
23+
public var headerFields: [String : String]? {
24+
return ["Content-Type": "application/json"]
25+
}
26+
27+
/**
28+
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.
29+
*/
30+
public var body: Data? {
31+
guard let object = serialized() else { return nil }
32+
33+
if !JSONSerialization.isValidJSONObject(object) {
34+
return nil
35+
}
36+
return try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted)
37+
}
38+
}
39+
1840
/**
1941
* 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.
2042
*/
@@ -44,20 +66,6 @@ public extension Serializable {
4466
return serialize(self, keyTransformer: keyTransformer)
4567
}
4668

47-
/**
48-
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.
49-
50-
- returns: The serialized object as `Data`
51-
*/
52-
func toData() -> Data? {
53-
guard let object = serialized() else { return nil }
54-
55-
if !JSONSerialization.isValidJSONObject(object) {
56-
return nil
57-
}
58-
return try? JSONSerialization.data(withJSONObject: object, options: .prettyPrinted)
59-
}
60-
6169
fileprivate func serializeObject(_ value: Any, keyTransformer: KeyTransformer?) -> Any? {
6270
if let value = value as? Serializable {
6371
return value.serialized(transformingKeys: keyTransformer)

Tests/JSONAPIErrorTests.swift

+1-23
Original file line numberDiff line numberDiff line change
@@ -80,31 +80,9 @@ class JSONAPIErrorsSpec: QuickSpec {
8080
let response = response as! HTTPURLResponse
8181
statusCode = response.statusCode
8282
}.resume()
83-
// still the only test randomly failing for no reasons...
84-
// 99,9 % is not a router problem (otherwise wouldn't be the only one)
85-
// https://pbs.twimg.com/media/CfSQdwUW8AErog1.jpg
83+
8684
expect(statusCode).toEventually(equal(403), timeout: 2)
8785
}
88-
89-
it("should affect the header fields of the response") {
90-
let router = Router.register("http://www.test1234.com")
91-
92-
router.get("/users") { _ in
93-
return JSONAPIError(statusCode: 404, headerFields: ["foo": "bar"]) { (error) in
94-
error.title = "test"
95-
}
96-
}
97-
98-
var foo: String?
99-
let url = URL(string: "http://www.test1234.com/users")!
100-
URLSession.shared.dataTask(with: url) { (_, response, _) in
101-
let response = response as! HTTPURLResponse
102-
let headers = response.allHeaderFields as? [String: String]
103-
foo = headers?["foo"]
104-
}.resume()
105-
106-
expect(foo).toEventually(equal("bar"))
107-
}
10886
}
10987
}
11088
}

Tests/RouterTests.swift

+12-7
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,16 @@ import Nimble
1515

1616
struct CustomResponse: ResponseFieldsProvider {
1717
let statusCode: Int
18-
let body: Serializable
18+
let wrapped: Serializable
1919
let headerFields: [String : String]?
20+
21+
var body: Data? {
22+
return wrapped.body
23+
}
2024

21-
init(statusCode: Int, body: Serializable, headerFields: [String : String]? = nil) {
25+
init(statusCode: Int, wrapped: Serializable, headerFields: [String : String]? = nil) {
2226
self.statusCode = statusCode
23-
self.body = body
27+
self.wrapped = wrapped
2428
self.headerFields = headerFields
2529
}
2630
}
@@ -548,7 +552,8 @@ class RouterTests: QuickSpec {
548552
var responseDictionary: [String: AnyObject]?
549553

550554
router.get("/users/:id") { request in
551-
return Response(statusCode: 200, body: store.find(User.self, id: request.components["id"]!)!)
555+
let user = store.find(User.self, id: request.components["id"]!)
556+
return Response(statusCode: 200, body: user.body)
552557
}
553558

554559
URLSession.shared.dataTask(with: URL(string: "http://www.test.com/users/2")!) { (data, response, _) in
@@ -568,7 +573,7 @@ class RouterTests: QuickSpec {
568573

569574
router.get("/users/:id") { _ in
570575
// Optional.some("none") -> not valid JSON object
571-
return Response(statusCode: 400, body: Optional.some("none"))
576+
return Response(statusCode: 400, body: Optional.some("none").body)
572577
}
573578

574579
URLSession.shared.dataTask(with: URL(string: "http://www.test.com/users/2")!) { (data, response, _) in
@@ -585,7 +590,7 @@ class RouterTests: QuickSpec {
585590
var allHeaders: [String : String]? = nil
586591

587592
router.get("/users/:id") { _ in
588-
let body = ["id": "foo", "type": "User"]
593+
let body = ["id": "foo", "type": "User"].body
589594
let headerFields = ["access_token": "094850348502", "user_id": "124"]
590595
return Response(statusCode: 400, body: body, headerFields: headerFields)
591596
}
@@ -606,7 +611,7 @@ class RouterTests: QuickSpec {
606611
var statusCode: Int? = nil
607612

608613
router.get("/users/:id") { _ in
609-
return CustomResponse(statusCode: 400, body: ["id": 2], headerFields: ["access_token": "094850348502"])
614+
return CustomResponse(statusCode: 400, wrapped: ["id": 2], headerFields: ["access_token": "094850348502"])
610615
}
611616

612617
URLSession.shared.dataTask(with: URL(string: "http://www.test.com/users/2")!) { (data, response, _) in

Tests/SerializationTransformerTests.swift

-8
Original file line numberDiff line numberDiff line change
@@ -145,14 +145,6 @@ class SerializationTransformerSpec: QuickSpec {
145145
}
146146
}
147147

148-
context("ResponseFieldsProvider") {
149-
it("should transform the keys") {
150-
let object = Response(statusCode: 200, body: friend)
151-
let serialized = UppercaseTransformer(wrapped: object).serialized() as! [String: AnyObject]
152-
expect(serialized["FRIENDS"]).toNot(beNil())
153-
}
154-
}
155-
156148
context("Array") {
157149
it("should transform the keys") {
158150
let object = [friend]

Tests/SerializerTests.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ class SerializeSpec: QuickSpec {
156156
it("produces nil data and serialized object when nil") {
157157
let nilInt: Int? = nil
158158
expect(nilInt.serialized()).to(beNil())
159-
expect(nilInt.toData()).to(beNil())
159+
expect(nilInt.body).to(beNil())
160160
}
161161

162162
it("serialize an optional") {

0 commit comments

Comments
 (0)