diff --git a/Kakapo.xcodeproj/project.pbxproj b/Kakapo.xcodeproj/project.pbxproj index a084a1c..e009767 100644 --- a/Kakapo.xcodeproj/project.pbxproj +++ b/Kakapo.xcodeproj/project.pbxproj @@ -81,6 +81,13 @@ DE76E1CD1D0DC857009721A4 /* XCTestCase+CustomAssertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE76E1B71D0DC857009721A4 /* XCTestCase+CustomAssertions.swift */; }; DE76E1CE1D0DC857009721A4 /* XCTestCase+CustomAssertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE76E1B71D0DC857009721A4 /* XCTestCase+CustomAssertions.swift */; }; DE76E1CF1D0DC857009721A4 /* XCTestCase+CustomAssertions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE76E1B71D0DC857009721A4 /* XCTestCase+CustomAssertions.swift */; }; + DE82027A1D20068A00552FEC /* JSONAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8202791D20068A00552FEC /* JSONAPIError.swift */; }; + DE82027B1D20068A00552FEC /* JSONAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8202791D20068A00552FEC /* JSONAPIError.swift */; }; + DE82027C1D20068A00552FEC /* JSONAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8202791D20068A00552FEC /* JSONAPIError.swift */; }; + DE82027D1D20068A00552FEC /* JSONAPIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE8202791D20068A00552FEC /* JSONAPIError.swift */; }; + DE82027F1D200C9000552FEC /* JSONAPIErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE82027E1D200C9000552FEC /* JSONAPIErrorTests.swift */; }; + DE8202801D200C9000552FEC /* JSONAPIErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE82027E1D200C9000552FEC /* JSONAPIErrorTests.swift */; }; + DE8202811D200C9000552FEC /* JSONAPIErrorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE82027E1D200C9000552FEC /* JSONAPIErrorTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -141,6 +148,8 @@ DE76E1B51D0DC857009721A4 /* SerializerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SerializerTests.swift; sourceTree = ""; }; DE76E1B61D0DC857009721A4 /* URLDecomposerTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLDecomposerTests.swift; sourceTree = ""; }; DE76E1B71D0DC857009721A4 /* XCTestCase+CustomAssertions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "XCTestCase+CustomAssertions.swift"; sourceTree = ""; }; + DE8202791D20068A00552FEC /* JSONAPIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONAPIError.swift; sourceTree = ""; }; + DE82027E1D200C9000552FEC /* JSONAPIErrorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSONAPIErrorTests.swift; sourceTree = ""; }; DEBD3C741D16BC9A004E0A23 /* README.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; path = README.playground; sourceTree = ""; }; DFC29110721D31BE30199824 /* Pods-Kakapo iOSTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Kakapo iOSTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Kakapo iOSTests/Pods-Kakapo iOSTests.debug.xcconfig"; sourceTree = ""; }; E357918ACA29146B64834E63 /* Pods-Kakapo tvOSTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Kakapo tvOSTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-Kakapo tvOSTests/Pods-Kakapo tvOSTests.release.xcconfig"; sourceTree = ""; }; @@ -273,6 +282,7 @@ DE76E0FE1D0DC38B009721A4 /* Info.plist */, 8BE0FC7E1D16BD8300FE706A /* JSONAPILinksTests.swift */, DE76E1B11D0DC857009721A4 /* JSONAPITests.swift */, + DE82027E1D200C9000552FEC /* JSONAPIErrorTests.swift */, DE76E1B21D0DC857009721A4 /* KakapoDBTests.swift */, DE76E1B31D0DC857009721A4 /* PropertyPolicyTests.swift */, DE76E1B41D0DC857009721A4 /* RouterTests.swift */, @@ -286,6 +296,7 @@ DE76E1001D0DC395009721A4 /* Source */ = { isa = PBXGroup; children = ( + DE8202791D20068A00552FEC /* JSONAPIError.swift */, 8BE0FC831D172EC900FE706A /* JSONAPILinks.swift */, DE76E1011D0DC395009721A4 /* JSONAPISerializer.swift */, DE76E1021D0DC395009721A4 /* KakapoDB.swift */, @@ -732,6 +743,7 @@ DE76E19C1D0DC755009721A4 /* Serializer.swift in Sources */, DE76E1961D0DC755009721A4 /* JSONAPISerializer.swift in Sources */, DE76E19B1D0DC755009721A4 /* Router.swift in Sources */, + DE82027B1D20068A00552FEC /* JSONAPIError.swift in Sources */, DE76E1991D0DC755009721A4 /* NSURLRequest+FixCopy.swift in Sources */, DE76E1A71D0DC762009721A4 /* CustomAssertions.swift in Sources */, ); @@ -749,6 +761,7 @@ DE76E1941D0DC755009721A4 /* Serializer.swift in Sources */, DE76E18E1D0DC755009721A4 /* JSONAPISerializer.swift in Sources */, DE76E1931D0DC755009721A4 /* Router.swift in Sources */, + DE82027C1D20068A00552FEC /* JSONAPIError.swift in Sources */, DE76E1911D0DC755009721A4 /* NSURLRequest+FixCopy.swift in Sources */, DE76E1A81D0DC762009721A4 /* CustomAssertions.swift in Sources */, ); @@ -764,6 +777,7 @@ DE76E1BF1D0DC857009721A4 /* KakapoDBTests.swift in Sources */, DE76E1C81D0DC857009721A4 /* SerializerTests.swift in Sources */, DE76E1C51D0DC857009721A4 /* RouterTests.swift in Sources */, + DE8202801D200C9000552FEC /* JSONAPIErrorTests.swift in Sources */, DE76E1CE1D0DC857009721A4 /* XCTestCase+CustomAssertions.swift in Sources */, DE76E1C21D0DC857009721A4 /* PropertyPolicyTests.swift in Sources */, ); @@ -781,6 +795,7 @@ DE76E18C1D0DC754009721A4 /* Serializer.swift in Sources */, DE76E1861D0DC754009721A4 /* JSONAPISerializer.swift in Sources */, DE76E18B1D0DC754009721A4 /* Router.swift in Sources */, + DE82027D1D20068A00552FEC /* JSONAPIError.swift in Sources */, DE76E1891D0DC754009721A4 /* NSURLRequest+FixCopy.swift in Sources */, DE76E1A91D0DC763009721A4 /* CustomAssertions.swift in Sources */, ); @@ -796,6 +811,7 @@ DE76E1C01D0DC857009721A4 /* KakapoDBTests.swift in Sources */, DE76E1C91D0DC857009721A4 /* SerializerTests.swift in Sources */, DE76E1C61D0DC857009721A4 /* RouterTests.swift in Sources */, + DE8202811D200C9000552FEC /* JSONAPIErrorTests.swift in Sources */, DE76E1CF1D0DC857009721A4 /* XCTestCase+CustomAssertions.swift in Sources */, DE76E1C31D0DC857009721A4 /* PropertyPolicyTests.swift in Sources */, ); @@ -813,6 +829,7 @@ DE76E1A41D0DC756009721A4 /* Serializer.swift in Sources */, DE76E19E1D0DC756009721A4 /* JSONAPISerializer.swift in Sources */, DE76E1A31D0DC756009721A4 /* Router.swift in Sources */, + DE82027A1D20068A00552FEC /* JSONAPIError.swift in Sources */, DE76E1A11D0DC756009721A4 /* NSURLRequest+FixCopy.swift in Sources */, DE76E1A61D0DC761009721A4 /* CustomAssertions.swift in Sources */, ); @@ -828,6 +845,7 @@ DE76E1BE1D0DC857009721A4 /* KakapoDBTests.swift in Sources */, DE76E1C71D0DC857009721A4 /* SerializerTests.swift in Sources */, DE76E1C41D0DC857009721A4 /* RouterTests.swift in Sources */, + DE82027F1D200C9000552FEC /* JSONAPIErrorTests.swift in Sources */, DE76E1CD1D0DC857009721A4 /* XCTestCase+CustomAssertions.swift in Sources */, DE76E1C11D0DC857009721A4 /* PropertyPolicyTests.swift in Sources */, ); diff --git a/Source/JSONAPIError.swift b/Source/JSONAPIError.swift new file mode 100644 index 0000000..2de328c --- /dev/null +++ b/Source/JSONAPIError.swift @@ -0,0 +1,90 @@ +// +// JSONAPIError.swift +// Kakapo +// +// Created by Alex Manzella on 26/06/16. +// Copyright © 2016 devlucky. All rights reserved. +// + +import Foundation + +// A convenince error object that conform to JSON API +public struct JSONAPIError: ResponseFieldsProvider { + + /// An object containing references to the source of the error, optionally including any of the following members + public struct Source: Serializable { + /// A JSON `Pointer` ([RFC6901](https://tools.ietf.org/html/rfc6901)) to the associated entity in the request document [e.g. `/data` for a primary data object, or `/data/attributes/title` for a specific attribute]. + public let pointer: String? + + /// A string indicating which URI query parameter caused the error. + public let parameter: String? + } + + /// A builder for JSONAPIError + public struct Builder: Serializable { + + /// A unique identifier for this particular occurrence of the problem. + public var id: String? + + /// A link object that leads to further details about this particular occurrence of the problem. + public var about: JSONAPILink? + + /// The HTTP status code applicable to this problem, expressed as a string value. + public var status: Int + + /// An application-specific error code, expressed as a string value + public var code: String? + + /// A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization + public var title: String? + + /// A human-readable explanation specific to this occurrence of the problem. Like title, this field’s value can be localized. + public var detail: String? + + /** + An object containing references to the source of the error, optionally including any of the following members: + + - pointer: a JSON `Pointer` ([RFC6901](https://tools.ietf.org/html/rfc6901)) to the associated entity in the request document [e.g. `/data` for a primary data object, or `/data/attributes/title` for a specific attribute]. + - parameter: a string indicating which URI query parameter caused the error. + */ + public var source: Source? + + /// A meta object containing non-standard meta-information about the error. + public var meta: Serializable? + + private init(statusCode: Int) { + status = statusCode + } + } + + private let builder: Builder + + // MARK: ResponseFieldsProvider + + public var statusCode: Int { + return builder.status + } + + public var body: Serializable { + return builder + } + + public var headerFields: [String : String]? { + return nil + } + + /** + 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 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, errorBuilder: (error: inout Builder) -> ()) { + var builder = Builder(statusCode: statusCode) + errorBuilder(error: &builder) + self.builder = builder + } + +} \ No newline at end of file diff --git a/Tests/JSONAPIErrorTests.swift b/Tests/JSONAPIErrorTests.swift new file mode 100644 index 0000000..0587796 --- /dev/null +++ b/Tests/JSONAPIErrorTests.swift @@ -0,0 +1,76 @@ +// +// JSONAPIErrorTests.swift +// Kakapo +// +// Created by Alex Manzella on 26/06/16. +// Copyright © 2016 devlucky. All rights reserved. +// + +import Foundation + +import Quick +import Nimble +import SwiftyJSON +@testable import Kakapo + +class JSONAPIErrorsSpec: QuickSpec { + + private struct ErrorDescription: Serializable { + let description: String + } + + override func spec() { + + func json(object: Serializable) -> JSON { + return JSON(object.serialize()!) + } + + describe("JSON API errors") { + + it("should serialize errors") { + let error = JSONAPIError(statusCode: 404) { (error) in + error.title = "test" + } + let object = json(error) + expect(object.count).to(equal(2)) + expect(object["status"]).to(equal(404)) + expect(object["title"]).to(equal("test")) + } + + it("should serialize members of the error") { + let error = JSONAPIError(statusCode: 404) { (error) in + error.source = JSONAPIError.Source(pointer: "ptr", parameter: "param") + error.meta = ErrorDescription(description: "test") + } + + let object = json(error) + expect(object.count).to(equal(3)) + + let source = object["source"].dictionaryValue + expect(source["pointer"]).to(equal("ptr")) + expect(source["parameter"]).to(equal("param")) + + let meta = object["meta"].dictionaryValue + expect(meta["description"]).to(equal("test")) + } + + it("should affect the status code of the request") { + let router = Router.register("http://www.test.com") + + router.get("/users"){ request in + return JSONAPIError(statusCode: 501) { (error) in + error.title = "test" + } + } + + var statusCode: Int? = nil + NSURLSession.sharedSession().dataTaskWithURL(NSURL(string: "http://www.test.com/users")!) { (data, response, _) in + let response = response as! NSHTTPURLResponse + statusCode = response.statusCode + }.resume() + + expect(statusCode).toEventually(equal(501)) + } + } + } +}