diff --git a/README.md b/README.md index 4a29612..5f6d170 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ The primary goals of this framework are: ### Encoding #### Document -- [ ] `data` +- [x] `data` - [x] `included` -- [ ] `errors` +- [x] `errors` (untested) - [ ] `meta` - [ ] `jsonapi` - [ ] `links` @@ -78,7 +78,6 @@ The primary goals of this framework are: - [ ] Property-based testing (using `SwiftCheck`) - [ ] Roll my own `Result` or find an alternative that doesn't use `Foundation`. - [ ] Create more descriptive errors that are easier to use for troubleshooting. -- [ ] Add errors that check consistency from one part of a document to another (i.e. includes must be referenced by a relationship in the primary resource object). ## Usage ### Prerequisites diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index bbcc29b..4c5f873 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -12,13 +12,13 @@ /// API uses snake case, you will want to use /// a conversion such as the one offerred by the /// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy` -public struct JSONAPIDocument { +public struct JSONAPIDocument: Equatable { public let body: Body // public let meta: Meta? // public let jsonApi: APIDescription? // public let links: Links? - public enum Body { + public enum Body: Equatable { case errors([Error]) case data(primary: ResourceBody, included: Includes) @@ -34,7 +34,7 @@ public struct JSONAPIDocument.self, forKey: .included) + let errors = try container.decodeIfPresent([Error].self, forKey: .errors) - - assert(!(maybeData != nil && errors != nil), "JSON API Spec dictates data and errors will not both be present.") - assert((maybeIncludes == nil || maybeData != nil), "JSON API Spec dictates that includes will not be present if data is not present.") - - // TODO come back to this and make robust - + if let errors = errors { body = .errors(errors) return } + + let data = try container.decode(ResourceBody.self, forKey: .data) + let maybeIncludes = try container.decodeIfPresent(Includes.self, forKey: .included) - guard let data = maybeData else { - body = .errors([.unknown]) // TODO: this should be more descriptive - return - } + // TODO come back to this and make robust body = .data(primary: data, included: maybeIncludes ?? Includes.none) } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: RootCodingKeys.self) + + switch body { + case .errors(let errors): + var errContainer = container.nestedUnkeyedContainer(forKey: .errors) + + for error in errors { + try errContainer.encode(error) + } + + case .data(primary: let resourceBody, included: let includes): + try container.encode(resourceBody, forKey: .data) + + if Include.self != NoIncludes.self { + try container.encode(includes, forKey: .included) + } + } + } } diff --git a/Sources/JSONAPI/Document/Error.swift b/Sources/JSONAPI/Document/Error.swift index 18ed2c5..fdcb3a5 100644 --- a/Sources/JSONAPI/Document/Error.swift +++ b/Sources/JSONAPI/Document/Error.swift @@ -5,16 +5,21 @@ // Created by Mathew Polzin on 11/10/18. // -public protocol JSONAPIError: Swift.Error { +public protocol JSONAPIError: Swift.Error, Equatable, Codable { static var unknown: Self { get } } -public enum BasicJSONAPIError: JSONAPIError & Decodable { +public enum BasicJSONAPIError: JSONAPIError { case unknownError public init(from decoder: Decoder) throws { self = .unknown } + + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode("unknown") + } public static var unknown: BasicJSONAPIError { return .unknownError diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index a4bfd67..081e1e9 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -10,68 +10,91 @@ import JSONAPI class DocumentTests: XCTestCase { - func test_singleDocumentNoIncludes() { - let document = try? JSONDecoder().decode(JSONAPIDocument, Include0, BasicJSONAPIError>.self, from: single_document_no_includes) - - XCTAssertNotNil(document) + func test_singleDocumentNull() { + let document = decoded(type: JSONAPIDocument, Include0, BasicJSONAPIError>.self, + data: single_document_null) - guard let d = document else { return } + XCTAssertFalse(document.body.isError) + XCTAssertNotNil(document.body.data) + XCTAssertNil(document.body.data?.primary.value) + XCTAssertEqual(document.body.data?.included.count, 0) + } + + func test_singleDocumentNull_encode() { + test_DecodeEncodeEquality(type: JSONAPIDocument, Include0, BasicJSONAPIError>.self, + data: single_document_null) + } + + func test_singleDocumentNoIncludes() { + let document = decoded(type: JSONAPIDocument, Include0, BasicJSONAPIError>.self, + data: single_document_no_includes) - XCTAssertFalse(d.body.isError) - XCTAssertNotNil(d.body.data) - XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1") - XCTAssertEqual(d.body.data?.included.count, 0) + XCTAssertFalse(document.body.isError) + XCTAssertNotNil(document.body.data) + XCTAssertEqual(document.body.data?.0.value?.id.rawValue, "1") + XCTAssertEqual(document.body.data?.included.count, 0) + } + + func test_singleDocumentNoIncludes_encode() { + test_DecodeEncodeEquality(type: JSONAPIDocument, Include0, BasicJSONAPIError>.self, + data: single_document_no_includes) } func test_singleDocumentSomeIncludes() { - let document = try? JSONDecoder().decode(JSONAPIDocument, Include1, BasicJSONAPIError>.self, from: single_document_some_includes) - - XCTAssertNotNil(document) + let document = decoded(type: JSONAPIDocument, Include1, BasicJSONAPIError>.self, + data: single_document_some_includes) - guard let d = document else { return } + XCTAssertFalse(document.body.isError) + XCTAssertNotNil(document.body.data) + XCTAssertEqual(document.body.data?.0.value?.id.rawValue, "1") + XCTAssertEqual(document.body.data?.included.count, 1) + XCTAssertEqual(document.body.data?.included[Author.self].count, 1) + XCTAssertEqual(document.body.data?.included[Author.self][0].id.rawValue, "33") + } - XCTAssertFalse(d.body.isError) - XCTAssertNotNil(d.body.data) - XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1") - XCTAssertEqual(d.body.data?.included.count, 1) - XCTAssertEqual(d.body.data?.included[Author.self].count, 1) - XCTAssertEqual(d.body.data?.included[Author.self][0].id.rawValue, "33") + func test_singleDocumentSomeIncludes_encode() { + test_DecodeEncodeEquality(type: JSONAPIDocument, Include1, BasicJSONAPIError>.self, + data: single_document_some_includes) } func test_manyDocumentNoIncludes() { - let document = try? JSONDecoder().decode(JSONAPIDocument, Include0, BasicJSONAPIError>.self, from: many_document_no_includes) + let document = decoded(type: JSONAPIDocument, Include0, BasicJSONAPIError>.self, + data: many_document_no_includes) - XCTAssertNotNil(document) - - guard let d = document else { return } - - XCTAssertFalse(d.body.isError) - XCTAssertNotNil(d.body.data) - XCTAssertEqual(d.body.data?.0.values.count, 3) - XCTAssertEqual(d.body.data?.0.values[0].id.rawValue, "1") - XCTAssertEqual(d.body.data?.0.values[1].id.rawValue, "2") - XCTAssertEqual(d.body.data?.0.values[2].id.rawValue, "3") - XCTAssertEqual(d.body.data?.included.count, 0) + XCTAssertFalse(document.body.isError) + XCTAssertNotNil(document.body.data) + XCTAssertEqual(document.body.data?.0.values.count, 3) + XCTAssertEqual(document.body.data?.0.values[0].id.rawValue, "1") + XCTAssertEqual(document.body.data?.0.values[1].id.rawValue, "2") + XCTAssertEqual(document.body.data?.0.values[2].id.rawValue, "3") + XCTAssertEqual(document.body.data?.included.count, 0) + } + + func test_manyDocumentNoIncludes_encode() { + test_DecodeEncodeEquality(type: JSONAPIDocument, Include0, BasicJSONAPIError>.self, + data: many_document_no_includes) } func test_manyDocumentSomeIncludes() { - let document = try? JSONDecoder().decode(JSONAPIDocument, Include1, BasicJSONAPIError>.self, from: many_document_some_includes) - - XCTAssertNotNil(document) - - guard let d = document else { return } - - XCTAssertFalse(d.body.isError) - XCTAssertNotNil(d.body.data) - XCTAssertEqual(d.body.data?.0.values.count, 3) - XCTAssertEqual(d.body.data?.0.values[0].id.rawValue, "1") - XCTAssertEqual(d.body.data?.0.values[1].id.rawValue, "2") - XCTAssertEqual(d.body.data?.0.values[2].id.rawValue, "3") - XCTAssertEqual(d.body.data?.included.count, 3) - XCTAssertEqual(d.body.data?.included[Author.self].count, 3) - XCTAssertEqual(d.body.data?.included[Author.self][0].id.rawValue, "33") - XCTAssertEqual(d.body.data?.included[Author.self][1].id.rawValue, "22") - XCTAssertEqual(d.body.data?.included[Author.self][2].id.rawValue, "11") + let document = decoded(type: JSONAPIDocument, Include1, BasicJSONAPIError>.self, + data: many_document_some_includes) + + XCTAssertFalse(document.body.isError) + XCTAssertNotNil(document.body.data) + XCTAssertEqual(document.body.data?.0.values.count, 3) + XCTAssertEqual(document.body.data?.0.values[0].id.rawValue, "1") + XCTAssertEqual(document.body.data?.0.values[1].id.rawValue, "2") + XCTAssertEqual(document.body.data?.0.values[2].id.rawValue, "3") + XCTAssertEqual(document.body.data?.included.count, 3) + XCTAssertEqual(document.body.data?.included[Author.self].count, 3) + XCTAssertEqual(document.body.data?.included[Author.self][0].id.rawValue, "33") + XCTAssertEqual(document.body.data?.included[Author.self][1].id.rawValue, "22") + XCTAssertEqual(document.body.data?.included[Author.self][2].id.rawValue, "11") + } + + func test_manyDocumentSomeIncludes_encode() { + test_DecodeEncodeEquality(type: JSONAPIDocument, Include1, BasicJSONAPIError>.self, + data: many_document_some_includes) } enum AuthorType: EntityDescription { diff --git a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift index 5a4ce19..d12d188 100644 --- a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift +++ b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift @@ -5,6 +5,12 @@ // Created by Mathew Polzin on 11/12/18. // +let single_document_null = """ +{ + "data": null +} +""".data(using: .utf8)! + let single_document_no_includes = """ { "data": { diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 374386b..b940012 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -3,9 +3,15 @@ import XCTest extension DocumentTests { static let __allTests = [ ("test_manyDocumentNoIncludes", test_manyDocumentNoIncludes), + ("test_manyDocumentNoIncludes_encode", test_manyDocumentNoIncludes_encode), ("test_manyDocumentSomeIncludes", test_manyDocumentSomeIncludes), + ("test_manyDocumentSomeIncludes_encode", test_manyDocumentSomeIncludes_encode), ("test_singleDocumentNoIncludes", test_singleDocumentNoIncludes), + ("test_singleDocumentNoIncludes_encode", test_singleDocumentNoIncludes_encode), + ("test_singleDocumentNull", test_singleDocumentNull), + ("test_singleDocumentNull_encode", test_singleDocumentNull_encode), ("test_singleDocumentSomeIncludes", test_singleDocumentSomeIncludes), + ("test_singleDocumentSomeIncludes_encode", test_singleDocumentSomeIncludes_encode), ] }