mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
146 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 61fa56e84b | |||
| a6bf3e0ae0 | |||
| 620f6989d2 | |||
| 715db33138 | |||
| e99abd26b3 | |||
| 099d17aa58 | |||
| 92d1239a98 | |||
| b7ce3d226e | |||
| 5b56d596e2 | |||
| e820f34253 | |||
| c75912ab79 | |||
| e4481c9e4f | |||
| e9b9a2bd78 | |||
| fc78958f76 | |||
| f73ca8bc2c | |||
| 176eca3dce | |||
| 4e0e44c45d | |||
| e09e3cd8ac | |||
| 502f82ad14 | |||
| 2bb238b092 | |||
| 491fe1fbea | |||
| 6b6f40c968 | |||
| 14bd29bf42 | |||
| cf4d8fc378 | |||
| 10b756f6db | |||
| 4cd697aae4 | |||
| db0f7329e4 | |||
| fb710c397b | |||
| b1adc60719 | |||
| 423e61b285 | |||
| 895d575cdb | |||
| cd75e9649b | |||
| c60dfc1584 | |||
| 16004051e9 | |||
| 864a66f1ea | |||
| abe16dda0d | |||
| 5257fd79fa | |||
| f7a81df451 | |||
| c8fbc09582 | |||
| a88bb77bd0 | |||
| 247848e788 | |||
| 7adbdb4505 | |||
| 0c7bf9a92d | |||
| ea663bb229 | |||
| f48c385f99 | |||
| c96bfadd82 | |||
| 88be701062 | |||
| 1e85e1d2a3 | |||
| 11a7727ac9 | |||
| e8bfbc881b | |||
| af757a2fac | |||
| 84955872f8 | |||
| cb2800abd4 | |||
| 85d5fef3c8 | |||
| e4ef61fd56 | |||
| 75ec4f156e | |||
| b45fc73188 | |||
| 3b73dc9989 | |||
| b2c81026f4 | |||
| 8d057b4398 | |||
| c8421cdd58 | |||
| cde10a8491 | |||
| ad05d3908a | |||
| 23b2b2e04f | |||
| 2988503d7d | |||
| 5ea83b07c1 | |||
| 2b59f54067 | |||
| 58a7c82436 | |||
| dc30cb3b9e | |||
| 7045373708 | |||
| 952fe8ba7e | |||
| 951c04ad44 | |||
| 57df6b147e | |||
| a88844fe5e | |||
| d1cf19f9fe | |||
| 59835fbe11 | |||
| 850a713dcb | |||
| e3c637a41e | |||
| 9cbc626410 | |||
| 6dd14daf53 | |||
| 744e08acf4 | |||
| 5da4a963d4 | |||
| 5433dddc81 | |||
| 95f9d8084d | |||
| d6911f170c | |||
| 9972d13a4e | |||
| 4dc63167d6 | |||
| 845d085455 | |||
| 45eb7d06bd | |||
| 6ce962cec9 | |||
| 477dae0a73 | |||
| 4afe4cfb31 | |||
| 65144d27ae | |||
| 1ec4b21c34 | |||
| 15e13e2cc7 | |||
| 1d8ceef317 | |||
| e546614c15 | |||
| 8939d637d4 | |||
| e3b005b89a | |||
| 2de6580519 | |||
| 875d938b95 | |||
| 1ed34b00dc | |||
| 531a7f35bd | |||
| dc42ec27fc | |||
| cf746e182f | |||
| 52d2e9819d | |||
| 81b78cd749 | |||
| 3a0ede2372 | |||
| cb04a01e71 | |||
| 3c29244540 | |||
| a9e1e49a79 | |||
| 9e6e713ad2 | |||
| 308f168a8c | |||
| 8726bcc9fa | |||
| 25bda795a7 | |||
| 14a0256885 | |||
| b2041e9bc8 | |||
| fd9a626551 | |||
| 69bb40aa00 | |||
| 060e0e3898 | |||
| b374367234 | |||
| b363db30cd | |||
| 6904416623 | |||
| b294cec542 | |||
| c04d3301b6 | |||
| 11e8d13f02 | |||
| 5a64bebc99 | |||
| 790b0fbf52 | |||
| 539ecc451a | |||
| 7eb1b05eae | |||
| 3408263c2a | |||
| 1d6e5d3810 | |||
| d68404db36 | |||
| 4f7db98a87 | |||
| 072b081ac3 | |||
| 897410492d | |||
| b46f5166ea | |||
| d5a24c4adb | |||
| 669d5d1342 | |||
| 923ab7d9f4 | |||
| 72769b6107 | |||
| 109e15d741 | |||
| 72180f64ef | |||
| fc962f9a0d | |||
| 52eb123166 | |||
| 4ef147ec45 |
+174
@@ -0,0 +1,174 @@
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
import Poly
|
||||
|
||||
// MARK: - Preamble (setup)
|
||||
|
||||
// We make String a CreatableRawIdType. This is actually done in
|
||||
// this Playground's Entities.swift file, so it is commented out here.
|
||||
/*
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<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: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
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: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct 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(resourceObject: 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: ResourceObjectDescription {
|
||||
static var jsonType: String { return "authors" }
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: Attribute<String>
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let articles: ToManyRelationship<Article, ToManyRelationshipMetadata, ToManyRelationshipLinks>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Author = JSONAPI.ResourceObject<AuthorDescription, EntityMetadata, EntityLinks, String>
|
||||
|
||||
/// Description of an Article entity.
|
||||
enum ArticleDescription: ResourceObjectDescription {
|
||||
static var jsonType: String { return "articles" }
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let title: Attribute<String>
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
/// The primary attributed author of the article.
|
||||
let primaryAuthor: ToOneRelationship<Author, NoMetadata, NoLinks>
|
||||
/// All authors excluding the primary author.
|
||||
/// It is customary to print the primary author's
|
||||
/// name first, followed by the other authors.
|
||||
let otherAuthors: ToManyRelationship<Author, ToManyRelationshipMetadata, ToManyRelationshipLinks>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONAPI.ResourceObject<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(resourceObject: 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)!)
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
import JSONAPITestLib
|
||||
import JSONAPITesting
|
||||
|
||||
/*******
|
||||
|
||||
@@ -11,12 +11,12 @@ 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
|
||||
// The JSONAPITesting framework provides literal expressibility for key types to
|
||||
// make creating tests easier
|
||||
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil), meta: .none, links: .none)
|
||||
|
||||
// MARK: - JSON API structure checking
|
||||
// The JSONAPITestLib provides a `check` function for each Entity type
|
||||
// The JSONAPITesting framework provides a `check` function for each Entity type
|
||||
// that uses reflection to catch mistakes that are not forbidden by
|
||||
// Swift's type system but will result in unexpected results when
|
||||
// encoding/decoding. It is a good idea to add a `check` to each of
|
||||
|
||||
@@ -11,15 +11,20 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none)
|
||||
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(resourceObject: dogFromCode), includes: .none, meta: .none, links: .none)
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.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()]
|
||||
@@ -27,16 +32,14 @@ let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner
|
||||
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(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(resourceObjects: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||
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]
|
||||
|
||||
@@ -46,11 +49,15 @@ print("-----")
|
||||
|
||||
// MARK: - Pass successfully parsed body to other parts of the code
|
||||
|
||||
/*
|
||||
---- CRASHING IN XCODE 10.2 PLAYGROUND ----
|
||||
|
||||
if case let .data(bodyData) = peopleResponse.body {
|
||||
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
|
||||
} else {
|
||||
print("no body data")
|
||||
}
|
||||
*/
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
|
||||
|
||||
@@ -24,14 +24,14 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
|
||||
// MARK: - typealiases for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
|
||||
public typealias ExampleEntity<Description: ResourceObjectDescription> = ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
|
||||
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
|
||||
|
||||
// MARK: - A few resource objects (entities)
|
||||
public enum PersonDescription: EntityDescription {
|
||||
public enum PersonDescription: ResourceObjectDescription {
|
||||
|
||||
public static var type: String { return "people" }
|
||||
public static var jsonType: String { return "people" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<[String]>
|
||||
@@ -62,15 +62,15 @@ public enum PersonDescription: EntityDescription {
|
||||
|
||||
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)), meta: .none, links: .none)
|
||||
public extension ResourceObject where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||
self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
public enum DogDescription: EntityDescription {
|
||||
public enum DogDescription: ResourceObjectDescription {
|
||||
|
||||
public static var type: String { return "dogs" }
|
||||
public static var jsonType: String { return "dogs" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
@@ -91,19 +91,47 @@ public enum DogDescription: EntityDescription {
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
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)), meta: .none, links: .none)
|
||||
public enum AlternativeDogDescription: ResourceObjectDescription {
|
||||
|
||||
public static var jsonType: String { return "dogs" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
|
||||
public init(name: Attribute<String>) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Id) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
|
||||
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 enum HouseDescription: EntityDescription {
|
||||
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
|
||||
|
||||
public static var type: String { return "houses" }
|
||||
public extension ResourceObject where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
init(name: String, owner: Person?) throws {
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none)
|
||||
}
|
||||
|
||||
init(name: String, owner: Person.Id) throws {
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
public enum HouseDescription: ResourceObjectDescription {
|
||||
|
||||
public static var jsonType: String { return "houses" }
|
||||
|
||||
public typealias Attributes = NoAttributes
|
||||
public typealias Relationships = NoRelationships
|
||||
@@ -111,4 +139,6 @@ public enum HouseDescription: EntityDescription {
|
||||
|
||||
public typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='6.0' target-platform='macos' executeOnSourceChanges='false'>
|
||||
<playground version='6.0' target-platform='macos'>
|
||||
<pages>
|
||||
<page name='Test Library'/>
|
||||
<page name='Usage'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
</pages>
|
||||
</playground>
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
#
|
||||
# Be sure to run `pod spec lint JSONAPI.podspec' to ensure this is a
|
||||
# valid spec and to remove all comments including this before submitting the spec.
|
||||
#
|
||||
# To learn more about Podspec attributes see https://docs.cocoapods.org/specification.html
|
||||
# To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/
|
||||
#
|
||||
|
||||
Pod::Spec.new do |spec|
|
||||
|
||||
# ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# These will help people to find your library, and whilst it
|
||||
# can feel like a chore to fill in it's definitely to your advantage. The
|
||||
# summary should be tweet-length, and the description more in depth.
|
||||
#
|
||||
|
||||
spec.name = "JSONAPI"
|
||||
spec.version = "0.31.1"
|
||||
spec.summary = "Swift Codable JSON API framework."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
# * Think: What does it do? Why did you write it? What is the focus?
|
||||
# * Try to keep it short, snappy and to the point.
|
||||
# * Write the description between the DESC delimiters below.
|
||||
# * Finally, don't worry about the indent, CocoaPods strips it!
|
||||
spec.description = <<-DESC
|
||||
A Swift package for encoding to- and decoding from JSON API compliant requests and responses.
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
DESC
|
||||
|
||||
spec.homepage = "https://github.com/mattpolzin/JSONAPI"
|
||||
# spec.screenshots = "www.example.com/screenshots_1.gif", "www.example.com/screenshots_2.gif"
|
||||
|
||||
|
||||
# ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# Licensing your code is important. See https://choosealicense.com for more info.
|
||||
# CocoaPods will detect a license file if there is a named LICENSE*
|
||||
# Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'.
|
||||
#
|
||||
|
||||
# spec.license = "MIT"
|
||||
spec.license = { :type => "MIT", :file => "LICENSE.txt" }
|
||||
|
||||
|
||||
# ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# Specify the authors of the library, with email addresses. Email addresses
|
||||
# of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also
|
||||
# accepts just a name if you'd rather not provide an email address.
|
||||
#
|
||||
# Specify a social_media_url where others can refer to, for example a twitter
|
||||
# profile URL.
|
||||
#
|
||||
|
||||
spec.author = { "Mathew Polzin" => "matt.polzin@gmail.com" }
|
||||
# Or just: spec.author = "Mathew Polzin"
|
||||
# spec.authors = { "Mathew Polzin" => "matt.polzin@gmail.com" }
|
||||
# spec.social_media_url = "https://twitter.com/Mathew Polzin"
|
||||
|
||||
# ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# If this Pod runs only on iOS or OS X, then specify the platform and
|
||||
# the deployment target. You can optionally include the target after the platform.
|
||||
#
|
||||
|
||||
# spec.platform = :ios
|
||||
# spec.platform = :ios, "5.0"
|
||||
|
||||
# When using multiple platforms
|
||||
spec.ios.deployment_target = "8.0"
|
||||
spec.osx.deployment_target = "10.9"
|
||||
# spec.watchos.deployment_target = "2.0"
|
||||
# spec.tvos.deployment_target = "9.0"
|
||||
|
||||
|
||||
# ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# Specify the location from where the source should be retrieved.
|
||||
# Supports git, hg, bzr, svn and HTTP.
|
||||
#
|
||||
|
||||
spec.source = { :git => "https://github.com/mattpolzin/JSONAPI.git", :tag => "#{spec.version}" }
|
||||
|
||||
|
||||
# ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# CocoaPods is smart about how it includes source code. For source files
|
||||
# giving a folder will include any swift, h, m, mm, c & cpp files.
|
||||
# For header files it will include any header in the folder.
|
||||
# Not including the public_header_files will make all headers public.
|
||||
#
|
||||
|
||||
spec.source_files = "Sources/JSONAPI/**/*.{swift}"
|
||||
# spec.exclude_files = "Classes/Exclude"
|
||||
|
||||
# spec.public_header_files = "Classes/**/*.h"
|
||||
|
||||
|
||||
# ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# A list of resources included with the Pod. These are copied into the
|
||||
# target bundle with a build phase script. Anything else will be cleaned.
|
||||
# You can preserve files from being cleaned, please don't preserve
|
||||
# non-essential files like tests, examples and documentation.
|
||||
#
|
||||
|
||||
# spec.resource = "icon.png"
|
||||
# spec.resources = "Resources/*.png"
|
||||
|
||||
# spec.preserve_paths = "FilesToSave", "MoreFilesToSave"
|
||||
|
||||
|
||||
# ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# Link your library with frameworks, or libraries. Libraries do not include
|
||||
# the lib prefix of their name.
|
||||
#
|
||||
|
||||
# spec.frameworks = "SomeFramework", "AnotherFramework"
|
||||
|
||||
# spec.library = "iconv"
|
||||
# spec.libraries = "iconv", "xml2"
|
||||
|
||||
|
||||
# ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― #
|
||||
#
|
||||
# If your library depends on compiler flags you can set them in the xcconfig hash
|
||||
# where they will only apply to your library. If you depend on other Podspecs
|
||||
# you can include multiple dependencies to ensure it works.
|
||||
|
||||
spec.swift_version = "5.0"
|
||||
# spec.requires_arc = true
|
||||
|
||||
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
|
||||
spec.dependency "Poly", "~> 2.0"
|
||||
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2018 Mathew Polzin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+9
-1
@@ -1,7 +1,15 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
|
||||
{
|
||||
"package": "Poly",
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "38051821d7ef49e590e26e819a2fe447e50be9ff",
|
||||
"version": "2.0.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
|
||||
+15
-7
@@ -1,30 +1,38 @@
|
||||
// swift-tools-version:4.2
|
||||
// swift-tools-version:5.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "JSONAPI",
|
||||
platforms: [
|
||||
.macOS(.v10_10),
|
||||
.iOS(.v10)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "JSONAPI",
|
||||
targets: ["JSONAPI"]),
|
||||
.library(
|
||||
name: "JSONAPITestLib",
|
||||
targets: ["JSONAPITestLib"])
|
||||
name: "JSONAPITesting",
|
||||
targets: ["JSONAPITesting"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "JSONAPI",
|
||||
dependencies: []),
|
||||
dependencies: ["Poly"]),
|
||||
.target(
|
||||
name: "JSONAPITestLib",
|
||||
name: "JSONAPITesting",
|
||||
dependencies: ["JSONAPI"]),
|
||||
.testTarget(
|
||||
name: "JSONAPITests",
|
||||
dependencies: ["JSONAPITestLib"])
|
||||
dependencies: ["JSONAPI", "JSONAPITesting"]),
|
||||
.testTarget(
|
||||
name: "JSONAPITestingTests",
|
||||
dependencies: ["JSONAPI", "JSONAPITesting"])
|
||||
],
|
||||
swiftLanguageVersions: [.v4_2]
|
||||
swiftLanguageVersions: [.v5]
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Mathew Polzin on 11/5/18.
|
||||
//
|
||||
|
||||
import Poly
|
||||
|
||||
public protocol JSONAPIDocument: Codable, Equatable {
|
||||
associatedtype PrimaryResourceBody: JSONAPI.ResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
@@ -110,11 +112,16 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
public init(apiDescription: APIDescription,
|
||||
body: PrimaryResourceBody,
|
||||
includes: Includes<Include>,
|
||||
meta: MetaType,
|
||||
links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
extension Document where IncludeType == NoIncludes {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
@@ -189,6 +196,74 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link
|
||||
}
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
public typealias Include = Poly
|
||||
import Poly
|
||||
|
||||
public typealias Include = JSONPoly
|
||||
|
||||
public struct Includes<I: Include>: Codable, Equatable {
|
||||
public static var none: Includes { return .init(values: []) }
|
||||
@@ -50,6 +52,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))"
|
||||
@@ -123,3 +135,17 @@ extension Includes where I: _Poly7 {
|
||||
}
|
||||
|
||||
// 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,19 +19,31 @@ 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
|
||||
|
||||
public init(entity: Entity) {
|
||||
self.value = entity
|
||||
public init(resourceObject: Entity) {
|
||||
self.value = resourceObject
|
||||
}
|
||||
}
|
||||
|
||||
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 init(resourceObjects: [Entity]) {
|
||||
values = resourceObjects
|
||||
}
|
||||
|
||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||
return ManyResourceBody(resourceObjects: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
//
|
||||
// EmptyObjectDecoder.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 7/2/19.
|
||||
//
|
||||
|
||||
/// `EmptyObjectDecoder` exists internally for the sole purpose of
|
||||
/// allowing certain fallback logic paths to attempt to create `Decodable`
|
||||
/// types from empty containers (specifically in a way that is agnostic
|
||||
/// of any given encoding). In other words, this serves the same purpose
|
||||
/// as `JSONDecoder().decode(Thing.self, from: "{}".data(using: .utf8)!)`
|
||||
/// without needing to use a third party or `Foundation` library decoder.
|
||||
struct EmptyObjectDecoder: Decoder {
|
||||
|
||||
var codingPath: [CodingKey] = []
|
||||
|
||||
var userInfo: [CodingUserInfoKey : Any] = [:]
|
||||
|
||||
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
|
||||
return KeyedDecodingContainer(EmptyKeyedContainer())
|
||||
}
|
||||
|
||||
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
return EmptyUnkeyedContainer()
|
||||
}
|
||||
|
||||
func singleValueContainer() throws -> SingleValueDecodingContainer {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeSingleValue
|
||||
}
|
||||
}
|
||||
|
||||
enum EmptyObjectDecodingError: Swift.Error {
|
||||
case emptyObjectCannotBeSingleValue
|
||||
case emptyObjectCannotBeUnkeyedValues
|
||||
case emptyObjectCannotHaveKeyedValues
|
||||
case emptyObjectCannotHaveNestedContainers
|
||||
case emptyObjectCannotHaveSuper
|
||||
}
|
||||
|
||||
struct EmptyUnkeyedContainer: UnkeyedDecodingContainer {
|
||||
var codingPath: [CodingKey] { return [] }
|
||||
|
||||
var count: Int? { return 0 }
|
||||
|
||||
var isAtEnd: Bool { return true }
|
||||
|
||||
var currentIndex: Int { return 0 }
|
||||
|
||||
mutating func decodeNil() throws -> Bool {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Bool.Type) throws -> Bool {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: String.Type) throws -> String {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Double.Type) throws -> Double {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Float.Type) throws -> Float {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int.Type) throws -> Int {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int8.Type) throws -> Int8 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int16.Type) throws -> Int16 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int32.Type) throws -> Int32 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: Int64.Type) throws -> Int64 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt.Type) throws -> UInt {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
|
||||
}
|
||||
|
||||
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||
}
|
||||
|
||||
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||
}
|
||||
|
||||
mutating func superDecoder() throws -> Decoder {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||
}
|
||||
}
|
||||
|
||||
struct EmptyKeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
|
||||
var codingPath: [CodingKey] { return [] }
|
||||
|
||||
var allKeys: [Key] { return [] }
|
||||
|
||||
func contains(_ key: Key) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func decodeNil(forKey key: Key) throws -> Bool {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: String.Type, forKey key: Key) throws -> String {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
|
||||
}
|
||||
|
||||
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||
}
|
||||
|
||||
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
|
||||
}
|
||||
|
||||
func superDecoder() throws -> Decoder {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||
}
|
||||
|
||||
func superDecoder(forKey key: Key) throws -> Decoder {
|
||||
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,22 @@ public extension TransformedAttribute {
|
||||
/// 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: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
|
||||
func map<T: Codable>(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
|
||||
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.
|
||||
func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +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 {
|
||||
let rawValue: RawValue
|
||||
public let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
|
||||
@@ -21,16 +26,19 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
|
||||
}
|
||||
}
|
||||
|
||||
// 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 typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
|
||||
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)
|
||||
@@ -45,15 +53,35 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: Equatable where RawValue: Equatable {}
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformedAttribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -84,3 +112,36 @@ extension TransformedAttribute {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -69,7 +69,7 @@ public protocol CreatableIdType: IdType {
|
||||
init()
|
||||
}
|
||||
|
||||
/// An Entity ID. These IDs can be encoded to or decoded from
|
||||
/// An ResourceObject ID. These IDs can be encoded to or decoded from
|
||||
/// JSON API IDs.
|
||||
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user