mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
187 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f41521e33b | |||
| 45ec7ba753 | |||
| 6ab4237c97 | |||
| 75711648a4 | |||
| 2b4209ccb1 | |||
| ebd11df104 | |||
| 96da1b4e21 | |||
| c7696d83fa | |||
| 8ee04d8932 | |||
| 4a7a14b1b0 | |||
| a6b7d7a94a | |||
| 1010489a02 | |||
| ae855c85ee | |||
| 440b649577 | |||
| 54551617b4 | |||
| 8c3a82ec23 | |||
| e9a3b35dc7 | |||
| 2eecf95995 | |||
| 4dc30ddc1c | |||
| 455ff64326 | |||
| 0b4baf35d5 | |||
| 11ef050d58 | |||
| 86344ef93f | |||
| 7fabe2574e | |||
| 19636a47f0 | |||
| 0538de48cb | |||
| 024fe2d452 | |||
| 832161628b | |||
| f37f44cfda | |||
| ae7e0f528a | |||
| 9f42e5fa3f | |||
| 3e96adf775 | |||
| 83233a7e1a | |||
| adcc6bfb10 | |||
| 6f9a2df605 | |||
| c24b70b49f | |||
| 7b56e35a91 | |||
| e6f82c6052 | |||
| d6f01d6c1d | |||
| a7f6ed5845 | |||
| 87271b93f9 | |||
| 706346e3a6 | |||
| 33a5ff41a0 | |||
| 0fe5c53ada | |||
| f7bfa91ccc | |||
| 1a15ab6f9d | |||
| eb85620379 | |||
| 57f85476c0 | |||
| ea5c0b8601 | |||
| b46429a0ad | |||
| dbc6ecc6d8 | |||
| dbbacab105 | |||
| 4eb36d4594 | |||
| 43e02351de | |||
| 0b307bd3bc | |||
| 662a84ccf0 | |||
| 44f9bca7dc | |||
| 774b53b9b6 | |||
| 654d3bfd2b | |||
| c26d6d99c0 | |||
| 43dcc4fb12 | |||
| abae975f59 | |||
| 0e6b2a7771 | |||
| 194a58ae56 | |||
| f1d6b22f61 | |||
| 3057992348 | |||
| d4806ff557 | |||
| b0801f7cee | |||
| 88c5d400aa | |||
| 6cd5aeaba6 | |||
| 7e28cd2606 | |||
| cf6fa39548 | |||
| a24f15dc4e | |||
| 87e9ee0606 | |||
| 5ed45078a1 | |||
| 34dc351810 | |||
| 3ff1b867ca | |||
| ae51be20d8 | |||
| c142e93fec | |||
| e99c6625f8 | |||
| e9048a4861 | |||
| ff06c36b36 | |||
| 8eb4c14bb3 | |||
| baee85fb1b | |||
| 44f21b4430 | |||
| f7f36c6e7a | |||
| af7eaeb684 | |||
| 0cc2368331 | |||
| 6c54845525 | |||
| 51b981c108 | |||
| b33c7f4ab5 | |||
| bf8245c7b8 | |||
| 7bd4021790 | |||
| 771641b82c | |||
| 198e5a9800 | |||
| 9db534dea0 | |||
| 2b409e464e | |||
| 7824bd8f68 | |||
| 89217f7187 | |||
| 86a9345fdd | |||
| 8f9ec11f27 | |||
| 9a9403bd98 | |||
| 453ce4b3a8 | |||
| 9a0bba8d07 | |||
| d5b4aa70c7 | |||
| 1df891ce06 | |||
| 6ba217f553 | |||
| e23ec090ed | |||
| ed23349351 | |||
| 6c8646a1b4 | |||
| 3a60ac5fe2 | |||
| 045e88f4d4 | |||
| fe1f4c6c19 | |||
| 83f7a7b60e | |||
| 61f2edb59a | |||
| 32d584099f | |||
| 9a07cf7066 | |||
| 0c7c7edcab | |||
| a596ecaecc | |||
| 265cffe8f0 | |||
| b98fb08353 | |||
| 61e00c2de5 | |||
| e0b53236bb | |||
| 2f3a619284 | |||
| 5aef44c3b3 | |||
| 99e97aadcc | |||
| 17e2ce3138 | |||
| 049dc1f7b5 | |||
| cf7b68c026 | |||
| 223c39cec0 | |||
| 5335f24e9b | |||
| 7ec185bfe8 | |||
| e0e6723222 | |||
| 4b4ebb4214 | |||
| 60cd515fd6 | |||
| 396f8453d1 | |||
| f5eb343bd4 | |||
| c4f96c0376 | |||
| 4d3597ef0e | |||
| fb72817de6 | |||
| 569cec05cf | |||
| 78b9b12592 | |||
| 9143281290 | |||
| badfa30668 | |||
| 0144a2ee80 | |||
| 880894d026 | |||
| 7b5b17918c | |||
| c996e7447c | |||
| 61fa56e84b | |||
| a6bf3e0ae0 | |||
| 620f6989d2 | |||
| 715db33138 | |||
| e99abd26b3 | |||
| 099d17aa58 | |||
| 92d1239a98 | |||
| b7ce3d226e | |||
| 5b56d596e2 | |||
| e820f34253 | |||
| c75912ab79 | |||
| e4481c9e4f | |||
| e9b9a2bd78 | |||
| fc78958f76 | |||
| c7b97567a9 | |||
| f73ca8bc2c | |||
| 176eca3dce | |||
| 4e0e44c45d | |||
| e09e3cd8ac | |||
| 502f82ad14 | |||
| 2bb238b092 | |||
| 491fe1fbea | |||
| 6b6f40c968 | |||
| 14bd29bf42 | |||
| cf4d8fc378 | |||
| 10b756f6db | |||
| 4cd697aae4 | |||
| db0f7329e4 | |||
| fb710c397b | |||
| b1adc60719 | |||
| 423e61b285 | |||
| 895d575cdb | |||
| cd75e9649b | |||
| c60dfc1584 | |||
| 16004051e9 | |||
| 864a66f1ea | |||
| 0c7bf9a92d | |||
| b294cec542 | |||
| 11e8d13f02 |
+77
-76
@@ -4,65 +4,66 @@ import Poly
|
|||||||
|
|
||||||
// MARK: - Preamble (setup)
|
// MARK: - Preamble (setup)
|
||||||
|
|
||||||
// We make String a CreatableRawIdType. This is actually done in
|
// Make String a CreatableRawIdType. This is actually done in
|
||||||
// this Playground's Entities.swift file, so it is commented out here.
|
// this Playground's Entities.swift file, so it is commented out here.
|
||||||
/*
|
/*
|
||||||
var GlobalStringId: Int = 0
|
var globalStringId: Int = 0
|
||||||
extension String: CreatableRawIdType {
|
extension String: CreatableRawIdType {
|
||||||
public static func unique() -> String {
|
public static func unique() -> String {
|
||||||
GlobalStringId += 1
|
globalStringId += 1
|
||||||
return String(GlobalStringId)
|
return String(globalStringId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// We create a typealias given that we do not expect JSON:API Resource
|
// Create a typealias because we do not expect JSON:API Resource
|
||||||
// Objects for this particular API to have Metadata or Links associated
|
// Objects for this particular API to have Metadata or Links associated
|
||||||
// with them. We also expect them to have String Identifiers.
|
// with them. We also expect them to have String Identifiers.
|
||||||
typealias JSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
|
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||||
|
|
||||||
// Similarly, we create a typealias for unidentified entities. JSON:API
|
// Similarly, create a typealias for unidentified entities. JSON:API
|
||||||
// only allows unidentified entities (i.e. no "id" field) for client
|
// only allows unidentified entities (i.e. no "id" field) for client
|
||||||
// requests that create new entities. In these situations, the server
|
// requests that create new entities. In these situations, the server
|
||||||
// is expected to assign the new entity a unique ID.
|
// is expected to assign the new entity a unique ID.
|
||||||
typealias UnidentifiedJSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
|
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||||
|
|
||||||
// We create typealiases given that we do not expect JSON:API Relationships
|
// Create relationship typealiases because we do not expect
|
||||||
// for this particular API to have Metadata or Links associated
|
// JSON:API Relationships for this particular API to have
|
||||||
// with them.
|
// Metadata or Links associated with them.
|
||||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<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
|
// Create a typealias for a Document because we do not expect
|
||||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
// useful Errors, or an APIDescription (The *SPEC* calls this
|
||||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
// "API Description" the "JSON:API Object").
|
||||||
|
typealias Document<PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
|
||||||
|
|
||||||
// MARK: Entity Definitions
|
// MARK: Entity Definitions
|
||||||
|
|
||||||
enum AuthorDescription: EntityDescription {
|
enum AuthorDescription: ResourceObjectDescription {
|
||||||
public static var jsonType: String { return "authors" }
|
public static var jsonType: String { return "authors" }
|
||||||
|
|
||||||
public struct Attributes: JSONAPI.Attributes {
|
public struct Attributes: JSONAPI.Attributes {
|
||||||
public let name: Attribute<String>
|
public let name: Attribute<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
public typealias Relationships = NoRelationships
|
public typealias Relationships = NoRelationships
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Author = JSONEntity<AuthorDescription>
|
typealias Author = JSONEntity<AuthorDescription>
|
||||||
|
|
||||||
enum ArticleDescription: EntityDescription {
|
enum ArticleDescription: ResourceObjectDescription {
|
||||||
public static var jsonType: String { return "articles" }
|
public static var jsonType: String { return "articles" }
|
||||||
|
|
||||||
public struct Attributes: JSONAPI.Attributes {
|
public struct Attributes: JSONAPI.Attributes {
|
||||||
public let title: Attribute<String>
|
public let title: Attribute<String>
|
||||||
public let abstract: Attribute<String>
|
public let abstract: Attribute<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Relationships: JSONAPI.Relationships {
|
public struct Relationships: JSONAPI.Relationships {
|
||||||
public let author: ToOneRelationship<Author>
|
public let author: ToOneRelationship<Author>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Article = JSONEntity<ArticleDescription>
|
typealias Article = JSONEntity<ArticleDescription>
|
||||||
@@ -83,38 +84,38 @@ typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoInclud
|
|||||||
// that creates a document. Note that this document is the entirety
|
// that creates a document. Note that this document is the entirety
|
||||||
// of a JSON:API response body.
|
// of a JSON:API response body.
|
||||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||||
// Let's pretend all of this is coming from a database:
|
// Let's pretend all of this is coming from a database:
|
||||||
|
|
||||||
let authorId = Author.Identifier(rawValue: "1234")
|
let authorId = Author.Identifier(rawValue: "1234")
|
||||||
|
|
||||||
let article = Article(id: .init(rawValue: "5678"),
|
let article = Article(id: .init(rawValue: "5678"),
|
||||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||||
abstract: .init(value: "Not yet written")),
|
abstract: .init(value: "Not yet written")),
|
||||||
relationships: .init(author: .init(id: authorId)),
|
relationships: .init(author: .init(id: authorId)),
|
||||||
meta: .none,
|
meta: .none,
|
||||||
links: .none)
|
links: .none)
|
||||||
|
|
||||||
let document = SingleArticleDocument(apiDescription: .none,
|
let document = SingleArticleDocument(apiDescription: .none,
|
||||||
body: .init(entity: article),
|
body: .init(resourceObject: article),
|
||||||
includes: .none,
|
includes: .none,
|
||||||
meta: .none,
|
meta: .none,
|
||||||
links: .none)
|
links: .none)
|
||||||
|
|
||||||
switch includeAuthor {
|
switch includeAuthor {
|
||||||
case false:
|
case false:
|
||||||
return .a(document)
|
return .init(document)
|
||||||
|
|
||||||
case true:
|
case true:
|
||||||
let author = Author(id: authorId,
|
let author = Author(id: authorId,
|
||||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||||
relationships: .none,
|
relationships: .none,
|
||||||
meta: .none,
|
meta: .none,
|
||||||
links: .none)
|
links: .none)
|
||||||
|
|
||||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||||
|
|
||||||
return .b(document.including(.init(values: [.init(author)])))
|
return .init(document.including(includes))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let encoder = JSONEncoder()
|
let encoder = JSONEncoder()
|
||||||
@@ -124,8 +125,8 @@ encoder.outputFormatting = .prettyPrinted
|
|||||||
let responseBody = articleDocument(includeAuthor: true)
|
let responseBody = articleDocument(includeAuthor: true)
|
||||||
let responseData = try! encoder.encode(responseBody)
|
let responseData = try! encoder.encode(responseBody)
|
||||||
|
|
||||||
// Next step would be encoding and setting as the HTTP body of a response.
|
// Next step would be setting the HTTP body of a response.
|
||||||
// we will just print it out instead:
|
// We will just print it out instead:
|
||||||
print("-----")
|
print("-----")
|
||||||
print(String(data: responseData, encoding: .utf8)!)
|
print(String(data: responseData, encoding: .utf8)!)
|
||||||
|
|
||||||
@@ -139,31 +140,31 @@ print(String(data: otherResponseData, encoding: .utf8)!)
|
|||||||
// MARK: - Client Pseudo-example
|
// MARK: - Client Pseudo-example
|
||||||
|
|
||||||
enum NetworkError: Swift.Error {
|
enum NetworkError: Swift.Error {
|
||||||
case serverError
|
case serverError
|
||||||
case quantityMismatch
|
case quantityMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skipping over all the API stuff, here's a chunk of code that will
|
// 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
|
// decode a document. We will assume we have made a request for a
|
||||||
// single article including the author.
|
// single article including the author.
|
||||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||||
let decoder = JSONDecoder()
|
let decoder = JSONDecoder()
|
||||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||||
|
|
||||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||||
|
|
||||||
switch articleDocument.body {
|
switch articleDocument.body {
|
||||||
case .data(let data):
|
case .data(let data):
|
||||||
let authors = data.includes[Author.self]
|
let authors = data.includes[Author.self]
|
||||||
|
|
||||||
guard authors.count == 1 else {
|
guard authors.count == 1 else {
|
||||||
throw NetworkError.quantityMismatch
|
throw NetworkError.quantityMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
return (article: data.primary.value, author: authors[0])
|
return (article: data.primary.value, author: authors[0])
|
||||||
case .errors(let errors, meta: _, links: _):
|
case .errors(let errors, meta: _, links: _):
|
||||||
throw NetworkError.serverError
|
throw NetworkError.serverError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let response = try! docode(articleResponseData: responseData)
|
let response = try! docode(articleResponseData: responseData)
|
||||||
|
|||||||
+5
-5
@@ -64,7 +64,7 @@ struct ToManyRelationshipLinks: JSONAPI.Links {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Description of an Author entity.
|
/// Description of an Author entity.
|
||||||
enum AuthorDescription: EntityDescription {
|
enum AuthorDescription: ResourceObjectDescription {
|
||||||
static var jsonType: String { return "authors" }
|
static var jsonType: String { return "authors" }
|
||||||
|
|
||||||
struct Attributes: JSONAPI.Attributes {
|
struct Attributes: JSONAPI.Attributes {
|
||||||
@@ -76,10 +76,10 @@ enum AuthorDescription: EntityDescription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Author = JSONAPI.Entity<AuthorDescription, EntityMetadata, EntityLinks, String>
|
typealias Author = JSONAPI.ResourceObject<AuthorDescription, EntityMetadata, EntityLinks, String>
|
||||||
|
|
||||||
/// Description of an Article entity.
|
/// Description of an Article entity.
|
||||||
enum ArticleDescription: EntityDescription {
|
enum ArticleDescription: ResourceObjectDescription {
|
||||||
static var jsonType: String { return "articles" }
|
static var jsonType: String { return "articles" }
|
||||||
|
|
||||||
struct Attributes: JSONAPI.Attributes {
|
struct Attributes: JSONAPI.Attributes {
|
||||||
@@ -96,7 +96,7 @@ enum ArticleDescription: EntityDescription {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
typealias Article = JSONAPI.Entity<ArticleDescription, EntityMetadata, EntityLinks, String>
|
typealias Article = JSONAPI.ResourceObject<ArticleDescription, EntityMetadata, EntityLinks, String>
|
||||||
|
|
||||||
/// Metadata associated with the API Description
|
/// Metadata associated with the API Description
|
||||||
struct APIDescriptionMetadata: JSONAPI.Meta {
|
struct APIDescriptionMetadata: JSONAPI.Meta {
|
||||||
@@ -196,7 +196,7 @@ let documentLinks = SingleArticleDocumentLinks(otherArticlesByPrimaryAuthor: .in
|
|||||||
meta: .init(expiry: tomorrow)))
|
meta: .init(expiry: tomorrow)))
|
||||||
|
|
||||||
let singleArticleDocument = SingleArticleDocument(apiDescription: apiDescription,
|
let singleArticleDocument = SingleArticleDocument(apiDescription: apiDescription,
|
||||||
body: .init(entity: article),
|
body: .init(resourceObject: article),
|
||||||
includes: .init(values: [.init(author1), .init(author2), .init(author3)]),
|
includes: .init(values: [.init(author1), .init(author2), .init(author3)]),
|
||||||
meta: documentMetadata,
|
meta: documentMetadata,
|
||||||
links: documentLinks)
|
links: documentLinks)
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
//: [Previous](@previous)
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import JSONAPI
|
||||||
|
|
||||||
|
/*******
|
||||||
|
|
||||||
|
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
|
||||||
|
|
||||||
|
This playground focuses on receiving a resource, making some changes, and then creating a request body for a PATCH request.
|
||||||
|
As with all examples in these playround pages, no actual networking code will be provided.
|
||||||
|
|
||||||
|
********/
|
||||||
|
|
||||||
|
// Mock up a server response
|
||||||
|
let mockDogData = """
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"id": "1234",
|
||||||
|
"type": "dogs",
|
||||||
|
"attributes": {
|
||||||
|
"name": "Sparky"
|
||||||
|
},
|
||||||
|
"relationships": {
|
||||||
|
"owner": {
|
||||||
|
"data": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: - EXAMPLE 1 (Mutable Attributes)
|
||||||
|
//
|
||||||
|
|
||||||
|
// pretend to have requested a Dog and received the mock data
|
||||||
|
// now parse it.
|
||||||
|
let parsedResponse = try! JSONDecoder().decode(MutableDogDocument.self, from: mockDogData)
|
||||||
|
|
||||||
|
// extract our Dog (skipping over any robustness to handle errors)
|
||||||
|
var dog = parsedResponse.body.primaryResource!.value
|
||||||
|
print("Received dog named: \(dog.name)")
|
||||||
|
|
||||||
|
// change the dog's name
|
||||||
|
let changedDog = dog.tappingAttributes { $0.name = .init(value: "Julia") }
|
||||||
|
|
||||||
|
// create a document to be used as a request body for a PATCH request
|
||||||
|
let patchRequest = MutableDogDocument(apiDescription: .none,
|
||||||
|
body: .init(resourceObject: changedDog),
|
||||||
|
includes: .none,
|
||||||
|
meta: .none,
|
||||||
|
links: .none)
|
||||||
|
|
||||||
|
// encode and send off to server
|
||||||
|
let encodedPatchRequest = try! JSONEncoder().encode(patchRequest)
|
||||||
|
print("----")
|
||||||
|
print(String(data: encodedPatchRequest, encoding:.utf8)!)
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: - EXAMPLE 2 (Immutable Attributes)
|
||||||
|
//
|
||||||
|
print()
|
||||||
|
print("####")
|
||||||
|
print()
|
||||||
|
|
||||||
|
// pretend to have requested a Dog and received the mock data
|
||||||
|
// now parse it.
|
||||||
|
let parsedResponse2 = try! JSONDecoder().decode(SingleDogDocument.self, from: mockDogData)
|
||||||
|
|
||||||
|
// extract our Dog (skipping over any robustness to handle errors)
|
||||||
|
var dog2 = parsedResponse2.body.primaryResource!.value
|
||||||
|
print("Received dog named: \(dog2.name)")
|
||||||
|
|
||||||
|
// change the dog's name
|
||||||
|
let changedDog2 = dog2.replacingAttributes { _ in
|
||||||
|
return .init(name: .init(value: "Nigel"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a document to be used as a request body for a PATCH request
|
||||||
|
let patchRequest2 = SingleDogDocument(apiDescription: .none,
|
||||||
|
body: .init(resourceObject: changedDog2),
|
||||||
|
includes: .none,
|
||||||
|
meta: .none,
|
||||||
|
links: .none)
|
||||||
|
|
||||||
|
// encode and send off to server
|
||||||
|
let encodedPatchRequest2 = try! JSONEncoder().encode(patchRequest2)
|
||||||
|
print("----")
|
||||||
|
print(String(data: encodedPatchRequest2, encoding:.utf8)!)
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// MARK: - EXAMPLE 3 (Change relationship)
|
||||||
|
//
|
||||||
|
print()
|
||||||
|
print("####")
|
||||||
|
print()
|
||||||
|
|
||||||
|
// pretend to have requested a Dog and received the mock data
|
||||||
|
// now parse it.
|
||||||
|
let parsedResponse3 = try! JSONDecoder().decode(SingleDogDocument.self, from: mockDogData)
|
||||||
|
|
||||||
|
// extract our Dog (skipping over any robustness to handle errors)
|
||||||
|
var dog3 = parsedResponse2.body.primaryResource!.value
|
||||||
|
print("Received dog with owner: \(dog3 ~> \.owner)")
|
||||||
|
|
||||||
|
// give the dog an owner
|
||||||
|
let changedDog3 = dog3.replacingRelationships { _ in
|
||||||
|
return .init(owner: .init(id: Id(rawValue: "1")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a document to be used as a request body for a PATCH request
|
||||||
|
let patchRequest3 = SingleDogDocument(apiDescription: .none,
|
||||||
|
body: .init(resourceObject: changedDog3),
|
||||||
|
includes: .none,
|
||||||
|
meta: .none,
|
||||||
|
links: .none)
|
||||||
|
|
||||||
|
// encode and send off to server
|
||||||
|
let encodedPatchRequest3 = try! JSONEncoder().encode(patchRequest3)
|
||||||
|
print("----")
|
||||||
|
print(String(data: encodedPatchRequest3, encoding:.utf8)!)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
import JSONAPI
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// MARK: - Resource Object
|
||||||
|
|
||||||
|
enum ThingWithPropertiesDescription: JSONAPI.ResourceObjectDescription {
|
||||||
|
static let jsonType: String = "thing"
|
||||||
|
|
||||||
|
//
|
||||||
|
// NOTE: `JSONAPI.SparsableAttributes` as opposed to `JSONAPI.Attributes`
|
||||||
|
//
|
||||||
|
struct Attributes: JSONAPI.SparsableAttributes {
|
||||||
|
let stringThing: Attribute<String>
|
||||||
|
let numberThing: Attribute<Double>
|
||||||
|
let boolThing: Attribute<Bool?>
|
||||||
|
|
||||||
|
//
|
||||||
|
// NOTE: Special implementation of `CodingKeys`
|
||||||
|
//
|
||||||
|
enum CodingKeys: String, JSONAPI.SparsableCodingKey {
|
||||||
|
case stringThing
|
||||||
|
case numberThing
|
||||||
|
case boolThing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias Relationships = NoRelationships
|
||||||
|
}
|
||||||
|
|
||||||
|
typealias ThingWithProperties = JSONAPI.ResourceObject<ThingWithPropertiesDescription, NoMetadata, NoLinks, String>
|
||||||
|
|
||||||
|
// MARK: - Document
|
||||||
|
|
||||||
|
//
|
||||||
|
// NOTE: Using `JSONAPI.EncodableResourceBody` which means the document type will be `Encodable` but not `Decodable`.
|
||||||
|
//
|
||||||
|
typealias Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
|
||||||
|
|
||||||
|
//
|
||||||
|
// NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable.
|
||||||
|
//
|
||||||
|
typealias SingleDocument<T: JSONAPI.EncodablePrimaryResource> = Document<SingleResourceBody<T>, NoIncludes>
|
||||||
|
|
||||||
|
// MARK: - Resource Initialization
|
||||||
|
|
||||||
|
let resource = ThingWithProperties(id: .init(rawValue: "1234"),
|
||||||
|
attributes: .init(stringThing: .init(value: "hello world"),
|
||||||
|
numberThing: .init(value: 10),
|
||||||
|
boolThing: .init(value: nil)),
|
||||||
|
relationships: .none,
|
||||||
|
meta: .none,
|
||||||
|
links: .none)
|
||||||
|
//
|
||||||
|
// NOTE: Creating a sparse resource that will only encode
|
||||||
|
// the attribute named "stringThing"
|
||||||
|
//
|
||||||
|
let sparseResource = resource.sparse(with: [.stringThing])
|
||||||
|
|
||||||
|
// MARK: - Encoding
|
||||||
|
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
|
||||||
|
let sparseResourceDoc = SingleDocument(apiDescription: .none,
|
||||||
|
body: .init(resourceObject: sparseResource),
|
||||||
|
includes: .none,
|
||||||
|
meta: .none,
|
||||||
|
links: .none)
|
||||||
|
|
||||||
|
let data = try! encoder.encode(sparseResourceDoc)
|
||||||
|
|
||||||
|
print(String(data: data, encoding: .utf8)!)
|
||||||
@@ -8,24 +8,28 @@ 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
|
// MARK: - Create a request or response body with one Dog in it
|
||||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||||
|
|
||||||
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none)
|
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(resourceObject: dogFromCode), includes: .none, meta: .none, links: .none)
|
||||||
|
|
||||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Parse a request or response body with one Dog in it
|
// MARK: - Parse a request or response body with one Dog in it
|
||||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||||
let dogFromData = dogResponse.body.primaryResource?.value
|
let dogFromData = dogResponse.body.primaryResource?.value
|
||||||
let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
|
let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
|
||||||
|
|
||||||
// MARKL - Parse a request or response body with one Dog in it using an alternative model
|
|
||||||
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
// MARK: - Parse a request or response body with one Dog in it using an alternative model
|
||||||
|
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||||
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
|
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
|
||||||
let altDogFromData = altDogResponse.body.primaryResource?.value
|
let altDogFromData = altDogResponse.body.primaryResource?.value
|
||||||
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||||
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
||||||
@@ -33,9 +37,10 @@ let houses = [House(attributes: .none, relationships: .none, meta: .none, links:
|
|||||||
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])]
|
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])]
|
||||||
|
|
||||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||||
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none)
|
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(resourceObjects: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||||
|
|
||||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||||
@@ -47,20 +52,48 @@ print("-----")
|
|||||||
print(peopleResponse)
|
print(peopleResponse)
|
||||||
print("-----")
|
print("-----")
|
||||||
|
|
||||||
|
|
||||||
// MARK: - Pass successfully parsed body to other parts of the code
|
// MARK: - Pass successfully parsed body to other parts of the code
|
||||||
|
|
||||||
if case let .data(bodyData) = peopleResponse.body {
|
if case let .data(bodyData) = peopleResponse.body {
|
||||||
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
|
print("first person's name: \(bodyData.primary.values[0].fullName)")
|
||||||
} else {
|
} else {
|
||||||
print("no body data")
|
print("no body data")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Work in the abstract
|
|
||||||
|
|
||||||
func process<T: JSONAPIDocument>(document: T) {
|
// MARK: - Work in the abstract
|
||||||
guard case let .data(body) = document.body else {
|
print("-----")
|
||||||
|
func process<T: CodableJSONAPIDocument>(document: T) {
|
||||||
|
guard let body = document.body.data else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let x: T.Body.Data = body
|
let x: T.BodyData = body
|
||||||
}
|
}
|
||||||
process(document: peopleResponse)
|
process(document: peopleResponse)
|
||||||
|
|
||||||
|
// MARK: - Work with errors
|
||||||
|
typealias ErrorDoc = JSONAPI.Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||||
|
|
||||||
|
let mockErrorData =
|
||||||
|
"""
|
||||||
|
{
|
||||||
|
"errors": [
|
||||||
|
{
|
||||||
|
"status": "500",
|
||||||
|
"title": "Internal Server Error",
|
||||||
|
"detail": "Server fell over while parsing your request."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".data(using: .utf8)!
|
||||||
|
|
||||||
|
let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData)
|
||||||
|
|
||||||
|
switch errorResponse.body {
|
||||||
|
case .data:
|
||||||
|
print("cool, data!")
|
||||||
|
case .errors(let errors, let meta, let links):
|
||||||
|
let errorDetails = errors.compactMap { $0.payload?.detail }
|
||||||
|
print("error details: \(errorDetails)")
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,21 +15,21 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
|||||||
********/
|
********/
|
||||||
|
|
||||||
// MARK: - String as CreatableRawIdType
|
// MARK: - String as CreatableRawIdType
|
||||||
var GlobalStringId: Int = 0
|
var globalStringId: Int = 0
|
||||||
extension String: CreatableRawIdType {
|
extension String: CreatableRawIdType {
|
||||||
public static func unique() -> String {
|
public static func unique() -> String {
|
||||||
GlobalStringId += 1
|
globalStringId += 1
|
||||||
return String(GlobalStringId)
|
return String(globalStringId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - typealiases for convenience
|
// MARK: - typealiases for convenience
|
||||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
|
public typealias ExampleEntity<Description: ResourceObjectDescription> = ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||||
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
|
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
|
||||||
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
|
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
|
||||||
|
|
||||||
// MARK: - A few resource objects (entities)
|
// MARK: - A few resource objects (entities)
|
||||||
public enum PersonDescription: EntityDescription {
|
public enum PersonDescription: ResourceObjectDescription {
|
||||||
|
|
||||||
public static var jsonType: String { return "people" }
|
public static var jsonType: String { return "people" }
|
||||||
|
|
||||||
@@ -62,13 +62,13 @@ public enum PersonDescription: EntityDescription {
|
|||||||
|
|
||||||
public typealias Person = ExampleEntity<PersonDescription>
|
public typealias Person = ExampleEntity<PersonDescription>
|
||||||
|
|
||||||
public extension Entity where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
public extension ResourceObject where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||||
init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||||
self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
|
self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum DogDescription: EntityDescription {
|
public enum DogDescription: ResourceObjectDescription {
|
||||||
|
|
||||||
public static var jsonType: String { return "dogs" }
|
public static var jsonType: String { return "dogs" }
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ public enum DogDescription: EntityDescription {
|
|||||||
|
|
||||||
public typealias Dog = ExampleEntity<DogDescription>
|
public typealias Dog = ExampleEntity<DogDescription>
|
||||||
|
|
||||||
public enum AlternativeDogDescription: EntityDescription {
|
public enum AlternativeDogDescription: ResourceObjectDescription {
|
||||||
|
|
||||||
public static var jsonType: String { return "dogs" }
|
public static var jsonType: String { return "dogs" }
|
||||||
|
|
||||||
@@ -119,9 +119,32 @@ public enum AlternativeDogDescription: EntityDescription {
|
|||||||
|
|
||||||
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
|
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
|
||||||
|
|
||||||
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
public enum MutableDogDescription: ResourceObjectDescription {
|
||||||
|
|
||||||
|
public static var jsonType: String { return "dogs" }
|
||||||
|
|
||||||
|
public struct Attributes: JSONAPI.Attributes {
|
||||||
|
public var name: Attribute<String>
|
||||||
|
|
||||||
|
public init(name: Attribute<String>) {
|
||||||
|
self.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Relationships: JSONAPI.Relationships {
|
||||||
|
public var owner: ToOne<Person?>
|
||||||
|
|
||||||
|
public init(owner: ToOne<Person?>) {
|
||||||
|
self.owner = owner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public typealias MutableDog = ExampleEntity<MutableDogDescription>
|
||||||
|
|
||||||
|
public extension ResourceObject where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||||
init(name: String, owner: Person?) throws {
|
init(name: String, owner: Person?) throws {
|
||||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
|
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(name: String, owner: Person.Id) throws {
|
init(name: String, owner: Person.Id) throws {
|
||||||
@@ -129,7 +152,7 @@ public extension Entity where Description == DogDescription, MetaType == NoMetad
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum HouseDescription: EntityDescription {
|
public enum HouseDescription: ResourceObjectDescription {
|
||||||
|
|
||||||
public static var jsonType: String { return "houses" }
|
public static var jsonType: String { return "houses" }
|
||||||
|
|
||||||
@@ -139,6 +162,8 @@ public enum HouseDescription: EntityDescription {
|
|||||||
|
|
||||||
public typealias House = ExampleEntity<HouseDescription>
|
public typealias House = ExampleEntity<HouseDescription>
|
||||||
|
|
||||||
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||||
|
|
||||||
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
public typealias MutableDogDocument = JSONAPI.Document<SingleResourceBody<MutableDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||||
|
|
||||||
|
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, BasicJSONAPIError<String>>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<playground version='6.0' target-platform='macos' executeOnSourceChanges='false'>
|
<playground version='6.0' target-platform='macos'>
|
||||||
<pages>
|
<pages>
|
||||||
<page name='Test Library'/>
|
<page name='Test Library'/>
|
||||||
<page name='Usage'/>
|
<page name='Usage'/>
|
||||||
<page name='Full Client & Server Example'/>
|
<page name='Full Client & Server Example'/>
|
||||||
<page name='Full Document Verbose Generation'/>
|
<page name='Full Document Verbose Generation'/>
|
||||||
|
<page name='Sparse Fieldsets Example'/>
|
||||||
|
<page name='PATCHing'/>
|
||||||
</pages>
|
</pages>
|
||||||
</playground>
|
</playground>
|
||||||
+141
@@ -0,0 +1,141 @@
|
|||||||
|
#
|
||||||
|
# Be sure to run `pod spec lint JSONAPI.podspec' to ensure this is a
|
||||||
|
# valid spec and to remove all comments including this before submitting the spec.
|
||||||
|
#
|
||||||
|
# To learn more about Podspec attributes see https://docs.cocoapods.org/specification.html
|
||||||
|
# To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
|
||||||
|
#
|
||||||
|
|
||||||
|
Pod::Spec.new do |spec|
|
||||||
|
|
||||||
|
# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# These will help people to find your library, and whilst it
|
||||||
|
# can feel like a chore to fill in it's definitely to your advantage. The
|
||||||
|
# summary should be tweet-length, and the description more in depth.
|
||||||
|
#
|
||||||
|
|
||||||
|
spec.name = "MP-JSONAPI"
|
||||||
|
spec.version = "3.0.0"
|
||||||
|
spec.summary = "Swift Codable JSON API framework."
|
||||||
|
|
||||||
|
# This description is used to generate tags and improve search results.
|
||||||
|
# * Think: What does it do? Why did you write it? What is the focus?
|
||||||
|
# * Try to keep it short, snappy and to the point.
|
||||||
|
# * Write the description between the DESC delimiters below.
|
||||||
|
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||||
|
spec.description = <<-DESC
|
||||||
|
A Swift package for encoding to- and decoding from JSON API compliant requests and responses.
|
||||||
|
|
||||||
|
See the JSON API Spec here: https://jsonapi.org/format/
|
||||||
|
DESC
|
||||||
|
|
||||||
|
spec.homepage = "https://github.com/mattpolzin/JSONAPI"
|
||||||
|
# spec.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif"
|
||||||
|
|
||||||
|
|
||||||
|
# ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# Licensing your code is important. See https://choosealicense.com for more info.
|
||||||
|
# CocoaPods will detect a license file if there is a named LICENSE*
|
||||||
|
# Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
|
||||||
|
#
|
||||||
|
|
||||||
|
# spec.license = "MIT"
|
||||||
|
spec.license = { :type => "MIT", :file => "LICENSE.txt" }
|
||||||
|
|
||||||
|
|
||||||
|
# ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# Specify the authors of the library, with email addresses. Email addresses
|
||||||
|
# of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
|
||||||
|
# accepts just a name if you'd rather not provide an email address.
|
||||||
|
#
|
||||||
|
# Specify a social_media_url where others can refer to, for example a twitter
|
||||||
|
# profile URL.
|
||||||
|
#
|
||||||
|
|
||||||
|
spec.author = { "Mathew Polzin" => "matt.polzin@gmail.com" }
|
||||||
|
# Or just: spec.author = "Mathew Polzin"
|
||||||
|
# spec.authors = { "Mathew Polzin" => "matt.polzin@gmail.com" }
|
||||||
|
# spec.social_media_url = "https://twitter.com/Mathew Polzin"
|
||||||
|
|
||||||
|
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# If this Pod runs only on iOS or OS X, then specify the platform and
|
||||||
|
# the deployment target. You can optionally include the target after the platform.
|
||||||
|
#
|
||||||
|
|
||||||
|
# spec.platform = :ios
|
||||||
|
# spec.platform = :ios, "5.0"
|
||||||
|
|
||||||
|
# When using multiple platforms
|
||||||
|
spec.ios.deployment_target = "8.0"
|
||||||
|
spec.osx.deployment_target = "10.9"
|
||||||
|
# spec.watchos.deployment_target = "2.0"
|
||||||
|
# spec.tvos.deployment_target = "9.0"
|
||||||
|
|
||||||
|
|
||||||
|
# ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# Specify the location from where the source should be retrieved.
|
||||||
|
# Supports git, hg, bzr, svn and HTTP.
|
||||||
|
#
|
||||||
|
|
||||||
|
spec.source = { :git => "https://github.com/mattpolzin/JSONAPI.git", :tag => "#{spec.version}" }
|
||||||
|
|
||||||
|
|
||||||
|
# ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# CocoaPods is smart about how it includes source code. For source files
|
||||||
|
# giving a folder will include any swift, h, m, mm, c & cpp files.
|
||||||
|
# For header files it will include any header in the folder.
|
||||||
|
# Not including the public_header_files will make all headers public.
|
||||||
|
#
|
||||||
|
|
||||||
|
spec.source_files = "Sources/JSONAPI/**/*.{swift}"
|
||||||
|
# spec.exclude_files = "Classes/Exclude"
|
||||||
|
|
||||||
|
# spec.public_header_files = "Classes/**/*.h"
|
||||||
|
|
||||||
|
|
||||||
|
# ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# A list of resources included with the Pod. These are copied into the
|
||||||
|
# target bundle with a build phase script. Anything else will be cleaned.
|
||||||
|
# You can preserve files from being cleaned, please don't preserve
|
||||||
|
# non-essential files like tests, examples and documentation.
|
||||||
|
#
|
||||||
|
|
||||||
|
# spec.resource = "icon.png"
|
||||||
|
# spec.resources = "Resources/*.png"
|
||||||
|
|
||||||
|
# spec.preserve_paths = "FilesToSave", "MoreFilesToSave"
|
||||||
|
|
||||||
|
|
||||||
|
# ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# Link your library with frameworks, or libraries. Libraries do not include
|
||||||
|
# the lib prefix of their name.
|
||||||
|
#
|
||||||
|
|
||||||
|
# spec.frameworks = "SomeFramework", "AnotherFramework"
|
||||||
|
|
||||||
|
# spec.library = "iconv"
|
||||||
|
# spec.libraries = "iconv", "xml2"
|
||||||
|
|
||||||
|
|
||||||
|
# ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||||
|
#
|
||||||
|
# If your library depends on compiler flags you can set them in the xcconfig hash
|
||||||
|
# where they will only apply to your library. If you depend on other Podspecs
|
||||||
|
# you can include multiple dependencies to ensure it works.
|
||||||
|
|
||||||
|
spec.swift_version = "5.1"
|
||||||
|
spec.module_name = "JSONAPI"
|
||||||
|
# spec.requires_arc = true
|
||||||
|
|
||||||
|
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
|
||||||
|
spec.dependency "Poly", "~> 2.3.1"
|
||||||
|
|
||||||
|
end
|
||||||
+2
-2
@@ -6,8 +6,8 @@
|
|||||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07",
|
"revision": "0c9c08204142babc480938d704a23513d11420e5",
|
||||||
"version": "1.0.0"
|
"version": "2.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+13
-9
@@ -1,28 +1,32 @@
|
|||||||
// swift-tools-version:4.2
|
// swift-tools-version:5.1
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
|
|
||||||
let package = Package(
|
let package = Package(
|
||||||
name: "JSONAPI",
|
name: "JSONAPI",
|
||||||
|
platforms: [
|
||||||
|
.macOS(.v10_10),
|
||||||
|
.iOS(.v10)
|
||||||
|
],
|
||||||
products: [
|
products: [
|
||||||
.library(
|
.library(
|
||||||
name: "JSONAPI",
|
name: "JSONAPI",
|
||||||
targets: ["JSONAPI"]),
|
targets: ["JSONAPI"]),
|
||||||
.library(
|
.library(
|
||||||
name: "JSONAPITesting",
|
name: "JSONAPITesting",
|
||||||
targets: ["JSONAPITesting"])
|
targets: ["JSONAPITesting"])
|
||||||
],
|
],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://github.com/mattpolzin/Poly.git", from: "1.0.0"),
|
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.1")),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
name: "JSONAPI",
|
name: "JSONAPI",
|
||||||
dependencies: ["Poly"]),
|
dependencies: ["Poly"]),
|
||||||
.target(
|
.target(
|
||||||
name: "JSONAPITesting",
|
name: "JSONAPITesting",
|
||||||
dependencies: ["JSONAPI"]),
|
dependencies: ["JSONAPI"]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "JSONAPITests",
|
name: "JSONAPITests",
|
||||||
dependencies: ["JSONAPI", "JSONAPITesting"]),
|
dependencies: ["JSONAPI", "JSONAPITesting"]),
|
||||||
@@ -30,5 +34,5 @@ let package = Package(
|
|||||||
name: "JSONAPITestingTests",
|
name: "JSONAPITestingTests",
|
||||||
dependencies: ["JSONAPI", "JSONAPITesting"])
|
dependencies: ["JSONAPI", "JSONAPITesting"])
|
||||||
],
|
],
|
||||||
swiftLanguageVersions: [.v4_2]
|
swiftLanguageVersions: [.v5]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,58 +7,58 @@
|
|||||||
|
|
||||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||||
public protocol APIDescriptionType: Codable, Equatable {
|
public protocol APIDescriptionType: Codable, Equatable {
|
||||||
associatedtype Meta
|
associatedtype Meta
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||||
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
|
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
|
||||||
public let version: String
|
public let version: String
|
||||||
public let meta: Meta
|
public let meta: Meta
|
||||||
|
|
||||||
public init(version: String, meta: Meta) {
|
public init(version: String, meta: Meta) {
|
||||||
self.version = version
|
self.version = version
|
||||||
self.meta = meta
|
self.meta = meta
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Can be used as `APIDescriptionType` for Documents that do not
|
/// Can be used as `APIDescriptionType` for Documents that do not
|
||||||
/// have any API Description (a.k.a. "JSON:API Object").
|
/// have any API Description (a.k.a. "JSON:API Object").
|
||||||
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
|
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
|
||||||
public typealias Meta = NoMetadata
|
public typealias Meta = NoMetadata
|
||||||
|
|
||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public static var none: NoAPIDescription { return .init() }
|
public static var none: NoAPIDescription { return .init() }
|
||||||
|
|
||||||
public var description: String { return "No JSON:API Object" }
|
public var description: String { return "No JSON:API Object" }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension APIDescription {
|
extension APIDescription {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case version
|
case version
|
||||||
case meta
|
case meta
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
|
// 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"
|
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
|
||||||
|
|
||||||
if let metaVal = NoMetadata() as? Meta {
|
if let metaVal = NoMetadata() as? Meta {
|
||||||
meta = metaVal
|
meta = metaVal
|
||||||
} else {
|
} else {
|
||||||
meta = try container.decode(Meta.self, forKey: .meta)
|
meta = try container.decode(Meta.self, forKey: .meta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
try container.encode(version, forKey: .version)
|
try container.encode(version, forKey: .version)
|
||||||
|
|
||||||
if Meta.self != NoMetadata.self {
|
if Meta.self != NoMetadata.self {
|
||||||
try container.encode(meta, forKey: .meta)
|
try container.encode(meta, forKey: .meta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
|||||||
|
//
|
||||||
|
// DocumentDecodingError.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by Mathew Polzin on 10/20/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
public enum DocumentDecodingError: Swift.Error, Equatable {
|
||||||
|
case primaryResource(error: ResourceObjectDecodingError, idx: Int?)
|
||||||
|
case primaryResourceMissing
|
||||||
|
case primaryResourcesMissing
|
||||||
|
|
||||||
|
case includes(error: IncludesDecodingError)
|
||||||
|
|
||||||
|
case foundErrorDocumentWhenExpectingSuccess
|
||||||
|
case foundSuccessDocumentWhenExpectingError
|
||||||
|
|
||||||
|
init(_ decodingError: ResourceObjectDecodingError) {
|
||||||
|
self = .primaryResource(error: decodingError, idx: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ decodingError: ManyResourceBodyDecodingError) {
|
||||||
|
self = .primaryResource(error: decodingError.error, idx: decodingError.idx)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ decodingError: IncludesDecodingError) {
|
||||||
|
self = .includes(error: decodingError)
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(_ decodingError: DecodingError) {
|
||||||
|
switch decodingError {
|
||||||
|
case .valueNotFound(let type, let context) where Location(context) == .data && type is AbstractResourceObject.Type:
|
||||||
|
self = .primaryResourceMissing
|
||||||
|
case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self:
|
||||||
|
self = .primaryResourcesMissing
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Location: Equatable {
|
||||||
|
case data
|
||||||
|
|
||||||
|
init?(_ context: DecodingError.Context) {
|
||||||
|
guard context.codingPath.contains(where: { $0.stringValue == "data" }) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self = .data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DocumentDecodingError: CustomStringConvertible {
|
||||||
|
public var description: String {
|
||||||
|
switch self {
|
||||||
|
case .primaryResource(error: let error, idx: let idx):
|
||||||
|
let idxString = idx.map { " \($0 + 1)" } ?? ""
|
||||||
|
return "Primary Resource\(idxString) failed to parse because \(error)"
|
||||||
|
case .primaryResourceMissing:
|
||||||
|
return "Primary Resource missing."
|
||||||
|
case .primaryResourcesMissing:
|
||||||
|
return "Primary Resources array missing."
|
||||||
|
|
||||||
|
case .includes(error: let error):
|
||||||
|
return "\(error)"
|
||||||
|
|
||||||
|
case .foundErrorDocumentWhenExpectingSuccess:
|
||||||
|
return "Expected a success document with a 'data' property but found an error document."
|
||||||
|
case .foundSuccessDocumentWhenExpectingError:
|
||||||
|
return "Expected an error document but found a success document with a 'data' property."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
//
|
|
||||||
// Error.swift
|
|
||||||
// JSONAPI
|
|
||||||
//
|
|
||||||
// Created by Mathew Polzin on 11/10/18.
|
|
||||||
//
|
|
||||||
|
|
||||||
public protocol JSONAPIError: Swift.Error, Equatable, Codable {
|
|
||||||
static var unknown: Self { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum UnknownJSONAPIError: JSONAPIError {
|
|
||||||
case unknownError
|
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
|
||||||
self = .unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
|
||||||
var container = encoder.singleValueContainer()
|
|
||||||
try container.encode("unknown")
|
|
||||||
}
|
|
||||||
|
|
||||||
public static var unknown: UnknownJSONAPIError {
|
|
||||||
return .unknownError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,71 +7,110 @@
|
|||||||
|
|
||||||
import Poly
|
import Poly
|
||||||
|
|
||||||
public typealias Include = JSONPoly
|
public typealias Include = EncodableJSONPoly
|
||||||
|
|
||||||
public struct Includes<I: Include>: Codable, Equatable {
|
/// A structure holding zero or more included Resource Objects.
|
||||||
public static var none: Includes { return .init(values: []) }
|
/// The resources are accessed by their type using a subscript.
|
||||||
|
///
|
||||||
let values: [I]
|
/// If you have
|
||||||
|
///
|
||||||
public init(values: [I]) {
|
/// let includes: Includes<Include2<Thing1, Thing2>> = ...
|
||||||
self.values = values
|
///
|
||||||
}
|
/// then you can access all `Thing1` included resources with
|
||||||
|
///
|
||||||
public init(from decoder: Decoder) throws {
|
/// let includedThings = includes[Thing1.self]
|
||||||
var container = try decoder.unkeyedContainer()
|
public struct Includes<I: Include>: Encodable, Equatable {
|
||||||
|
public static var none: Includes { return .init(values: []) }
|
||||||
// If not parsing includes, no need to loop over them.
|
|
||||||
guard I.self != NoIncludes.self else {
|
|
||||||
values = []
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var valueAggregator = [I]()
|
|
||||||
while !container.isAtEnd {
|
|
||||||
valueAggregator.append(try container.decode(I.self))
|
|
||||||
}
|
|
||||||
|
|
||||||
values = valueAggregator
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
public let values: [I]
|
||||||
var container = encoder.unkeyedContainer()
|
|
||||||
|
|
||||||
guard I.self != NoIncludes.self else {
|
public init(values: [I]) {
|
||||||
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.")
|
self.values = values
|
||||||
}
|
}
|
||||||
|
|
||||||
for value in values {
|
public func encode(to encoder: Encoder) throws {
|
||||||
try container.encode(value)
|
var container = encoder.unkeyedContainer()
|
||||||
}
|
|
||||||
}
|
guard I.self != NoIncludes.self else {
|
||||||
|
throw JSONAPICodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath)
|
||||||
public var count: Int {
|
}
|
||||||
return values.count
|
|
||||||
}
|
for value in values {
|
||||||
|
try container.encode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var count: Int {
|
||||||
|
return values.count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Includes: Decodable where I: Decodable {
|
||||||
|
public init(from decoder: Decoder) throws {
|
||||||
|
var container = try decoder.unkeyedContainer()
|
||||||
|
|
||||||
|
// If not parsing includes, no need to loop over them.
|
||||||
|
guard I.self != NoIncludes.self else {
|
||||||
|
values = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var valueAggregator = [I]()
|
||||||
|
var idx = 0
|
||||||
|
while !container.isAtEnd {
|
||||||
|
do {
|
||||||
|
valueAggregator.append(try container.decode(I.self))
|
||||||
|
idx = idx + 1
|
||||||
|
} catch let error as PolyDecodeNoTypesMatchedError {
|
||||||
|
let errors: [ResourceObjectDecodingError] = error
|
||||||
|
.individualTypeFailures
|
||||||
|
.compactMap { decodingError in
|
||||||
|
switch decodingError.error {
|
||||||
|
case .typeMismatch(_, let context),
|
||||||
|
.valueNotFound(_, let context),
|
||||||
|
.keyNotFound(_, let context),
|
||||||
|
.dataCorrupted(let context):
|
||||||
|
return context.underlyingError as? ResourceObjectDecodingError
|
||||||
|
@unknown default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard errors.count == error.individualTypeFailures.count else {
|
||||||
|
throw IncludesDecodingError(error: error, idx: idx)
|
||||||
|
}
|
||||||
|
throw IncludesDecodingError(
|
||||||
|
error: IncludeDecodingError(failures: errors),
|
||||||
|
idx: idx
|
||||||
|
)
|
||||||
|
} catch let error {
|
||||||
|
throw IncludesDecodingError(error: error, idx: idx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
values = valueAggregator
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Includes {
|
extension Includes {
|
||||||
public func appending(_ other: Includes<I>) -> Includes {
|
public func appending(_ other: Includes<I>) -> Includes {
|
||||||
return Includes(values: values + other.values)
|
return Includes(values: values + other.values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
|
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
|
||||||
return left.appending(right)
|
return left.appending(right)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Includes: CustomStringConvertible {
|
extension Includes: CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
return "Includes(\(String(describing: values))"
|
return "Includes(\(String(describing: values))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Includes where I == NoIncludes {
|
extension Includes where I == NoIncludes {
|
||||||
public init() {
|
public init() {
|
||||||
values = []
|
values = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 0 includes
|
// MARK: - 0 includes
|
||||||
@@ -81,71 +120,116 @@ public typealias NoIncludes = Include0
|
|||||||
// MARK: - 1 include
|
// MARK: - 1 include
|
||||||
public typealias Include1 = Poly1
|
public typealias Include1 = Poly1
|
||||||
extension Includes where I: _Poly1 {
|
extension Includes where I: _Poly1 {
|
||||||
public subscript(_ lookup: I.A.Type) -> [I.A] {
|
public subscript(_ lookup: I.A.Type) -> [I.A] {
|
||||||
return values.compactMap { $0.a }
|
return values.compactMap { $0.a }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 2 includes
|
// MARK: - 2 includes
|
||||||
public typealias Include2 = Poly2
|
public typealias Include2 = Poly2
|
||||||
extension Includes where I: _Poly2 {
|
extension Includes where I: _Poly2 {
|
||||||
public subscript(_ lookup: I.B.Type) -> [I.B] {
|
public subscript(_ lookup: I.B.Type) -> [I.B] {
|
||||||
return values.compactMap { $0.b }
|
return values.compactMap { $0.b }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 3 includes
|
// MARK: - 3 includes
|
||||||
public typealias Include3 = Poly3
|
public typealias Include3 = Poly3
|
||||||
extension Includes where I: _Poly3 {
|
extension Includes where I: _Poly3 {
|
||||||
public subscript(_ lookup: I.C.Type) -> [I.C] {
|
public subscript(_ lookup: I.C.Type) -> [I.C] {
|
||||||
return values.compactMap { $0.c }
|
return values.compactMap { $0.c }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 4 includes
|
// MARK: - 4 includes
|
||||||
public typealias Include4 = Poly4
|
public typealias Include4 = Poly4
|
||||||
extension Includes where I: _Poly4 {
|
extension Includes where I: _Poly4 {
|
||||||
public subscript(_ lookup: I.D.Type) -> [I.D] {
|
public subscript(_ lookup: I.D.Type) -> [I.D] {
|
||||||
return values.compactMap { $0.d }
|
return values.compactMap { $0.d }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 5 includes
|
// MARK: - 5 includes
|
||||||
public typealias Include5 = Poly5
|
public typealias Include5 = Poly5
|
||||||
extension Includes where I: _Poly5 {
|
extension Includes where I: _Poly5 {
|
||||||
public subscript(_ lookup: I.E.Type) -> [I.E] {
|
public subscript(_ lookup: I.E.Type) -> [I.E] {
|
||||||
return values.compactMap { $0.e }
|
return values.compactMap { $0.e }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 6 includes
|
// MARK: - 6 includes
|
||||||
public typealias Include6 = Poly6
|
public typealias Include6 = Poly6
|
||||||
extension Includes where I: _Poly6 {
|
extension Includes where I: _Poly6 {
|
||||||
public subscript(_ lookup: I.F.Type) -> [I.F] {
|
public subscript(_ lookup: I.F.Type) -> [I.F] {
|
||||||
return values.compactMap { $0.f }
|
return values.compactMap { $0.f }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 7 includes
|
// MARK: - 7 includes
|
||||||
public typealias Include7 = Poly7
|
public typealias Include7 = Poly7
|
||||||
extension Includes where I: _Poly7 {
|
extension Includes where I: _Poly7 {
|
||||||
public subscript(_ lookup: I.G.Type) -> [I.G] {
|
public subscript(_ lookup: I.G.Type) -> [I.G] {
|
||||||
return values.compactMap { $0.g }
|
return values.compactMap { $0.g }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 8 includes
|
// MARK: - 8 includes
|
||||||
public typealias Include8 = Poly8
|
public typealias Include8 = Poly8
|
||||||
extension Includes where I: _Poly8 {
|
extension Includes where I: _Poly8 {
|
||||||
public subscript(_ lookup: I.H.Type) -> [I.H] {
|
public subscript(_ lookup: I.H.Type) -> [I.H] {
|
||||||
return values.compactMap { $0.h }
|
return values.compactMap { $0.h }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 9 includes
|
// MARK: - 9 includes
|
||||||
public typealias Include9 = Poly9
|
public typealias Include9 = Poly9
|
||||||
extension Includes where I: _Poly9 {
|
extension Includes where I: _Poly9 {
|
||||||
public subscript(_ lookup: I.I.Type) -> [I.I] {
|
public subscript(_ lookup: I.I.Type) -> [I.I] {
|
||||||
return values.compactMap { $0.i }
|
return values.compactMap { $0.i }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 10 includes
|
||||||
|
public typealias Include10 = Poly10
|
||||||
|
extension Includes where I: _Poly10 {
|
||||||
|
public subscript(_ lookup: I.J.Type) -> [I.J] {
|
||||||
|
return values.compactMap { $0.j }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - 11 includes
|
||||||
|
public typealias Include11 = Poly11
|
||||||
|
extension Includes where I: _Poly11 {
|
||||||
|
public subscript(_ lookup: I.K.Type) -> [I.K] {
|
||||||
|
return values.compactMap { $0.k }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - DecodingError
|
||||||
|
public struct IncludesDecodingError: Swift.Error, Equatable {
|
||||||
|
public let error: Swift.Error
|
||||||
|
public let idx: Int
|
||||||
|
|
||||||
|
public static func ==(lhs: Self, rhs: Self) -> Bool {
|
||||||
|
return lhs.idx == rhs.idx
|
||||||
|
&& String(describing: lhs) == String(describing: rhs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension IncludesDecodingError: CustomStringConvertible {
|
||||||
|
public var description: String {
|
||||||
|
return "Include \(idx + 1) failed to parse: \(error)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct IncludeDecodingError: Swift.Error, Equatable, CustomStringConvertible {
|
||||||
|
public let failures: [ResourceObjectDecodingError]
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
return failures
|
||||||
|
.enumerated()
|
||||||
|
.map {
|
||||||
|
"\nCould not have been Include Type \($0.offset + 1) because:\n\($0.element)"
|
||||||
|
}.joined(separator: "\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,110 +5,158 @@
|
|||||||
// Created by Mathew Polzin on 11/10/18.
|
// Created by Mathew Polzin on 11/10/18.
|
||||||
//
|
//
|
||||||
|
|
||||||
public protocol MaybePrimaryResource: Equatable, Codable {}
|
/// This protocol allows for a `SingleResourceBody` to contain a `null`
|
||||||
|
/// data object where `ManyResourceBody` cannot (because an empty
|
||||||
|
/// array should be used for no results).
|
||||||
|
public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {}
|
||||||
|
|
||||||
/// A PrimaryResource is a type that can be used in the body of a JSON API
|
/// An `EncodablePrimaryResource` is a `CodablePrimaryResource` that only supports encoding.
|
||||||
|
public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {}
|
||||||
|
|
||||||
|
/// This protocol allows for `SingleResourceBody` to contain a `null`
|
||||||
|
/// data object where `ManyResourceBody` cannot (because an empty
|
||||||
|
/// array should be used for no results).
|
||||||
|
public protocol OptionalCodablePrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
|
||||||
|
|
||||||
|
/// A `CodablePrimaryResource` is a type that can be used in the body of a JSON API
|
||||||
/// document as the primary resource.
|
/// document as the primary resource.
|
||||||
public protocol PrimaryResource: MaybePrimaryResource {}
|
public protocol CodablePrimaryResource: EncodablePrimaryResource, OptionalCodablePrimaryResource {}
|
||||||
|
|
||||||
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
|
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
|
||||||
|
|
||||||
/// A ResourceBody is a representation of the body of the JSON API Document.
|
extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimaryResource {}
|
||||||
|
|
||||||
|
/// An `EncodableResourceBody` is a `ResourceBody` that only supports being
|
||||||
|
/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding
|
||||||
|
/// and decoding.
|
||||||
|
public protocol EncodableResourceBody: Equatable, Encodable {
|
||||||
|
associatedtype PrimaryResource
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A `CodableResourceBody` is a representation of the body of the JSON:API Document.
|
||||||
/// It can either be one resource (which can be specified as optional or not)
|
/// It can either be one resource (which can be specified as optional or not)
|
||||||
/// or it can contain many resources (and array with zero or more entries).
|
/// or it can contain many resources (and array with zero or more entries).
|
||||||
public protocol ResourceBody: Codable, Equatable {
|
public protocol CodableResourceBody: Decodable, EncodableResourceBody {}
|
||||||
|
|
||||||
|
/// A `ResourceBody` that has the ability to take on more primary
|
||||||
|
/// resources by appending another similarly typed `ResourceBody`.
|
||||||
|
public protocol ResourceBodyAppendable {
|
||||||
|
func appending(_ other: Self) -> Self
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol AppendableResourceBody: ResourceBody {
|
public func +<R: ResourceBodyAppendable>(_ left: R, right: R) -> R {
|
||||||
func appending(_ other: Self) -> Self
|
return left.appending(right)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
/// A type allowing for a document body containing 1 primary resource.
|
||||||
return left.appending(right)
|
/// If the `Entity` specialization is an `Optional` type, the body can contain
|
||||||
|
/// 0 or 1 primary resources.
|
||||||
|
public struct SingleResourceBody<PrimaryResource: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
|
||||||
|
public let value: PrimaryResource
|
||||||
|
|
||||||
|
public init(resourceObject: PrimaryResource) {
|
||||||
|
self.value = resourceObject
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
/// A type allowing for a document body containing 0 or more primary resources.
|
||||||
public let value: Entity
|
public struct ManyResourceBody<PrimaryResource: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
|
||||||
|
public let values: [PrimaryResource]
|
||||||
|
|
||||||
public init(entity: Entity) {
|
public init(resourceObjects: [PrimaryResource]) {
|
||||||
self.value = entity
|
values = resourceObjects
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
|
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||||
public let values: [Entity]
|
return ManyResourceBody(resourceObjects: values + other.values)
|
||||||
|
}
|
||||||
public init(entities: [Entity]) {
|
|
||||||
values = entities
|
|
||||||
}
|
|
||||||
|
|
||||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
|
||||||
return ManyResourceBody(entities: values + other.values)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Use NoResourceBody to indicate you expect a JSON API document to not
|
/// Use NoResourceBody to indicate you expect a JSON API document to not
|
||||||
/// contain a "data" top-level key.
|
/// contain a "data" top-level key.
|
||||||
public struct NoResourceBody: ResourceBody {
|
public struct NoResourceBody: CodableResourceBody {
|
||||||
public static var none: NoResourceBody { return NoResourceBody() }
|
public typealias PrimaryResource = Void
|
||||||
|
|
||||||
|
public static var none: NoResourceBody { return NoResourceBody() }
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Codable
|
// MARK: Codable
|
||||||
extension SingleResourceBody {
|
extension SingleResourceBody {
|
||||||
public init(from decoder: Decoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
let container = try decoder.singleValueContainer()
|
var container = encoder.singleValueContainer()
|
||||||
|
|
||||||
let anyNil: Any? = nil
|
let anyNil: Any? = nil
|
||||||
if container.decodeNil(),
|
let nilValue = anyNil as? PrimaryResource
|
||||||
let val = anyNil as? Entity {
|
guard value != nilValue else {
|
||||||
value = val
|
try container.encodeNil()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
value = try container.decode(Entity.self)
|
try container.encode(value)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource {
|
||||||
var container = encoder.singleValueContainer()
|
public init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.singleValueContainer()
|
||||||
|
|
||||||
if (value as Any?) == nil {
|
let anyNil: Any? = nil
|
||||||
try container.encodeNil()
|
if container.decodeNil(),
|
||||||
return
|
let val = anyNil as? PrimaryResource {
|
||||||
}
|
value = val
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try container.encode(value)
|
value = try container.decode(PrimaryResource.self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ManyResourceBody {
|
extension ManyResourceBody {
|
||||||
public init(from decoder: Decoder) throws {
|
public func encode(to encoder: Encoder) throws {
|
||||||
var container = try decoder.unkeyedContainer()
|
var container = encoder.unkeyedContainer()
|
||||||
var valueAggregator = [Entity]()
|
|
||||||
while !container.isAtEnd {
|
|
||||||
valueAggregator.append(try container.decode(Entity.self))
|
|
||||||
}
|
|
||||||
values = valueAggregator
|
|
||||||
}
|
|
||||||
|
|
||||||
public func encode(to encoder: Encoder) throws {
|
for value in values {
|
||||||
var container = encoder.unkeyedContainer()
|
try container.encode(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for value in values {
|
extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource {
|
||||||
try container.encode(value)
|
public init(from decoder: Decoder) throws {
|
||||||
}
|
var container = try decoder.unkeyedContainer()
|
||||||
}
|
var valueAggregator = [PrimaryResource]()
|
||||||
|
var idx = 0
|
||||||
|
while !container.isAtEnd {
|
||||||
|
do {
|
||||||
|
valueAggregator.append(try container.decode(PrimaryResource.self))
|
||||||
|
} catch let error as ResourceObjectDecodingError {
|
||||||
|
throw ManyResourceBodyDecodingError(
|
||||||
|
error: error,
|
||||||
|
idx: idx
|
||||||
|
)
|
||||||
|
}
|
||||||
|
idx = idx + 1
|
||||||
|
}
|
||||||
|
values = valueAggregator
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: CustomStringConvertible
|
// MARK: CustomStringConvertible
|
||||||
|
|
||||||
extension SingleResourceBody: CustomStringConvertible {
|
extension SingleResourceBody: CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
return "PrimaryResourceBody(\(String(describing: value)))"
|
return "PrimaryResourceBody(\(String(describing: value)))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ManyResourceBody: CustomStringConvertible {
|
extension ManyResourceBody: CustomStringConvertible {
|
||||||
public var description: String {
|
public var description: String {
|
||||||
return "PrimaryResourceBody(\(String(describing: values)))"
|
return "PrimaryResourceBody(\(String(describing: values)))"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - DecodingError
|
||||||
|
public struct ManyResourceBodyDecodingError: Swift.Error, Equatable {
|
||||||
|
public let error: ResourceObjectDecodingError
|
||||||
|
public let idx: Int
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
//
|
||||||
|
// EmptyObjectDecoder.swift
|
||||||
|
// JSONAPI
|
||||||
|
//
|
||||||
|
// Created by Mathew Polzin on 7/2/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// `EmptyObjectDecoder` exists internally for the sole purpose of
|
||||||
|
/// allowing certain fallback logic paths to attempt to create `Decodable`
|
||||||
|
/// types from empty containers (specifically in a way that is agnostic
|
||||||
|
/// of any given encoding). In other words, this serves the same purpose
|
||||||
|
/// as `JSONDecoder().decode(Thing.self, from: "{}".data(using: .utf8)!)`
|
||||||
|
/// without needing to use a third party or `Foundation` library decoder.
|
||||||
|
struct EmptyObjectDecoder: Decoder {
|
||||||
|
|
||||||
|
var codingPath: [CodingKey] = []
|
||||||
|
|
||||||
|
var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||||
|
|
||||||
|
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
|
||||||
|
return KeyedDecodingContainer(EmptyKeyedContainer())
|
||||||
|
}
|
||||||
|
|
||||||
|
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||||
|
return EmptyUnkeyedContainer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func singleValueContainer() throws -> SingleValueDecodingContainer {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeSingleValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum EmptyObjectDecodingError: Swift.Error {
|
||||||
|
case emptyObjectCannotBeSingleValue
|
||||||
|
case emptyObjectCannotBeUnkeyedValues
|
||||||
|
case emptyObjectCannotHaveKeyedValues
|
||||||
|
case emptyObjectCannotHaveNestedContainers
|
||||||
|
case emptyObjectCannotHaveSuper
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmptyUnkeyedContainer: UnkeyedDecodingContainer {
|
||||||
|
var codingPath: [CodingKey] { return [] }
|
||||||
|
|
||||||
|
var count: Int? { return 0 }
|
||||||
|
|
||||||
|
var isAtEnd: Bool { return true }
|
||||||
|
|
||||||
|
var currentIndex: Int { return 0 }
|
||||||
|
|
||||||
|
mutating func decodeNil() throws -> Bool {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: Bool.Type) throws -> Bool {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: String.Type) throws -> String {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: Double.Type) throws -> Double {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: Float.Type) throws -> Float {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: Int.Type) throws -> Int {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: Int8.Type) throws -> Int8 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: Int16.Type) throws -> Int16 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: Int32.Type) throws -> Int32 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: Int64.Type) throws -> Int64 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: UInt.Type) throws -> UInt {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func superDecoder() throws -> Decoder {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmptyKeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
|
||||||
|
var codingPath: [CodingKey] { return [] }
|
||||||
|
|
||||||
|
var allKeys: [Key] { return [] }
|
||||||
|
|
||||||
|
func contains(_ key: Key) -> Bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeNil(forKey key: Key) throws -> Bool {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: String.Type, forKey key: Key) throws -> String {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||||
|
}
|
||||||
|
|
||||||
|
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder() throws -> Decoder {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||||
|
}
|
||||||
|
|
||||||
|
func superDecoder(forKey key: Key) throws -> Decoder {
|
||||||
|
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
//
|
|
||||||
// EncodingError.swift
|
|
||||||
// JSONAPI
|
|
||||||
//
|
|
||||||
// Created by Mathew Polzin on 12/7/18.
|
|
||||||
//
|
|
||||||
|
|
||||||
public enum JSONAPIEncodingError: Swift.Error {
|
|
||||||
case typeMismatch(expected: String, found: String)
|
|
||||||
case illegalEncoding(String)
|
|
||||||
case illegalDecoding(String)
|
|
||||||
case missingOrMalformedMetadata
|
|
||||||
case missingOrMalformedLinks
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
//
|
||||||
|
// BasicJSONAPIError.swift
|
||||||
|
// JSONAPI
|
||||||
|
//
|
||||||
|
// Created by Mathew Polzin on 9/29/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
/// Most of the JSON:API Spec defined Error fields.
|
||||||
|
public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType, CustomStringConvertible {
|
||||||
|
/// a unique identifier for this particular occurrence of the problem
|
||||||
|
public let id: IdType?
|
||||||
|
|
||||||
|
// public let links: Links? // we skip this for now to avoid adding complexity to using this basic type.
|
||||||
|
|
||||||
|
/// the HTTP status code applicable to this problem
|
||||||
|
public let status: String?
|
||||||
|
/// an application-specific error code
|
||||||
|
public let code: String?
|
||||||
|
/// a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization
|
||||||
|
public let title: String?
|
||||||
|
/// a human-readable explanation specific to this occurrence of the problem. Like `title`, this field’s value can be localized
|
||||||
|
public let detail: String?
|
||||||
|
/// an object containing references to the source of the error
|
||||||
|
public let source: Source?
|
||||||
|
|
||||||
|
// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type
|
||||||
|
|
||||||
|
public init(id: IdType? = nil,
|
||||||
|
status: String? = nil,
|
||||||
|
code: String? = nil,
|
||||||
|
title: String? = nil,
|
||||||
|
detail: String? = nil,
|
||||||
|
source: Source? = nil) {
|
||||||
|
self.id = id
|
||||||
|
self.status = status
|
||||||
|
self.code = code
|
||||||
|
self.title = title
|
||||||
|
self.detail = detail
|
||||||
|
self.source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Source: Codable, Equatable {
|
||||||
|
/// a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].
|
||||||
|
public let pointer: String?
|
||||||
|
/// which URI query parameter caused the error
|
||||||
|
public let parameter: String?
|
||||||
|
|
||||||
|
public init(pointer: String? = nil,
|
||||||
|
parameter: String? = nil) {
|
||||||
|
self.pointer = pointer
|
||||||
|
self.parameter = parameter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var definedFields: [String: String] {
|
||||||
|
let keysAndValues = [
|
||||||
|
id.map { ("id", String(describing: $0)) },
|
||||||
|
status.map { ("status", $0) },
|
||||||
|
code.map { ("code", $0) },
|
||||||
|
title.map { ("title", $0) },
|
||||||
|
detail.map { ("detail", $0) },
|
||||||
|
source.flatMap { $0.pointer.map { ("pointer", $0) } },
|
||||||
|
source.flatMap { $0.parameter.map { ("parameter", $0) } }
|
||||||
|
].compactMap { $0 }
|
||||||
|
return Dictionary(uniqueKeysWithValues: keysAndValues)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var description: String {
|
||||||
|
return definedFields.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `BasicJSONAPIError` optionally decodes many possible fields
|
||||||
|
/// specified by the JSON:API 1.0 Spec. It gives no type-guarantees of what
|
||||||
|
/// will be non-nil, but could provide good diagnostic information when
|
||||||
|
/// you do not know what error structure to expect.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// Fields:
|
||||||
|
/// - id
|
||||||
|
/// - status
|
||||||
|
/// - code
|
||||||
|
/// - title
|
||||||
|
/// - detail
|
||||||
|
/// - source
|
||||||
|
/// - pointer
|
||||||
|
/// - parameter
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The JSON:API Spec does not dictate the type of this particular Id field,
|
||||||
|
/// so you must specify whether to expect, for example, an `Int` or a `String`
|
||||||
|
/// in the id field.
|
||||||
|
///
|
||||||
|
/// Something like `AnyCodable` from *Flight-School* could be
|
||||||
|
/// a good option if you do not know what to expect. You could also use
|
||||||
|
/// `Either<Int, String>` (provided by the `Poly` package that is
|
||||||
|
/// already a dependency of `JSONAPI`).
|
||||||
|
///
|
||||||
|
/// - Important: The `definedFields` property will include fields
|
||||||
|
/// with non-nil values in a flattened way. There will be no `source` key
|
||||||
|
/// but there will be `pointer` and `parameter` keys (if those values
|
||||||
|
/// are non-nil).
|
||||||
|
public typealias BasicJSONAPIError<IdType: Codable & Equatable> = GenericJSONAPIError<BasicJSONAPIErrorPayload<IdType>>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user