From 897410492d8aa5de73500be36754507f59e53c5c Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Wed, 2 Jan 2019 17:14:58 -0800 Subject: [PATCH] Add much more substantial example to README and mirror it in the included playground. Add convenient methods for adding includes to a Document. Make Poly less picky about what type of things it contains (don't need to be entities anymore). Typealias Either to Poly2 because they are isomorphic. --- .../Contents.swift | 173 +++++++++++++ JSONAPI.playground/contents.xcplayground | 1 + README.md | 232 +++++++++++++++++- Sources/JSONAPI/Document/Document.swift | 55 ++++- Sources/JSONAPI/Resource/Entity.swift | 8 + Sources/JSONAPI/Resource/Poly.swift | 48 ++-- .../JSONAPITests/Document/DocumentTests.swift | 79 ++++++ Tests/JSONAPITests/XCTestManifests.swift | 4 + 8 files changed, 576 insertions(+), 24 deletions(-) create mode 100644 JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift diff --git a/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift new file mode 100644 index 0000000..b8dad3e --- /dev/null +++ b/JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift @@ -0,0 +1,173 @@ +import Foundation +import JSONAPI + +// MARK: - Preamble (setup) + +// We make String a CreatableRawIdType. This is actually done in +// this Playground's Entities.swift file, so it is commented out here. +/* +var GlobalStringId: Int = 0 +extension String: CreatableRawIdType { + public static func unique() -> String { + GlobalStringId += 1 + return String(GlobalStringId) + } +} +*/ + +// We create a typealias given that we do not expect JSON:API Resource +// Objects for this particular API to have Metadata or Links associated +// with them. We also expect them to have String Identifiers. +typealias JSONEntity = JSONAPI.Entity + +// Similarly, we create a typealias for unidentified entities. JSON:API +// only allows unidentified entities (i.e. no "id" field) for client +// requests that create new entities. In these situations, the server +// is expected to assign the new entity a unique ID. +typealias UnidentifiedJSONEntity = JSONAPI.Entity + +// We create typealiases given that we do not expect JSON:API Relationships +// for this particular API to have Metadata or Links associated +// with them. +typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToManyRelationship = JSONAPI.ToManyRelationship + +// We create a typealias for a Document given that we do not expect +// JSON:API Documents for this particular API to have Metadata, Links, +// useful Errors, or a JSON:API Object (i.e. APIDescription). +typealias Document = JSONAPI.Document + +// MARK: Entity Definitions + +enum AuthorDescription: EntityDescription { + public static var type: String { return "authors" } + + public struct Attributes: JSONAPI.Attributes { + public let name: Attribute + } + + public typealias Relationships = NoRelationships +} + +typealias Author = JSONEntity + +enum ArticleDescription: EntityDescription { + public static var type: String { return "articles" } + + public struct Attributes: JSONAPI.Attributes { + public let title: Attribute + public let abstract: Attribute + } + + public struct Relationships: JSONAPI.Relationships { + public let author: ToOneRelationship + } +} + +typealias Article = JSONEntity + +// MARK: Document Definitions + +// We create a typealias to represent a document containing one Article +// and including its Author +typealias SingleArticleDocumentWithIncludes = Document, Include1> + +// ... and a typealias to represent a document containing one Article and +// not including any related entities. +typealias SingleArticleDocument = Document, NoIncludes> + +// MARK: - Server Pseudo-example + +// Skipping over all the API and database stuff, here's a chunk of code +// that creates a document. Note that this document is the entirety +// of a JSON:API response body. +func articleDocument(includeAuthor: Bool) -> Either { + // Let's pretend all of this is coming from a database: + + let authorId = Author.Identifier(rawValue: "1234") + + let article = Article(id: .init(rawValue: "5678"), + attributes: .init(title: .init(value: "JSON:API in Swift"), + abstract: .init(value: "Not yet written")), + relationships: .init(author: .init(id: authorId)), + meta: .none, + links: .none) + + let document = SingleArticleDocument(apiDescription: .none, + body: .init(entity: article), + includes: .none, + meta: .none, + links: .none) + + switch includeAuthor { + case false: + return .a(document) + + case true: + let author = Author(id: authorId, + attributes: .init(name: .init(value: "Janice Bluff")), + relationships: .none, + meta: .none, + links: .none) + + let includes: Includes = .init(values: [.init(author)]) + + return .b(document.including(.init(values: [.init(author)]))) + } +} + +let encoder = JSONEncoder() +encoder.keyEncodingStrategy = .convertToSnakeCase +encoder.outputFormatting = .prettyPrinted + +let responseBody = articleDocument(includeAuthor: true) +let responseData = try! encoder.encode(responseBody) + +// Next step would be encoding and setting as the HTTP body of a response. +// we will just print it out instead: +print("-----") +print(String(data: responseData, encoding: .utf8)!) + +// ... and if we had received a request for an article without +// including the author: +let otherResponseBody = articleDocument(includeAuthor: false) +let otherResponseData = try! encoder.encode(otherResponseBody) +print("-----") +print(String(data: otherResponseData, encoding: .utf8)!) + +// MARK: - Client Pseudo-example + +enum NetworkError: Swift.Error { + case serverError + case quantityMismatch +} + +// Skipping over all the API stuff, here's a chunk of code that will +// decode a document. We will assume we have made a request for a +// single article including the author. +func docode(articleResponseData: Data) throws -> (article: Article, author: Author) { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData) + + switch articleDocument.body { + case .data(let data): + let authors = data.includes[Author.self] + + guard authors.count == 1 else { + throw NetworkError.quantityMismatch + } + + return (article: data.primary.value, author: authors[0]) + case .errors(let errors, meta: _, links: _): + throw NetworkError.serverError + } +} + +let response = try! docode(articleResponseData: responseData) + +// Next step would be to do something useful with the article and author but we will print them instead. +print("-----") +print(response.article) +print(response.author) diff --git a/JSONAPI.playground/contents.xcplayground b/JSONAPI.playground/contents.xcplayground index 45f30de..0e5a1ca 100644 --- a/JSONAPI.playground/contents.xcplayground +++ b/JSONAPI.playground/contents.xcplayground @@ -4,5 +4,6 @@ + \ No newline at end of file diff --git a/README.md b/README.md index fa9ef4e..791007c 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,61 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques See the JSON API Spec here: https://jsonapi.org/format/ +## Table of Contents + + +- [Table of Contents](#table-of-contents) +- [Primary Goals](#primary-goals) + - [Caveat](#caveat) +- [Dev Environment](#dev-environment) + - [Prerequisites](#prerequisites) + - [Xcode project](#xcode-project) + - [Running the Playground](#running-the-playground) +- [Project Status](#project-status) + - [Encoding/Decoding](#encodingdecoding) + - [Document](#document) + - [Resource Object](#resource-object) + - [Relationship Object](#relationship-object) + - [Links Object](#links-object) + - [Misc](#misc) + - [JSONAPITestLib](#jsonapitestlib) + - [Entity Validator](#entity-validator) + - [Potential Improvements](#potential-improvements) +- [Usage](#usage) + - [`JSONAPI.EntityDescription`](#jsonapientitydescription) + - [`JSONAPI.Entity`](#jsonapientity) + - [`Meta`](#meta) + - [`Links`](#links) + - [`IdType`](#idtype) + - [`MaybeRawId`](#mayberawid) + - [Convenient `typealiases`](#convenient-typealiases) + - [`JSONAPI.Relationships`](#jsonapirelationships) + - [`JSONAPI.Attributes`](#jsonapiattributes) + - [`Transformer`](#transformer) + - [`Validator`](#validator) + - [Computed `Attribute`](#computed-attribute) + - [Copying `Entities`](#copying-entities) + - [`JSONAPI.Document`](#jsonapidocument) + - [`ResourceBody`](#resourcebody) + - [nullable `PrimaryResource`](#nullable-primaryresource) + - [`MetaType`](#metatype) + - [`LinksType`](#linkstype) + - [`IncludeType`](#includetype) + - [`APIDescriptionType`](#apidescriptiontype) + - [`Error`](#error) + - [`JSONAPI.Meta`](#jsonapimeta) + - [`JSONAPI.Links`](#jsonapilinks) + - [`JSONAPI.RawIdType`](#jsonapirawidtype) + - [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping) + - [Custom Attribute Encode/Decode](#custom-attribute-encodedecode) +- [Example](#example) + - [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client) + - [Server Pseudo-example](#server-pseudo-example) + - [Client Pseudo-example](#client-pseudo-example) +- [JSONAPITestLib](#jsonapitestlib) + + + ## Primary Goals The primary goals of this framework are: @@ -62,6 +117,7 @@ Note that Playground support for importing non-system Frameworks is still a bit ### Misc - [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`) - [x] Support validation on `Attributes`. +- [ ] Support sparse fieldsets. At the moment, not sure what this support will look like. A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, it becomes much more appealing to be able to support arbitrary combinations of omitted fields. - [ ] Create more descriptive errors that are easier to use for troubleshooting. ### JSONAPITestLib @@ -73,6 +129,7 @@ Note that Playground support for importing non-system Frameworks is still a bit ### Potential Improvements - [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources. - [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies. +- [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`. - [ ] Property-based testing (using `SwiftCheck`). - [ ] Error or warning if an included entity is not related to a primary entity or another included entity (Turned off or at least not throwing by default). @@ -469,5 +526,178 @@ extension EntityDescription1.Attributes { } ``` -# JSONAPITestLib +## Example +The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo. + +### Preamble (Setup shared by server and client) +``` +// We make String a CreatableRawIdType. +var GlobalStringId: Int = 0 +extension String: CreatableRawIdType { + public static func unique() -> String { + GlobalStringId += 1 + return String(GlobalStringId) + } +} + +// We create a typealias given that we do not expect JSON:API Resource +// Objects for this particular API to have Metadata or Links associated +// with them. We also expect them to have String Identifiers. +typealias JSONEntity = JSONAPI.Entity + +// Similarly, we create a typealias for unidentified entities. JSON:API +// only allows unidentified entities (i.e. no "id" field) for client +// requests that create new entities. In these situations, the server +// is expected to assign the new entity a unique ID. +typealias UnidentifiedJSONEntity = JSONAPI.Entity + +// We create typealiases given that we do not expect JSON:API Relationships +// for this particular API to have Metadata or Links associated +// with them. +typealias ToOneRelationship = JSONAPI.ToOneRelationship +typealias ToManyRelationship = JSONAPI.ToManyRelationship + +// We create a typealias for a Document given that we do not expect +// JSON:API Documents for this particular API to have Metadata, Links, +// useful Errors, or a JSON:API Object (i.e. APIDescription). +typealias Document = JSONAPI.Document + +// MARK: Entity Definitions + +enum AuthorDescription: EntityDescription { + public static var type: String { return "authors" } + + public struct Attributes: JSONAPI.Attributes { + public let name: Attribute + } + + public typealias Relationships = NoRelationships +} + +typealias Author = JSONEntity + +enum ArticleDescription: EntityDescription { + public static var type: String { return "articles" } + + public struct Attributes: JSONAPI.Attributes { + public let title: Attribute + public let abstract: Attribute + } + + public struct Relationships: JSONAPI.Relationships { + public let author: ToOneRelationship + } +} + +typealias Article = JSONEntity + +// MARK: Document Definitions + +// We create a typealias to represent a document containing one Article +// and including its Author +typealias SingleArticleDocumentWithIncludes = Document, Include1> + +// ... and a typealias to represent a document containing one Article and +// not including any related entities. +typealias SingleArticleDocument = Document, NoIncludes> +``` +### Server Pseudo-example +``` +// Skipping over all the API and database stuff, here's a chunk of code +// that creates a document. Note that this document is the entirety +// of a JSON:API response body. +func articleDocument(includeAuthor: Bool) -> Either { + // Let's pretend all of this is coming from a database: + + let authorId = Author.Identifier(rawValue: "1234") + + let article = Article(id: .init(rawValue: "5678"), + attributes: .init(title: .init(value: "JSON:API in Swift"), + abstract: .init(value: "Not yet written")), + relationships: .init(author: .init(id: authorId)), + meta: .none, + links: .none) + + let document = SingleArticleDocument(apiDescription: .none, + body: .init(entity: article), + includes: .none, + meta: .none, + links: .none) + + switch includeAuthor { + case false: + return .a(document) + + case true: + let author = Author(id: authorId, + attributes: .init(name: .init(value: "Janice Bluff")), + relationships: .none, + meta: .none, + links: .none) + + let includes: Includes = .init(values: [.init(author)]) + + return .b(document.including(.init(values: [.init(author)]))) + } +} + +let encoder = JSONEncoder() +encoder.keyEncodingStrategy = .convertToSnakeCase +encoder.outputFormatting = .prettyPrinted + +let responseBody = articleDocument(includeAuthor: true) +let responseData = try! encoder.encode(responseBody) + +// Next step would be encoding and setting as the HTTP body of a response. +// we will just print it out instead: +print("-----") +print(String(data: responseData, encoding: .utf8)!) + +// ... and if we had received a request for an article without +// including the author: +let otherResponseBody = articleDocument(includeAuthor: false) +let otherResponseData = try! encoder.encode(otherResponseBody) +print("-----") +print(String(data: otherResponseData, encoding: .utf8)!) +``` + +### Client Pseudo-example +``` +enum NetworkError: Swift.Error { + case serverError + case quantityMismatch +} + +// Skipping over all the API stuff, here's a chunk of code that will +// decode a document. We will assume we have made a request for a +// single article including the author. +func docode(articleResponseData: Data) throws -> (article: Article, author: Author) { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + + let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData) + + switch articleDocument.body { + case .data(let data): + let authors = data.includes[Author.self] + + guard authors.count == 1 else { + throw NetworkError.quantityMismatch + } + + return (article: data.primary.value, author: authors[0]) + case .errors(let errors, meta: _, links: _): + throw NetworkError.serverError + } +} + +let response = try! docode(articleResponseData: responseData) + +// Next step would be to do something useful with the article and author but we will print them instead. +print("-----") +print(response.article) +print(response.author) +``` + +## JSONAPITestLib The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITestLib` in action in the Playground included with the `JSONAPI` repository. diff --git a/Sources/JSONAPI/Document/Document.swift b/Sources/JSONAPI/Document/Document.swift index ac9a12e..09f04f7 100644 --- a/Sources/JSONAPI/Document/Document.swift +++ b/Sources/JSONAPI/Document/Document.swift @@ -110,11 +110,16 @@ public struct Document, meta: MetaType, links: LinksType) { + public init(apiDescription: APIDescription, + body: PrimaryResourceBody, + includes: Includes, + meta: MetaType, + links: LinksType) { self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links)) self.apiDescription = apiDescription } } + /* extension Document where IncludeType == NoIncludes { public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) { @@ -208,6 +213,54 @@ extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, } } +extension Document where IncludeType == NoIncludes { + /// Create a new Document with the given includes. + public func including(_ includes: Includes) -> Document { + // Note that if IncludeType is NoIncludes, then we allow anything + // to be included, but if IncludeType already specifies a type + // of thing to be expected then we lock that down. + // See: Document.including() where IncludeType: _Poly1 + switch body { + case .data(let data): + return .init(apiDescription: apiDescription, + body: data.primary, + includes: includes, + meta: data.meta, + links: data.links) + case .errors(let errors, meta: let meta, links: let links): + return .init(apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links) + } + } +} + +// extending where _Poly1 means all non-zero _Poly arities are included +extension Document where IncludeType: _Poly1 { + /// Create a new Document adding the given includes. This does not + /// remove existing includes; it is additive. + public func including(_ includes: Includes) -> Document { + // Note that if IncludeType is NoIncludes, then we allow anything + // to be included, but if IncludeType already specifies a type + // of thing to be expected then we lock that down. + // See: Document.including() where IncludeType == NoIncludes + switch body { + case .data(let data): + return .init(apiDescription: apiDescription, + body: data.primary, + includes: data.includes + includes, + meta: data.meta, + links: data.links) + case .errors(let errors, meta: let meta, links: let links): + return .init(apiDescription: apiDescription, + errors: errors, + meta: meta, + links: links) + } + } +} + // MARK: - Codable extension Document { private enum RootCodingKeys: String, CodingKey { diff --git a/Sources/JSONAPI/Resource/Entity.swift b/Sources/JSONAPI/Resource/Entity.swift index e78caff..6a53977 100644 --- a/Sources/JSONAPI/Resource/Entity.swift +++ b/Sources/JSONAPI/Resource/Entity.swift @@ -20,12 +20,20 @@ public struct NoRelationships: Relationships { public static var none: NoRelationships { return .init() } } +extension NoRelationships: CustomStringConvertible { + public var description: String { return "No Relationships" } +} + /// Can be used as `Attributes` Type for Entities that do not /// have any Attributes. public struct NoAttributes: Attributes { public static var none: NoAttributes { return .init() } } +extension NoAttributes: CustomStringConvertible { + public var description: String { return "No Attributes" } +} + /// Something that is JSONTyped provides a String representation /// of its type. public protocol JSONTyped { diff --git a/Sources/JSONAPI/Resource/Poly.swift b/Sources/JSONAPI/Resource/Poly.swift index a9c3c4b..2f56276 100644 --- a/Sources/JSONAPI/Resource/Poly.swift +++ b/Sources/JSONAPI/Resource/Poly.swift @@ -17,14 +17,14 @@ public protocol Poly: PrimaryResource {} // MARK: - Generic Decoding -private func decode(_ type: Entity.Type, from container: SingleValueDecodingContainer) throws -> Result { - let ret: Result +private func decode(_ type: Thing.Type, from container: SingleValueDecodingContainer) throws -> Result { + let ret: Result do { - ret = try .success(container.decode(Entity.self)) + ret = try .success(container.decode(Thing.self)) } catch (let err as DecodingError) { ret = .failure(err) } catch (let err) { - ret = .failure(DecodingError.typeMismatch(Entity.Description.self, + ret = .failure(DecodingError.typeMismatch(Thing.self, .init(codingPath: container.codingPath, debugDescription: String(describing: err), underlyingError: err))) @@ -47,9 +47,11 @@ public struct Poly0: _Poly0 { } } +public typealias PolyWrapped = Codable & Equatable + // MARK: - 1 type public protocol _Poly1: _Poly0 { - associatedtype A: EntityType + associatedtype A: PolyWrapped var a: A? { get } init(_ a: A) @@ -61,7 +63,7 @@ public extension _Poly1 { } } -public enum Poly1: _Poly1 { +public enum Poly1: _Poly1 { case a(A) public var a: A? { @@ -102,7 +104,7 @@ extension Poly1: CustomStringConvertible { // MARK: - 2 types public protocol _Poly2: _Poly1 { - associatedtype B: EntityType + associatedtype B: PolyWrapped var b: B? { get } init(_ b: B) @@ -114,7 +116,9 @@ public extension _Poly2 { } } -public enum Poly2: _Poly2 { +public typealias Either = Poly2 + +public enum Poly2: _Poly2 { case a(A) case b(B) @@ -181,7 +185,7 @@ extension Poly2: CustomStringConvertible { // MARK: - 3 types public protocol _Poly3: _Poly2 { - associatedtype C: EntityType + associatedtype C: PolyWrapped var c: C? { get } init(_ c: C) @@ -193,7 +197,7 @@ public extension _Poly3 { } } -public enum Poly3: _Poly3 { +public enum Poly3: _Poly3 { case a(A) case b(B) case c(C) @@ -275,7 +279,7 @@ extension Poly3: CustomStringConvertible { // MARK: - 4 types public protocol _Poly4: _Poly3 { - associatedtype D: EntityType + associatedtype D: PolyWrapped var d: D? { get } init(_ d: D) @@ -287,7 +291,7 @@ public extension _Poly4 { } } -public enum Poly4: _Poly4 { +public enum Poly4: _Poly4 { case a(A) case b(B) case c(C) @@ -384,7 +388,7 @@ extension Poly4: CustomStringConvertible { // MARK: - 5 types public protocol _Poly5: _Poly4 { - associatedtype E: EntityType + associatedtype E: PolyWrapped var e: E? { get } init(_ e: E) @@ -396,7 +400,7 @@ public extension _Poly5 { } } -public enum Poly5: _Poly5 { +public enum Poly5: _Poly5 { case a(A) case b(B) case c(C) @@ -508,7 +512,7 @@ extension Poly5: CustomStringConvertible { // MARK: - 6 types public protocol _Poly6: _Poly5 { - associatedtype F: EntityType + associatedtype F: PolyWrapped var f: F? { get } init(_ f: F) @@ -520,7 +524,7 @@ public extension _Poly6 { } } -public enum Poly6: _Poly6 { +public enum Poly6: _Poly6 { case a(A) case b(B) case c(C) @@ -647,7 +651,7 @@ extension Poly6: CustomStringConvertible { // MARK: - 7 types public protocol _Poly7: _Poly6 { - associatedtype G: EntityType + associatedtype G: PolyWrapped var g: G? { get } init(_ g: G) @@ -659,7 +663,7 @@ public extension _Poly7 { } } -public enum Poly7: _Poly7 { +public enum Poly7: _Poly7 { case a(A) case b(B) case c(C) @@ -802,7 +806,7 @@ extension Poly7: CustomStringConvertible { // MARK: - 8 types public protocol _Poly8: _Poly7 { - associatedtype H: EntityType + associatedtype H: PolyWrapped var h: H? { get } init(_ h: H) @@ -814,7 +818,7 @@ public extension _Poly8 { } } -public enum Poly8: _Poly8 { +public enum Poly8: _Poly8 { case a(A) case b(B) case c(C) @@ -973,7 +977,7 @@ extension Poly8: CustomStringConvertible { // MARK: - 9 types public protocol _Poly9: _Poly8 { - associatedtype I: EntityType + associatedtype I: PolyWrapped var i: I? { get } init(_ i: I) @@ -985,7 +989,7 @@ public extension _Poly9 { } } -public enum Poly9: _Poly9 { +public enum Poly9: _Poly9 { case a(A) case b(B) case c(C) diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index cc0bcec..2dfeab8 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -88,6 +88,42 @@ extension DocumentTests { XCTAssertEqual(errors.meta, NoMetadata()) } + func test_unknownErrorDocumentAddIncludingType() { + let author = Author(id: "1", + attributes: .none, + relationships: .none, + meta: .none, + links: .none) + + let document = decoded(type: Document.self, + data: error_document_no_metadata) + + let documentWithIncludes = document.including(Includes>(values: [.init(author)])) + + XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors) + XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta) + XCTAssertEqual(document.body.links, documentWithIncludes.body.links) + XCTAssertNil(documentWithIncludes.body.includes) + } + + func test_unknownErrorDocumentAddIncludes() { + let author = Author(id: "1", + attributes: .none, + relationships: .none, + meta: .none, + links: .none) + + let document = decoded(type: Document, NoAPIDescription, UnknownJSONAPIError>.self, + data: error_document_no_metadata) + + let documentWithIncludes = document.including(.init(values: [.init(author)])) + + XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors) + XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta) + XCTAssertEqual(document.body.links, documentWithIncludes.body.links) + XCTAssertNil(documentWithIncludes.body.includes) + } + func test_unknownErrorDocumentNoMeta_encode() { test_DecodeEncodeEquality(type: Document.self, data: error_document_no_metadata) @@ -536,6 +572,25 @@ extension DocumentTests { data: single_document_no_includes) } + func test_singleDocumentNoIncludesAddIncludingType() { + let author = Author(id: "1", + attributes: .none, + relationships: .none, + meta: .none, + links: .none) + + let document = decoded(type: Document.self, + data: single_document_no_includes) + + let documentWithIncludes = document.including(Includes>(values: [.init(author)])) + + XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors) + XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta) + XCTAssertEqual(document.body.links, documentWithIncludes.body.links) + XCTAssertEqual(document.body.includes, Includes.none) + XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author]) + } + func test_singleDocumentNoIncludesWithAPIDescription() { let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self, data: single_document_no_includes_with_api_description) @@ -740,6 +795,30 @@ extension DocumentTests { data: single_document_some_includes) } + func test_singleDocumentSomeIncludesAddIncludes() { + let existingAuthor = Author(id: "33", + attributes: .none, + relationships: .none, + meta: .none, + links: .none) + + let newAuthor = Author(id: "1", + attributes: .none, + relationships: .none, + meta: .none, + links: .none) + + let document = decoded(type: Document, NoMetadata, NoLinks, Include1, NoAPIDescription, UnknownJSONAPIError>.self, + data: single_document_some_includes) + + let documentWithIncludes = document.including(.init(values: [.init(newAuthor)])) + + XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors) + XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta) + XCTAssertEqual(document.body.links, documentWithIncludes.body.links) + XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor]) + } + func test_singleDocumentSomeIncludesWithAPIDescription() { let document = decoded(type: Document, NoMetadata, NoLinks, Include1, TestAPIDescription, UnknownJSONAPIError>.self, data: single_document_some_includes_with_api_description) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 54f8a92..9714887 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -114,6 +114,7 @@ extension DocumentTests { ("test_singleDocument_PolyPrimaryResourceWithAPIDescription_encode", test_singleDocument_PolyPrimaryResourceWithAPIDescription_encode), ("test_singleDocumentNoIncludes", test_singleDocumentNoIncludes), ("test_singleDocumentNoIncludes_encode", test_singleDocumentNoIncludes_encode), + ("test_singleDocumentNoIncludesAddIncludingType", test_singleDocumentNoIncludesAddIncludingType), ("test_singleDocumentNoIncludesMissingAPIDescription", test_singleDocumentNoIncludesMissingAPIDescription), ("test_singleDocumentNoIncludesMissingMetadata", test_singleDocumentNoIncludesMissingMetadata), ("test_singleDocumentNoIncludesOptionalNotNull", test_singleDocumentNoIncludesOptionalNotNull), @@ -147,12 +148,15 @@ extension DocumentTests { ("test_singleDocumentNullWithAPIDescription_encode", test_singleDocumentNullWithAPIDescription_encode), ("test_singleDocumentSomeIncludes", test_singleDocumentSomeIncludes), ("test_singleDocumentSomeIncludes_encode", test_singleDocumentSomeIncludes_encode), + ("test_singleDocumentSomeIncludesAddIncludes", test_singleDocumentSomeIncludesAddIncludes), ("test_singleDocumentSomeIncludesWithAPIDescription", test_singleDocumentSomeIncludesWithAPIDescription), ("test_singleDocumentSomeIncludesWithAPIDescription_encode", test_singleDocumentSomeIncludesWithAPIDescription_encode), ("test_singleDocumentSomeIncludesWithMetadata", test_singleDocumentSomeIncludesWithMetadata), ("test_singleDocumentSomeIncludesWithMetadata_encode", test_singleDocumentSomeIncludesWithMetadata_encode), ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription), ("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode), + ("test_unknownErrorDocumentAddIncludes", test_unknownErrorDocumentAddIncludes), + ("test_unknownErrorDocumentAddIncludingType", test_unknownErrorDocumentAddIncludingType), ("test_unknownErrorDocumentMissingLinks", test_unknownErrorDocumentMissingLinks), ("test_unknownErrorDocumentMissingLinks_encode", test_unknownErrorDocumentMissingLinks_encode), ("test_unknownErrorDocumentMissingLinksWithAPIDescription", test_unknownErrorDocumentMissingLinksWithAPIDescription),