mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 16004051e9 | |||
| 864a66f1ea | |||
| 0c7bf9a92d | |||
| b294cec542 | |||
| 11e8d13f02 |
+77
-76
@@ -4,65 +4,66 @@ import Poly
|
||||
|
||||
// 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.
|
||||
/*
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
var globalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
globalStringId += 1
|
||||
return String(globalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Create a typealias because we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
|
||||
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
|
||||
// requests that create new entities. In these situations, the server
|
||||
// is expected to assign the new entity a unique ID.
|
||||
typealias UnidentifiedJSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
|
||||
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
// Create relationship typealiases because we do not expect
|
||||
// JSON:API Relationships for this particular API to have
|
||||
// Metadata or Links associated with them.
|
||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// We create a typealias for a Document given that we do not expect
|
||||
// Create a typealias for a Document because we do not expect
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
// useful Errors, or an APIDescription (The *SPEC* calls this
|
||||
// "API Description" the "JSON:API Object").
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: EntityDescription {
|
||||
public static var jsonType: String { return "authors" }
|
||||
enum AuthorDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: EntityDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
enum ArticleDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// of a JSON:API response body.
|
||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// Let's pretend all of this is coming from a database:
|
||||
// Let'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"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(entity: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .a(document)
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .init(document)
|
||||
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
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()
|
||||
@@ -124,8 +125,8 @@ encoder.outputFormatting = .prettyPrinted
|
||||
let responseBody = articleDocument(includeAuthor: true)
|
||||
let responseData = try! encoder.encode(responseBody)
|
||||
|
||||
// Next step would be encoding and setting as the HTTP body of a response.
|
||||
// we will just print it out instead:
|
||||
// Next step would be setting the HTTP body of a response.
|
||||
// We will just print it out instead:
|
||||
print("-----")
|
||||
print(String(data: responseData, encoding: .utf8)!)
|
||||
|
||||
@@ -139,31 +140,31 @@ print(String(data: otherResponseData, encoding: .utf8)!)
|
||||
// MARK: - Client Pseudo-example
|
||||
|
||||
enum NetworkError: Swift.Error {
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
}
|
||||
|
||||
// Skipping over all the API stuff, here's a chunk of code that will
|
||||
// decode a document. We will assume we have made a request for a
|
||||
// single article including the author.
|
||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
}
|
||||
|
||||
let response = try! docode(articleResponseData: responseData)
|
||||
|
||||
+5
-5
@@ -64,7 +64,7 @@ struct ToManyRelationshipLinks: JSONAPI.Links {
|
||||
}
|
||||
|
||||
/// Description of an Author entity.
|
||||
enum AuthorDescription: EntityDescription {
|
||||
enum AuthorDescription: ResourceObjectDescription {
|
||||
static var jsonType: String { return "authors" }
|
||||
|
||||
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.
|
||||
enum ArticleDescription: EntityDescription {
|
||||
enum ArticleDescription: ResourceObjectDescription {
|
||||
static var jsonType: String { return "articles" }
|
||||
|
||||
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
|
||||
struct APIDescriptionMetadata: JSONAPI.Meta {
|
||||
@@ -196,7 +196,7 @@ let documentLinks = SingleArticleDocumentLinks(otherArticlesByPrimaryAuthor: .in
|
||||
meta: .init(expiry: tomorrow)))
|
||||
|
||||
let singleArticleDocument = SingleArticleDocument(apiDescription: apiDescription,
|
||||
body: .init(entity: article),
|
||||
body: .init(resourceObject: article),
|
||||
includes: .init(values: [.init(author1), .init(author2), .init(author3)]),
|
||||
meta: documentMetadata,
|
||||
links: documentLinks)
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.primaryResource?.value
|
||||
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 altDogFromData = altDogResponse.body.primaryResource?.value
|
||||
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
||||
|
||||
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
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])]
|
||||
@@ -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 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)
|
||||
|
||||
|
||||
// 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)
|
||||
@@ -47,16 +52,18 @@ print("-----")
|
||||
print(peopleResponse)
|
||||
print("-----")
|
||||
|
||||
|
||||
// MARK: - Pass successfully parsed body to other parts of the code
|
||||
|
||||
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 {
|
||||
print("no body data")
|
||||
}
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
print("-----")
|
||||
func process<T: JSONAPIDocument>(document: T) {
|
||||
guard case let .data(body) = document.body else {
|
||||
return
|
||||
@@ -64,3 +71,29 @@ func process<T: JSONAPIDocument>(document: T) {
|
||||
let x: T.Body.Data = body
|
||||
}
|
||||
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
|
||||
var GlobalStringId: Int = 0
|
||||
var globalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
globalStringId += 1
|
||||
return String(globalStringId)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
|
||||
|
||||
// MARK: - A few resource objects (entities)
|
||||
public enum PersonDescription: EntityDescription {
|
||||
public enum PersonDescription: ResourceObjectDescription {
|
||||
|
||||
public static var jsonType: String { return "people" }
|
||||
|
||||
@@ -62,13 +62,13 @@ public enum PersonDescription: EntityDescription {
|
||||
|
||||
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 {
|
||||
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" }
|
||||
|
||||
@@ -91,7 +91,7 @@ public enum DogDescription: EntityDescription {
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public enum AlternativeDogDescription: EntityDescription {
|
||||
public enum AlternativeDogDescription: ResourceObjectDescription {
|
||||
|
||||
public static var jsonType: String { return "dogs" }
|
||||
|
||||
@@ -119,9 +119,9 @@ public enum AlternativeDogDescription: EntityDescription {
|
||||
|
||||
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public extension ResourceObject where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
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 {
|
||||
@@ -129,7 +129,7 @@ public extension Entity where Description == DogDescription, MetaType == NoMetad
|
||||
}
|
||||
}
|
||||
|
||||
public enum HouseDescription: EntityDescription {
|
||||
public enum HouseDescription: ResourceObjectDescription {
|
||||
|
||||
public static var jsonType: String { return "houses" }
|
||||
|
||||
@@ -139,6 +139,6 @@ public enum HouseDescription: EntityDescription {
|
||||
|
||||
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 BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<?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>
|
||||
<page name='Test Library'/>
|
||||
<page name='Usage'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
<page name='Sparse Fieldsets Example'/>
|
||||
</pages>
|
||||
</playground>
|
||||
+6
-6
@@ -15,8 +15,8 @@ Pod::Spec.new do |spec|
|
||||
# summary should be tweet-length, and the description more in depth.
|
||||
#
|
||||
|
||||
spec.name = "JSONAPI"
|
||||
spec.version = "0.19.0"
|
||||
spec.name = "MP-JSONAPI"
|
||||
spec.version = "2.2.0"
|
||||
spec.summary = "Swift Codable JSON API framework."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
@@ -93,7 +93,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
# Not including the public_header_files will make all headers public.
|
||||
#
|
||||
|
||||
spec.source_files = "Sources", "Sources/**/*.{swift}"
|
||||
spec.source_files = "Sources/JSONAPI/**/*.{swift}"
|
||||
# spec.exclude_files = "Classes/Exclude"
|
||||
|
||||
# spec.public_header_files = "Classes/**/*.h"
|
||||
@@ -119,7 +119,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
# the lib prefix of their name.
|
||||
#
|
||||
|
||||
spec.framework = "Poly"
|
||||
# spec.frameworks = "SomeFramework", "AnotherFramework"
|
||||
|
||||
# spec.library = "iconv"
|
||||
@@ -132,10 +131,11 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
# 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 = "4.2"
|
||||
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", "~> 1.1"
|
||||
spec.dependency "Poly", "~> 2.1"
|
||||
|
||||
end
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07",
|
||||
"version": "1.0.0"
|
||||
"revision": "4a08517b24f8e9f6dd8c02ec7da316aac5c00e2e",
|
||||
"version": "2.1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+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.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "JSONAPI",
|
||||
platforms: [
|
||||
.macOS(.v10_10),
|
||||
.iOS(.v10)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "JSONAPI",
|
||||
targets: ["JSONAPI"]),
|
||||
.library(
|
||||
name: "JSONAPITesting",
|
||||
targets: ["JSONAPITesting"])
|
||||
.library(
|
||||
name: "JSONAPITesting",
|
||||
targets: ["JSONAPITesting"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.1.0")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "JSONAPI",
|
||||
dependencies: ["Poly"]),
|
||||
.target(
|
||||
name: "JSONAPITesting",
|
||||
dependencies: ["JSONAPI"]),
|
||||
.target(
|
||||
name: "JSONAPITesting",
|
||||
dependencies: ["JSONAPI"]),
|
||||
.testTarget(
|
||||
name: "JSONAPITests",
|
||||
dependencies: ["JSONAPI", "JSONAPITesting"]),
|
||||
@@ -30,5 +34,5 @@ let package = Package(
|
||||
name: "JSONAPITestingTests",
|
||||
dependencies: ["JSONAPI", "JSONAPITesting"])
|
||||
],
|
||||
swiftLanguageVersions: [.v4_2]
|
||||
swiftLanguageVersions: [.v5]
|
||||
)
|
||||
|
||||
@@ -7,19 +7,26 @@
|
||||
|
||||
import Poly
|
||||
|
||||
public protocol JSONAPIDocument: Codable, Equatable {
|
||||
associatedtype PrimaryResourceBody: JSONAPI.ResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype APIDescription: APIDescriptionType
|
||||
associatedtype Error: JSONAPIError
|
||||
/// An `EncodableJSONAPIDocument` supports encoding but not decoding.
|
||||
/// It is actually more restrictive than `JSONAPIDocument` which supports both
|
||||
/// encoding and decoding.
|
||||
public protocol EncodableJSONAPIDocument: Equatable, Encodable {
|
||||
associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype APIDescription: APIDescriptionType
|
||||
associatedtype Error: JSONAPIError
|
||||
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
|
||||
var body: Body { get }
|
||||
var body: Body { get }
|
||||
}
|
||||
|
||||
/// A `JSONAPIDocument` supports encoding and decoding of a JSON:API
|
||||
/// compliant Document.
|
||||
public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {}
|
||||
|
||||
/// A JSON API Document represents the entire body
|
||||
/// of a JSON API request or the entire body of
|
||||
/// a JSON API response.
|
||||
@@ -27,7 +34,7 @@ public protocol JSONAPIDocument: Codable, Equatable {
|
||||
/// API uses snake case, you will want to use
|
||||
/// a conversion such as the one offerred by the
|
||||
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: EncodableJSONAPIDocument {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
/// The JSON API Spec calls this the JSON:API Object. It contains version
|
||||
@@ -46,7 +53,9 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
case data(Data)
|
||||
|
||||
public struct Data: Equatable {
|
||||
/// The document's Primary Resource object(s)
|
||||
public let primary: PrimaryResourceBody
|
||||
/// The document's included objects
|
||||
public let includes: Includes<Include>
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
@@ -59,6 +68,8 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` if the document represents one or more errors. `false` if the
|
||||
/// document represents JSON:API data and/or metadata.
|
||||
public var isError: Bool {
|
||||
guard case .errors = self else { return false }
|
||||
return true
|
||||
@@ -196,7 +207,7 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link
|
||||
}
|
||||
*/
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
extension Document.Body.Data where PrimaryResourceBody: Appendable {
|
||||
public func merging(_ other: Document.Body.Data,
|
||||
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
|
||||
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
|
||||
@@ -207,7 +218,7 @@ extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
extension Document.Body.Data where PrimaryResourceBody: Appendable, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
|
||||
return merging(other,
|
||||
combiningMetaWith: { _, _ in .none },
|
||||
@@ -273,66 +284,6 @@ extension Document {
|
||||
case links
|
||||
case jsonapi
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
do {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
} catch {
|
||||
meta = nil
|
||||
}
|
||||
}
|
||||
|
||||
let links: LinksType?
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
do {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
} catch {
|
||||
links = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there are errors, there cannot be a body. Return errors and any metadata found.
|
||||
if let errors = errors {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
return
|
||||
}
|
||||
|
||||
let data: PrimaryResourceBody
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
|
||||
// TODO come back to this and make robust
|
||||
|
||||
guard let metaVal = meta else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedMetadata
|
||||
}
|
||||
guard let linksVal = links else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
}
|
||||
|
||||
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: RootCodingKeys.self)
|
||||
@@ -377,6 +328,68 @@ extension Document {
|
||||
}
|
||||
}
|
||||
|
||||
extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
do {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
} catch {
|
||||
meta = nil
|
||||
}
|
||||
}
|
||||
|
||||
let links: LinksType?
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
do {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
} catch {
|
||||
links = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there are errors, there cannot be a body. Return errors and any metadata found.
|
||||
if let errors = errors {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
return
|
||||
}
|
||||
|
||||
let data: PrimaryResourceBody
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
|
||||
// TODO come back to this and make robust
|
||||
|
||||
guard let metaVal = meta else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedMetadata
|
||||
}
|
||||
guard let linksVal = links else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
}
|
||||
|
||||
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Document: CustomStringConvertible {
|
||||
|
||||
@@ -7,9 +7,19 @@
|
||||
|
||||
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.
|
||||
/// The resources are accessed by their type using a subscript.
|
||||
///
|
||||
/// If you have
|
||||
///
|
||||
/// `let includes: Includes<Include2<Thing1, Thing2>> = ...`
|
||||
///
|
||||
/// then you can access all `Thing1` included resources with
|
||||
///
|
||||
/// `let includedThings = includes[Thing1.self]`
|
||||
public struct Includes<I: Include>: Encodable, Equatable {
|
||||
public static var none: Includes { return .init(values: []) }
|
||||
|
||||
let values: [I]
|
||||
@@ -17,23 +27,6 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
public init(values: [I]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
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]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
}
|
||||
|
||||
values = valueAggregator
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
@@ -52,6 +45,25 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
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]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
}
|
||||
|
||||
values = valueAggregator
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes {
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
@@ -149,3 +161,11 @@ extension Includes where I: _Poly9 {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,45 +5,70 @@
|
||||
// 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 `PrimaryResource` that only supports encoding.
|
||||
/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and
|
||||
/// decoding.
|
||||
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 OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
|
||||
|
||||
/// A `PrimaryResource` is a type that can be used in the body of a JSON API
|
||||
/// document as the primary resource.
|
||||
public protocol PrimaryResource: MaybePrimaryResource {}
|
||||
public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {}
|
||||
|
||||
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
|
||||
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
|
||||
|
||||
extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {}
|
||||
|
||||
/// 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 {}
|
||||
|
||||
/// A ResourceBody 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)
|
||||
/// or it can contain many resources (and array with zero or more entries).
|
||||
public protocol ResourceBody: Codable, Equatable {
|
||||
}
|
||||
public protocol ResourceBody: Decodable, EncodableResourceBody {}
|
||||
|
||||
public protocol AppendableResourceBody: ResourceBody {
|
||||
/// A `ResourceBody` that has the ability to take on more primary
|
||||
/// resources by appending another similarly typed `ResourceBody`.
|
||||
public protocol Appendable {
|
||||
func appending(_ other: Self) -> Self
|
||||
}
|
||||
|
||||
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
||||
public func +<R: Appendable>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
||||
/// A type allowing for a document body containing 1 primary resource.
|
||||
/// If the `Entity` specialization is an `Optional` type, the body can contain
|
||||
/// 0 or 1 primary resources.
|
||||
public struct SingleResourceBody<Entity: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
|
||||
public let value: Entity
|
||||
|
||||
public init(entity: Entity) {
|
||||
self.value = entity
|
||||
public init(resourceObject: Entity) {
|
||||
self.value = resourceObject
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
|
||||
/// A type allowing for a document body containing 0 or more primary resources.
|
||||
public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, Appendable {
|
||||
public let values: [Entity]
|
||||
|
||||
public init(entities: [Entity]) {
|
||||
values = entities
|
||||
public init(resourceObjects: [Entity]) {
|
||||
values = resourceObjects
|
||||
}
|
||||
|
||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||
return ManyResourceBody(entities: values + other.values)
|
||||
return ManyResourceBody(resourceObjects: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,41 +80,36 @@ public struct NoResourceBody: ResourceBody {
|
||||
|
||||
// MARK: Codable
|
||||
extension SingleResourceBody {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
value = try container.decode(Entity.self)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
if (value as Any?) == nil {
|
||||
try container.encodeNil()
|
||||
return
|
||||
}
|
||||
let anyNil: Any? = nil
|
||||
let nilValue = anyNil as? Entity
|
||||
guard value != nilValue else {
|
||||
try container.encodeNil()
|
||||
return
|
||||
}
|
||||
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
var valueAggregator = [Entity]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(Entity.self))
|
||||
}
|
||||
values = valueAggregator
|
||||
}
|
||||
extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
value = try container.decode(Entity.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
|
||||
@@ -99,6 +119,17 @@ extension ManyResourceBody {
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
var valueAggregator = [Entity]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(Entity.self))
|
||||
}
|
||||
values = valueAggregator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
extension SingleResourceBody: CustomStringConvertible {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// BasicError.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 {
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// `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>>
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// GenericError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 9/29/19.
|
||||
//
|
||||
|
||||
/// `GenericJSONAPIError` can be used to specify whatever error
|
||||
/// payload you expect to need to parse in responses and handle any
|
||||
/// other payload structure as `.unknownError`.
|
||||
public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError {
|
||||
case unknownError
|
||||
case error(ErrorPayload)
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
do {
|
||||
self = .error(try container.decode(ErrorPayload.self))
|
||||
} catch {
|
||||
self = .unknown
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self {
|
||||
case .error(let payload):
|
||||
try container.encode(payload)
|
||||
case .unknownError:
|
||||
try container.encode("unknown")
|
||||
}
|
||||
}
|
||||
|
||||
public static var unknown: Self {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
|
||||
public extension GenericJSONAPIError {
|
||||
var payload: ErrorPayload? {
|
||||
switch self {
|
||||
case .unknownError:
|
||||
return nil
|
||||
case .error(let payload):
|
||||
return payload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ErrorDictType {
|
||||
var definedFields: [String: String] { get }
|
||||
}
|
||||
|
||||
extension GenericJSONAPIError: ErrorDictType where ErrorPayload: ErrorDictType {
|
||||
/// Get a dictionary of all defined fields and their values.
|
||||
public var definedFields: [String: String] {
|
||||
switch self {
|
||||
case .unknownError:
|
||||
return [:]
|
||||
case .error(let basicPayload):
|
||||
return basicPayload.definedFields
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,12 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
|
||||
static var unknown: Self { get }
|
||||
}
|
||||
|
||||
/// `UnknownJSONAPIError` can actually be used in any sitaution
|
||||
/// where you don't know what errors are possible _or_ you just don't
|
||||
/// care what errors might show up. If you don't know how the error
|
||||
/// will be structured but you would like to have access to more
|
||||
/// information the server might be providing in the error payload,
|
||||
/// use `BasicJSONAPIError` instead.
|
||||
public enum UnknownJSONAPIError: JSONAPIError {
|
||||
case unknownError
|
||||
|
||||
@@ -21,7 +27,7 @@ public enum UnknownJSONAPIError: JSONAPIError {
|
||||
try container.encode("unknown")
|
||||
}
|
||||
|
||||
public static var unknown: UnknownJSONAPIError {
|
||||
public static var unknown: Self {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,8 @@ public protocol Meta: Codable, Equatable {
|
||||
// nullable.
|
||||
extension Optional: Meta where Wrapped: Meta {}
|
||||
|
||||
/// Use this type when you want to specify not to encode or decode any metadata
|
||||
/// for a type.
|
||||
public struct NoMetadata: Meta, CustomStringConvertible {
|
||||
public static var none: NoMetadata { return NoMetadata() }
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ public protocol AttributeType: Codable {
|
||||
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
|
||||
public let rawValue: RawValue
|
||||
|
||||
|
||||
public let value: Transformer.To
|
||||
|
||||
|
||||
public init(rawValue: RawValue) throws {
|
||||
self.rawValue = rawValue
|
||||
value = try Transformer.transform(rawValue)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user