mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
204 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb2800abd4 | |||
| 85d5fef3c8 | |||
| e4ef61fd56 | |||
| 75ec4f156e | |||
| b45fc73188 | |||
| 3b73dc9989 | |||
| b2c81026f4 | |||
| 8d057b4398 | |||
| c8421cdd58 | |||
| cde10a8491 | |||
| ad05d3908a | |||
| 23b2b2e04f | |||
| 2988503d7d | |||
| 5ea83b07c1 | |||
| 2b59f54067 | |||
| 58a7c82436 | |||
| dc30cb3b9e | |||
| 7045373708 | |||
| 952fe8ba7e | |||
| 951c04ad44 | |||
| 57df6b147e | |||
| a88844fe5e | |||
| d1cf19f9fe | |||
| 59835fbe11 | |||
| 850a713dcb | |||
| e3c637a41e | |||
| 9cbc626410 | |||
| 6dd14daf53 | |||
| 744e08acf4 | |||
| 5da4a963d4 | |||
| 5433dddc81 | |||
| 95f9d8084d | |||
| d6911f170c | |||
| 9972d13a4e | |||
| 4dc63167d6 | |||
| 845d085455 | |||
| 45eb7d06bd | |||
| 6ce962cec9 | |||
| 477dae0a73 | |||
| 4afe4cfb31 | |||
| 65144d27ae | |||
| 1ec4b21c34 | |||
| 15e13e2cc7 | |||
| 1d8ceef317 | |||
| e546614c15 | |||
| 8939d637d4 | |||
| e3b005b89a | |||
| 2de6580519 | |||
| 875d938b95 | |||
| 1ed34b00dc | |||
| 531a7f35bd | |||
| dc42ec27fc | |||
| cf746e182f | |||
| 52d2e9819d | |||
| 81b78cd749 | |||
| 3a0ede2372 | |||
| cb04a01e71 | |||
| 3c29244540 | |||
| a9e1e49a79 | |||
| 9e6e713ad2 | |||
| 308f168a8c | |||
| 8726bcc9fa | |||
| 25bda795a7 | |||
| 14a0256885 | |||
| b2041e9bc8 | |||
| fd9a626551 | |||
| 69bb40aa00 | |||
| 060e0e3898 | |||
| b374367234 | |||
| b363db30cd | |||
| 6904416623 | |||
| c04d3301b6 | |||
| 5a64bebc99 | |||
| 790b0fbf52 | |||
| 539ecc451a | |||
| 7eb1b05eae | |||
| 3408263c2a | |||
| 1d6e5d3810 | |||
| d68404db36 | |||
| 4f7db98a87 | |||
| 072b081ac3 | |||
| 897410492d | |||
| b46f5166ea | |||
| d5a24c4adb | |||
| 669d5d1342 | |||
| 923ab7d9f4 | |||
| 72769b6107 | |||
| 109e15d741 | |||
| 72180f64ef | |||
| fc962f9a0d | |||
| 52eb123166 | |||
| 4ef147ec45 | |||
| 61074ecc69 | |||
| 4dbcff6023 | |||
| d6e82fab55 | |||
| 5fa9848f02 | |||
| f38399a1d6 | |||
| 28f664326d | |||
| 3b7ef4aeb9 | |||
| bb1ed30e89 | |||
| dfdc266645 | |||
| 585cad0a83 | |||
| 89abdd4cca | |||
| 67a43be26c | |||
| 8f279ce191 | |||
| c9d388579f | |||
| 1061283905 | |||
| 08949d0a93 | |||
| 3047e2d23a | |||
| 41a2a01788 | |||
| 53f7f55e07 | |||
| d8d030286d | |||
| 005a981bf7 | |||
| fad45203dd | |||
| 3468cb555a | |||
| 8defe82536 | |||
| 1f83216b03 | |||
| 6e6973c170 | |||
| 4f52cc2fd1 | |||
| 35d6cbb440 | |||
| d667e91a6a | |||
| 75c8ce7405 | |||
| 3a8237ec0f | |||
| 49e33baa96 | |||
| f2a9b5a7b9 | |||
| 24baadd7a8 | |||
| 8d18be6154 | |||
| 8aa887b527 | |||
| 7d0a686dd9 | |||
| e4eb7816d7 | |||
| 482174f5b4 | |||
| 339480264e | |||
| 5d666ec998 | |||
| 9cacbe9b8b | |||
| 6663ad042e | |||
| 07402259c4 | |||
| 8274ecb523 | |||
| 853c7e892a | |||
| 04bf96a0cf | |||
| 8668311307 | |||
| 689517c633 | |||
| 65b80ee0eb | |||
| e91a03b396 | |||
| 82088c7852 | |||
| 34a4c8e7fc | |||
| 6aeb859c24 | |||
| 163ac94c51 | |||
| e67b9fc142 | |||
| a628992fcb | |||
| cf47f88a61 | |||
| 0425e2adcb | |||
| d3763ba713 | |||
| fcc1796731 | |||
| 921bcef05d | |||
| 661ff6eca5 | |||
| e36180c9b9 | |||
| abee0c4d0e | |||
| 9c8b2fbebb | |||
| a9ef71f383 | |||
| 232554ec51 | |||
| 8f36bb40e3 | |||
| 55f2f52676 | |||
| dd5f3b1737 | |||
| 7d7b3d7f19 | |||
| aedd5dc29b | |||
| 3964202ea2 | |||
| dcabafd583 | |||
| 9df9efc2dc | |||
| a55a4cbed0 | |||
| 6f6ed87b4c | |||
| 59ff145aa8 | |||
| 20a685e67b | |||
| 3d8d10584b | |||
| b93580c900 | |||
| 8927938d56 | |||
| 372e2de721 | |||
| 1a2ba17f02 | |||
| 9f2c7aa2e4 | |||
| 650937dbb9 | |||
| b08cbdb0ae | |||
| c30a2615f2 | |||
| 86e87ebf5d | |||
| 987bf2b750 | |||
| af1aca9cf4 | |||
| b97649fab6 | |||
| 8114132189 | |||
| b61a41c99b | |||
| 16b83ddbef | |||
| 6dff02bd51 | |||
| 2563edf419 | |||
| 98bb64268f | |||
| 399644b62a | |||
| 3b1e5f7414 | |||
| bf842bd781 | |||
| 7edb631f09 | |||
| 3c5a356b44 | |||
| f526963707 | |||
| c4032eb351 | |||
| 7a3a1b8b65 | |||
| b3a4591a5d | |||
| 23d057c47b | |||
| 3327a93df5 | |||
| 713fd2ba3a | |||
| 072479c160 |
@@ -2,3 +2,4 @@
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
/*.xcworkspace
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
//
|
||||
// Examples.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
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.
|
||||
|
||||
********/
|
||||
|
||||
|
||||
typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
|
||||
|
||||
// MARK: - A few resource objects (entities)
|
||||
enum PersonDescription: EntityDescription {
|
||||
|
||||
static var type: String { return "people" }
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: [String]
|
||||
let favoriteColor: String
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let friends: ToManyRelationship<Person>
|
||||
let dogs: ToManyRelationship<Dog>
|
||||
let home: ToOneRelationship<House>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
enum DogDescription: EntityDescription {
|
||||
|
||||
static var type: String { return "dogs" }
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: String
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let owner: ToOneRelationship<Person?>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
enum HouseDescription: EntityDescription {
|
||||
|
||||
static var type: String { return "houses" }
|
||||
|
||||
typealias Attributes = NoAttributes
|
||||
typealias Relationships = NoRelatives
|
||||
}
|
||||
|
||||
typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
// MARK: - Parse a response body with one Dog in it
|
||||
typealias SingleDogResponse = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
|
||||
|
||||
let dummyData = "".data(using: .utf8)!
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogResponse.self, from: dummyData)
|
||||
let dog = dogResponse.body.data?.primary.value
|
||||
|
||||
// MARK: Parse a response body with multiple people in it and dogs and houses included
|
||||
typealias BatchPeopleResponse = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleResponse.self, from: dummyData)
|
||||
let people = peopleResponse.body.data?.primary.values
|
||||
let dogs = peopleResponse.body.data?.included[Dog.self]
|
||||
let houses = peopleResponse.body.data?.included[House.self]
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
import Poly
|
||||
|
||||
// MARK: - Preamble (setup)
|
||||
|
||||
// We make String a CreatableRawIdType. This is actually done in
|
||||
// this Playground's Entities.swift file, so it is commented out here.
|
||||
/*
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// Similarly, we create a typealias for unidentified entities. JSON:API
|
||||
// only allows unidentified entities (i.e. no "id" field) for client
|
||||
// requests that create new entities. In these situations, the server
|
||||
// is expected to assign the new entity a unique ID.
|
||||
typealias UnidentifiedJSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// We create a typealias for a Document given that we do not expect
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: EntityDescription {
|
||||
public static var jsonType: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: EntityDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
|
||||
// MARK: Document Definitions
|
||||
|
||||
// We create a typealias to represent a document containing one Article
|
||||
// and including its Author
|
||||
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
|
||||
|
||||
// ... and a typealias to represent a document containing one Article and
|
||||
// not including any related entities.
|
||||
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
|
||||
|
||||
// MARK: - Server Pseudo-example
|
||||
|
||||
// Skipping over all the API and database stuff, here's a chunk of code
|
||||
// that creates a document. Note that this document is the entirety
|
||||
// of a JSON:API response body.
|
||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// Let's pretend all of this is coming from a database:
|
||||
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(entity: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .a(document)
|
||||
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
|
||||
return .b(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
|
||||
let responseBody = articleDocument(includeAuthor: true)
|
||||
let responseData = try! encoder.encode(responseBody)
|
||||
|
||||
// Next step would be encoding and setting as the HTTP body of a response.
|
||||
// we will just print it out instead:
|
||||
print("-----")
|
||||
print(String(data: responseData, encoding: .utf8)!)
|
||||
|
||||
// ... and if we had received a request for an article without
|
||||
// including the author:
|
||||
let otherResponseBody = articleDocument(includeAuthor: false)
|
||||
let otherResponseData = try! encoder.encode(otherResponseBody)
|
||||
print("-----")
|
||||
print(String(data: otherResponseData, encoding: .utf8)!)
|
||||
|
||||
// MARK: - Client Pseudo-example
|
||||
|
||||
enum NetworkError: Swift.Error {
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
}
|
||||
|
||||
// Skipping over all the API stuff, here's a chunk of code that will
|
||||
// decode a document. We will assume we have made a request for a
|
||||
// single article including the author.
|
||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
}
|
||||
|
||||
let response = try! docode(articleResponseData: responseData)
|
||||
|
||||
// Next step would be to do something useful with the article and author but we will print them instead.
|
||||
print("-----")
|
||||
print(response.article)
|
||||
print(response.author)
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
/*
|
||||
|
||||
This is not realistically what JSONAPI code will look like. It makes
|
||||
every bit of sense to typealias the long-winded generics of this
|
||||
framework to make working with them more concise and readable.
|
||||
|
||||
The purpose of this example, then, is not to show "good" JSONAPI
|
||||
code, but rather to generate a JSONAPI example document that uses
|
||||
all the various (often optional) parts of a JSONAPI document.
|
||||
|
||||
The JSON example produced in this manner can be used to compare the
|
||||
nomenclature of this library with the nomenclature of the JSON:API Spec.
|
||||
|
||||
*/
|
||||
|
||||
// MARK: - Extensions
|
||||
extension Foundation.URL: JSONAPIURL {}
|
||||
|
||||
// extension String: CreatableRawIdType {} found in playground Entities.swift file.
|
||||
|
||||
// MARK: - Definitions
|
||||
/// Metadata associated with entities
|
||||
struct EntityMetadata: JSONAPI.Meta {
|
||||
let creationDate: Date
|
||||
let updatedDate: Date
|
||||
}
|
||||
|
||||
/// Metadata associated with links
|
||||
struct LinkMeta: JSONAPI.Meta {
|
||||
/// Don't trust this link past the expiry date.
|
||||
let expiry: Date
|
||||
}
|
||||
|
||||
/// Links associated with entities
|
||||
struct EntityLinks: JSONAPI.Links {
|
||||
let `self`: Link<Foundation.URL, LinkMeta>
|
||||
|
||||
init(selfLink: Link<Foundation.URL, LinkMeta>) {
|
||||
self.`self` = selfLink
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata associated with relationships
|
||||
struct ToManyRelationshipMetadata: JSONAPI.Meta {
|
||||
/// Pagination for the relationship (to support really large
|
||||
/// to-many relationships)
|
||||
let pagination: Pagination
|
||||
|
||||
struct Pagination: Codable, Equatable {
|
||||
let total: Int
|
||||
let limit: Int
|
||||
let offset: Int
|
||||
}
|
||||
}
|
||||
|
||||
/// Links associated with relationships
|
||||
struct ToManyRelationshipLinks: JSONAPI.Links {
|
||||
/// next page of relationships.
|
||||
let next: Link<Foundation.URL, LinkMeta>
|
||||
}
|
||||
|
||||
/// Description of an Author entity.
|
||||
enum AuthorDescription: EntityDescription {
|
||||
static var jsonType: String { return "authors" }
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: Attribute<String>
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let articles: ToManyRelationship<Article, ToManyRelationshipMetadata, ToManyRelationshipLinks>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Author = JSONAPI.Entity<AuthorDescription, EntityMetadata, EntityLinks, String>
|
||||
|
||||
/// Description of an Article entity.
|
||||
enum ArticleDescription: EntityDescription {
|
||||
static var jsonType: String { return "articles" }
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let title: Attribute<String>
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
/// The primary attributed author of the article.
|
||||
let primaryAuthor: ToOneRelationship<Author, NoMetadata, NoLinks>
|
||||
/// All authors excluding the primary author.
|
||||
/// It is customary to print the primary author's
|
||||
/// name first, followed by the other authors.
|
||||
let otherAuthors: ToManyRelationship<Author, ToManyRelationshipMetadata, ToManyRelationshipLinks>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONAPI.Entity<ArticleDescription, EntityMetadata, EntityLinks, String>
|
||||
|
||||
/// Metadata associated with the API Description
|
||||
struct APIDescriptionMetadata: JSONAPI.Meta {
|
||||
/// A clever codename for this API version.
|
||||
let codename: String
|
||||
}
|
||||
|
||||
/// Metadata associated with any Document
|
||||
struct DocumentMetadata: JSONAPI.Meta {
|
||||
/// Assuming caching is in play, this is when
|
||||
/// the cached document being returned was created.
|
||||
let updatedDate: Date
|
||||
}
|
||||
|
||||
/// Links associated with an Article Document
|
||||
struct SingleArticleDocumentLinks: JSONAPI.Links {
|
||||
let otherArticlesByPrimaryAuthor: Link<Foundation.URL, LinkMeta>
|
||||
}
|
||||
|
||||
/// Error that can occur when retrieving a Single Article Document
|
||||
enum ArticleDocumentError: String, JSONAPIError, Codable {
|
||||
case unknownError = "unknown"
|
||||
case missing = "missing"
|
||||
|
||||
static var unknown: ArticleDocumentError {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
|
||||
typealias SingleArticleDocument = JSONAPI.Document<SingleResourceBody<Article>, DocumentMetadata, SingleArticleDocumentLinks, Include1<Author>, APIDescription<APIDescriptionMetadata>, ArticleDocumentError>
|
||||
|
||||
// MARK: - Instantiations
|
||||
let authorId1 = Author.Identifier()
|
||||
let authorId2 = Author.Identifier()
|
||||
let authorId3 = Author.Identifier()
|
||||
|
||||
let now = Date()
|
||||
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now)!
|
||||
|
||||
let tomorrowEntityMeta = EntityMetadata(creationDate: Calendar.current.date(byAdding: .day, value: -10, to: now)!,
|
||||
updatedDate: Calendar.current.date(byAdding: .day, value: -2, to: now)!)
|
||||
let articleLinks = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/article/1234")!,
|
||||
meta: .init(expiry: tomorrow)))
|
||||
let article = Article(attributes: .init(title: .init(value: "How to read JSONAPI")),
|
||||
relationships: .init(primaryAuthor: .init(id: authorId1),
|
||||
otherAuthors: .init(ids: [authorId2, authorId3],
|
||||
meta: .init(pagination: .init(total: 2,
|
||||
limit: 50,
|
||||
offset: 0)),
|
||||
links: .init(next: .init(url: URL(string: "https://articles.com/article/1234?otherAuthors[offset]=50")!,
|
||||
meta: .init(expiry: tomorrow))))),
|
||||
meta: tomorrowEntityMeta,
|
||||
links: articleLinks)
|
||||
|
||||
let author1Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId1.rawValue)")!,
|
||||
meta: .init(expiry: tomorrow)))
|
||||
let author1 = Author(id: authorId1,
|
||||
attributes: .init(name: .init(value: "James Kinney")),
|
||||
relationships: .init(articles: .init(ids: [article.id, Article.Identifier(), Article.Identifier()],
|
||||
meta: .init(pagination: .init(total: 3,
|
||||
limit: 50,
|
||||
offset: 0)),
|
||||
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId1.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
|
||||
meta: tomorrowEntityMeta,
|
||||
links: author1Links)
|
||||
|
||||
let author2Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId2.rawValue)")!,
|
||||
meta: .init(expiry: tomorrow)))
|
||||
let author2 = Author(id: authorId2,
|
||||
attributes: .init(name: .init(value: "James Kinney")),
|
||||
relationships: .init(articles: .init(ids: [article.id, Article.Identifier()],
|
||||
meta: .init(pagination: .init(total: 2,
|
||||
limit: 50,
|
||||
offset: 0)),
|
||||
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId2.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
|
||||
meta: tomorrowEntityMeta,
|
||||
links: author2Links)
|
||||
|
||||
let author3Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId3.rawValue)")!,
|
||||
meta: .init(expiry: tomorrow)))
|
||||
let author3 = Author(id: authorId3,
|
||||
attributes: .init(name: .init(value: "James Kinney")),
|
||||
relationships: .init(articles: .init(ids: [article.id],
|
||||
meta: .init(pagination: .init(total: 1,
|
||||
limit: 50,
|
||||
offset: 0)),
|
||||
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId3.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
|
||||
meta: tomorrowEntityMeta,
|
||||
links: author3Links)
|
||||
|
||||
let apiDescription = APIDescription<APIDescriptionMetadata>(version: "1.2",
|
||||
meta: APIDescriptionMetadata(codename: "cobra"))
|
||||
|
||||
let documentMetadata = DocumentMetadata(updatedDate: Date())
|
||||
|
||||
let documentLinks = SingleArticleDocumentLinks(otherArticlesByPrimaryAuthor: .init(url: URL(string: "https://articles.com/articles?author=\(authorId1.rawValue)")!,
|
||||
meta: .init(expiry: tomorrow)))
|
||||
|
||||
let singleArticleDocument = SingleArticleDocument(apiDescription: apiDescription,
|
||||
body: .init(entity: article),
|
||||
includes: .init(values: [.init(author1), .init(author2), .init(author3)]),
|
||||
meta: documentMetadata,
|
||||
links: documentLinks)
|
||||
|
||||
// MARK: - Encoding
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
encoder.dateEncodingStrategy = .iso8601
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
|
||||
let jsonData = try! encoder.encode(singleArticleDocument)
|
||||
|
||||
// MARK: - Printing JSON
|
||||
print(String(data: jsonData, encoding: .utf8)!)
|
||||
@@ -0,0 +1,46 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
import JSONAPIOpenAPI
|
||||
import Poly
|
||||
|
||||
// print Entity Schema
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
|
||||
let personSchemaData = try? encoder.encode(Person.openAPINode())
|
||||
|
||||
print("Person Schema")
|
||||
print("====")
|
||||
print(personSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
|
||||
print("====")
|
||||
|
||||
let dogDocumentSchemaData = try? encoder.encode(SingleDogDocument.openAPINodeWithExample(using: encoder))
|
||||
|
||||
print("Dog Document Schema")
|
||||
print("====")
|
||||
print(dogDocumentSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
|
||||
print("====")
|
||||
|
||||
let batchPersonSchemaData = try? encoder.encode(BatchPeopleDocument.openAPINodeWithExample(using: encoder))
|
||||
|
||||
print("Batch Person Document Schema")
|
||||
print("====")
|
||||
print(batchPersonSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
|
||||
print("====")
|
||||
|
||||
let tmp: [String: OpenAPIComponents.SchemasDict.RefType] = [
|
||||
"BatchPerson": try! BatchPeopleDocument.openAPINodeWithExample()
|
||||
]
|
||||
|
||||
let components = OpenAPIComponents(schemas: tmp)
|
||||
|
||||
let batchPeopleRef = JSONReference(type: \OpenAPIComponents.schemas, selector: "BatchPerson")
|
||||
|
||||
let tmp2 = JSONNode.reference(batchPeopleRef)
|
||||
|
||||
print("====")
|
||||
print("====")
|
||||
//print(String(data: try! encoder.encode(components), encoding: .utf8)!)
|
||||
print(String(data: try! encoder.encode(tmp2), encoding: .utf8)!)
|
||||
@@ -0,0 +1,24 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
import JSONAPITesting
|
||||
|
||||
/*******
|
||||
|
||||
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
|
||||
|
||||
********/
|
||||
|
||||
// MARK: - Literal Expressibility
|
||||
// The JSONAPITesting framework provides literal expressibility for key types to
|
||||
// make creating tests easier
|
||||
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil), meta: .none, links: .none)
|
||||
|
||||
// MARK: - JSON API structure checking
|
||||
// The JSONAPITesting framework provides a `check` function for each Entity type
|
||||
// that uses reflection to catch mistakes that are not forbidden by
|
||||
// Swift's type system but will result in unexpected results when
|
||||
// encoding/decoding. It is a good idea to add a `check` to each of
|
||||
// your unit tests that create Entities.
|
||||
try Dog.check(dog)
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
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.
|
||||
|
||||
********/
|
||||
|
||||
// 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 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>
|
||||
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])]
|
||||
let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)]
|
||||
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 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)
|
||||
let peopleFromData = peopleResponse.body.primaryResource?.values
|
||||
let dogsFromData = peopleResponse.body.includes?[Dog.self]
|
||||
let housesFromData = peopleResponse.body.includes?[House.self]
|
||||
|
||||
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])")
|
||||
} else {
|
||||
print("no body data")
|
||||
}
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
|
||||
func process<T: JSONAPIDocument>(document: T) {
|
||||
guard case let .data(body) = document.body else {
|
||||
return
|
||||
}
|
||||
let x: T.Body.Data = body
|
||||
}
|
||||
process(document: peopleResponse)
|
||||
@@ -0,0 +1,144 @@
|
||||
//
|
||||
// Examples.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
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.
|
||||
|
||||
********/
|
||||
|
||||
// MARK: - String as CreatableRawIdType
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - typealiases for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<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 static var jsonType: String { return "people" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<[String]>
|
||||
public let favoriteColor: Attribute<String>
|
||||
|
||||
public var fullName: Attribute<String> {
|
||||
return name.map { $0.joined(separator: " ") }
|
||||
}
|
||||
|
||||
public init(name: Attribute<[String]>, favoriteColor: Attribute<String>) {
|
||||
self.name = name
|
||||
self.favoriteColor = favoriteColor
|
||||
}
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let friends: ToMany<Person>
|
||||
public let dogs: ToMany<Dog>
|
||||
public let home: ToOne<House>
|
||||
|
||||
public init(friends: ToMany<Person>, dogs: ToMany<Dog>, home: ToOne<House>) {
|
||||
self.friends = friends
|
||||
self.dogs = dogs
|
||||
self.home = home
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
public extension Entity where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
|
||||
public enum DogDescription: EntityDescription {
|
||||
|
||||
public static var jsonType: String { return "dogs" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
|
||||
public init(name: Attribute<String>) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let owner: ToOne<Person?>
|
||||
|
||||
public init(owner: ToOne<Person?>) {
|
||||
self.owner = owner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public enum AlternativeDogDescription: EntityDescription {
|
||||
|
||||
public static var jsonType: String { return "dogs" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
|
||||
public init(name: Attribute<String>) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let human: ToOne<Person?>
|
||||
|
||||
public init(human: ToOne<Person?>) {
|
||||
self.human = human
|
||||
}
|
||||
|
||||
// define custom key mapping:
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case human = "owner"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public 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)
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Id) throws {
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
public enum HouseDescription: EntityDescription {
|
||||
|
||||
public static var jsonType: String { return "houses" }
|
||||
|
||||
public typealias Attributes = NoAttributes
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
public typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
@@ -0,0 +1,71 @@
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
import JSONAPITesting // for the convenience of literal initialization
|
||||
import JSONAPIOpenAPI
|
||||
import SwiftCheck
|
||||
import JSONAPIArbitrary
|
||||
|
||||
extension PersonDescription.Attributes: Arbitrary, Sampleable {
|
||||
public static var arbitrary: Gen<PersonDescription.Attributes> {
|
||||
return Gen.compose { c in
|
||||
return PersonDescription.Attributes(name: c.generate(),
|
||||
favoriteColor: c.generate())
|
||||
}
|
||||
}
|
||||
|
||||
public static var sample: PersonDescription.Attributes {
|
||||
return .init(name: ["Abbie", "Eibba"], favoriteColor: "Blue")
|
||||
}
|
||||
}
|
||||
|
||||
extension PersonDescription.Relationships: Arbitrary, Sampleable {
|
||||
public static var arbitrary: Gen<PersonDescription.Relationships> {
|
||||
return Gen.compose { c in
|
||||
return PersonDescription.Relationships(friends: c.generate(),
|
||||
dogs: c.generate(),
|
||||
home: c.generate())
|
||||
}
|
||||
}
|
||||
|
||||
public static var sample: PersonDescription.Relationships {
|
||||
return .init(friends: ["1", "2"], dogs: ["2"], home: "1")
|
||||
}
|
||||
}
|
||||
|
||||
extension DogDescription.Attributes: Arbitrary, Sampleable {
|
||||
public static var arbitrary: Gen<DogDescription.Attributes> {
|
||||
return Gen.compose { c in
|
||||
return DogDescription.Attributes(name: c.generate())
|
||||
}
|
||||
}
|
||||
|
||||
public static var sample: DogDescription.Attributes {
|
||||
return DogDescription.Attributes.arbitrary.generate
|
||||
}
|
||||
}
|
||||
|
||||
extension DogDescription.Relationships: Arbitrary, Sampleable {
|
||||
public static var arbitrary: Gen<DogDescription.Relationships> {
|
||||
return Gen.compose { c in
|
||||
return DogDescription.Relationships(owner: c.generate())
|
||||
}
|
||||
}
|
||||
|
||||
public static var sample: DogDescription.Relationships {
|
||||
return DogDescription.Relationships.arbitrary.generate
|
||||
}
|
||||
}
|
||||
|
||||
extension Document: Sampleable where PrimaryResourceBody: Arbitrary, IncludeType: Arbitrary, MetaType: Arbitrary, LinksType: Arbitrary, Error: Arbitrary, APIDescription: Arbitrary {
|
||||
public static var sample: Document {
|
||||
return Document.arbitrary.generate
|
||||
}
|
||||
|
||||
public static var successSample: Document? {
|
||||
return Document.arbitraryData.generate
|
||||
}
|
||||
|
||||
public static var failureSample: Document? {
|
||||
return Document.arbitraryErrors.generate
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='macos' executeOnSourceChanges='false'>
|
||||
<pages>
|
||||
<page name='Test Library'/>
|
||||
<page name='Usage'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
</pages>
|
||||
</playground>
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2018 Mathew Polzin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+31
-4
@@ -2,12 +2,39 @@
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Result",
|
||||
"repositoryURL": "https://github.com/antitypical/Result.git",
|
||||
"package": "AnyCodable",
|
||||
"repositoryURL": "https://github.com/Flight-School/AnyCodable.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8fc088dcf72802801efeecba76ea8fb041fb773d",
|
||||
"version": "4.0.0"
|
||||
"revision": "396ccc3dba5bdee04c1e742e7fab40582861401e",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "FileCheck",
|
||||
"repositoryURL": "https://github.com/llvm-swift/FileCheck.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "89b8480055f9adf8ce2f9ad5e2fac7ac1076242e",
|
||||
"version": "0.0.8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "Poly",
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "SwiftCheck",
|
||||
"repositoryURL": "https://github.com/typelift/SwiftCheck.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "cf9958085b2ee1643e541e407c3233d1b76c18ff",
|
||||
"version": "0.11.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+32
-6
@@ -6,23 +6,49 @@ import PackageDescription
|
||||
let package = Package(
|
||||
name: "JSONAPI",
|
||||
products: [
|
||||
// Products define the executables and libraries produced by a package, and make them visible to other packages.
|
||||
.library(
|
||||
name: "JSONAPI",
|
||||
targets: ["JSONAPI"]),
|
||||
.library(
|
||||
name: "JSONAPITesting",
|
||||
targets: ["JSONAPITesting"]),
|
||||
.library(
|
||||
name: "JSONAPIArbitrary",
|
||||
targets: ["JSONAPIArbitrary"]),
|
||||
.library(
|
||||
name: "JSONAPIOpenAPI",
|
||||
targets: ["JSONAPIOpenAPI"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/antitypical/Result.git", from: "4.0.0")
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", from: "1.0.0"),
|
||||
.package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.1.0"),
|
||||
.package(url: "https://github.com/typelift/SwiftCheck.git", from: "0.11.0")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
|
||||
.target(
|
||||
name: "JSONAPI",
|
||||
dependencies: ["Result"]),
|
||||
dependencies: ["Poly"]),
|
||||
.target(
|
||||
name: "JSONAPITesting",
|
||||
dependencies: ["JSONAPI"]),
|
||||
.target(
|
||||
name: "JSONAPIArbitrary",
|
||||
dependencies: ["JSONAPI", "SwiftCheck"]),
|
||||
.target(
|
||||
name: "JSONAPIOpenAPI",
|
||||
dependencies: ["JSONAPI", "AnyCodable", "JSONAPIArbitrary"]),
|
||||
.testTarget(
|
||||
name: "JSONAPITests",
|
||||
dependencies: ["JSONAPI"]),
|
||||
dependencies: ["JSONAPI", "JSONAPITesting"]),
|
||||
.testTarget(
|
||||
name: "JSONAPITestingTests",
|
||||
dependencies: ["JSONAPI", "JSONAPITesting"]),
|
||||
.testTarget(
|
||||
name: "JSONAPIArbitraryTests",
|
||||
dependencies: ["JSONAPI", "SwiftCheck", "JSONAPIArbitrary"]),
|
||||
.testTarget(
|
||||
name: "JSONAPIOpenAPITests",
|
||||
dependencies: ["JSONAPI", "JSONAPIOpenAPI"])
|
||||
],
|
||||
swiftLanguageVersions: [.v4_2]
|
||||
)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// APIDescription.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/1/18.
|
||||
//
|
||||
|
||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||
public protocol APIDescriptionType: Codable, Equatable {
|
||||
associatedtype Meta
|
||||
}
|
||||
|
||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
|
||||
public let version: String
|
||||
public let meta: Meta
|
||||
|
||||
public init(version: String, meta: Meta) {
|
||||
self.version = version
|
||||
self.meta = meta
|
||||
}
|
||||
}
|
||||
|
||||
/// Can be used as `APIDescriptionType` for Documents that do not
|
||||
/// have any API Description (a.k.a. "JSON:API Object").
|
||||
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
|
||||
public typealias Meta = NoMetadata
|
||||
|
||||
public init() {}
|
||||
|
||||
public static var none: NoAPIDescription { return .init() }
|
||||
|
||||
public var description: String { return "No JSON:API Object" }
|
||||
}
|
||||
|
||||
extension APIDescription {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
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
|
||||
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
|
||||
|
||||
if let metaVal = NoMetadata() as? Meta {
|
||||
meta = metaVal
|
||||
} else {
|
||||
meta = try container.decode(Meta.self, forKey: .meta)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(version, forKey: .version)
|
||||
|
||||
if Meta.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,21 @@
|
||||
// Created by Mathew Polzin on 11/5/18.
|
||||
//
|
||||
|
||||
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
|
||||
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
|
||||
var body: Body { get }
|
||||
}
|
||||
|
||||
/// A JSON API Document represents the entire body
|
||||
/// of a JSON API request or the entire body of
|
||||
/// a JSON API response.
|
||||
@@ -12,29 +27,244 @@
|
||||
/// API uses snake case, you will want to use
|
||||
/// a conversion such as the one offerred by the
|
||||
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
|
||||
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError>: Equatable {
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
/// The JSON API Spec calls this the JSON:API Object. It contains version
|
||||
/// and metadata information about the API itself.
|
||||
public let apiDescription: APIDescription
|
||||
|
||||
/// The Body of the Document. This body is either one or more errors
|
||||
/// with links and metadata attempted to parse but not guaranteed or
|
||||
/// it is a successful data struct containing all the primary and
|
||||
/// included resources, the metadata, and the links that this
|
||||
/// document type specifies.
|
||||
public let body: Body
|
||||
// public let meta: Meta?
|
||||
// public let jsonApi: APIDescription?
|
||||
// public let links: Links?
|
||||
|
||||
public enum Body: Equatable {
|
||||
case errors([Error])
|
||||
case data(primary: ResourceBody, included: Includes<Include>)
|
||||
|
||||
case errors([Error], meta: MetaType?, links: LinksType?)
|
||||
case data(Data)
|
||||
|
||||
public struct Data: Equatable {
|
||||
public let primary: PrimaryResourceBody
|
||||
public let includes: Includes<Include>
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
|
||||
public init(primary: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.primary = primary
|
||||
self.includes = includes
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
}
|
||||
|
||||
public var isError: Bool {
|
||||
guard case .errors = self else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
public var errors: [Error]? {
|
||||
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
|
||||
return errors
|
||||
}
|
||||
|
||||
public var data: Data? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data
|
||||
}
|
||||
|
||||
public var data: (primary: ResourceBody, included: Includes<Include>)? {
|
||||
guard case let .data(primary: body, included: includes) = self else { return nil }
|
||||
return (primary: body, included: includes)
|
||||
public var primaryResource: PrimaryResourceBody? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.primary
|
||||
}
|
||||
|
||||
public var includes: Includes<Include>? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.includes
|
||||
}
|
||||
|
||||
public var meta: MetaType? {
|
||||
switch self {
|
||||
case .data(let data):
|
||||
return data.meta
|
||||
case .errors(_, meta: let metadata?, links: _):
|
||||
return metadata
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var links: LinksType? {
|
||||
switch self {
|
||||
case .data(let data):
|
||||
return data.links
|
||||
case .errors(_, meta: _, links: let links?):
|
||||
return links
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
|
||||
public init(apiDescription: APIDescription,
|
||||
body: PrimaryResourceBody,
|
||||
includes: Includes<Include>,
|
||||
meta: MetaType,
|
||||
links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
extension Document where IncludeType == NoIncludes {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: .none, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType) {
|
||||
self.init(apiDescription: apiDescription, body: body, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody) {
|
||||
self.init(apiDescription: .none, body: body)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
public func merging(_ other: Document.Body.Data,
|
||||
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
|
||||
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
|
||||
return Document.Body.Data(primary: primary.appending(other.primary),
|
||||
includes: includes.appending(other.includes),
|
||||
meta: metaMerge(meta, other.meta),
|
||||
links: linksMerge(links, other.links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
|
||||
return merging(other,
|
||||
combiningMetaWith: { _, _ in .none },
|
||||
combiningLinksWith: { _, _ in .none })
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes {
|
||||
/// Create a new Document with the given includes.
|
||||
public func including<I: JSONAPI.Include>(_ includes: Includes<I>) -> Document<PrimaryResourceBody, MetaType, LinksType, I, APIDescription, Error> {
|
||||
// Note that if IncludeType is NoIncludes, then we allow anything
|
||||
// to be included, but if IncludeType already specifies a type
|
||||
// of thing to be expected then we lock that down.
|
||||
// See: Document.including() where IncludeType: _Poly1
|
||||
switch body {
|
||||
case .data(let data):
|
||||
return .init(apiDescription: apiDescription,
|
||||
body: data.primary,
|
||||
includes: includes,
|
||||
meta: data.meta,
|
||||
links: data.links)
|
||||
case .errors(let errors, meta: let meta, links: let links):
|
||||
return .init(apiDescription: apiDescription,
|
||||
errors: errors,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument: Codable {
|
||||
// extending where _Poly1 means all non-zero _Poly arities are included
|
||||
extension Document where IncludeType: _Poly1 {
|
||||
/// Create a new Document adding the given includes. This does not
|
||||
/// remove existing includes; it is additive.
|
||||
public func including(_ includes: Includes<IncludeType>) -> Document {
|
||||
// Note that if IncludeType is NoIncludes, then we allow anything
|
||||
// to be included, but if IncludeType already specifies a type
|
||||
// of thing to be expected then we lock that down.
|
||||
// See: Document.including() where IncludeType == NoIncludes
|
||||
switch body {
|
||||
case .data(let data):
|
||||
return .init(apiDescription: apiDescription,
|
||||
body: data.primary,
|
||||
includes: data.includes + includes,
|
||||
meta: data.meta,
|
||||
links: data.links)
|
||||
case .errors(let errors, meta: let meta, links: let links):
|
||||
return .init(apiDescription: apiDescription,
|
||||
errors: errors,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
extension Document {
|
||||
private enum RootCodingKeys: String, CodingKey {
|
||||
case data
|
||||
case errors
|
||||
@@ -47,38 +277,127 @@ extension JSONAPIDocument: Codable {
|
||||
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)
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
return
|
||||
}
|
||||
|
||||
let data = try container.decode(ResourceBody.self, forKey: .data)
|
||||
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(primary: data, included: maybeIncludes ?? Includes<Include>.none)
|
||||
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)
|
||||
|
||||
switch body {
|
||||
case .errors(let errors):
|
||||
case .errors(let errors, meta: let meta, links: let links):
|
||||
var errContainer = container.nestedUnkeyedContainer(forKey: .errors)
|
||||
|
||||
for error in errors {
|
||||
try errContainer.encode(error)
|
||||
}
|
||||
|
||||
case .data(primary: let resourceBody, included: let includes):
|
||||
try container.encode(resourceBody, forKey: .data)
|
||||
if MetaType.self != NoMetadata.self,
|
||||
let metaVal = meta {
|
||||
try container.encode(metaVal, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self,
|
||||
let linksVal = links {
|
||||
try container.encode(linksVal, forKey: .links)
|
||||
}
|
||||
|
||||
case .data(let data):
|
||||
try container.encode(data.primary, forKey: .data)
|
||||
|
||||
if Include.self != NoIncludes.self {
|
||||
try container.encode(includes, forKey: .included)
|
||||
try container.encode(data.includes, forKey: .included)
|
||||
}
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(data.meta, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(data.links, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
if APIDescription.self != NoAPIDescription.self {
|
||||
try container.encode(apiDescription, forKey: .jsonapi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Document: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Document(\(String(describing: body)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .errors(let errors, meta: let meta, links: let links):
|
||||
return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))"
|
||||
case .data(let data):
|
||||
return String(describing: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
|
||||
static var unknown: Self { get }
|
||||
}
|
||||
|
||||
public enum BasicJSONAPIError: JSONAPIError {
|
||||
public enum UnknownJSONAPIError: JSONAPIError {
|
||||
case unknownError
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -21,7 +21,7 @@ public enum BasicJSONAPIError: JSONAPIError {
|
||||
try container.encode("unknown")
|
||||
}
|
||||
|
||||
public static var unknown: BasicJSONAPIError {
|
||||
public static var unknown: UnknownJSONAPIError {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,67 @@
|
||||
//
|
||||
// ResourceBody.swift
|
||||
// PrimaryResourceBody.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
public protocol MaybePrimaryResource: Equatable, Codable {}
|
||||
|
||||
/// 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 {}
|
||||
|
||||
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
|
||||
|
||||
/// 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 struct SingleResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
|
||||
public let value: Entity?
|
||||
public protocol AppendableResourceBody: ResourceBody {
|
||||
func appending(_ other: Self) -> Self
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
|
||||
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
||||
public let value: Entity
|
||||
|
||||
public init(entity: Entity) {
|
||||
self.value = entity
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
|
||||
public let values: [Entity]
|
||||
|
||||
public init(entities: [Entity]) {
|
||||
values = entities
|
||||
}
|
||||
|
||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||
return ManyResourceBody(entities: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Decodable
|
||||
/// Use NoResourceBody to indicate you expect a JSON API document to not
|
||||
/// contain a "data" top-level key.
|
||||
public struct NoResourceBody: ResourceBody {
|
||||
public static var none: NoResourceBody { return NoResourceBody() }
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
extension SingleResourceBody {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
if container.decodeNil() {
|
||||
value = nil
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
@@ -32,7 +71,7 @@ extension SingleResourceBody {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
if value == nil {
|
||||
if (value as Any?) == nil {
|
||||
try container.encodeNil()
|
||||
return
|
||||
}
|
||||
@@ -59,3 +98,17 @@ extension ManyResourceBody {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
extension SingleResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "PrimaryResourceBody(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "PrimaryResourceBody(\(String(describing: values)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user