Compare commits

..

63 Commits

Author SHA1 Message Date
Mathew Polzin 65144d27ae Merge pull request #10 from mattpolzin/feature/OpenAPISchema
Feature/open api schema
2019-01-21 11:50:02 -08:00
Mathew Polzin 1ec4b21c34 Add missing case in Sampleable generic OpenAPINode implementation. Add info on new auxiliary frameworks to the README. 2019-01-20 19:18:10 -08:00
Mathew Polzin 15e13e2cc7 Remove unused error and add a bit of code documentation 2019-01-20 18:54:37 -08:00
Mathew Polzin 1d8ceef317 Update README, add a bit more code documentation 2019-01-20 18:52:21 -08:00
Mathew Polzin e546614c15 merge with Arbitrary branch 2019-01-20 18:41:54 -08:00
Mathew Polzin 8939d637d4 Add placeholder test to arbitrary test target. 2019-01-20 18:34:24 -08:00
Mathew Polzin e3b005b89a Update README 2019-01-20 18:25:28 -08:00
Mathew Polzin 2de6580519 Add some tests around Attribute OpenAPI descriptions 2019-01-20 18:18:35 -08:00
Mathew Polzin 875d938b95 Add id and type properties to Entity OpenAPI Schema 2019-01-20 16:19:26 -08:00
Mathew Polzin 1ed34b00dc Add todos for Entity Id and type. Add example usage of OpenAPI entity schema support in Playground 2019-01-20 16:10:31 -08:00
Mathew Polzin 531a7f35bd Cleanup and a bit of documentation 2019-01-20 15:50:46 -08:00
Mathew Polzin dc42ec27fc Not crazy about how I got there, but now it is relatively easy to print arbitrary enum's allCases as the list of possible values in the format specced out by OpenAPI. 2019-01-20 15:39:54 -08:00
Mathew Polzin cf746e182f currently in a pretty broken state with support for enumerations being turned into allowed values via reflection. I think I am going to have to give up type safety if I want to use reflection and keep things open ended 2019-01-19 15:30:09 -08:00
Mathew Polzin 52d2e9819d Update README project status to include OpenAPI and Arbitrary work. 2019-01-15 09:40:49 -08:00
Mathew Polzin 81b78cd749 Add more Arbitrary conformances 2019-01-15 09:27:09 -08:00
Mathew Polzin 3a0ede2372 Get rid of OpenAPI namespace because a compiler bug was making it more hassle than it was worth. 2019-01-14 23:46:39 -08:00
Mathew Polzin cb04a01e71 Add some todos and delete an old file 2019-01-14 23:29:49 -08:00
Mathew Polzin 3c29244540 fill out some of the relationship tests 2019-01-14 23:14:25 -08:00
Mathew Polzin a9e1e49a79 Add test helpers for Open API test target 2019-01-14 21:18:49 -08:00
Mathew Polzin 9e6e713ad2 Add Codable for the parts of OpenAPI that are built so far. 2019-01-14 21:17:07 -08:00
Mathew Polzin 308f168a8c Trying to map out the Open API Schema Object using Swift enums 2019-01-13 23:24:02 -08:00
Mathew Polzin 8726bcc9fa Update all references to JSONAPITestLib to reflect renaming 2019-01-13 18:35:20 -08:00
Mathew Polzin 25bda795a7 Rename JSONAPITestLib to JSONAPITesting to fit with naming convention I am adopting going forward. Added OpenAPI JSON schema types. Added default schema types for a handful of Swift primitives. 2019-01-13 18:28:41 -08:00
Mathew Polzin 14a0256885 Add new targets 2019-01-13 17:27:10 -08:00
Mathew Polzin b2041e9bc8 Update to bleeding edge Poly library 2019-01-13 07:47:20 -08:00
Mathew Polzin fd9a626551 Ditch unused test constant. 2019-01-13 07:42:13 -08:00
Mathew Polzin 69bb40aa00 Move JSONAPITestLib tests into their own test target. 2019-01-13 07:16:46 -08:00
Mathew Polzin 060e0e3898 Move PolyWrapped out of Poly and into JSONAPI. 2019-01-12 21:51:51 -08:00
Mathew Polzin b374367234 Move Poly into its own library. 2019-01-12 21:30:20 -08:00
Mathew Polzin b363db30cd Add MIT license docuemnt 2019-01-12 18:41:04 -08:00
Mathew Polzin 6904416623 Begin making Poly more useful in a broader context than JSONAPI. 2019-01-12 17:08:46 -08:00
Mathew Polzin c04d3301b6 Add Meta-Relationship access. 2019-01-08 21:23:17 -08:00
Mathew Polzin 5a64bebc99 Merge branch 'master' of github.com:mattpolzin/JSONAPI 2019-01-08 20:13:11 -08:00
Mathew Polzin 790b0fbf52 Change capitalization 2019-01-06 18:20:18 -08:00
Mathew Polzin 539ecc451a Remove swift syntax highlighting from JSON snippet. 2019-01-03 22:26:25 -08:00
Mathew Polzin 7eb1b05eae Add swift syntax highlighting to README. 2019-01-03 22:23:17 -08:00
Mathew Polzin 3408263c2a Add warning note to README 2019-01-03 22:09:43 -08:00
Mathew Polzin 1d6e5d3810 Added Meta-Attribute support and documentation 2019-01-02 22:49:38 -08:00
Mathew Polzin d68404db36 update README to reflect change made in v0.12.0 2019-01-02 19:46:30 -08:00
Mathew Polzin 4f7db98a87 Update playground files to work with v0.12.0 changes 2019-01-02 19:42:32 -08:00
Mathew Polzin 072b081ac3 Breaking - Rename static var type: String to static var jsonType:String to avoid unnecessary conflict with Swift.type(of:) 2019-01-02 19:35:50 -08:00
Mathew Polzin 897410492d Add much more substantial example to README and mirror it in the included playground. Add convenient methods for adding includes to a Document. Make Poly less picky about what type of things it contains (don't need to be entities anymore). Typealias Either to Poly2 because they are isomorphic. 2019-01-02 17:14:58 -08:00
Mathew Polzin b46f5166ea APIDescription gets a public initializer. A new playground page explores the full potential of the Framework, in a sense. 2018-12-31 17:19:51 -08:00
Mathew Polzin d5a24c4adb Split Attribute out into its own Type (no longer just a type alias to TransformedAttribute) 2018-12-29 23:07:14 -08:00
Mathew Polzin 669d5d1342 Add ability to merge Document.Body.Data values 2018-12-28 19:11:24 -08:00
Mathew Polzin 923ab7d9f4 Add support for Poly8/Poly9 and Include8/Include9 2018-12-28 11:05:18 -08:00
Mathew Polzin 72769b6107 Fix small problem with sample code in README 2018-12-27 18:22:31 -08:00
Mathew Polzin 109e15d741 Add convenience method for default decoding of attributes. add tests for custom decoding and encoding as well as custom coding keys. add documentation. 2018-12-27 18:18:34 -08:00
Mathew Polzin 72180f64ef clarify some things about computed attribute properties. 2018-12-27 16:27:10 -08:00
Mathew Polzin fc962f9a0d Lift the constraint that Attributes and Relationships are Codable for EntityProxies. 2018-12-24 07:05:35 -08:00
Mathew Polzin 52eb123166 update documentation 2018-12-22 13:57:38 -08:00
Mathew Polzin 4ef147ec45 Update linuxmain 2018-12-22 13:49:10 -08:00
Mathew Polzin 61074ecc69 Add methods that make it easy to copy an entity with a new ID or copy an unidentified entity and give it an ID 2018-12-22 13:41:34 -08:00
Mathew Polzin 4dbcff6023 Add another way to initialize a nullable attribute to the tests 2018-12-22 13:10:05 -08:00
Mathew Polzin d6e82fab55 get id and type tests into the encoded entity test function 2018-12-21 08:53:55 -08:00
Mathew Polzin 5fa9848f02 test some attribute encoding a bit further 2018-12-21 08:45:51 -08:00
Mathew Polzin f38399a1d6 Add a bit of entity structure testing 2018-12-21 07:50:32 -08:00
Mathew Polzin 28f664326d Added a couple of tests 2018-12-12 21:05:07 -08:00
Mathew Polzin 3b7ef4aeb9 Add access to easy optional access to entire data on Document.Body 2018-12-12 20:05:29 -08:00
Mathew Polzin bb1ed30e89 Rename Document.Body.primaryData to primaryResource 2018-12-12 20:04:14 -08:00
Mathew Polzin dfdc266645 remove a bunch of convenience initializers that appeared to be giving the compiler some strife 2018-12-10 22:45:28 -08:00
Mathew Polzin 585cad0a83 Added literal initialization for nullable Attributes (TestLib) 2018-12-10 21:50:30 -08:00
Mathew Polzin 89abdd4cca fix example code in Playground 2018-12-10 21:11:48 -08:00
73 changed files with 5259 additions and 1226 deletions
@@ -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 jsonType: String { return "authors" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
}
public typealias Relationships = NoRelationships
}
typealias Author = JSONEntity<AuthorDescription>
enum ArticleDescription: EntityDescription {
public static var jsonType: String { return "articles" }
public struct Attributes: JSONAPI.Attributes {
public let title: Attribute<String>
public let abstract: Attribute<String>
}
public struct Relationships: JSONAPI.Relationships {
public let author: ToOneRelationship<Author>
}
}
typealias Article = JSONEntity<ArticleDescription>
// MARK: Document Definitions
// We create a typealias to represent a document containing one Article
// and including its Author
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
// ... and a typealias to represent a document containing one Article and
// not including any related entities.
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
// MARK: - Server Pseudo-example
// Skipping over all the API and database stuff, here's a chunk of code
// that creates a document. Note that this document is the entirety
// of a JSON:API response body.
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
// Let's pretend all of this is coming from a database:
let authorId = Author.Identifier(rawValue: "1234")
let article = Article(id: .init(rawValue: "5678"),
attributes: .init(title: .init(value: "JSON:API in Swift"),
abstract: .init(value: "Not yet written")),
relationships: .init(author: .init(id: authorId)),
meta: .none,
links: .none)
let document = SingleArticleDocument(apiDescription: .none,
body: .init(entity: article),
includes: .none,
meta: .none,
links: .none)
switch includeAuthor {
case false:
return .a(document)
case true:
let author = Author(id: authorId,
attributes: .init(name: .init(value: "Janice Bluff")),
relationships: .none,
meta: .none,
links: .none)
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
return .b(document.including(.init(values: [.init(author)])))
}
}
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.outputFormatting = .prettyPrinted
let responseBody = articleDocument(includeAuthor: true)
let responseData = try! encoder.encode(responseBody)
// Next step would be encoding and setting as the HTTP body of a response.
// we will just print it out instead:
print("-----")
print(String(data: responseData, encoding: .utf8)!)
// ... and if we had received a request for an article without
// including the author:
let otherResponseBody = articleDocument(includeAuthor: false)
let otherResponseData = try! encoder.encode(otherResponseBody)
print("-----")
print(String(data: otherResponseData, encoding: .utf8)!)
// MARK: - Client Pseudo-example
enum NetworkError: Swift.Error {
case serverError
case quantityMismatch
}
// Skipping over all the API stuff, here's a chunk of code that will
// decode a document. We will assume we have made a request for a
// single article including the author.
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
switch articleDocument.body {
case .data(let data):
let authors = data.includes[Author.self]
guard authors.count == 1 else {
throw NetworkError.quantityMismatch
}
return (article: data.primary.value, author: authors[0])
case .errors(let errors, meta: _, links: _):
throw NetworkError.serverError
}
}
let response = try! docode(articleResponseData: responseData)
// Next step would be to do something useful with the article and author but we will print them instead.
print("-----")
print(response.article)
print(response.author)
@@ -0,0 +1,213 @@
import Foundation
import JSONAPI
/*
This is not realistically what JSONAPI code will look like. It makes
every bit of sense to typealias the long-winded generics of this
framework to make working with them more concise and readable.
The purpose of this example, then, is not to show "good" JSONAPI
code, but rather to generate a JSONAPI example document that uses
all the various (often optional) parts of a JSONAPI document.
The JSON example produced in this manner can be used to compare the
nomenclature of this library with the nomenclature of the JSON:API Spec.
*/
// MARK: - Extensions
extension Foundation.URL: JSONAPIURL {}
// extension String: CreatableRawIdType {} found in playground Entities.swift file.
// MARK: - Definitions
/// Metadata associated with entities
struct EntityMetadata: JSONAPI.Meta {
let creationDate: Date
let updatedDate: Date
}
/// Metadata associated with links
struct LinkMeta: JSONAPI.Meta {
/// Don't trust this link past the expiry date.
let expiry: Date
}
/// Links associated with entities
struct EntityLinks: JSONAPI.Links {
let `self`: Link<Foundation.URL, LinkMeta>
init(selfLink: Link<Foundation.URL, LinkMeta>) {
self.`self` = selfLink
}
}
/// Metadata associated with relationships
struct ToManyRelationshipMetadata: JSONAPI.Meta {
/// Pagination for the relationship (to support really large
/// to-many relationships)
let pagination: Pagination
struct Pagination: Codable, Equatable {
let total: Int
let limit: Int
let offset: Int
}
}
/// Links associated with relationships
struct ToManyRelationshipLinks: JSONAPI.Links {
/// next page of relationships.
let next: Link<Foundation.URL, LinkMeta>
}
/// Description of an Author entity.
enum AuthorDescription: EntityDescription {
static var jsonType: String { return "authors" }
struct Attributes: JSONAPI.Attributes {
let name: Attribute<String>
}
struct Relationships: JSONAPI.Relationships {
let articles: ToManyRelationship<Article, ToManyRelationshipMetadata, ToManyRelationshipLinks>
}
}
typealias Author = JSONAPI.Entity<AuthorDescription, EntityMetadata, EntityLinks, String>
/// Description of an Article entity.
enum ArticleDescription: EntityDescription {
static var jsonType: String { return "articles" }
struct Attributes: JSONAPI.Attributes {
let title: Attribute<String>
}
struct Relationships: JSONAPI.Relationships {
/// The primary attributed author of the article.
let primaryAuthor: ToOneRelationship<Author, NoMetadata, NoLinks>
/// All authors excluding the primary author.
/// It is customary to print the primary author's
/// name first, followed by the other authors.
let otherAuthors: ToManyRelationship<Author, ToManyRelationshipMetadata, ToManyRelationshipLinks>
}
}
typealias Article = JSONAPI.Entity<ArticleDescription, EntityMetadata, EntityLinks, String>
/// Metadata associated with the API Description
struct APIDescriptionMetadata: JSONAPI.Meta {
/// A clever codename for this API version.
let codename: String
}
/// Metadata associated with any Document
struct DocumentMetadata: JSONAPI.Meta {
/// Assuming caching is in play, this is when
/// the cached document being returned was created.
let updatedDate: Date
}
/// Links associated with an Article Document
struct SingleArticleDocumentLinks: JSONAPI.Links {
let otherArticlesByPrimaryAuthor: Link<Foundation.URL, LinkMeta>
}
/// Error that can occur when retrieving a Single Article Document
enum ArticleDocumentError: String, JSONAPIError, Codable {
case unknownError = "unknown"
case missing = "missing"
static var unknown: ArticleDocumentError {
return .unknownError
}
}
typealias SingleArticleDocument = JSONAPI.Document<SingleResourceBody<Article>, DocumentMetadata, SingleArticleDocumentLinks, Include1<Author>, APIDescription<APIDescriptionMetadata>, ArticleDocumentError>
// MARK: - Instantiations
let authorId1 = Author.Identifier()
let authorId2 = Author.Identifier()
let authorId3 = Author.Identifier()
let now = Date()
let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: now)!
let tomorrowEntityMeta = EntityMetadata(creationDate: Calendar.current.date(byAdding: .day, value: -10, to: now)!,
updatedDate: Calendar.current.date(byAdding: .day, value: -2, to: now)!)
let articleLinks = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/article/1234")!,
meta: .init(expiry: tomorrow)))
let article = Article(attributes: .init(title: .init(value: "How to read JSONAPI")),
relationships: .init(primaryAuthor: .init(id: authorId1),
otherAuthors: .init(ids: [authorId2, authorId3],
meta: .init(pagination: .init(total: 2,
limit: 50,
offset: 0)),
links: .init(next: .init(url: URL(string: "https://articles.com/article/1234?otherAuthors[offset]=50")!,
meta: .init(expiry: tomorrow))))),
meta: tomorrowEntityMeta,
links: articleLinks)
let author1Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId1.rawValue)")!,
meta: .init(expiry: tomorrow)))
let author1 = Author(id: authorId1,
attributes: .init(name: .init(value: "James Kinney")),
relationships: .init(articles: .init(ids: [article.id, Article.Identifier(), Article.Identifier()],
meta: .init(pagination: .init(total: 3,
limit: 50,
offset: 0)),
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId1.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
meta: tomorrowEntityMeta,
links: author1Links)
let author2Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId2.rawValue)")!,
meta: .init(expiry: tomorrow)))
let author2 = Author(id: authorId2,
attributes: .init(name: .init(value: "James Kinney")),
relationships: .init(articles: .init(ids: [article.id, Article.Identifier()],
meta: .init(pagination: .init(total: 2,
limit: 50,
offset: 0)),
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId2.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
meta: tomorrowEntityMeta,
links: author2Links)
let author3Links = EntityLinks(selfLink: .init(url: URL(string: "https://articles.com/author/\(authorId3.rawValue)")!,
meta: .init(expiry: tomorrow)))
let author3 = Author(id: authorId3,
attributes: .init(name: .init(value: "James Kinney")),
relationships: .init(articles: .init(ids: [article.id],
meta: .init(pagination: .init(total: 1,
limit: 50,
offset: 0)),
links: .init(next: .init(url: URL(string: "https://articles.com/author/\(authorId3.rawValue)?articles[offset]=50")!, meta: .init(expiry: tomorrow))))),
meta: tomorrowEntityMeta,
links: author3Links)
let apiDescription = APIDescription<APIDescriptionMetadata>(version: "1.2",
meta: APIDescriptionMetadata(codename: "cobra"))
let documentMetadata = DocumentMetadata(updatedDate: Date())
let documentLinks = SingleArticleDocumentLinks(otherArticlesByPrimaryAuthor: .init(url: URL(string: "https://articles.com/articles?author=\(authorId1.rawValue)")!,
meta: .init(expiry: tomorrow)))
let singleArticleDocument = SingleArticleDocument(apiDescription: apiDescription,
body: .init(entity: article),
includes: .init(values: [.init(author1), .init(author2), .init(author3)]),
meta: documentMetadata,
links: documentLinks)
// MARK: - Encoding
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
encoder.dateEncodingStrategy = .iso8601
encoder.outputFormatting = .prettyPrinted
let jsonData = try! encoder.encode(singleArticleDocument)
// MARK: - Printing JSON
print(String(data: jsonData, encoding: .utf8)!)
@@ -0,0 +1,13 @@
//: [Previous](@previous)
import Foundation
import JSONAPI
import JSONAPIOpenAPI
// print Entity Schema
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let personSchemaData = try? encoder.encode(Person.openAPINode())
print(personSchemaData.map { String(data: $0, encoding: .utf8)! } ?? "Schema Construction Failed")
@@ -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))
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
@@ -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]
+35 -7
View File
@@ -25,13 +25,13 @@ extension String: CreatableRawIdType {
// MARK: - typealiases for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
public typealias ToOne<E: OptionalRelatable> = ToOneRelationship<E, NoMetadata, NoLinks>
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
public static var type: String { return "people" }
public static var jsonType: String { return "people" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<[String]>
@@ -64,13 +64,13 @@ 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)
}
}
public enum DogDescription: EntityDescription {
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 enum AlternativeDogDescription: EntityDescription {
public static var jsonType: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
public init(name: Attribute<String>) {
self.name = name
}
}
public struct Relationships: JSONAPI.Relationships {
public let human: ToOne<Person?>
public init(human: ToOne<Person?>) {
self.human = human
}
// define custom key mapping:
enum CodingKeys: String, CodingKey {
case human = "owner"
}
}
}
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
public init(name: String, owner: Person?) throws {
self = 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)
}
}
public enum HouseDescription: EntityDescription {
public static var type: String { return "houses" }
public static var jsonType: String { return "houses" }
public typealias Attributes = NoAttributes
public typealias Relationships = NoRelationships
@@ -0,0 +1,16 @@
import Foundation
import JSONAPI
import JSONAPITesting // for the convenience of literal initialization
import JSONAPIOpenAPI
extension PersonDescription.Attributes: Sampleable {
public static var sample: PersonDescription.Attributes {
return .init(name: ["Abbie", "Eibba"], favoriteColor: "Blue")
}
}
extension PersonDescription.Relationships: Sampleable {
public static var sample: PersonDescription.Relationships {
return .init(friends: ["1", "2"], dogs: ["2"], home: "1")
}
}
+3
View File
@@ -3,5 +3,8 @@
<pages>
<page name='Test Library'/>
<page name='Usage'/>
<page name='Full Client &amp; Server Example'/>
<page name='Full Document Verbose Generation'/>
<page name='OpenAPI Documentation'/>
</pages>
</playground>
+7
View File
@@ -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.
+36 -1
View File
@@ -1,7 +1,42 @@
{
"object": {
"pins": [
{
"package": "AnyCodable",
"repositoryURL": "https://github.com/Flight-School/AnyCodable.git",
"state": {
"branch": null,
"revision": "396ccc3dba5bdee04c1e742e7fab40582861401e",
"version": "0.1.0"
}
},
{
"package": "FileCheck",
"repositoryURL": "https://github.com/llvm-swift/FileCheck.git",
"state": {
"branch": null,
"revision": "89b8480055f9adf8ce2f9ad5e2fac7ac1076242e",
"version": "0.0.8"
}
},
{
"package": "Poly",
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": {
"branch": "master",
"revision": "77f45b8963a51c02d71fc4075eba5cff47ff0d07",
"version": null
}
},
{
"package": "SwiftCheck",
"repositoryURL": "https://github.com/typelift/SwiftCheck.git",
"state": {
"branch": null,
"revision": "cf9958085b2ee1643e541e407c3233d1b76c18ff",
"version": "0.11.0"
}
}
]
},
"version": 1
+29 -5
View File
@@ -10,21 +10,45 @@ let package = Package(
name: "JSONAPI",
targets: ["JSONAPI"]),
.library(
name: "JSONAPITestLib",
targets: ["JSONAPITestLib"])
name: "JSONAPITesting",
targets: ["JSONAPITesting"]),
.library(
name: "JSONAPIArbitrary",
targets: ["JSONAPIArbitrary"]),
.library(
name: "JSONAPIOpenAPI",
targets: ["JSONAPIOpenAPI"])
],
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", .branch("master")),
.package(url: "https://github.com/Flight-School/AnyCodable.git", from: "0.1.0"),
.package(url: "https://github.com/typelift/SwiftCheck.git", from: "0.11.0")
],
targets: [
.target(
name: "JSONAPI",
dependencies: []),
dependencies: ["Poly"]),
.target(
name: "JSONAPITestLib",
name: "JSONAPITesting",
dependencies: ["JSONAPI"]),
.target(
name: "JSONAPIArbitrary",
dependencies: ["JSONAPI", "SwiftCheck"]),
.target(
name: "JSONAPIOpenAPI",
dependencies: ["JSONAPI", "AnyCodable", "JSONAPIArbitrary"]),
.testTarget(
name: "JSONAPITests",
dependencies: ["JSONAPITestLib"])
dependencies: ["JSONAPI", "JSONAPITesting"]),
.testTarget(
name: "JSONAPITestingTests",
dependencies: ["JSONAPI", "JSONAPITesting"]),
.testTarget(
name: "JSONAPIArbitraryTests",
dependencies: ["JSONAPI", "SwiftCheck", "JSONAPIArbitrary"]),
.testTarget(
name: "JSONAPIOpenAPITests",
dependencies: ["JSONAPI", "JSONAPIOpenAPI"])
],
swiftLanguageVersions: [.v4_2]
)
+507 -42
View File
File diff suppressed because it is too large Load Diff
@@ -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
+83 -2
View File
@@ -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
@@ -66,8 +68,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 +112,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 +194,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
+27 -1
View File
@@ -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 }
}
}
+13 -1
View File
@@ -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
@@ -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))
}
}
+75 -14
View File
@@ -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 {
private 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)
}
}
+94 -21
View File
@@ -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,16 +20,31 @@ 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 type: String { get }
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
@@ -37,16 +52,13 @@ public protocol JSONTyped {
/// itself is encoded and decoded as an
/// `Entity`, which gets specialized on an
/// `EntityDescription`.
public protocol EntityDescription: JSONTyped {
associatedtype Attributes: JSONAPI.Attributes
associatedtype Relationships: JSONAPI.Relationships
}
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, JSONTyped {
associatedtype Description: EntityDescription
associatedtype Description: EntityProxyDescription
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
typealias Id = JSONAPI.Id<EntityRawIdType, Self>
@@ -69,13 +81,15 @@ public protocol EntityProxy: Equatable, JSONTyped {
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 {}
@@ -85,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,
@@ -118,7 +136,7 @@ extension Entity: Identifiable, IdentifiableEntityType, Relatable where EntityRa
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)))"
}
}
@@ -143,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)
@@ -370,15 +389,21 @@ extension Entity where MetaType == NoMetadata, LinksType == NoLinks, EntityRawId
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 given that
/// 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: Pointer {
@@ -390,26 +415,47 @@ public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
}
}
// 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)
}
}
// MARK: Attribute Access
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 }
@@ -425,6 +471,15 @@ public extension EntityProxy {
}
}
// MARK: Meta-Attribute Access
public extension EntityProxy {
/// Access an attribute requiring a transformation on the RawValue _and_
/// a secondary transformation on this entity (self).
subscript<T>(_ path: KeyPath<Description.Attributes, (Self) -> T>) -> T {
return attributes[keyPath: path](self)
}
}
// MARK: Relationship Access
public extension EntityProxy {
/// Access to an Id of a `ToOneRelationship`.
@@ -467,6 +522,23 @@ public extension EntityProxy {
}
}
// MARK: Meta-Relationship Access
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 ~><Identifier: IdType>(entity: Self, path: KeyPath<Description.Relationships, (Self) -> Identifier>) -> Identifier {
return entity.relationships[keyPath: path](entity)
}
/// Access to all Ids of a `ToManyRelationship`.
/// This allows you to write `entity ~> \.others` instead
/// of `entity.relationships.others.ids`.
public static func ~><Identifier: IdType>(entity: Self, path: KeyPath<Description.Relationships, (Self) -> [Identifier]>) -> [Identifier] {
return entity.relationships[keyPath: path](entity)
}
}
infix operator ~>
// MARK: - Codable
@@ -483,7 +555,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)
@@ -512,15 +584,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)
@@ -0,0 +1,57 @@
//
// Poly+PrimaryResource.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/22/18.
//
import Poly
/// Poly is a protocol to which types that
/// are polymorphic belong to. Specifically,
/// Poly1, Poly2, Poly3, etc. types conform
/// to the Poly protocol. These types allow
/// typesafe grouping of a number of
/// disparate types under one roof for
/// the purposes of JSON API compliant
/// encoding or decoding.
public typealias JSONPoly = Poly & PrimaryResource
public typealias PolyWrapped = Codable & Equatable
extension Poly0: PrimaryResource {
public init(from decoder: Decoder) throws {
throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.")
}
public func encode(to encoder: Encoder) throws {
throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.")
}
}
// MARK: - 1 type
extension Poly1: PrimaryResource, MaybePrimaryResource where A: PolyWrapped {}
// MARK: - 2 types
extension Poly2: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped {}
// MARK: - 3 types
extension Poly3: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {}
// MARK: - 4 types
extension Poly4: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {}
// MARK: - 5 types
extension Poly5: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {}
// MARK: - 6 types
extension Poly6: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {}
// MARK: - 7 types
extension Poly7: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {}
// MARK: - 8 types
extension Poly8: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {}
// MARK: - 9 types
extension Poly9: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}

Some files were not shown because too many files have changed in this diff Show More