mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
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.
This commit is contained in:
+173
@@ -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<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// 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<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// 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<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// 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<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: EntityDescription {
|
||||
public static var type: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: EntityDescription {
|
||||
public static var type: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
|
||||
// MARK: Document Definitions
|
||||
|
||||
// We create a typealias to represent a document containing one Article
|
||||
// and including its Author
|
||||
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
|
||||
|
||||
// ... and a typealias to represent a document containing one Article and
|
||||
// not including any related entities.
|
||||
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, 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<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// 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<SingleArticleDocumentWithIncludes.Include> = .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)
|
||||
@@ -4,5 +4,6 @@
|
||||
<page name='Test Library'/>
|
||||
<page name='Usage'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
</pages>
|
||||
</playground>
|
||||
@@ -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
|
||||
<!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
|
||||
|
||||
- [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)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## 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<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// 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<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// 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<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// 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<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: EntityDescription {
|
||||
public static var type: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: EntityDescription {
|
||||
public static var type: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
|
||||
// MARK: Document Definitions
|
||||
|
||||
// We create a typealias to represent a document containing one Article
|
||||
// and including its Author
|
||||
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
|
||||
|
||||
// ... and a typealias to represent a document containing one Article and
|
||||
// not including any related entities.
|
||||
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, 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<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// 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<SingleArticleDocumentWithIncludes.Include> = .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.
|
||||
|
||||
@@ -110,11 +110,16 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
public init(apiDescription: APIDescription,
|
||||
body: PrimaryResourceBody,
|
||||
includes: Includes<Include>,
|
||||
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<I: JSONAPI.Include>(_ includes: Includes<I>) -> Document<PrimaryResourceBody, MetaType, LinksType, I, APIDescription, Error> {
|
||||
// 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<IncludeType>) -> 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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -17,14 +17,14 @@ public protocol Poly: PrimaryResource {}
|
||||
|
||||
// MARK: - Generic Decoding
|
||||
|
||||
private func decode<Entity: JSONAPI.EntityType>(_ type: Entity.Type, from container: SingleValueDecodingContainer) throws -> Result<Entity, DecodingError> {
|
||||
let ret: Result<Entity, DecodingError>
|
||||
private func decode<Thing: Codable>(_ type: Thing.Type, from container: SingleValueDecodingContainer) throws -> Result<Thing, DecodingError> {
|
||||
let ret: Result<Thing, DecodingError>
|
||||
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<A: EntityType>: _Poly1 {
|
||||
public enum Poly1<A: PolyWrapped>: _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<A: EntityType, B: EntityType>: _Poly2 {
|
||||
public typealias Either = Poly2
|
||||
|
||||
public enum Poly2<A: PolyWrapped, B: PolyWrapped>: _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<A: EntityType, B: EntityType, C: EntityType>: _Poly3 {
|
||||
public enum Poly3<A: PolyWrapped, B: PolyWrapped, C: PolyWrapped>: _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<A: EntityType, B: EntityType, C: EntityType, D: EntityType>: _Poly4 {
|
||||
public enum Poly4<A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped>: _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<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType>: _Poly5 {
|
||||
public enum Poly5<A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped>: _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<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType>: _Poly6 {
|
||||
public enum Poly6<A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped>: _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<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType, G: EntityType>: _Poly7 {
|
||||
public enum Poly7<A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped>: _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<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType, G: EntityType, H: EntityType>: _Poly8 {
|
||||
public enum Poly8<A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped>: _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<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType, G: EntityType, H: EntityType, I: EntityType>: _Poly9 {
|
||||
public enum Poly9<A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped>: _Poly9 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
case c(C)
|
||||
|
||||
@@ -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<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: error_document_no_metadata)
|
||||
|
||||
let documentWithIncludes = document.including(Includes<Include1<Author>>(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<NoResourceBody, NoMetadata, NoLinks, Include1<Author>, 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<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.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<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_no_includes)
|
||||
|
||||
let documentWithIncludes = document.including(Includes<Include1<Author>>(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<NoIncludes>.none)
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
|
||||
}
|
||||
|
||||
func test_singleDocumentNoIncludesWithAPIDescription() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, 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<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, 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<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_some_includes_with_api_description)
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user