From e4eb7816d7bee1cb1a5fb2e5400095a569573618 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sun, 2 Dec 2018 21:35:09 -0800 Subject: [PATCH] Added an APIDescription type to Document that supports the JSON API Spec's JSON:API Object. --- .../Usage.xcplaygroundpage/Contents.swift | 4 +- README.md | 40 +- Sources/JSONAPI/Document/APIDescription.swift | 5 +- .../APIDescription/APIDescriptionTests.swift | 52 ++ .../stubs/APIDescriptionsStubs.swift | 35 ++ .../JSONAPITests/Document/DocumentTests.swift | 458 ++++++++++++++++++ .../Document/stubs/DocumentStubs.swift | 447 +++++++++++++++++ 7 files changed, 1009 insertions(+), 32 deletions(-) create mode 100644 Tests/JSONAPITests/APIDescription/APIDescriptionTests.swift create mode 100644 Tests/JSONAPITests/APIDescription/stubs/APIDescriptionsStubs.swift diff --git a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift index 234294c..f657492 100644 --- a/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift @@ -11,7 +11,7 @@ Please enjoy these examples, but allow me the forced casting and the lack of err // MARK: - Create a request or response body with one Dog in it let dogFromCode = try! Dog(name: "Buddy", owner: nil) -typealias SingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoJSONAPIDescription, UnknownJSONAPIError> +typealias SingleDogDocument = JSONAPI.Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError> let singleDogDocument = SingleDogDocument(body: SingleResourceBody(entity: dogFromCode)) @@ -27,7 +27,7 @@ let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner let houses = [House(), House()] let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])] -typealias BatchPeopleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoJSONAPIDescription, UnknownJSONAPIError> +typealias BatchPeopleDocument = JSONAPI.Document, NoMetadata, NoLinks, Include2, NoAPIDescription, UnknownJSONAPIError> let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) } let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes)) diff --git a/README.md b/README.md index a1ddf3a..cf19926 100644 --- a/README.md +++ b/README.md @@ -33,39 +33,13 @@ Note that Playground support for importing non-system Frameworks is still a bit ## Project Status -### Decoding +### Encoding/Decoding #### Document - [x] `data` - [x] `included` - [x] `errors` - [x] `meta` -- [ ] `jsonapi` -- [x] `links` - -#### Resource Object -- [x] `id` -- [x] `type` -- [x] `attributes` -- [x] `relationships` -- [ ] `links` -- [ ] `meta` - -#### Relationship Object -- [x] `data` -- [ ] `links` -- [ ] `meta` - -#### Links Object -- [x] `href` -- [x] `meta` - -### Encoding -#### Document -- [x] `data` -- [x] `included` -- [x] `errors` -- [x] `meta` -- [ ] `jsonapi` +- [x] `jsonapi` - [x] `links` #### Resource Object @@ -346,6 +320,16 @@ The fourth generic type of a `JSONAPIDocument` is an `Include`. This type contro To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1` instead of `NoIncludes`. +#### `APIDescriptionType` + +The fifth generic type of a `JSONAPIDocument` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API. + +You can specify this is not part of the document by using the `NoAPIDescription` type. + +You can describe the API by a version with no metadata by using `APIDescription`. + +You can supply any `JSONAPI.Meta` type as the metadata type of the API description. + #### `Error` The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the **SPEC**, these errors will be found in the root document member `errors`. diff --git a/Sources/JSONAPI/Document/APIDescription.swift b/Sources/JSONAPI/Document/APIDescription.swift index 9a9875e..228f46e 100644 --- a/Sources/JSONAPI/Document/APIDescription.swift +++ b/Sources/JSONAPI/Document/APIDescription.swift @@ -12,7 +12,7 @@ public protocol APIDescriptionType: Codable, Equatable { /// This is what the JSON API Spec calls the "JSON:API Object" public struct APIDescription: APIDescriptionType { - public let version: String? + public let version: String public let meta: Meta } @@ -35,7 +35,8 @@ extension APIDescription { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - version = try container.decode(String?.self, forKey: .version) + // The spec says that if a version is not specified, it should be assumed to be at least 1.0 + version = (try? container.decode(String.self, forKey: .version)) ?? "1.0" if let metaVal = NoMetadata() as? Meta { meta = metaVal diff --git a/Tests/JSONAPITests/APIDescription/APIDescriptionTests.swift b/Tests/JSONAPITests/APIDescription/APIDescriptionTests.swift new file mode 100644 index 0000000..e70d53e --- /dev/null +++ b/Tests/JSONAPITests/APIDescription/APIDescriptionTests.swift @@ -0,0 +1,52 @@ +// +// APIDescriptionTests.swift +// JSONAPITests +// +// Created by Mathew Polzin on 12/2/18. +// + +import XCTest +import JSONAPI + +class APIDescriptionTests: XCTestCase { + + func test_empty() { + let description = decoded(type: APIDescription.self, data: api_description_empty) + + XCTAssertEqual(description.version, "1.0") + } + + func test_WithVersion() { + let description = decoded(type: APIDescription.self, data: api_description_with_version) + + XCTAssertEqual(description.version, "1.5") + } + + func test_WithMeta() { + let description = decoded(type: APIDescription.self, data: api_description_with_meta) + + XCTAssertEqual(description.version, "1.0") + XCTAssertEqual(description.meta.hello, "world") + XCTAssertEqual(description.meta.number, 10) + } + + func test_WithVersionAndMeta() { + let description = decoded(type: APIDescription.self, data: api_description_with_version_and_meta) + + XCTAssertEqual(description.version, "2.0") + XCTAssertEqual(description.meta.hello, "world") + XCTAssertEqual(description.meta.number, 10) + } + + func test_failsMissingMeta() { + XCTAssertThrowsError(try JSONDecoder().decode(APIDescription.self, from: api_description_with_version)) + } +} + +// MARK: - Test types +extension APIDescriptionTests { + struct TestMetadata: JSONAPI.Meta { + let hello: String + let number: Int + } +} diff --git a/Tests/JSONAPITests/APIDescription/stubs/APIDescriptionsStubs.swift b/Tests/JSONAPITests/APIDescription/stubs/APIDescriptionsStubs.swift new file mode 100644 index 0000000..0c4cf53 --- /dev/null +++ b/Tests/JSONAPITests/APIDescription/stubs/APIDescriptionsStubs.swift @@ -0,0 +1,35 @@ +// +// APIDescriptionsStubs.swift +// JSONAPITests +// +// Created by Mathew Polzin on 12/2/18. +// + +let api_description_empty = """ +{} +""".data(using: .utf8)! + +let api_description_with_version = """ +{ + "version": "1.5" +} +""".data(using: .utf8)! + +let api_description_with_meta = """ +{ + "meta": { + "hello": "world", + "number": 10 + } +} +""".data(using: .utf8)! + +let api_description_with_version_and_meta = """ +{ + "version": "2.0", + "meta": { + "hello": "world", + "number": 10 + } +} +""".data(using: .utf8)! diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index e850a10..8066e21 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -52,10 +52,21 @@ class DocumentTests: XCTestCase { XCTAssertThrowsError(try JSONDecoder().decode(Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, from: single_document_null)) } + + func test_singleDocumentNullFailsWithNoAPIDescription() { + XCTAssertThrowsError(try JSONDecoder().decode(Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + from: single_document_null)) + } } // MARK: - Error Document Tests extension DocumentTests { + + func test_errorDocumentFailsWithNoAPIDescription() { + XCTAssertThrowsError(try JSONDecoder().decode(Document.self, + from: error_document_no_metadata)) + } + func test_unknownErrorDocumentNoMeta() { let document = decoded(type: Document.self, data: error_document_no_metadata) @@ -81,6 +92,32 @@ extension DocumentTests { data: error_document_no_metadata) } + func test_unknownErrorDocumentNoMetaWithAPIDescription() { + let document = decoded(type: Document.self, + data: error_document_no_metadata_with_api_description) + + XCTAssertTrue(document.body.isError) + XCTAssertEqual(document.body.meta, NoMetadata()) + XCTAssertNil(document.body.primaryData) + XCTAssertNil(document.body.includes) + XCTAssertEqual(document.apiDescription.version, "1.0") + + guard case let .errors(errors) = document.body else { + XCTFail("Needed body to be in errors case but it was not.") + return + } + + XCTAssertEqual(errors.0.count, 1) + XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.0[0], .unknown) + XCTAssertEqual(errors.meta, NoMetadata()) + } + + func test_unknownErrorDocumentNoMetaWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, + data: error_document_no_metadata_with_api_description) + } + func test_unknownErrorDocumentMissingMeta() { let document = decoded(type: Document.self, data: error_document_no_metadata) @@ -104,6 +141,30 @@ extension DocumentTests { test_DecodeEncodeEquality(type: Document.self, data: error_document_no_metadata) } + func test_unknownErrorDocumentMissingMetaWithAPIDescription() { + let document = decoded(type: Document.self, data: error_document_no_metadata_with_api_description) + + XCTAssertTrue(document.body.isError) + XCTAssertNil(document.body.meta) + XCTAssertNil(document.body.primaryData) + XCTAssertNil(document.body.includes) + XCTAssertEqual(document.apiDescription.version, "1.0") + + guard case let .errors(errors) = document.body else { + XCTFail("Needed body to be in errors case but it was not.") + return + } + + XCTAssertEqual(errors.0.count, 1) + XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.0[0], .unknown) + XCTAssertNil(errors.meta) + } + + func test_unknownErrorDocumentMissingMetaWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, data: error_document_no_metadata_with_api_description) + } + func test_errorDocumentNoMeta() { let document = decoded(type: Document.self, data: error_document_no_metadata) @@ -129,6 +190,32 @@ extension DocumentTests { data: error_document_no_metadata) } + func test_errorDocumentNoMetaWithAPIDescription() { + let document = decoded(type: Document.self, + data: error_document_no_metadata_with_api_description) + + XCTAssertTrue(document.body.isError) + XCTAssertEqual(document.body.meta, NoMetadata()) + XCTAssertNil(document.body.primaryData) + XCTAssertNil(document.body.includes) + XCTAssertEqual(document.apiDescription.version, "1.0") + + guard case let .errors(errors) = document.body else { + XCTFail("Needed body to be in errors case but it was not.") + return + } + + XCTAssertEqual(errors.0.count, 1) + XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.0[0], TestError.basic(.init(code: 1, description: "Boooo!"))) + XCTAssertEqual(errors.meta, NoMetadata()) + } + + func test_errorDocumentNoMetaWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, + data: error_document_no_metadata_with_api_description) + } + func test_unknownErrorDocumentWithMeta() { let document = decoded(type: Document.self, data: error_document_with_metadata) @@ -153,6 +240,31 @@ extension DocumentTests { data: error_document_with_metadata) } + func test_unknownErrorDocumentWithMetaWithAPIDescription() { + let document = decoded(type: Document.self, + data: error_document_with_metadata_with_api_description) + + XCTAssertTrue(document.body.isError) + XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertNil(document.body.primaryData) + XCTAssertNil(document.body.includes) + XCTAssertEqual(document.apiDescription.version, "1.0") + + guard case let .errors(errors) = document.body else { + XCTFail("Needed body to be in errors case but it was not.") + return + } + + XCTAssertEqual(errors.0.count, 1) + XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + } + + func test_unknownErrorDocumentWithMetaWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, + data: error_document_with_metadata_with_api_description) + } + func test_unknownErrorDocumentWithMetaWithLinks() { let document = decoded(type: Document.self, data: error_document_with_metadata_with_links) @@ -181,6 +293,35 @@ extension DocumentTests { data: error_document_with_metadata_with_links) } + func test_unknownErrorDocumentWithMetaWithLinksWithAPIDescription() { + let document = decoded(type: Document.self, + data: error_document_with_metadata_with_links_with_api_description) + + XCTAssertTrue(document.body.isError) + XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertNil(document.body.primaryData) + XCTAssertNil(document.body.includes) + XCTAssertEqual(document.apiDescription.version, "1.0") + + guard case let .errors(errors) = document.body else { + XCTFail("Needed body to be in errors case but it was not.") + return + } + + XCTAssertEqual(errors.0.count, 1) + XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(errors.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(document.body.links?.link.url, "https://website.com") + XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) + XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") + XCTAssertEqual(document.body.links?.link2.meta, TestLinks.TestMetadata(hello: "world")) + } + + func test_unknownErrorDocumentWithMetaWithLinksWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, + data: error_document_with_metadata_with_links_with_api_description) + } + func test_unknownErrorDocumentWithLinks() { let document = decoded(type: Document.self, data: error_document_with_links) @@ -207,6 +348,33 @@ extension DocumentTests { data: error_document_with_links) } + func test_unknownErrorDocumentWithLinksWithAPIDescription() { + let document = decoded(type: Document.self, + data: error_document_with_links_with_api_description) + + XCTAssertTrue(document.body.isError) + XCTAssertNil(document.body.primaryData) + XCTAssertNil(document.body.includes) + XCTAssertEqual(document.apiDescription.version, "1.0") + + guard case let .errors(errors) = document.body else { + XCTFail("Needed body to be in errors case but it was not.") + return + } + + XCTAssertEqual(errors.0.count, 1) + XCTAssertEqual(errors.0, document.body.errors) + XCTAssertEqual(document.body.links?.link.url, "https://website.com") + XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) + XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") + XCTAssertEqual(document.body.links?.link2.meta, TestLinks.TestMetadata(hello: "world")) + } + + func test_unknownErrorDocumentWithLinksWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, + data: error_document_with_links_with_api_description) + } + func test_unknownErrorDocumentMissingLinks() { let document = decoded(type: Document.self, data: error_document_no_metadata) @@ -229,10 +397,38 @@ extension DocumentTests { test_DecodeEncodeEquality(type: Document.self, data: error_document_no_metadata) } + + func test_unknownErrorDocumentMissingLinksWithAPIDescription() { + let document = decoded(type: Document.self, + data: error_document_no_metadata_with_api_description) + + XCTAssertTrue(document.body.isError) + XCTAssertNil(document.body.primaryData) + XCTAssertNil(document.body.includes) + XCTAssertEqual(document.apiDescription.version, "1.0") + + guard case let .errors(errors) = document.body else { + XCTFail("Needed body to be in errors case but it was not.") + return + } + + XCTAssertEqual(errors.0.count, 1) + XCTAssertEqual(errors.0, document.body.errors) + XCTAssertNil(document.body.links) + } + + func test_unknownErrorDocumentMissingLinksWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, + data: error_document_no_metadata_with_api_description) + } } // MARK: - Meta Document Tests extension DocumentTests { + func test_metaDataDocumentFailsIfMissingAPIDescription() { + XCTAssertThrowsError(try JSONDecoder().decode(Document.self, from: metadata_document)) + } + func test_metaDataDocument() { let document = decoded(type: Document.self, data: metadata_document) @@ -249,6 +445,23 @@ extension DocumentTests { data: metadata_document) } + func test_metaDataDocumentWithAPIDescription() { + let document = decoded(type: Document.self, + data: metadata_document_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertEqual(document.body.meta?.total, 100) + XCTAssertEqual(document.body.meta?.limit, 50) + XCTAssertEqual(document.body.meta?.offset, 0) + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_metaDataDocumentWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, + data: metadata_document_with_api_description) + } + func test_metaDataDocumentWithLinks() { let document = decoded(type: Document.self, data: metadata_document_with_links) @@ -269,6 +482,27 @@ extension DocumentTests { data: metadata_document_with_links) } + func test_metaDataDocumentWithLinksWithAPIDescription() { + let document = decoded(type: Document.self, + data: metadata_document_with_links_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertEqual(document.body.meta?.total, 100) + XCTAssertEqual(document.body.meta?.limit, 50) + XCTAssertEqual(document.body.meta?.offset, 0) + XCTAssertEqual(document.body.links?.link.url, "https://website.com") + XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) + XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") + XCTAssertEqual(document.body.links?.link2.meta, TestLinks.TestMetadata(hello: "world")) + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_metaDataDocumentWithLinksWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document.self, + data: metadata_document_with_links_with_api_description) + } + func test_metaDocumentMissingMeta() { XCTAssertThrowsError(try JSONDecoder().decode(Document.self, from: metadata_document_missing_metadata)) @@ -279,6 +513,10 @@ extension DocumentTests { // MARK: Single Document Tests extension DocumentTests { + func test_singleDocumentNoIncludesMissingAPIDescription() { + XCTAssertThrowsError(try JSONDecoder().decode(Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, from: single_document_no_includes)) + } + func test_singleDocumentNoIncludes() { let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes) @@ -296,6 +534,24 @@ extension DocumentTests { data: single_document_no_includes) } + func test_singleDocumentNoIncludesWithAPIDescription() { + let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") + XCTAssertEqual(document.body.includes?.count, 0) + XCTAssertEqual(document.body.meta, NoMetadata()) + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_singleDocumentNoIncludesWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_api_description) + } + func test_singleDocumentNoIncludesOptionalNotNull() { let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes) @@ -313,6 +569,24 @@ extension DocumentTests { data: single_document_no_includes) } + func test_singleDocumentNoIncludesOptionalNotNullWithAPIDescription() { + let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.includes?.count, 0) + XCTAssertEqual(document.body.meta, NoMetadata()) + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_singleDocumentNoIncludesOptionalNotNullWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_api_description) + } + func test_singleDocumentNoIncludesWithMetadata() { let document = decoded(type: Document, TestPageMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes_with_metadata) @@ -330,6 +604,24 @@ extension DocumentTests { data: single_document_no_includes_with_metadata) } + func test_singleDocumentNoIncludesWithMetadataWithAPIDescription() { + let document = decoded(type: Document, TestPageMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_metadata_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") + XCTAssertEqual(document.body.includes?.count, 0) + XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_singleDocumentNoIncludesWithMetadataWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, TestPageMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_metadata_with_api_description) + } + func test_singleDocumentNoIncludesWithLinks() { let document = decoded(type: Document, NoMetadata, TestLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes_with_links) @@ -352,6 +644,29 @@ extension DocumentTests { data: single_document_no_includes_with_links) } + func test_singleDocumentNoIncludesWithLinksWithAPIDescription() { + let document = decoded(type: Document, NoMetadata, TestLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_links_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") + XCTAssertEqual(document.body.includes?.count, 0) + XCTAssertEqual(document.body.meta, NoMetadata()) + XCTAssertEqual(document.body.links?.link.url, "https://website.com") + XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) + XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") + XCTAssertEqual(document.body.links?.link2.meta, TestLinks.TestMetadata(hello: "world")) + XCTAssertEqual(document.apiDescription.version, "1.0") + + } + + func test_singleDocumentNoIncludesWithLinksWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, NoMetadata, TestLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_links_with_api_description) + } + func test_singleDocumentNoIncludesWithMetadataWithLinks() { let document = decoded(type: Document, TestPageMetadata, TestLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes_with_metadata_with_links) @@ -374,6 +689,29 @@ extension DocumentTests { data: single_document_no_includes_with_metadata_with_links) } + func test_singleDocumentNoIncludesWithMetadataWithLinksWithAPIDescription() { + let document = decoded(type: Document, TestPageMetadata, TestLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_metadata_with_links_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") + XCTAssertEqual(document.body.includes?.count, 0) + XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(document.body.links?.link.url, "https://website.com") + XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) + XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") + XCTAssertEqual(document.body.links?.link2.meta, TestLinks.TestMetadata(hello: "world")) + XCTAssertEqual(document.apiDescription.version, "1.0") + + } + + func test_singleDocumentNoIncludesWithMetadataWithLinksWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, TestPageMetadata, TestLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_no_includes_with_metadata_with_links_with_api_description) + } + func test_singleDocumentNoIncludesWithMetadataMissingLinks() { XCTAssertThrowsError(try JSONDecoder().decode(Document, TestPageMetadata, TestLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, from: single_document_no_includes_with_metadata)) } @@ -400,6 +738,25 @@ extension DocumentTests { data: single_document_some_includes) } + func test_singleDocumentSomeIncludesWithAPIDescription() { + let document = decoded(type: Document, NoMetadata, NoLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_some_includes_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") + XCTAssertEqual(document.body.includes?.count, 1) + XCTAssertEqual(document.body.includes?[Author.self].count, 1) + XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33") + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_singleDocumentSomeIncludesWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_some_includes_with_api_description) + } + func test_singleDocumentSomeIncludesWithMetadata() { let document = decoded(type: Document, TestPageMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_some_includes_with_metadata) @@ -419,6 +776,26 @@ extension DocumentTests { data: single_document_some_includes_with_metadata) } + func test_singleDocumentSomeIncludesWithMetadataWithAPIDescription() { + let document = decoded(type: Document, TestPageMetadata, NoLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_some_includes_with_metadata_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") + XCTAssertEqual(document.body.includes?.count, 1) + XCTAssertEqual(document.body.includes?[Author.self].count, 1) + XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33") + XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, TestPageMetadata, NoLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_some_includes_with_metadata_with_api_description) + } + func test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinks() { let document = decoded(type: Document, TestPageMetadata, TestLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_some_includes_with_metadata_with_links) @@ -441,6 +818,30 @@ extension DocumentTests { test_DecodeEncodeEquality(type: Document, TestPageMetadata, TestLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_some_includes_with_metadata_with_links) } + + func test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinksWithAPIDescription() { + let document = decoded(type: Document, TestPageMetadata, TestLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_some_includes_with_metadata_with_links_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") + XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) + XCTAssertEqual(document.body.links?.link.url, "https://website.com") + XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) + XCTAssertEqual(document.body.links?.link2.url, "https://othersite.com") + XCTAssertEqual(document.body.links?.link2.meta, TestLinks.TestMetadata(hello: "world")) + XCTAssertEqual(document.body.includes?.count, 1) + XCTAssertEqual(document.body.includes?[Author.self].count, 1) + XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33") + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinksWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, TestPageMetadata, TestLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, + data: single_document_some_includes_with_metadata_with_links_with_api_description) + } } // MARK: Poly PrimaryResource Tests @@ -456,6 +857,19 @@ extension DocumentTests { func test_singleDocument_PolyPrimaryResource_encode() { test_DecodeEncodeEquality(type: Document>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes) } + + func test_singleDocument_PolyPrimaryResourceWithAPIDescription() { + let article = Article(id: Id(rawValue: "1"), relationships: .init(author: ToOneRelationship(id: Id(rawValue: "33")))) + let document = decoded(type: Document>, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes_with_api_description) + + XCTAssertEqual(document.body.primaryData?.value[Article.self], article) + XCTAssertNil(document.body.primaryData?.value[Author.self]) + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_singleDocument_PolyPrimaryResourceWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document>, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes_with_api_description) + } } // MARK: - ManyResourceBody Tests @@ -478,6 +892,26 @@ extension DocumentTests { test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self, data: many_document_no_includes) } + + func test_manyDocumentNoIncludesWithAPIDescription() { + let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: many_document_no_includes_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.values.count, 3) + XCTAssertEqual(document.body.primaryData?.values[0].id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.values[1].id.rawValue, "2") + XCTAssertEqual(document.body.primaryData?.values[2].id.rawValue, "3") + XCTAssertEqual(document.body.includes?.count, 0) + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_manyDocumentNoIncludesWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, + data: many_document_no_includes_with_api_description) + } func test_manyDocumentSomeIncludes() { let document = decoded(type: Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, @@ -501,6 +935,30 @@ extension DocumentTests { test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, data: many_document_some_includes) } + + func test_manyDocumentSomeIncludesWithAPIDescription() { + let document = decoded(type: Document, NoMetadata, NoLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, + data: many_document_some_includes_with_api_description) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.values.count, 3) + XCTAssertEqual(document.body.primaryData?.values[0].id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.values[1].id.rawValue, "2") + XCTAssertEqual(document.body.primaryData?.values[2].id.rawValue, "3") + XCTAssertEqual(document.body.includes?.count, 3) + XCTAssertEqual(document.body.includes?[Author.self].count, 3) + XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33") + XCTAssertEqual(document.body.includes?[Author.self][1].id.rawValue, "22") + XCTAssertEqual(document.body.includes?[Author.self][2].id.rawValue, "11") + XCTAssertEqual(document.apiDescription.version, "1.0") + } + + func test_manyDocumentSomeIncludesWithAPIDescription_encode() { + test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, + data: many_document_some_includes_with_api_description) + } } // MARK: - Test Types diff --git a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift index 0c3fb0e..7455a0c 100644 --- a/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift +++ b/Tests/JSONAPITests/Document/stubs/DocumentStubs.swift @@ -37,6 +37,26 @@ let single_document_no_includes = """ } """.data(using: .utf8)! +let single_document_no_includes_with_api_description = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let single_document_no_includes_with_metadata = """ { "data": { @@ -59,6 +79,31 @@ let single_document_no_includes_with_metadata = """ } """.data(using: .utf8)! +let single_document_no_includes_with_metadata_with_api_description = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "meta": { + "total": 70, + "limit": 40, + "offset": 10 + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let single_document_no_includes_with_links = """ { "data": { @@ -85,6 +130,35 @@ let single_document_no_includes_with_links = """ } """.data(using: .utf8)! +let single_document_no_includes_with_links_with_api_description = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "links": { + "link": "https://website.com", + "link2": { + "href": "https://othersite.com", + "meta": { + "hello": "world" + } + } + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let single_document_no_includes_with_metadata_with_links = """ { "data": { @@ -116,6 +190,40 @@ let single_document_no_includes_with_metadata_with_links = """ } """.data(using: .utf8)! +let single_document_no_includes_with_metadata_with_links_with_api_description = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "links": { + "link": "https://website.com", + "link2": { + "href": "https://othersite.com", + "meta": { + "hello": "world" + } + } + }, + "meta": { + "total": 70, + "limit": 40, + "offset": 10 + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let single_document_some_includes = """ { "data": { @@ -139,6 +247,32 @@ let single_document_some_includes = """ } """.data(using: .utf8)! +let single_document_some_includes_with_api_description = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "included": [ + { + "id": "33", + "type": "authors" + } + ], + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let single_document_some_includes_with_metadata_with_links = """ { "data": { @@ -176,6 +310,46 @@ let single_document_some_includes_with_metadata_with_links = """ } """.data(using: .utf8)! +let single_document_some_includes_with_metadata_with_links_with_api_description = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "included": [ + { + "id": "33", + "type": "authors" + } + ], + "links": { + "link": "https://website.com", + "link2": { + "href": "https://othersite.com", + "meta": { + "hello": "world" + } + } + }, + "meta": { + "total": 70, + "limit": 40, + "offset": 10 + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let single_document_some_includes_with_metadata = """ { "data": { @@ -204,6 +378,37 @@ let single_document_some_includes_with_metadata = """ } """.data(using: .utf8)! +let single_document_some_includes_with_metadata_with_api_description = """ +{ + "data": { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + "included": [ + { + "id": "33", + "type": "authors" + } + ], + "meta": { + "total": 70, + "limit": 40, + "offset": 10 + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let many_document_no_includes = """ { "data": [ @@ -247,6 +452,52 @@ let many_document_no_includes = """ } """.data(using: .utf8)! +let many_document_no_includes_with_api_description = """ +{ + "data": [ + { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + { + "id": "2", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "22" + } + } + } + }, + { + "id": "3", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "11" + } + } + } + } + ], + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let many_document_some_includes = """ { "data": [ @@ -304,6 +555,66 @@ let many_document_some_includes = """ } """.data(using: .utf8)! +let many_document_some_includes_with_api_description = """ +{ + "data": [ + { + "id": "1", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "33" + } + } + } + }, + { + "id": "2", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "22" + } + } + } + }, + { + "id": "3", + "type": "articles", + "relationships": { + "author": { + "data": { + "type": "authors", + "id": "11" + } + } + } + } + ], + "included": [ + { + "id": "33", + "type": "authors" + }, + { + "id": "22", + "type": "authors" + }, + { + "id": "11", + "type": "authors" + } + ], + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let error_document_no_metadata = """ { "errors": [ @@ -315,6 +626,20 @@ let error_document_no_metadata = """ } """.data(using: .utf8)! +let error_document_no_metadata_with_api_description = """ +{ + "errors": [ + { + "description": "Boooo!", + "code": 1 + } + ], + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let error_document_with_metadata = """ { "errors": [ @@ -331,6 +656,25 @@ let error_document_with_metadata = """ } """.data(using: .utf8)! +let error_document_with_metadata_with_api_description = """ +{ + "errors": [ + { + "description": "Boooo!", + "code": 1 + } + ], + "meta": { + "total": 70, + "limit": 40, + "offset": 10 + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let error_document_with_links = """ { "errors": [ @@ -351,6 +695,29 @@ let error_document_with_links = """ } """.data(using: .utf8)! +let error_document_with_links_with_api_description = """ +{ + "errors": [ + { + "description": "Boooo!", + "code": 1 + } + ], + "links": { + "link": "https://website.com", + "link2": { + "href": "https://othersite.com", + "meta": { + "hello": "world" + } + } + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let error_document_with_metadata_with_links = """ { "errors": [ @@ -376,6 +743,34 @@ let error_document_with_metadata_with_links = """ } """.data(using: .utf8)! +let error_document_with_metadata_with_links_with_api_description = """ +{ + "errors": [ + { + "description": "Boooo!", + "code": 1 + } + ], + "meta": { + "total": 70, + "limit": 40, + "offset": 10 + }, + "links": { + "link": "https://website.com", + "link2": { + "href": "https://othersite.com", + "meta": { + "hello": "world" + } + } + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let metadata_document = """ { "meta": { @@ -386,6 +781,19 @@ let metadata_document = """ } """.data(using: .utf8)! +let metadata_document_with_api_description = """ +{ + "meta": { + "total": 100, + "limit": 50, + "offset": 0 + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let metadata_document_with_links = """ { "meta": { @@ -405,13 +813,52 @@ let metadata_document_with_links = """ } """.data(using: .utf8)! +let metadata_document_with_links_with_api_description = """ +{ + "meta": { + "total": 100, + "limit": 50, + "offset": 0 + }, + "links": { + "link": "https://website.com", + "link2": { + "href": "https://othersite.com", + "meta": { + "hello": "world" + } + } + }, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let metadata_document_missing_metadata = """ { } """.data(using: .utf8)! +let metadata_document_missing_metadata_with_api_description = """ +{ + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)! + let metadata_document_missing_metadata2 = """ { "meta": null } """.data(using: .utf8)! + +let metadata_document_missing_metadata2_with_api_description = """ +{ + "meta": null, + "jsonapi": { + "version": "1.0" + } +} +""".data(using: .utf8)!