mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
+173
@@ -0,0 +1,173 @@
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
// MARK: - Preamble (setup)
|
||||
|
||||
// We make String a CreatableRawIdType. This is actually done in
|
||||
// this Playground's Entities.swift file, so it is commented out here.
|
||||
/*
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// Similarly, we create a typealias for unidentified entities. JSON:API
|
||||
// only allows unidentified entities (i.e. no "id" field) for client
|
||||
// requests that create new entities. In these situations, the server
|
||||
// is expected to assign the new entity a unique ID.
|
||||
typealias UnidentifiedJSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// We create a typealias for a Document given that we do not expect
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: EntityDescription {
|
||||
public static var type: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: EntityDescription {
|
||||
public static var type: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
|
||||
// MARK: Document Definitions
|
||||
|
||||
// We create a typealias to represent a document containing one Article
|
||||
// and including its Author
|
||||
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
|
||||
|
||||
// ... and a typealias to represent a document containing one Article and
|
||||
// not including any related entities.
|
||||
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
|
||||
|
||||
// MARK: - Server Pseudo-example
|
||||
|
||||
// Skipping over all the API and database stuff, here's a chunk of code
|
||||
// that creates a document. Note that this document is the entirety
|
||||
// of a JSON:API response body.
|
||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// Let's pretend all of this is coming from a database:
|
||||
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(entity: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .a(document)
|
||||
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
|
||||
return .b(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
|
||||
let responseBody = articleDocument(includeAuthor: true)
|
||||
let responseData = try! encoder.encode(responseBody)
|
||||
|
||||
// Next step would be encoding and setting as the HTTP body of a response.
|
||||
// we will just print it out instead:
|
||||
print("-----")
|
||||
print(String(data: responseData, encoding: .utf8)!)
|
||||
|
||||
// ... and if we had received a request for an article without
|
||||
// including the author:
|
||||
let otherResponseBody = articleDocument(includeAuthor: false)
|
||||
let otherResponseData = try! encoder.encode(otherResponseBody)
|
||||
print("-----")
|
||||
print(String(data: otherResponseData, encoding: .utf8)!)
|
||||
|
||||
// MARK: - Client Pseudo-example
|
||||
|
||||
enum NetworkError: Swift.Error {
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
}
|
||||
|
||||
// Skipping over all the API stuff, here's a chunk of code that will
|
||||
// decode a document. We will assume we have made a request for a
|
||||
// single article including the author.
|
||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
}
|
||||
|
||||
let response = try! docode(articleResponseData: responseData)
|
||||
|
||||
// Next step would be to do something useful with the article and author but we will print them instead.
|
||||
print("-----")
|
||||
print(response.article)
|
||||
print(response.author)
|
||||
+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 type: 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 type: 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)!)
|
||||
@@ -13,7 +13,7 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
||||
// MARK: - Literal Expressibility
|
||||
// The JSONAPITestLib 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))
|
||||
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 JSONAPITestLib provides a `check` function for each Entity type
|
||||
|
||||
@@ -13,30 +13,37 @@ let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let singleDogDocument = SingleDogDocument(body: SingleResourceBody(entity: dogFromCode))
|
||||
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.primaryData?.value
|
||||
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(), House()]
|
||||
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])]
|
||||
|
||||
typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||
let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes))
|
||||
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.primaryData?.values
|
||||
let peopleFromData = peopleResponse.body.primaryResource?.values
|
||||
let dogsFromData = peopleResponse.body.includes?[Dog.self]
|
||||
let housesFromData = peopleResponse.body.includes?[House.self]
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Entity typealias for convenience
|
||||
// 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 {
|
||||
@@ -46,11 +48,11 @@ public enum PersonDescription: EntityDescription {
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let friends: ToManyRelationship<Person>
|
||||
public let dogs: ToManyRelationship<Dog>
|
||||
public let home: ToOneRelationship<House>
|
||||
public let friends: ToMany<Person>
|
||||
public let dogs: ToMany<Dog>
|
||||
public let home: ToOne<House>
|
||||
|
||||
public init(friends: ToManyRelationship<Person>, dogs: ToManyRelationship<Dog>, home: ToOneRelationship<House>) {
|
||||
public init(friends: ToMany<Person>, dogs: ToMany<Dog>, home: ToOne<House>) {
|
||||
self.friends = friends
|
||||
self.dogs = dogs
|
||||
self.home = home
|
||||
@@ -62,7 +64,7 @@ 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 = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +81,9 @@ public enum DogDescription: EntityDescription {
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let owner: ToOneRelationship<Person?>
|
||||
public let owner: ToOne<Person?>
|
||||
|
||||
public init(owner: ToOneRelationship<Person?>) {
|
||||
public init(owner: ToOne<Person?>) {
|
||||
self.owner = owner
|
||||
}
|
||||
}
|
||||
@@ -89,13 +91,41 @@ public enum DogDescription: EntityDescription {
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public enum AlternativeDogDescription: EntityDescription {
|
||||
|
||||
public static var type: 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 = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
|
||||
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 = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,5 +3,7 @@
|
||||
<pages>
|
||||
<page name='Test Library'/>
|
||||
<page name='Usage'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
</pages>
|
||||
</playground>
|
||||
+1
-9
@@ -1,15 +1,7 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Result",
|
||||
"repositoryURL": "https://github.com/mattpolzin/Result",
|
||||
"state": {
|
||||
"branch": "master",
|
||||
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
|
||||
+1
-3
@@ -14,13 +14,11 @@ let package = Package(
|
||||
targets: ["JSONAPITestLib"])
|
||||
],
|
||||
dependencies: [
|
||||
// antitypical/Result without the Foundation requirement:
|
||||
.package(url: "https://github.com/mattpolzin/Result", .branch("master"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "JSONAPI",
|
||||
dependencies: ["Result"]),
|
||||
dependencies: []),
|
||||
.target(
|
||||
name: "JSONAPITestLib",
|
||||
dependencies: ["JSONAPI"]),
|
||||
|
||||
@@ -5,6 +5,61 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
|
||||
## Table of Contents
|
||||
<!-- TOC depthFrom:2 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
|
||||
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Primary Goals](#primary-goals)
|
||||
- [Caveat](#caveat)
|
||||
- [Dev Environment](#dev-environment)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Xcode project](#xcode-project)
|
||||
- [Running the Playground](#running-the-playground)
|
||||
- [Project Status](#project-status)
|
||||
- [Encoding/Decoding](#encodingdecoding)
|
||||
- [Document](#document)
|
||||
- [Resource Object](#resource-object)
|
||||
- [Relationship Object](#relationship-object)
|
||||
- [Links Object](#links-object)
|
||||
- [Misc](#misc)
|
||||
- [JSONAPITestLib](#jsonapitestlib)
|
||||
- [Entity Validator](#entity-validator)
|
||||
- [Potential Improvements](#potential-improvements)
|
||||
- [Usage](#usage)
|
||||
- [`JSONAPI.EntityDescription`](#jsonapientitydescription)
|
||||
- [`JSONAPI.Entity`](#jsonapientity)
|
||||
- [`Meta`](#meta)
|
||||
- [`Links`](#links)
|
||||
- [`IdType`](#idtype)
|
||||
- [`MaybeRawId`](#mayberawid)
|
||||
- [Convenient `typealiases`](#convenient-typealiases)
|
||||
- [`JSONAPI.Relationships`](#jsonapirelationships)
|
||||
- [`JSONAPI.Attributes`](#jsonapiattributes)
|
||||
- [`Transformer`](#transformer)
|
||||
- [`Validator`](#validator)
|
||||
- [Computed `Attribute`](#computed-attribute)
|
||||
- [Copying `Entities`](#copying-entities)
|
||||
- [`JSONAPI.Document`](#jsonapidocument)
|
||||
- [`ResourceBody`](#resourcebody)
|
||||
- [nullable `PrimaryResource`](#nullable-primaryresource)
|
||||
- [`MetaType`](#metatype)
|
||||
- [`LinksType`](#linkstype)
|
||||
- [`IncludeType`](#includetype)
|
||||
- [`APIDescriptionType`](#apidescriptiontype)
|
||||
- [`Error`](#error)
|
||||
- [`JSONAPI.Meta`](#jsonapimeta)
|
||||
- [`JSONAPI.Links`](#jsonapilinks)
|
||||
- [`JSONAPI.RawIdType`](#jsonapirawidtype)
|
||||
- [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping)
|
||||
- [Custom Attribute Encode/Decode](#custom-attribute-encodedecode)
|
||||
- [Example](#example)
|
||||
- [Preamble (Setup shared by server and client)](#preamble-setup-shared-by-server-and-client)
|
||||
- [Server Pseudo-example](#server-pseudo-example)
|
||||
- [Client Pseudo-example](#client-pseudo-example)
|
||||
- [JSONAPITestLib](#jsonapitestlib)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## Primary Goals
|
||||
|
||||
The primary goals of this framework are:
|
||||
@@ -52,8 +107,8 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
|
||||
#### Relationship Object
|
||||
- [x] `data`
|
||||
- [ ] `links`
|
||||
- [ ] `meta`
|
||||
- [x] `links`
|
||||
- [x] `meta`
|
||||
|
||||
#### Links Object
|
||||
- [x] `href`
|
||||
@@ -62,17 +117,19 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
### Misc
|
||||
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
|
||||
- [x] Support validation on `Attributes`.
|
||||
- [ ] Support sparse fieldsets. At the moment, not sure what this support will look like. A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, it becomes much more appealing to be able to support arbitrary combinations of omitted fields.
|
||||
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
|
||||
|
||||
### JSONAPITestLib
|
||||
#### Entity Validator
|
||||
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
|
||||
- [x] Only allow `TransformedAttribute` and its derivatives within `Attributes` struct.
|
||||
- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded.
|
||||
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
|
||||
|
||||
### Potential Improvements
|
||||
- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources.
|
||||
- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies.
|
||||
- [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`.
|
||||
- [ ] Property-based testing (using `SwiftCheck`).
|
||||
- [ ] Error or warning if an included entity is not related to a primary entity or another included entity (Turned off or at least not throwing by default).
|
||||
|
||||
@@ -186,9 +243,11 @@ Note that I am assuming an unidentified person is a "new" person. I suspect that
|
||||
|
||||
There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. An `EntityDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of an `EntityDescription`.
|
||||
|
||||
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value might be `null` or it might be a resource identifier) like this:
|
||||
In addition to identifying entities by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document.
|
||||
|
||||
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this:
|
||||
```
|
||||
let nullableRelative: ToOneRelationship<Person?>
|
||||
let nullableRelative: ToOneRelationship<Person?, NoMetadata, NoLinks>
|
||||
```
|
||||
|
||||
An entity that does not have relationships can be described by adding the following to an `EntityDescription`:
|
||||
@@ -225,6 +284,11 @@ typealias Attributes = NoAttributes
|
||||
let favoriteColor: String = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
NOTE: Because of support for computed properties that are not wrapped in `Attribute`, `TransformedAttribute`, or `ValidatedAttribute`, the compiler cannot always infer the type of thing you want back when using subscript attribute access. The following code is ambiguous about whether it should return a `String` or an `Attribute<String>`:
|
||||
```
|
||||
let favoriteColor = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
#### `Transformer`
|
||||
|
||||
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`.
|
||||
@@ -263,7 +327,7 @@ You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is ju
|
||||
|
||||
#### Computed `Attribute`
|
||||
|
||||
You can add computed properties to your `EntityDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties should still have the type `Attribute` because that way you can take advantage of the quick access provided by `Entity`'s subscript operator. Here's an example of how you might take the `Person[\.name]` attribute from the example above and create a `fullName` computed property.
|
||||
You can add computed properties to your `EntityDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `Person[\.name]` attribute from the example above and create a `fullName` computed property.
|
||||
|
||||
```
|
||||
public var fullName: Attribute<String> {
|
||||
@@ -271,6 +335,23 @@ public var fullName: Attribute<String> {
|
||||
}
|
||||
```
|
||||
|
||||
### Copying `Entities`
|
||||
`Entity` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying an `Entity`:
|
||||
1. Assigning a new `Identifier` to the copy of an identified `Entity`.
|
||||
2. Assigning a new `Identifier` to the copy of an unidentified `Entity`.
|
||||
|
||||
The above can be accomplished with code like the following:
|
||||
|
||||
```
|
||||
// use case 1
|
||||
let person1 = person.withNewIdentifier()
|
||||
|
||||
// use case 2
|
||||
let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self)
|
||||
|
||||
let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232")
|
||||
```
|
||||
|
||||
### `JSONAPI.Document`
|
||||
|
||||
The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
|
||||
@@ -375,5 +456,248 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
```
|
||||
|
||||
# JSONAPITestLib
|
||||
### Custom Attribute or Relationship Key Mapping
|
||||
There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase:
|
||||
```
|
||||
public enum EntityDescription1: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let coolProperty: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
public enum EntityDescription2: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let wholeOtherThing: Attribute<String>
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case wholeOtherThing = "coolProperty"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Attribute Encode/Decode
|
||||
You can safely provide your own encoding or decoding functions for your Attributes struct if you need to as long as you are careful that your encode operation correctly reverses your decode operation. Although this is generally not necessary, `AttributeType` provides a convenience method to make your decoding a bit less boilerplate ridden. This is what it looks like:
|
||||
```
|
||||
public enum EntityDescription1: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let property1: Attribute<String>
|
||||
public let property2: Attribute<Int>
|
||||
public let property3: Attribute<String>
|
||||
|
||||
public let weirdThing: Attribute<String>
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case property1
|
||||
case property2
|
||||
case property3
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
extension EntityDescription1.Attributes {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
property1 = try .defaultDecoding(from: container, forKey: .property1)
|
||||
property2 = try .defaultDecoding(from: container, forKey: .property2)
|
||||
property3 = try .defaultDecoding(from: container, forKey: .property3)
|
||||
|
||||
weirdThing = .init(value: "hello world")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(property1, forKey: .property1)
|
||||
try container.encode(property2, forKey: .property2)
|
||||
try container.encode(property3, forKey: .property3)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Example
|
||||
The following serves as a sort of pseudo-example. It skips server/client implementation details not related to JSON:API but still gives a more complete picture of what an implementation using this framework might look like. You can play with this example code in the Playground provided with this repo.
|
||||
|
||||
### Preamble (Setup shared by server and client)
|
||||
```
|
||||
// We make String a CreatableRawIdType.
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// Similarly, we create a typealias for unidentified entities. JSON:API
|
||||
// only allows unidentified entities (i.e. no "id" field) for client
|
||||
// requests that create new entities. In these situations, the server
|
||||
// is expected to assign the new entity a unique ID.
|
||||
typealias UnidentifiedJSONEntity<Description: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// We create a typealias for a Document given that we do not expect
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: EntityDescription {
|
||||
public static var type: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: EntityDescription {
|
||||
public static var type: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
|
||||
// MARK: Document Definitions
|
||||
|
||||
// We create a typealias to represent a document containing one Article
|
||||
// and including its Author
|
||||
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
|
||||
|
||||
// ... and a typealias to represent a document containing one Article and
|
||||
// not including any related entities.
|
||||
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
|
||||
```
|
||||
### Server Pseudo-example
|
||||
```
|
||||
// Skipping over all the API and database stuff, here's a chunk of code
|
||||
// that creates a document. Note that this document is the entirety
|
||||
// of a JSON:API response body.
|
||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// Let's pretend all of this is coming from a database:
|
||||
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(entity: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .a(document)
|
||||
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
|
||||
return .b(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
encoder.keyEncodingStrategy = .convertToSnakeCase
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
|
||||
let responseBody = articleDocument(includeAuthor: true)
|
||||
let responseData = try! encoder.encode(responseBody)
|
||||
|
||||
// Next step would be encoding and setting as the HTTP body of a response.
|
||||
// we will just print it out instead:
|
||||
print("-----")
|
||||
print(String(data: responseData, encoding: .utf8)!)
|
||||
|
||||
// ... and if we had received a request for an article without
|
||||
// including the author:
|
||||
let otherResponseBody = articleDocument(includeAuthor: false)
|
||||
let otherResponseData = try! encoder.encode(otherResponseBody)
|
||||
print("-----")
|
||||
print(String(data: otherResponseData, encoding: .utf8)!)
|
||||
```
|
||||
|
||||
### Client Pseudo-example
|
||||
```
|
||||
enum NetworkError: Swift.Error {
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
}
|
||||
|
||||
// Skipping over all the API stuff, here's a chunk of code that will
|
||||
// decode a document. We will assume we have made a request for a
|
||||
// single article including the author.
|
||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
}
|
||||
|
||||
let response = try! docode(articleResponseData: responseData)
|
||||
|
||||
// Next step would be to do something useful with the article and author but we will print them instead.
|
||||
print("-----")
|
||||
print(response.article)
|
||||
print(response.author)
|
||||
```
|
||||
|
||||
## JSONAPITestLib
|
||||
The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITestLib` in action in the Playground included with the `JSONAPI` repository.
|
||||
|
||||
@@ -14,8 +14,15 @@ public protocol APIDescriptionType: Codable, Equatable {
|
||||
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
|
||||
|
||||
|
||||
@@ -66,8 +66,13 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
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 primaryData: PrimaryResourceBody? {
|
||||
public var primaryResource: PrimaryResourceBody? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.primary
|
||||
}
|
||||
@@ -105,12 +110,17 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
public init(apiDescription: APIDescription,
|
||||
body: PrimaryResourceBody,
|
||||
includes: Includes<Include>,
|
||||
meta: MetaType,
|
||||
links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
extension Document where IncludeType == NoIncludes {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: .none, meta: meta, links: links)
|
||||
@@ -182,7 +192,76 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -50,6 +50,16 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes {
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
extension Includes: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Includes(\(String(describing: values))"
|
||||
@@ -115,4 +125,25 @@ extension Includes where I: _Poly6 {
|
||||
}
|
||||
|
||||
// MARK: - 7 includes
|
||||
public typealias Include7 = Poly7
|
||||
extension Includes where I: _Poly7 {
|
||||
public subscript(_ lookup: I.G.Type) -> [I.G] {
|
||||
return values.compactMap { $0.g }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 8 includes
|
||||
public typealias Include8 = Poly8
|
||||
extension Includes where I: _Poly8 {
|
||||
public subscript(_ lookup: I.H.Type) -> [I.H] {
|
||||
return values.compactMap { $0.h }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 9 includes
|
||||
public typealias Include9 = Poly9
|
||||
extension Includes where I: _Poly9 {
|
||||
public subscript(_ lookup: I.I.Type) -> [I.I] {
|
||||
return values.compactMap { $0.i }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@ extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
|
||||
public protocol ResourceBody: Codable, Equatable {
|
||||
}
|
||||
|
||||
public protocol AppendableResourceBody: ResourceBody {
|
||||
func appending(_ other: Self) -> Self
|
||||
}
|
||||
|
||||
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
||||
public let value: Entity
|
||||
|
||||
@@ -27,12 +35,16 @@ public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: Resource
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: ResourceBody {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
/// Use NoResourceBody to indicate you expect a JSON API document to not
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -19,3 +19,18 @@ public extension TransformedAttribute {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
|
||||
public extension Attribute {
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
public func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,17 @@
|
||||
//
|
||||
|
||||
public protocol AttributeType: Codable {
|
||||
associatedtype RawValue: Codable
|
||||
associatedtype ValueType
|
||||
|
||||
var value: ValueType { get }
|
||||
}
|
||||
|
||||
// MARK: TransformedAttribute
|
||||
|
||||
/// 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 {
|
||||
private let rawValue: RawValue
|
||||
let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
|
||||
@@ -19,7 +26,19 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer: ReversibleTransformer {
|
||||
/// Initialize a TransformedAttribute from its transformed value. The
|
||||
/// RawValue, which is what gets encoded/decoded, is determined using
|
||||
/// The Transformer's reverse function.
|
||||
public init(transformedValue: Transformer.To) throws {
|
||||
self.value = transformedValue
|
||||
rawValue = try Transformer.reverse(value)
|
||||
@@ -34,18 +53,34 @@ extension TransformedAttribute: CustomStringConvertible {
|
||||
|
||||
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
|
||||
|
||||
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
// MARK: ValidatedAttribute
|
||||
|
||||
/// A ValidatedAttribute does not transform its raw value, but it throws
|
||||
/// an error if the raw value does not match expectations.
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
|
||||
// MARK: Attribute
|
||||
|
||||
/// An Attribute simply represents a type that can be encoded and decoded.
|
||||
public struct Attribute<RawValue: Codable>: AttributeType {
|
||||
let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>>
|
||||
|
||||
public var value: RawValue {
|
||||
return attribute.value
|
||||
}
|
||||
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
attribute = .init(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
extension Attribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
|
||||
extension Attribute: Equatable where RawValue: Equatable {}
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformedAttribute {
|
||||
@@ -73,15 +108,40 @@ extension TransformedAttribute {
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
// See note in decode above about the weirdness
|
||||
// going on here.
|
||||
let anyNil: Any? = nil
|
||||
if let _ = anyNil as? Transformer.From,
|
||||
(rawValue as Any?) == nil {
|
||||
try container.encodeNil()
|
||||
}
|
||||
|
||||
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? RawValue {
|
||||
attribute = .init(value: val)
|
||||
} else {
|
||||
attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Attribute decoding and encoding defaults
|
||||
|
||||
extension AttributeType {
|
||||
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container, forKey key: Container.Key) throws -> Self {
|
||||
return try container.decode(Self.self, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
/// A JSON API structure within an Entity that contains
|
||||
/// named properties of types `ToOneRelationship` and
|
||||
/// `ToManyRelationship`.
|
||||
public typealias Relationships = Codable & Equatable
|
||||
public protocol Relationships: Codable & Equatable {}
|
||||
|
||||
/// A JSON API structure within an Entity that contains
|
||||
/// properties of any types that are JSON encodable.
|
||||
public typealias Attributes = Codable & Equatable
|
||||
public protocol Attributes: Codable & Equatable {}
|
||||
|
||||
/// Can be used as `Relationships` Type for Entities that do not
|
||||
/// have any Relationships.
|
||||
@@ -20,29 +20,45 @@ public struct NoRelationships: Relationships {
|
||||
public static var none: NoRelationships { return .init() }
|
||||
}
|
||||
|
||||
extension NoRelationships: CustomStringConvertible {
|
||||
public var description: String { return "No Relationships" }
|
||||
}
|
||||
|
||||
/// Can be used as `Attributes` Type for Entities that do not
|
||||
/// have any Attributes.
|
||||
public struct NoAttributes: Attributes {
|
||||
public static var none: NoAttributes { return .init() }
|
||||
}
|
||||
|
||||
extension NoAttributes: CustomStringConvertible {
|
||||
public var description: String { return "No Attributes" }
|
||||
}
|
||||
|
||||
/// Something that is JSONTyped provides a String representation
|
||||
/// of its type.
|
||||
public protocol JSONTyped {
|
||||
static var jsonType: String { get }
|
||||
}
|
||||
|
||||
/// An `EntityProxyDescription` is an `EntityDescription`
|
||||
/// without Codable conformance.
|
||||
public protocol EntityProxyDescription: JSONTyped {
|
||||
associatedtype Attributes: Equatable
|
||||
associatedtype Relationships: Equatable
|
||||
}
|
||||
|
||||
/// An `EntityDescription` describes a JSON API
|
||||
/// Resource Object. The Resource Object
|
||||
/// itself is encoded and decoded as an
|
||||
/// `Entity`, which gets specialized on an
|
||||
/// `EntityDescription`.
|
||||
public protocol EntityDescription {
|
||||
associatedtype Attributes: JSONAPI.Attributes
|
||||
associatedtype Relationships: JSONAPI.Relationships
|
||||
|
||||
static var type: String { get }
|
||||
}
|
||||
public protocol EntityDescription: EntityProxyDescription where Attributes: JSONAPI.Attributes, Relationships: JSONAPI.Relationships {}
|
||||
|
||||
/// EntityProxy is a protocol that can be used to create
|
||||
/// types that _act_ like Entities but cannot be encoded
|
||||
/// or decoded as Entities.
|
||||
public protocol EntityProxy: Equatable {
|
||||
associatedtype Description: EntityDescription
|
||||
public protocol EntityProxy: Equatable, JSONTyped {
|
||||
associatedtype Description: EntityProxyDescription
|
||||
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
|
||||
|
||||
typealias Id = JSONAPI.Id<EntityRawIdType, Self>
|
||||
@@ -65,13 +81,15 @@ public protocol EntityProxy: Equatable {
|
||||
|
||||
extension EntityProxy {
|
||||
/// The JSON API compliant "type" of this `Entity`.
|
||||
public static var type: String { return Description.type }
|
||||
public static var jsonType: String { return Description.jsonType }
|
||||
}
|
||||
|
||||
/// EntityType is the protocol that Entity conforms to. This
|
||||
/// protocol lets other types accept any Entity as a generic
|
||||
/// specialization.
|
||||
public protocol EntityType: EntityProxy, PrimaryResource {
|
||||
public protocol EntityType: EntityProxy, PrimaryResource where Description: EntityDescription {
|
||||
associatedtype Meta: JSONAPI.Meta
|
||||
associatedtype Links: JSONAPI.Links
|
||||
}
|
||||
|
||||
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
|
||||
@@ -81,6 +99,10 @@ public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdT
|
||||
/// "Resource Object."
|
||||
/// See https://jsonapi.org/format/#document-resource-objects
|
||||
public struct Entity<Description: JSONAPI.EntityDescription, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
|
||||
|
||||
public typealias Meta = MetaType
|
||||
public typealias Links = LinksType
|
||||
|
||||
/// The `Entity`'s Id. This can be of type `Unidentified` if
|
||||
/// the entity is being created clientside and the
|
||||
/// server is being asked to create a unique Id. Otherwise,
|
||||
@@ -108,14 +130,13 @@ public struct Entity<Description: JSONAPI.EntityDescription, MetaType: JSONAPI.M
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity: IdentifiableEntityType, Relatable, WrappedRelatable where EntityRawIdType: JSONAPI.RawIdType {
|
||||
extension Entity: Identifiable, IdentifiableEntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {
|
||||
public typealias Identifier = Entity.Id
|
||||
public typealias WrappedIdentifier = Identifier
|
||||
}
|
||||
|
||||
extension Entity: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Entity<\(Entity.type)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
|
||||
return "Entity<\(Entity.jsonType)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +161,7 @@ extension Entity where EntityRawIdType == Unidentified {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
extension Entity where Description.Attributes == NoAttributes {
|
||||
public init(id: Entity.Id, relationships: Description.Relationships, meta: MetaType, links: LinksType) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: relationships, meta: meta, links: links)
|
||||
@@ -326,6 +348,12 @@ extension Entity where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdTy
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where LinksType == NoLinks {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) {
|
||||
self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none)
|
||||
@@ -338,6 +366,12 @@ extension Entity where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where LinksType == NoLinks, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none)
|
||||
@@ -350,12 +384,55 @@ extension Entity where MetaType == NoMetadata, LinksType == NoLinks, EntityRawId
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// MARK: Pointer for Relationships use.
|
||||
public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
|
||||
|
||||
/// An Entity.Pointer is a `ToOneRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference an Entity so that
|
||||
/// other Entities' Relationships can be built up from it.
|
||||
public typealias Pointer = ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
/// Entity.Pointers is a `ToManyRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a bunch of Entities so
|
||||
/// that other Entities' Relationships can be built up from them.
|
||||
public typealias Pointers = ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
/// Get a pointer to this entity that can be used as a
|
||||
/// relationship to another entity.
|
||||
public var pointer: ToOneRelationship<Entity> {
|
||||
return ToOneRelationship(entity: self)
|
||||
public var pointer: Pointer {
|
||||
return Pointer(entity: self)
|
||||
}
|
||||
|
||||
public func pointer<MType: JSONAPI.Meta, LType: JSONAPI.Links>(withMeta meta: MType, links: LType) -> ToOneRelationship<Entity, MType, LType> {
|
||||
return ToOneRelationship(entity: self, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Identifying Unidentified Entities
|
||||
public extension Entity where EntityRawIdType == Unidentified {
|
||||
/// Create a new Entity from this one with a newly created
|
||||
/// unique Id of the given type.
|
||||
public func identified<RawIdType: CreatableRawIdType>(byType: RawIdType.Type) -> Entity<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
|
||||
/// Create a new Entity from this one with the given Id.
|
||||
public func identified<RawIdType: JSONAPI.RawIdType>(by id: RawIdType) -> Entity<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(id: Entity<Description, MetaType, LinksType, RawIdType>.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Entity where EntityRawIdType: CreatableRawIdType {
|
||||
/// Create a copy of this Entity with a new unique Id.
|
||||
public func withNewIdentifier() -> Entity {
|
||||
return Entity(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,23 +441,34 @@ public extension EntityProxy {
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>>) -> TFRM.To {
|
||||
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T>) -> T.ValueType {
|
||||
return attributes[keyPath: path].value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> TFRM.To? {
|
||||
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T?>) -> T.ValueType? {
|
||||
return attributes[keyPath: path]?.value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
|
||||
subscript<T: AttributeType, U>(_ path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
|
||||
// Implementation Note: Handles Transform that returns optional
|
||||
// type.
|
||||
return attributes[keyPath: path].flatMap { $0.value }
|
||||
}
|
||||
|
||||
/// Access the computed attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T>(_ path: KeyPath<Description.Attributes, T>) -> T {
|
||||
// Implementation Note: Handles attributes that are not
|
||||
// AttributeType. These should only exist as computed properties.
|
||||
return attributes[keyPath: path]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Relationship Access
|
||||
@@ -388,16 +476,41 @@ public extension EntityProxy {
|
||||
/// Access to an Id of a `ToOneRelationship`.
|
||||
/// This allows you to write `entity ~> \.other` instead
|
||||
/// of `entity.relationships.other.id`.
|
||||
public static func ~><OtherEntity: OptionalRelatable>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
|
||||
public static func ~><OtherEntity: Identifiable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>>) -> OtherEntity.Identifier {
|
||||
return entity.relationships[keyPath: path].id
|
||||
}
|
||||
|
||||
/// Access to an Id of an optional `ToOneRelationship`.
|
||||
/// This allows you to write `entity ~> \.other` instead
|
||||
/// of `entity.relationships.other?.id`.
|
||||
public static func ~><OtherEntity: OptionalRelatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier {
|
||||
// Implementation Note: This signature applies to `ToOneRelationship<E?, _, _>?`
|
||||
// whereas the one below applies to `ToOneRelationship<E, _, _>?`
|
||||
return entity.relationships[keyPath: path]?.id
|
||||
}
|
||||
|
||||
/// Access to an Id of an optional `ToOneRelationship`.
|
||||
/// This allows you to write `entity ~> \.other` instead
|
||||
/// of `entity.relationships.other?.id`.
|
||||
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier? {
|
||||
// Implementation Note: This signature applies to `ToOneRelationship<E, _, _>?`
|
||||
// whereas the one above applies to `ToOneRelationship<E?, _, _>?`
|
||||
return entity.relationships[keyPath: path]?.id
|
||||
}
|
||||
|
||||
/// Access to all Ids of a `ToManyRelationship`.
|
||||
/// This allows you to write `entity ~> \.others` instead
|
||||
/// of `entity.relationships.others.ids`.
|
||||
public static func ~><OtherEntity: Relatable>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
|
||||
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>>) -> [OtherEntity.Identifier] {
|
||||
return entity.relationships[keyPath: path].ids
|
||||
}
|
||||
|
||||
/// Access to all Ids of an optional `ToManyRelationship`.
|
||||
/// This allows you to write `entity ~> \.others` instead
|
||||
/// of `entity.relationships.others?.ids`.
|
||||
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>?>) -> [OtherEntity.Identifier]? {
|
||||
return entity.relationships[keyPath: path]?.ids
|
||||
}
|
||||
}
|
||||
|
||||
infix operator ~>
|
||||
@@ -416,7 +529,7 @@ public extension Entity {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
try container.encode(Entity.type, forKey: .type)
|
||||
try container.encode(Entity.jsonType, forKey: .type)
|
||||
|
||||
if EntityRawIdType.self != Unidentified.self {
|
||||
try container.encode(id, forKey: .id)
|
||||
@@ -445,15 +558,16 @@ public extension Entity {
|
||||
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
|
||||
guard Entity.type == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.type, found: type)
|
||||
guard Entity.jsonType == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type)
|
||||
}
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { Entity.Id(rawValue: $0) } ?? container.decode(Entity.Id.self, forKey: .id)
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ??
|
||||
container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
relationships = try (NoRelationships() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
|
||||
|
||||
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
|
||||
|
||||
@@ -34,13 +34,31 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible {
|
||||
public var description: String { return "Unidentified" }
|
||||
}
|
||||
|
||||
public protocol MaybeId: Codable {
|
||||
associatedtype EntityType: JSONAPI.EntityProxy
|
||||
public protocol OptionalId: Codable {
|
||||
associatedtype IdentifiableType: JSONAPI.JSONTyped
|
||||
associatedtype RawType: MaybeRawId
|
||||
|
||||
var rawValue: RawType { get }
|
||||
init(rawValue: RawType)
|
||||
}
|
||||
|
||||
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
|
||||
var rawValue: RawType { get }
|
||||
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
|
||||
|
||||
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
|
||||
extension Optional: OptionalId where Wrapped: IdType {
|
||||
public typealias IdentifiableType = Wrapped.IdentifiableType
|
||||
public typealias RawType = Wrapped.RawType?
|
||||
|
||||
public var rawValue: Wrapped.RawType? {
|
||||
guard case .some(let value) = self else {
|
||||
return nil
|
||||
}
|
||||
return value.rawValue
|
||||
}
|
||||
|
||||
public init(rawValue: Wrapped.RawType?) {
|
||||
self = rawValue.map { Wrapped(rawValue: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
public extension IdType {
|
||||
@@ -53,7 +71,7 @@ public protocol CreatableIdType: IdType {
|
||||
|
||||
/// An Entity ID. These IDs can be encoded to or decoded from
|
||||
/// JSON API IDs.
|
||||
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable, Equatable, MaybeId {
|
||||
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
|
||||
|
||||
public let rawValue: RawType
|
||||
|
||||
@@ -63,7 +81,8 @@ public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable,
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
rawValue = try container.decode(RawType.self)
|
||||
let rawValue = try container.decode(RawType.self)
|
||||
self.init(rawValue: rawValue)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@@ -72,7 +91,11 @@ public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable,
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
|
||||
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {
|
||||
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
|
||||
return Id(rawValue: rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: CreatableIdType where RawType: CreatableRawIdType {
|
||||
public init() {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,30 +5,59 @@
|
||||
// Created by Mathew Polzin on 8/31/18.
|
||||
//
|
||||
|
||||
public protocol RelationshipType: Codable {}
|
||||
public protocol RelationshipType {
|
||||
associatedtype LinksType
|
||||
associatedtype MetaType
|
||||
|
||||
var links: LinksType { get }
|
||||
var meta: MetaType { get }
|
||||
}
|
||||
|
||||
/// An Entity relationship that can be encoded to or decoded from
|
||||
/// a JSON API "Resource Linkage."
|
||||
/// See https://jsonapi.org/format/#document-resource-object-linkage
|
||||
/// A convenient typealias might make your code much more legible: `One<EntityDescription>`
|
||||
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: RelationshipType, Equatable {
|
||||
public struct ToOneRelationship<Identifiable: JSONAPI.Identifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
|
||||
|
||||
public let id: Relatable.WrappedIdentifier
|
||||
public let id: Identifiable.Identifier
|
||||
|
||||
public init(id: Relatable.WrappedIdentifier) {
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
|
||||
public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) {
|
||||
self.id = id
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
|
||||
public init<E: EntityType>(entity: E) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
id = entity.id
|
||||
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(id: Identifiable.Identifier) {
|
||||
self.init(id: id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier? {
|
||||
public init<E: EntityType>(entity: E?) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
id = entity?.id
|
||||
extension ToOneRelationship {
|
||||
public init<E: EntityType>(entity: E, meta: MetaType, links: LinksType) where E.Id == Identifiable.Identifier {
|
||||
self.init(id: entity.id, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init<E: EntityType>(entity: E) where E.Id == Identifiable.Identifier {
|
||||
self.init(id: entity.id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Identifiable: OptionalRelatable {
|
||||
public init<E: EntityType>(entity: E?, meta: MetaType, links: LinksType) where E.Id == Identifiable.Wrapped.Identifier {
|
||||
self.init(id: entity?.id, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init<E: EntityType>(entity: E?) where E.Id == Identifiable.Wrapped.Identifier {
|
||||
self.init(id: entity?.id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,73 +65,105 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Ident
|
||||
/// a JSON API "Resource Linkage."
|
||||
/// See https://jsonapi.org/format/#document-resource-object-linkage
|
||||
/// A convenient typealias might make your code much more legible: `Many<EntityDescription>`
|
||||
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType, Equatable {
|
||||
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
|
||||
|
||||
public let ids: [Relatable.Identifier]
|
||||
|
||||
public init(ids: [Relatable.Identifier]) {
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
|
||||
public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) {
|
||||
self.ids = ids
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
|
||||
public init<T: JSONAPI.Relatable>(relationships: [ToOneRelationship<T>]) where T.WrappedIdentifier == Relatable.Identifier {
|
||||
ids = relationships.map { $0.id }
|
||||
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier {
|
||||
ids = pointers.map { $0.id }
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
|
||||
private init() {
|
||||
ids = []
|
||||
public init<E: EntityType>(entities: [E], meta: MetaType, links: LinksType) where E.Id == Relatable.Identifier {
|
||||
self.init(ids: entities.map { $0.id }, meta: meta, links: links)
|
||||
}
|
||||
|
||||
|
||||
private init(meta: MetaType, links: LinksType) {
|
||||
self.init(ids: [], meta: meta, links: links)
|
||||
}
|
||||
|
||||
public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship {
|
||||
return ToManyRelationship(meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
|
||||
public init(ids: [Relatable.Identifier]) {
|
||||
self.init(ids: ids, meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>]) where T.Identifier == Relatable.Identifier {
|
||||
self.init(pointers: pointers, meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public static var none: ToManyRelationship {
|
||||
return .init()
|
||||
return .none(withMeta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init<E: EntityType>(entities: [E]) where E.Id == Relatable.Identifier {
|
||||
self.init(entities: entities, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship {
|
||||
public init<E: EntityType>(entities: [E]) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
ids = entities.map { $0.id }
|
||||
}
|
||||
public protocol Identifiable: JSONTyped {
|
||||
associatedtype Identifier: Equatable
|
||||
}
|
||||
|
||||
/// The WrappedRelatable (a.k.a OptionalRelatable) protocol
|
||||
/// describes Optional<T: Relatable> and Relatable types.
|
||||
public protocol WrappedRelatable: Codable, Equatable {
|
||||
associatedtype Description: EntityDescription
|
||||
associatedtype Identifier: JSONAPI.IdType
|
||||
associatedtype WrappedIdentifier: Codable, Equatable
|
||||
}
|
||||
public typealias OptionalRelatable = WrappedRelatable
|
||||
|
||||
/// The Relatable protocol describes anything that
|
||||
/// has an IdType Identifier
|
||||
public protocol Relatable: WrappedRelatable {}
|
||||
public protocol Relatable: Identifiable where Identifier: JSONAPI.IdType {
|
||||
}
|
||||
|
||||
extension Optional: OptionalRelatable where Wrapped: Relatable {
|
||||
public typealias Description = Wrapped.Description
|
||||
public typealias Identifier = Wrapped.Identifier
|
||||
public typealias WrappedIdentifier = Identifier?
|
||||
/// OptionalRelatable just describes an Optional
|
||||
/// with a Reltable Wrapped type.
|
||||
public protocol OptionalRelatable: Identifiable where Identifier == Wrapped.Identifier? {
|
||||
associatedtype Wrapped: JSONAPI.Relatable
|
||||
}
|
||||
|
||||
extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable {
|
||||
public typealias Identifier = Wrapped.Identifier?
|
||||
|
||||
public static var jsonType: String { return Wrapped.jsonType }
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
private enum ResourceLinkageCodingKeys: String, CodingKey {
|
||||
case data = "data"
|
||||
case meta = "meta"
|
||||
case links = "links"
|
||||
}
|
||||
private enum ResourceIdentifierCodingKeys: String, CodingKey {
|
||||
case id = "id"
|
||||
case entityType = "type"
|
||||
}
|
||||
|
||||
public enum JSONAPIEncodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String)
|
||||
case illegalEncoding(String)
|
||||
case illegalDecoding(String)
|
||||
case missingOrMalformedMetadata
|
||||
case missingOrMalformedLinks
|
||||
}
|
||||
|
||||
extension ToOneRelationship {
|
||||
extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
|
||||
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
}
|
||||
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
|
||||
// A little trickery follows. If the id is nil, the
|
||||
// container.decode(Identifier.self) will fail even if Identifier
|
||||
// is Optional. However, we can check if decoding nil
|
||||
@@ -110,7 +171,7 @@ extension ToOneRelationship {
|
||||
// type at which point we can store nil in `id`.
|
||||
let anyNil: Any? = nil
|
||||
if try container.decodeNil(forKey: .data),
|
||||
let val = anyNil as? Relatable.WrappedIdentifier {
|
||||
let val = anyNil as? Identifiable.Identifier {
|
||||
id = val
|
||||
return
|
||||
}
|
||||
@@ -119,11 +180,11 @@ extension ToOneRelationship {
|
||||
|
||||
let type = try identifier.decode(String.self, forKey: .entityType)
|
||||
|
||||
guard type == Relatable.Description.type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.Description.type, found: type)
|
||||
guard type == Identifiable.jsonType else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type)
|
||||
}
|
||||
|
||||
id = try identifier.decode(Relatable.WrappedIdentifier.self, forKey: .id)
|
||||
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@@ -133,17 +194,46 @@ extension ToOneRelationship {
|
||||
try container.encodeNil(forKey: .data)
|
||||
}
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
|
||||
// If id is nil, instead of {id: , type: } we will just
|
||||
// encode `null`
|
||||
let anyNil: Any? = nil
|
||||
let nilId = anyNil as? Identifiable.Identifier
|
||||
guard id != nilId else {
|
||||
try container.encodeNil(forKey: .data)
|
||||
return
|
||||
}
|
||||
|
||||
var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
|
||||
|
||||
try identifier.encode(id, forKey: .id)
|
||||
try identifier.encode(Relatable.Description.type, forKey: .entityType)
|
||||
try identifier.encode(id.rawValue, forKey: .id)
|
||||
try identifier.encode(Identifiable.jsonType, forKey: .entityType)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship {
|
||||
extension ToManyRelationship: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
|
||||
|
||||
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
}
|
||||
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
|
||||
var identifiers = try container.nestedUnkeyedContainer(forKey: .data)
|
||||
|
||||
var newIds = [Relatable.Identifier]()
|
||||
@@ -152,24 +242,33 @@ extension ToManyRelationship {
|
||||
|
||||
let type = try identifier.decode(String.self, forKey: .entityType)
|
||||
|
||||
guard type == Relatable.Description.type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.Description.type, found: type)
|
||||
guard type == Relatable.jsonType else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type)
|
||||
}
|
||||
|
||||
newIds.append(try identifier.decode(Relatable.Identifier.self, forKey: .id))
|
||||
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
|
||||
}
|
||||
ids = newIds
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self)
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
|
||||
var identifiers = container.nestedUnkeyedContainer(forKey: .data)
|
||||
|
||||
for id in ids {
|
||||
var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
|
||||
|
||||
try identifier.encode(id, forKey: .id)
|
||||
try identifier.encode(Relatable.Description.type, forKey: .entityType)
|
||||
try identifier.encode(id.rawValue, forKey: .id)
|
||||
try identifier.encode(Relatable.jsonType, forKey: .entityType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user