Compare commits

..

57 Commits

Author SHA1 Message Date
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
Mathew Polzin 67a43be26c update linuxmain 2018-12-08 20:05:00 -08:00
Mathew Polzin 8f279ce191 Add tests around nullable attributes and remove unnecessary encoding code 2018-12-08 20:04:11 -08:00
Mathew Polzin c9d388579f Made it much more convenient to work with Non-EntityType relationships. Discovered and fixed a bug where nullable relationships were encoded incorrectly. 2018-12-08 19:48:10 -08:00
Mathew Polzin 1061283905 loosen the grip entity had on Ids 2018-12-08 01:17:47 -08:00
Mathew Polzin 08949d0a93 Move encoding error type to its own file. Restructure Relatable and OptionalRelatable to not be dependent upon EntityDescription 2018-12-08 00:36:05 -08:00
Mathew Polzin 3047e2d23a Add access to computed attributes that are not AttributeType 2018-12-07 21:19:08 -08:00
Mathew Polzin 41a2a01788 Added support for relationship operator ~> to optional relationships 2018-12-07 20:59:39 -08:00
Mathew Polzin 53f7f55e07 Add Poly7 and Include7 because why not 2018-12-06 18:38:20 -08:00
Mathew Polzin d8d030286d Add a little bit of code doc 2018-12-06 18:12:56 -08:00
Mathew Polzin 005a981bf7 Add a couple of missing initializers to Entity 2018-12-05 22:51:12 -08:00
Mathew Polzin fad45203dd small refactor to consider ToOneRelationship with no meta or links to be even more synonymous with 'pointer' 2018-12-05 22:42:34 -08:00
Mathew Polzin 3468cb555a Remove Result dependency in favor of a super stripped down internally scoped Result enum. This won't interfere with other Result libraries and I was not exposing the Result anyway. 2018-12-05 22:02:59 -08:00
Mathew Polzin 8defe82536 Update README and linuxmain 2018-12-05 18:33:07 -08:00
Mathew Polzin 1f83216b03 Adding a few tests here and there 2018-12-05 18:31:53 -08:00
Mathew Polzin 6e6973c170 Adding test coverage for Relationship Meta and Links 2018-12-05 18:12:00 -08:00
Mathew Polzin 4f52cc2fd1 update README 2018-12-05 17:30:48 -08:00
Mathew Polzin 35d6cbb440 Add support for Relationship Meta and Links (untested) 2018-12-05 09:14:38 -08:00
Mathew Polzin d667e91a6a Add some missing initializers for Entity and fix Playground sources. 2018-12-03 19:08:09 -08:00
Mathew Polzin 75c8ce7405 Update linuxmain 2018-12-03 18:46:03 -08:00
Mathew Polzin 3a8237ec0f Add test coverage for Meta and Links on Entity 2018-12-03 18:45:07 -08:00
Mathew Polzin 49e33baa96 give all top-level-ish headers the JSONAPI prefix 2018-12-03 18:14:05 -08:00
Mathew Polzin f2a9b5a7b9 That should do the trick 2018-12-03 18:10:36 -08:00
Mathew Polzin 24baadd7a8 try to make headers unique 2018-12-03 18:09:40 -08:00
Mathew Polzin 8d18be6154 Update README after adding Meta and Links to Entity. 2018-12-03 18:04:53 -08:00
Mathew Polzin 8aa887b527 Add metadata and links to resource objects (i.e. Entity). untested. 2018-12-02 22:57:59 -08:00
Mathew Polzin 7d0a686dd9 update linuxmain 2018-12-02 21:39:45 -08:00
Mathew Polzin e4eb7816d7 Added an APIDescription type to Document that supports the JSON API Spec's JSON:API Object. 2018-12-02 21:35:09 -08:00
Mathew Polzin 482174f5b4 update linuxmain 2018-12-02 01:44:02 -08:00
Mathew Polzin 339480264e Add JSON:API Object to Document as the APIDescription type. Begin to add testing around this new type. 2018-12-02 01:43:27 -08:00
Mathew Polzin 5d666ec998 move entity validator (i.e. check function) section of project status into a TestLib subsection. 2018-12-01 09:58:07 -08:00
Mathew Polzin 9cacbe9b8b Revamp project status section and remove old todo list items. 2018-12-01 09:55:31 -08:00
57 changed files with 6107 additions and 579 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)!)
@@ -13,7 +13,7 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
// MARK: - Literal Expressibility
// The JSONAPITestLib provides literal expressibility for key types to
// make creating tests easier
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil))
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil), meta: .none, links: .none)
// MARK: - JSON API structure checking
// The JSONAPITestLib provides a `check` function for each Entity type
@@ -11,32 +11,39 @@ 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, UnknownJSONAPIError>
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>, UnknownJSONAPIError>
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]
+46 -16
View File
@@ -23,13 +23,15 @@ extension String: CreatableRawIdType {
}
}
// MARK: - Entity typealias for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, String>
// MARK: - typealiases for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
public static var type: String { return "people" }
public static var jsonType: String { return "people" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<[String]>
@@ -46,11 +48,11 @@ public enum PersonDescription: EntityDescription {
}
public struct Relationships: JSONAPI.Relationships {
public let friends: ToManyRelationship<Person>
public let dogs: ToManyRelationship<Dog>
public let home: ToOneRelationship<House>
public let friends: ToMany<Person>
public let dogs: ToMany<Dog>
public let home: ToOne<House>
public init(friends: ToManyRelationship<Person>, dogs: ToManyRelationship<Dog>, home: ToOneRelationship<House>) {
public init(friends: ToMany<Person>, dogs: ToMany<Dog>, home: ToOne<House>) {
self.friends = friends
self.dogs = dogs
self.home = home
@@ -60,15 +62,15 @@ public enum PersonDescription: EntityDescription {
public typealias Person = ExampleEntity<PersonDescription>
public extension Entity where Description == PersonDescription, EntityRawIdType == String {
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>
@@ -79,9 +81,9 @@ public enum DogDescription: EntityDescription {
}
public struct Relationships: JSONAPI.Relationships {
public let owner: ToOneRelationship<Person?>
public let owner: ToOne<Person?>
public init(owner: ToOneRelationship<Person?>) {
public init(owner: ToOne<Person?>) {
self.owner = owner
}
}
@@ -89,19 +91,47 @@ public enum DogDescription: EntityDescription {
public typealias Dog = ExampleEntity<DogDescription>
public extension Entity where Description == DogDescription, EntityRawIdType == String {
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
+1 -9
View File
@@ -1,15 +1,7 @@
{
"object": {
"pins": [
{
"package": "Result",
"repositoryURL": "https://github.com/mattpolzin/Result",
"state": {
"branch": "master",
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
"version": null
}
}
]
},
"version": 1
+1 -3
View File
@@ -14,13 +14,11 @@ let package = Package(
targets: ["JSONAPITestLib"])
],
dependencies: [
// antitypical/Result without the Foundation requirement:
.package(url: "https://github.com/mattpolzin/Result", .branch("master"))
],
targets: [
.target(
name: "JSONAPI",
dependencies: ["Result"]),
dependencies: []),
.target(
name: "JSONAPITestLib",
dependencies: ["JSONAPI"]),
+447 -74
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
//
// APIDescription.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/1/18.
//
/// This is what the JSON API Spec calls the "JSON:API Object"
public protocol APIDescriptionType: Codable, Equatable {
associatedtype Meta
}
/// This is what the JSON API Spec calls the "JSON:API Object"
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
public let version: String
public let meta: Meta
public init(version: String, meta: Meta) {
self.version = version
self.meta = meta
}
}
/// Can be used as `APIDescriptionType` for Documents that do not
/// have any API Description (a.k.a. "JSON:API Object").
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
public typealias Meta = NoMetadata
public init() {}
public static var none: NoAPIDescription { return .init() }
public var description: String { return "No JSON:API Object" }
}
extension APIDescription {
private enum CodingKeys: String, CodingKey {
case version
case meta
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
if let metaVal = NoMetadata() as? Meta {
meta = metaVal
} else {
meta = try container.decode(Meta.self, forKey: .meta)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(version, forKey: .version)
if Meta.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
}
}
+150 -20
View File
@@ -10,9 +10,10 @@ public protocol JSONAPIDocument: Codable, Equatable {
associatedtype MetaType: JSONAPI.Meta
associatedtype LinksType: JSONAPI.Links
associatedtype IncludeType: JSONAPI.Include
associatedtype APIDescription: APIDescriptionType
associatedtype Error: JSONAPIError
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, Error>.Body
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
var body: Body { get }
}
@@ -24,11 +25,19 @@ public protocol JSONAPIDocument: Codable, Equatable {
/// API uses snake case, you will want to use
/// a conversion such as the one offerred by the
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, Error: JSONAPIError>: JSONAPIDocument {
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: JSONAPIDocument {
public typealias Include = IncludeType
/// The JSON API Spec calls this the JSON:API Object. It contains version
/// and metadata information about the API itself.
public let apiDescription: APIDescription
/// The Body of the Document. This body is either one or more errors
/// with links and metadata attempted to parse but not guaranteed or
/// it is a successful data struct containing all the primary and
/// included resources, the metadata, and the links that this
/// document type specifies.
public let body: Body
// public let jsonApi: APIDescription?
public enum Body: Equatable {
case errors([Error], meta: MetaType?, links: LinksType?)
@@ -57,8 +66,13 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
return errors
}
public var data: Data? {
guard case let .data(data) = self else { return nil }
return data
}
public var primaryData: PrimaryResourceBody? {
public var primaryResource: PrimaryResourceBody? {
guard case let .data(data) = self else { return nil }
return data.primary
}
@@ -91,57 +105,163 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
}
}
public init(errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
body = .errors(errors, meta: meta, links: links)
self.apiDescription = apiDescription
}
public init(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(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: links))
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, includes: .none, meta: meta, links: links)
}
}
extension Document where MetaType == NoMetadata {
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: links))
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: .none, links: links)
}
}
extension Document where LinksType == NoLinks {
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: .none))
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none)
}
}
extension Document where APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links)
}
}
extension Document where IncludeType == NoIncludes, LinksType == NoLinks {
public init(body: PrimaryResourceBody, meta: MetaType) {
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: .none))
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType) {
self.init(apiDescription: apiDescription, body: body, meta: meta, links: .none)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata {
public init(body: PrimaryResourceBody, links: LinksType) {
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: links))
public init(apiDescription: APIDescription, body: PrimaryResourceBody, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, meta: .none, links: links)
}
}
extension Document where IncludeType == NoIncludes, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
self.init(apiDescription: .none, body: body, meta: meta, links: links)
}
}
extension Document where MetaType == NoMetadata, LinksType == NoLinks {
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: .none))
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>) {
self.init(apiDescription: apiDescription, body: body, includes: includes, links: .none)
}
}
extension Document where MetaType == NoMetadata, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
self.init(apiDescription: .none, body: body, includes: includes, links: links)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
public init(body: PrimaryResourceBody) {
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: .none))
public init(apiDescription: APIDescription, body: PrimaryResourceBody) {
self.init(apiDescription: apiDescription, body: body, includes: .none)
}
}
extension Document where MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
self.init(apiDescription: .none, body: body, includes: includes)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody) {
self.init(apiDescription: .none, body: body)
}
}
*/
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
public func merging(_ other: Document.Body.Data,
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
return Document.Body.Data(primary: primary.appending(other.primary),
includes: includes.appending(other.includes),
meta: metaMerge(meta, other.meta),
links: linksMerge(links, other.links))
}
}
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks {
public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
return merging(other,
combiningMetaWith: { _, _ in .none },
combiningLinksWith: { _, _ in .none })
}
}
extension Document where IncludeType == NoIncludes {
/// Create a new Document with the given includes.
public func including<I: JSONAPI.Include>(_ includes: Includes<I>) -> Document<PrimaryResourceBody, MetaType, LinksType, I, APIDescription, Error> {
// Note that if IncludeType is NoIncludes, then we allow anything
// to be included, but if IncludeType already specifies a type
// of thing to be expected then we lock that down.
// See: Document.including() where IncludeType: _Poly1
switch body {
case .data(let data):
return .init(apiDescription: apiDescription,
body: data.primary,
includes: includes,
meta: data.meta,
links: data.links)
case .errors(let errors, meta: let meta, links: let links):
return .init(apiDescription: apiDescription,
errors: errors,
meta: meta,
links: links)
}
}
}
// 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
@@ -155,6 +275,12 @@ extension Document {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootCodingKeys.self)
if let noData = NoAPIDescription() as? APIDescription {
apiDescription = noData
} else {
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
}
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
let meta: MetaType?
@@ -242,6 +368,10 @@ extension Document {
try container.encode(data.links, forKey: .links)
}
}
if APIDescription.self != NoAPIDescription.self {
try container.encode(apiDescription, forKey: .jsonapi)
}
}
}
+31
View File
@@ -50,6 +50,16 @@ public struct Includes<I: Include>: Codable, Equatable {
}
}
extension Includes {
public func appending(_ other: Includes<I>) -> Includes {
return Includes(values: values + other.values)
}
}
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
return left.appending(right)
}
extension Includes: CustomStringConvertible {
public var description: String {
return "Includes(\(String(describing: values))"
@@ -115,4 +125,25 @@ extension Includes where I: _Poly6 {
}
// MARK: - 7 includes
public typealias Include7 = Poly7
extension Includes where I: _Poly7 {
public subscript(_ lookup: I.G.Type) -> [I.G] {
return values.compactMap { $0.g }
}
}
// MARK: - 8 includes
public typealias Include8 = Poly8
extension Includes where I: _Poly8 {
public subscript(_ lookup: I.H.Type) -> [I.H] {
return values.compactMap { $0.h }
}
}
// MARK: - 9 includes
public typealias Include9 = Poly9
extension Includes where I: _Poly9 {
public subscript(_ lookup: I.I.Type) -> [I.I] {
return values.compactMap { $0.i }
}
}
+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
+14
View File
@@ -0,0 +1,14 @@
//
// EncodingError.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/7/18.
//
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
}
+11
View File
@@ -21,6 +21,17 @@ public protocol JSONAPIURL: Codable, Equatable {}
public struct Link<URL: JSONAPI.JSONAPIURL, Meta: JSONAPI.Meta>: Equatable, Codable {
public let url: URL
public let meta: Meta
public init(url: URL, meta: Meta) {
self.url = url
self.meta = meta
}
}
extension Link where Meta == NoMetadata {
public init(url: URL) {
self.init(url: url, meta: .none)
}
}
public extension Link {
@@ -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))
}
}
+77 -17
View File
@@ -6,10 +6,17 @@
//
public protocol AttributeType: Codable {
associatedtype RawValue: Codable
associatedtype ValueType
var value: ValueType { get }
}
// MARK: TransformedAttribute
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
private let rawValue: RawValue
let rawValue: RawValue
public let value: Transformer.To
@@ -19,7 +26,19 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
}
}
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
// If we are using the identity transform, we can skip the transform and guarantee no
// error is thrown.
public init(value: RawValue) {
rawValue = value
self.value = value
}
}
extension TransformedAttribute where Transformer: ReversibleTransformer {
/// Initialize a TransformedAttribute from its transformed value. The
/// RawValue, which is what gets encoded/decoded, is determined using
/// The Transformer's reverse function.
public init(transformedValue: Transformer.To) throws {
self.value = transformedValue
rawValue = try Transformer.reverse(value)
@@ -34,18 +53,34 @@ extension TransformedAttribute: CustomStringConvertible {
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
// If we are using the identity transform, we can skip the transform and guarantee no
// error is thrown.
// MARK: ValidatedAttribute
/// A ValidatedAttribute does not transform its raw value, but it throws
/// an error if the raw value does not match expectations.
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
// MARK: Attribute
/// An Attribute simply represents a type that can be encoded and decoded.
public struct Attribute<RawValue: Codable>: AttributeType {
let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>>
public var value: RawValue {
return attribute.value
}
public init(value: RawValue) {
rawValue = value
self.value = value
attribute = .init(value: value)
}
}
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
extension Attribute: CustomStringConvertible {
public var description: String {
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
}
}
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
extension Attribute: Equatable where RawValue: Equatable {}
// MARK: - Codable
extension TransformedAttribute {
@@ -73,15 +108,40 @@ extension TransformedAttribute {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
// See note in decode above about the weirdness
// going on here.
let anyNil: Any? = nil
if let _ = anyNil as? Transformer.From,
(rawValue as Any?) == nil {
try container.encodeNil()
}
try container.encode(rawValue)
}
}
extension Attribute {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// A little trickery follows. If the value is nil, the
// container.decode(Value.self) will fail even if Value
// is Optional. However, we can check if decoding nil
// succeeds and then attempt to coerce nil to a Value
// type at which point we can store nil in `value`.
let anyNil: Any? = nil
if container.decodeNil(),
let val = anyNil as? RawValue {
attribute = .init(value: val)
} else {
attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(attribute)
}
}
// MARK: Attribute decoding and encoding defaults
extension AttributeType {
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container, forKey key: Container.Key) throws -> Self {
return try container.decode(Self.self, forKey: key)
}
}
File diff suppressed because it is too large Load Diff
+30 -7
View File
@@ -34,13 +34,31 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible {
public var description: String { return "Unidentified" }
}
public protocol MaybeId: Codable {
associatedtype EntityType: JSONAPI.EntityProxy
public protocol OptionalId: Codable {
associatedtype IdentifiableType: JSONAPI.JSONTyped
associatedtype RawType: MaybeRawId
var rawValue: RawType { get }
init(rawValue: RawType)
}
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
var rawValue: RawType { get }
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
extension Optional: OptionalId where Wrapped: IdType {
public typealias IdentifiableType = Wrapped.IdentifiableType
public typealias RawType = Wrapped.RawType?
public var rawValue: Wrapped.RawType? {
guard case .some(let value) = self else {
return nil
}
return value.rawValue
}
public init(rawValue: Wrapped.RawType?) {
self = rawValue.map { Wrapped(rawValue: $0) }
}
}
public extension IdType {
@@ -53,7 +71,7 @@ public protocol CreatableIdType: IdType {
/// An Entity ID. These IDs can be encoded to or decoded from
/// JSON API IDs.
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable, Equatable, MaybeId {
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
public let rawValue: RawType
@@ -63,7 +81,8 @@ public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable,
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
rawValue = try container.decode(RawType.self)
let rawValue = try container.decode(RawType.self)
self.init(rawValue: rawValue)
}
public func encode(to encoder: Encoder) throws {
@@ -72,7 +91,11 @@ public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable,
}
}
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
return Id(rawValue: rawValue)
}
}
extension Id: CreatableIdType where RawType: CreatableRawIdType {
public init() {
File diff suppressed because it is too large Load Diff
+157 -58
View File
@@ -5,30 +5,59 @@
// Created by Mathew Polzin on 8/31/18.
//
public protocol RelationshipType: Codable {}
public protocol RelationshipType {
associatedtype LinksType
associatedtype MetaType
var links: LinksType { get }
var meta: MetaType { get }
}
/// An Entity relationship that can be encoded to or decoded from
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `One<EntityDescription>`
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: RelationshipType, Equatable {
public struct ToOneRelationship<Identifiable: JSONAPI.Identifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
public let id: Relatable.WrappedIdentifier
public let id: Identifiable.Identifier
public init(id: Relatable.WrappedIdentifier) {
public let meta: MetaType
public let links: LinksType
public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) {
self.id = id
self.meta = meta
self.links = links
}
}
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
public init<E: EntityType>(entity: E) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
id = entity.id
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init(id: Identifiable.Identifier) {
self.init(id: id, meta: .none, links: .none)
}
}
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier? {
public init<E: EntityType>(entity: E?) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
id = entity?.id
extension ToOneRelationship {
public init<E: EntityType>(entity: E, meta: MetaType, links: LinksType) where E.Id == Identifiable.Identifier {
self.init(id: entity.id, meta: meta, links: links)
}
}
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init<E: EntityType>(entity: E) where E.Id == Identifiable.Identifier {
self.init(id: entity.id, meta: .none, links: .none)
}
}
extension ToOneRelationship where Identifiable: OptionalRelatable {
public init<E: EntityType>(entity: E?, meta: MetaType, links: LinksType) where E.Id == Identifiable.Wrapped.Identifier {
self.init(id: entity?.id, meta: meta, links: links)
}
}
extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks {
public init<E: EntityType>(entity: E?) where E.Id == Identifiable.Wrapped.Identifier {
self.init(id: entity?.id, meta: .none, links: .none)
}
}
@@ -36,73 +65,105 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Ident
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `Many<EntityDescription>`
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType, Equatable {
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
public let ids: [Relatable.Identifier]
public init(ids: [Relatable.Identifier]) {
public let meta: MetaType
public let links: LinksType
public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) {
self.ids = ids
self.meta = meta
self.links = links
}
public init<T: JSONAPI.Relatable>(relationships: [ToOneRelationship<T>]) where T.WrappedIdentifier == Relatable.Identifier {
ids = relationships.map { $0.id }
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier {
ids = pointers.map { $0.id }
self.meta = meta
self.links = links
}
private init() {
ids = []
public init<E: EntityType>(entities: [E], meta: MetaType, links: LinksType) where E.Id == Relatable.Identifier {
self.init(ids: entities.map { $0.id }, meta: meta, links: links)
}
private init(meta: MetaType, links: LinksType) {
self.init(ids: [], meta: meta, links: links)
}
public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship {
return ToManyRelationship(meta: meta, links: links)
}
}
extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init(ids: [Relatable.Identifier]) {
self.init(ids: ids, meta: .none, links: .none)
}
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>]) where T.Identifier == Relatable.Identifier {
self.init(pointers: pointers, meta: .none, links: .none)
}
public static var none: ToManyRelationship {
return .init()
return .none(withMeta: .none, links: .none)
}
public init<E: EntityType>(entities: [E]) where E.Id == Relatable.Identifier {
self.init(entities: entities, meta: .none, links: .none)
}
}
extension ToManyRelationship {
public init<E: EntityType>(entities: [E]) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
ids = entities.map { $0.id }
}
public protocol Identifiable: JSONTyped {
associatedtype Identifier: Equatable
}
/// The WrappedRelatable (a.k.a OptionalRelatable) protocol
/// describes Optional<T: Relatable> and Relatable types.
public protocol WrappedRelatable: Codable, Equatable {
associatedtype Description: EntityDescription
associatedtype Identifier: JSONAPI.IdType
associatedtype WrappedIdentifier: Codable, Equatable
}
public typealias OptionalRelatable = WrappedRelatable
/// The Relatable protocol describes anything that
/// has an IdType Identifier
public protocol Relatable: WrappedRelatable {}
public protocol Relatable: Identifiable where Identifier: JSONAPI.IdType {
}
extension Optional: OptionalRelatable where Wrapped: Relatable {
public typealias Description = Wrapped.Description
public typealias Identifier = Wrapped.Identifier
public typealias WrappedIdentifier = Identifier?
/// OptionalRelatable just describes an Optional
/// with a Reltable Wrapped type.
public protocol OptionalRelatable: Identifiable where Identifier == Wrapped.Identifier? {
associatedtype Wrapped: JSONAPI.Relatable
}
extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable {
public typealias Identifier = Wrapped.Identifier?
public static var jsonType: String { return Wrapped.jsonType }
}
// MARK: Codable
private enum ResourceLinkageCodingKeys: String, CodingKey {
case data = "data"
case meta = "meta"
case links = "links"
}
private enum ResourceIdentifierCodingKeys: String, CodingKey {
case id = "id"
case entityType = "type"
}
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
}
extension ToOneRelationship {
extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
if let noMeta = NoMetadata() as? MetaType {
meta = noMeta
} else {
meta = try container.decode(MetaType.self, forKey: .meta)
}
if let noLinks = NoLinks() as? LinksType {
links = noLinks
} else {
links = try container.decode(LinksType.self, forKey: .links)
}
// A little trickery follows. If the id is nil, the
// container.decode(Identifier.self) will fail even if Identifier
// is Optional. However, we can check if decoding nil
@@ -110,7 +171,7 @@ extension ToOneRelationship {
// type at which point we can store nil in `id`.
let anyNil: Any? = nil
if try container.decodeNil(forKey: .data),
let val = anyNil as? Relatable.WrappedIdentifier {
let val = anyNil as? Identifiable.Identifier {
id = val
return
}
@@ -119,11 +180,11 @@ extension ToOneRelationship {
let type = try identifier.decode(String.self, forKey: .entityType)
guard type == Relatable.Description.type else {
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.Description.type, found: type)
guard type == Identifiable.jsonType else {
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type)
}
id = try identifier.decode(Relatable.WrappedIdentifier.self, forKey: .id)
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
}
public func encode(to encoder: Encoder) throws {
@@ -133,17 +194,46 @@ extension ToOneRelationship {
try container.encodeNil(forKey: .data)
}
if MetaType.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(links, forKey: .links)
}
// If id is nil, instead of {id: , type: } we will just
// encode `null`
let anyNil: Any? = nil
let nilId = anyNil as? Identifiable.Identifier
guard id != nilId else {
try container.encodeNil(forKey: .data)
return
}
var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
try identifier.encode(id, forKey: .id)
try identifier.encode(Relatable.Description.type, forKey: .entityType)
try identifier.encode(id.rawValue, forKey: .id)
try identifier.encode(Identifiable.jsonType, forKey: .entityType)
}
}
extension ToManyRelationship {
extension ToManyRelationship: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
if let noMeta = NoMetadata() as? MetaType {
meta = noMeta
} else {
meta = try container.decode(MetaType.self, forKey: .meta)
}
if let noLinks = NoLinks() as? LinksType {
links = noLinks
} else {
links = try container.decode(LinksType.self, forKey: .links)
}
var identifiers = try container.nestedUnkeyedContainer(forKey: .data)
var newIds = [Relatable.Identifier]()
@@ -152,24 +242,33 @@ extension ToManyRelationship {
let type = try identifier.decode(String.self, forKey: .entityType)
guard type == Relatable.Description.type else {
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.Description.type, found: type)
guard type == Relatable.jsonType else {
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type)
}
newIds.append(try identifier.decode(Relatable.Identifier.self, forKey: .id))
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
}
ids = newIds
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self)
if MetaType.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(links, forKey: .links)
}
var identifiers = container.nestedUnkeyedContainer(forKey: .data)
for id in ids {
var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
try identifier.encode(id, forKey: .id)
try identifier.encode(Relatable.Description.type, forKey: .entityType)
try identifier.encode(id.rawValue, forKey: .id)
try identifier.encode(Relatable.jsonType, forKey: .entityType)
}
}
}

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