Compare commits

..

116 Commits

Author SHA1 Message Date
Mathew Polzin be2530f41e Add parsing invariant structure even though I do not have a solid enough use-case to implement an invariant fully just yet. 2018-12-26 14:19:54 -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
Mathew Polzin 6663ad042e A cleanup and clarification pass at the README 2018-12-01 01:13:05 -08:00
Mathew Polzin 07402259c4 Add ability to specify that a SingleResourceBody should be optional or not (or specifically that its PrimaryResource is nullable or not). add tests. update documentation. 2018-12-01 00:36:39 -08:00
Mathew Polzin 8274ecb523 Add tests confirming that initializers work as expected without using any TestLib conveniences 2018-11-30 23:56:18 -08:00
Mathew Polzin 853c7e892a Fix playground bug caused by not running after removing a typealias 2018-11-30 20:18:08 -08:00
Mathew Polzin 04bf96a0cf update linuxmain 2018-11-29 20:13:21 -08:00
Mathew Polzin 8668311307 better poly proxy tests 2018-11-29 20:10:56 -08:00
Mathew Polzin 689517c633 Add ability to differentiate between things that _act_ like Entities and things that _are_ entities in the sense that the latter are persisted between client and server but the former is just a clientside convenience. 2018-11-29 20:03:44 -08:00
Mathew Polzin 65b80ee0eb Add ReversibleTransformer and tests. remove unneeded check in Entity.check(). 2018-11-29 18:39:59 -08:00
Mathew Polzin e91a03b396 Expose initializer for Document.Body.Data 2018-11-29 13:37:06 -08:00
Mathew Polzin 82088c7852 Remove BodyData which just aliased Body.Data 2018-11-29 13:15:07 -08:00
Mathew Polzin 34a4c8e7fc Add TestLib ability to represent to-one relationships with the values of their IDs. 2018-11-29 11:31:17 -08:00
Mathew Polzin 6aeb859c24 Remove now deleted test from linuxmain 2018-11-28 21:13:47 -08:00
Mathew Polzin 163ac94c51 I did some more type wrangling to finally get the Id type to specialize on Entity rather than EntityDescription. The compiler gets into trouble depending on which of a few semantically identical routes are taken, but I finally stumbled upon the correct combination of protocols and extensions to get the job done. this was always the ideal outcome, but I was not sure the Swift compiler would allow it. 2018-11-28 21:13:07 -08:00
Mathew Polzin e67b9fc142 Update playground and README for computed attributes. 2018-11-28 09:20:50 -08:00
Mathew Polzin a628992fcb Make Attribute a Functor to make computed attributes easier to write. 2018-11-28 09:09:23 -08:00
Mathew Polzin cf47f88a61 Add tests to confirm that Entities can safely have computed attributes or relationships. 2018-11-28 08:15:57 -08:00
Mathew Polzin 0425e2adcb rename folder in tests 2018-11-28 07:50:47 -08:00
Mathew Polzin d3763ba713 Add array literal expressibility for ToManyRelationship 2018-11-27 23:59:12 -08:00
Mathew Polzin fcc1796731 Expose Entity properties on EntityType protocol. 2018-11-27 18:58:48 -08:00
Mathew Polzin 921bcef05d Ids are now Hashable 2018-11-27 18:39:57 -08:00
Mathew Polzin 661ff6eca5 A little renaming and easier access to important types under the JSONAPIDocument protocol 2018-11-27 18:20:01 -08:00
Mathew Polzin e36180c9b9 Make properties of Body.Data public 2018-11-27 18:05:02 -08:00
Mathew Polzin abee0c4d0e Add body variable to JSONAPIDocument protocol 2018-11-27 17:55:56 -08:00
Mathew Polzin 9c8b2fbebb Renamed JSONAPIDocument to Document and created a protocol describing a JSONAPI Document. 2018-11-27 17:35:13 -08:00
Mathew Polzin a9ef71f383 indentation change 2018-11-27 17:15:15 -08:00
Mathew Polzin 232554ec51 Wrap JSONAPIDocument body up a bit nicer for use when the JSONAPIDocument does not represent errors. 2018-11-27 16:12:36 -08:00
Mathew Polzin 8f36bb40e3 Add note about the library being a bit 'opinionated' 2018-11-27 14:04:53 -08:00
Mathew Polzin 55f2f52676 minor README change 2018-11-27 13:59:01 -08:00
Mathew Polzin dd5f3b1737 Added check for nullable array attributes 2018-11-27 13:57:55 -08:00
Mathew Polzin 7d7b3d7f19 Update linuxmain 2018-11-27 13:34:22 -08:00
Mathew Polzin aedd5dc29b Add Entity validation via a function in JSONAPITestLib. 2018-11-27 13:33:55 -08:00
Mathew Polzin 3964202ea2 Mention JSONAPITestLib in the README 2018-11-27 11:53:21 -08:00
Mathew Polzin dcabafd583 Add a playground page to show off JSONAPITestLib and add nil literal expressibility to ToOneRelationship 2018-11-27 11:45:15 -08:00
Mathew Polzin 9df9efc2dc rename NoRelatives to NoRelationships to better pair with NoAttributes and NoLinks 2018-11-27 11:08:53 -08:00
Mathew Polzin a55a4cbed0 Added test library, attribute literal expressibility, and tests 2018-11-27 10:54:51 -08:00
Mathew Polzin 6f6ed87b4c Update included playground 2018-11-27 09:14:47 -08:00
Mathew Polzin 59ff145aa8 Rename the included Error type. 2018-11-27 09:11:13 -08:00
Mathew Polzin 20a685e67b Add goal of making it easier to construct Attributes values 2018-11-26 22:29:31 -08:00
Mathew Polzin 3d8d10584b Add typealiases to make accessing Entity Relationships and Attributes types more convenient 2018-11-26 22:27:10 -08:00
Mathew Polzin b93580c900 Allow ManyResourceBody to use any PrimaryResource type 2018-11-26 18:57:34 -08:00
Mathew Polzin 8927938d56 Rename URL to JSONAPIURL to avoid conflict with Foundation.URL 2018-11-26 18:29:24 -08:00
Mathew Polzin 372e2de721 update linuxmain 2018-11-25 20:07:47 -08:00
Mathew Polzin 1a2ba17f02 more documentation updates 2018-11-25 20:06:27 -08:00
Mathew Polzin 9f2c7aa2e4 Update README 2018-11-25 19:48:50 -08:00
Mathew Polzin 650937dbb9 improve test coverage of some JSONAPIDocument accessors. 2018-11-25 19:41:10 -08:00
Mathew Polzin b08cbdb0ae Add some tests around documents with links. 2018-11-25 19:12:30 -08:00
Mathew Polzin c30a2615f2 Added links to document but currently untested 2018-11-25 00:02:37 -08:00
Mathew Polzin 86e87ebf5d Add Links and tests 2018-11-24 22:46:53 -08:00
Mathew Polzin 987bf2b750 Update linuxmain 2018-11-23 20:54:41 -08:00
Mathew Polzin af1aca9cf4 More failure test cases. pretty ok test coverage at this point. 2018-11-23 20:44:12 -08:00
Mathew Polzin b97649fab6 Add some failure testing 2018-11-23 20:34:43 -08:00
Mathew Polzin 8114132189 Add attribute validation tests 2018-11-23 20:21:00 -08:00
Mathew Polzin b61a41c99b Allow EntityType and Poly to be PrimaryResource in JSONAPIDocument 2018-11-23 19:33:06 -08:00
Mathew Polzin 16b83ddbef lots of tests of Poly 2018-11-23 19:09:53 -08:00
Mathew Polzin 6dff02bd51 Update to version of Result that does not even import Foundation for tests 2018-11-23 08:58:13 -08:00
Mathew Polzin 2563edf419 Add subscript access to Poly types. Add empty test class for Poly type. Remove unneeded Foundation import. Small README change to read more clearly. 2018-11-23 08:49:55 -08:00
Mathew Polzin 98bb64268f Split Include1 through Include6 off to a new file and a type I am calling Poly. This new type will allow me to use the same structures to represent polymorphic relationships. 2018-11-22 22:52:21 -08:00
Mathew Polzin 399644b62a update readme 2018-11-22 22:23:44 -08:00
Mathew Polzin 3b1e5f7414 Switched to a fork of the Result repo that does not depend on Foundation (outside of tests) 2018-11-22 22:08:18 -08:00
Mathew Polzin bf842bd781 Remove a couple unneeded Foundation imports that snuck in 2018-11-22 21:35:58 -08:00
Mathew Polzin 7edb631f09 Update documentation 2018-11-22 21:32:44 -08:00
Mathew Polzin 3c5a356b44 Add a couple more tests around metadata. 2018-11-22 21:14:19 -08:00
Mathew Polzin f526963707 Update README and update linuxmain 2018-11-22 21:06:04 -08:00
Mathew Polzin c4032eb351 Added meta data to JSONAPIDocument. Added tests around JSONAPIDocument error finally. Needs a few more tests around metadata. 2018-11-22 21:04:58 -08:00
Mathew Polzin 7a3a1b8b65 Round off CustomStringConvertible implementations to make print output a lot easier to read. 2018-11-20 07:27:28 -08:00
Mathew Polzin b3a4591a5d comment out the rest of the once again relevant playground example 2018-11-18 21:51:22 -08:00
Mathew Polzin 23d057c47b Adding more custom string convertible implementations to reduce the verbosity of printing to terminal. expose JSONAPIDocument.Include so that its constructor can be used. 2018-11-18 21:47:00 -08:00
Mathew Polzin 3327a93df5 Moved Examples.swift into a Playground, added some initialization code required to create entities and documents in code. 2018-11-18 17:47:26 -08:00
Mathew Polzin 713fd2ba3a Add xcode workspace files to git ignore. 2018-11-18 15:53:00 -08:00
Mathew Polzin 072479c160 Move Transformer into its own class and add a Validator protocol based on Transformer 2018-11-18 15:49:06 -08:00
62 changed files with 7739 additions and 916 deletions
+1
View File
@@ -2,3 +2,4 @@
/.build
/Packages
/*.xcodeproj
/*.xcworkspace
-78
View File
@@ -1,78 +0,0 @@
//
// Examples.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/12/18.
//
import Foundation
import JSONAPI
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
// MARK: - A few resource objects (entities)
enum PersonDescription: EntityDescription {
static var type: String { return "people" }
struct Attributes: JSONAPI.Attributes {
let name: [String]
let favoriteColor: String
}
struct Relationships: JSONAPI.Relationships {
let friends: ToManyRelationship<Person>
let dogs: ToManyRelationship<Dog>
let home: ToOneRelationship<House>
}
}
typealias Person = ExampleEntity<PersonDescription>
enum DogDescription: EntityDescription {
static var type: String { return "dogs" }
struct Attributes: JSONAPI.Attributes {
let name: String
}
struct Relationships: JSONAPI.Relationships {
let owner: ToOneRelationship<Person?>
}
}
typealias Dog = ExampleEntity<DogDescription>
enum HouseDescription: EntityDescription {
static var type: String { return "houses" }
typealias Attributes = NoAttributes
typealias Relationships = NoRelatives
}
typealias House = ExampleEntity<HouseDescription>
// MARK: - Parse a response body with one Dog in it
typealias SingleDogResponse = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
let dummyData = "".data(using: .utf8)!
let dogResponse = try! JSONDecoder().decode(SingleDogResponse.self, from: dummyData)
let dog = dogResponse.body.data?.primary.value
// MARK: Parse a response body with multiple people in it and dogs and houses included
typealias BatchPeopleResponse = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
let peopleResponse = try! JSONDecoder().decode(BatchPeopleResponse.self, from: dummyData)
let people = peopleResponse.body.data?.primary.values
let dogs = peopleResponse.body.data?.included[Dog.self]
let houses = peopleResponse.body.data?.included[House.self]
@@ -0,0 +1,24 @@
//: [Previous](@previous)
import Foundation
import JSONAPI
import JSONAPITestLib
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
// MARK: - Literal Expressibility
// The 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), meta: .none, links: .none)
// MARK: - JSON API structure checking
// The JSONAPITestLib provides a `check` function for each Entity type
// that uses reflection to catch mistakes that are not forbidden by
// Swift's type system but will result in unexpected results when
// encoding/decoding. It is a good idea to add a `check` to each of
// your unit tests that create Entities.
try Dog.check(dog)
@@ -0,0 +1,63 @@
import Foundation
import JSONAPI
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
// MARK: - Create a request or response body with one Dog in it
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none)
let 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
// MARK: - Create a request or response with multiple people and dogs and houses included
let personIds = [Person.Identifier(), Person.Identifier()]
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)]
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none)
let 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 dogsFromData = peopleResponse.body.includes?[Dog.self]
let housesFromData = peopleResponse.body.includes?[House.self]
print("-----")
print(peopleResponse)
print("-----")
// MARK: - Pass successfully parsed body to other parts of the code
if case let .data(bodyData) = peopleResponse.body {
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
} else {
print("no body data")
}
// MARK: - Work in the abstract
func process<T: JSONAPIDocument>(document: T) {
guard case let .data(body) = document.body else {
return
}
let x: T.Body.Data = body
}
process(document: peopleResponse)
+114
View File
@@ -0,0 +1,114 @@
//
// Examples.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/12/18.
//
import Foundation
import JSONAPI
/*******
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
********/
// MARK: - String as CreatableRawIdType
var GlobalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
GlobalStringId += 1
return String(GlobalStringId)
}
}
// MARK: - typealiases for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
public static var type: String { return "people" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<[String]>
public let favoriteColor: Attribute<String>
public var fullName: Attribute<String> {
return name.map { $0.joined(separator: " ") }
}
public init(name: Attribute<[String]>, favoriteColor: Attribute<String>) {
self.name = name
self.favoriteColor = favoriteColor
}
}
public struct Relationships: JSONAPI.Relationships {
public let friends: ToMany<Person>
public let dogs: ToMany<Dog>
public let home: ToOne<House>
public init(friends: ToMany<Person>, dogs: ToMany<Dog>, home: ToOne<House>) {
self.friends = friends
self.dogs = dogs
self.home = home
}
}
}
public typealias Person = ExampleEntity<PersonDescription>
public extension Entity where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
public init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
self = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
}
}
public enum DogDescription: EntityDescription {
public static var type: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
public init(name: Attribute<String>) {
self.name = name
}
}
public struct Relationships: JSONAPI.Relationships {
public let owner: ToOne<Person?>
public init(owner: ToOne<Person?>) {
self.owner = owner
}
}
}
public typealias Dog = ExampleEntity<DogDescription>
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
public init(name: String, owner: Person?) throws {
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
}
public init(name: String, owner: Person.Id) throws {
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
}
}
public enum HouseDescription: EntityDescription {
public static var type: String { return "houses" }
public typealias Attributes = NoAttributes
public typealias Relationships = NoRelationships
}
public typealias House = ExampleEntity<HouseDescription>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='6.0' target-platform='macos' executeOnSourceChanges='false'>
<pages>
<page name='Test Library'/>
<page name='Usage'/>
</pages>
</playground>
+1 -9
View File
@@ -1,15 +1,7 @@
{
"object": {
"pins": [
{
"package": "Result",
"repositoryURL": "https://github.com/antitypical/Result.git",
"state": {
"branch": null,
"revision": "8fc088dcf72802801efeecba76ea8fb041fb773d",
"version": "4.0.0"
}
}
]
},
"version": 1
+8 -6
View File
@@ -6,23 +6,25 @@ import PackageDescription
let package = Package(
name: "JSONAPI",
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
.library(
name: "JSONAPI",
targets: ["JSONAPI"]),
.library(
name: "JSONAPITestLib",
targets: ["JSONAPITestLib"])
],
dependencies: [
.package(url: "https://github.com/antitypical/Result.git", from: "4.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "JSONAPI",
dependencies: ["Result"]),
dependencies: []),
.target(
name: "JSONAPITestLib",
dependencies: ["JSONAPI"]),
.testTarget(
name: "JSONAPITests",
dependencies: ["JSONAPI"]),
dependencies: ["JSONAPITestLib"])
],
swiftLanguageVersions: [.v4_2]
)
+224 -103
View File
@@ -1,91 +1,88 @@
# JSONAPI
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 4.2](http://img.shields.io/badge/Swift-4.2-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
A Swift package for encoding and decoding to *JSON API* compliant requests and responses.
A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses.
See the JSON API Spec here: https://jsonapi.org/format/
## Primary Goals
The primary goals of this framework are:
1. Allow creation of Swift types that are easy to use in code but also can be encoded to- or decoded from *JSON API* compliant payloads without lots of boilerplate code.
1. Allow creation of Swift types that are easy to use in code but also can be encoded to- or decoded from **JSON API v1.0 Spec** compliant payloads without lots of boilerplate code.
2. Leverage `Codable` to avoid additional outside dependencies and get operability with non-JSON encoders/decoders for free.
3. Do not sacrifice type safety.
4. Be platform agnostic so that Swift code can be written once and used by both the client and the server.
## Project Status
### Caveat
The big caveat is that, although the aim is to support the JSON API spec, this framework ends up being _naturally_ opinionated about certain things that the API Spec does not specify. These caveats are largely a side effect of attempting to write the library in a "Swifty" way.
### Decoding
#### Document
- [x] `data`
- [x] `included`
- [x] `errors` (untested)
- [ ] `meta`
- [ ] `jsonapi`
- [ ] `links`
If you find something wrong with this library and it isn't already mentioned under **Project Status**, let me know! I want to keep working towards a library implementation that is useful in any application.
#### Resource Object
- [x] `id`
- [x] `type`
- [x] `attributes`
- [x] `relationships`
- [ ] `links`
- [ ] `meta`
#### Relationship Object
- [x] `data`
- [ ] `links`
- [ ] `meta`
### Encoding
#### Document
- [x] `data`
- [x] `included`
- [x] `errors` (untested)
- [ ] `meta`
- [ ] `jsonapi`
- [ ] `links`
#### Resource Object
- [x] `id`
- [x] `type`
- [x] `attributes`
- [x] `relationships`
- [ ] `links`
- [ ] `meta`
#### Relationship Object
- [x] `data`
- [ ] `links`
- [ ] `meta`
### EntityDescription Validator (using reflection)
- [ ] Disallow optional array in `Attribute` and `Relationship` (should be empty array, not `null`).
- [ ] Only allow `Attribute` and `TransformAttribute` within `Attributes` struct.
- [ ] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
### Strict Decoding/Encoding Settings
- [ ] Error (potentially while still encoding/decoding successfully) if an included entity is not related to a primary entity (Turned off by default).
### Misc
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
- [x] Support ability to distinguish between `Attributes` fields that are optional (i.e. the key might not be there) and `Attributes` values that are optional (i.e. the key is guaranteed to be there but it might be `null`).
- [x] Fix `ToOneRelationship` so that it is possible to specify an optional relationship where the value is `null` rather than the key being omitted.
- [ ] Conform to `CustomStringConvertible`
- [ ] More tests around failing to decode improperly structured JSON (not bad JSON, but JSON that is not to spec)
- [ ] Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources????
- [x] For `NoIncludes`, do not even loop over the "included" JSON API section if it exists.
- [ ] Property-based testing (using `SwiftCheck`)
- [ ] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
## Usage
## Dev Environment
### Prerequisites
1. Swift 4.2+ and Swift Package Manager
### `EntityDescription`
### Xcode project
To create an Xcode project for JSONAPI, run
`swift package generate-xcodeproj`
An `EntityDescription` is the `JSONAPI` framework's specification for what the JSON API spec calls a *Resource Object*. You might create the following `EntityDescription` to represent a person in a network of friends:
### Running the Playground
To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace.
Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the Entities.swift Playground Source file) can get things working for me when I am getting an error about `JSONAPI` not being found.
## Project Status
### Encoding/Decoding
#### Document
- [x] `data`
- [x] `included`
- [x] `errors`
- [x] `meta`
- [x] `jsonapi`
- [x] `links`
#### Resource Object
- [x] `id`
- [x] `type`
- [x] `attributes`
- [x] `relationships`
- [x] `links`
- [x] `meta`
#### Relationship Object
- [x] `data`
- [x] `links`
- [x] `meta`
#### Links Object
- [x] `href`
- [x] `meta`
### Misc
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
- [x] Support validation on `Attributes`.
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
### JSONAPITestLib
#### Entity Validator
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded.
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
### Potential Improvements
- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources.
- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies.
- [ ] Property-based testing (using `SwiftCheck`).
- [ ] Error or warning if an included entity is not related to a primary entity or another included entity (Turned off or at least not throwing by default).
## Usage
In this documentation, in order to draw attention to the difference between the `JSONAPI` framework (this Swift library) and the **JSON API Spec** (the specification this library helps you follow), the specification will consistently be referred to below as simply the **SPEC**.
### `JSONAPI.EntityDescription`
An `EntityDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `EntityDescription` to represent a person in a network of friends:
```
enum PersonDescription: IdentifiedEntityDescription {
@@ -102,14 +99,14 @@ enum PersonDescription: IdentifiedEntityDescription {
}
```
To enumerate them, the requirements of an `EntityDescription` are
1. A static `var` "type" that matches the JSON type; The JSON spec requires every *Resource Object* to have a "type".
The requirements of an `EntityDescription` are:
1. A static `var` "type" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type".
2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes`
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelatives`
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships`
Note that an `enum` type is used here for the `EntityDescription`; it could have been a `struct`, but `EntityDescription`s do not ever need to be created so an `enum` with no `case`s is a nice fit for the job.
This readme doesn't go into detail on the JSON API Spec, but the following JSON API *Resource Object* would be described by the above `PersonDescription`:
This readme doesn't go into detail on the **SPEC**, but the following *Resource Object* would be described by the above `PersonDescription`:
```
{
@@ -139,58 +136,76 @@ This readme doesn't go into detail on the JSON API Spec, but the following JSON
}
```
### `Entity`
### `JSONAPI.Entity`
Once you have an `EntityDescription`, you _create_, _encode_, and _decode_ `Entity`s that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `Entity`s that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with entities.
Once you have an `EntityDescription`, you _create_, _encode_, and _decode_ `Entities` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `Entities` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with entities.
The `Entity` and `EntityDescription` together embody the rules and properties of a JSON API *Resource Object*.
The `Entity` and `EntityDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*.
An `Entity` needs to be specialized on two generic types. The first is the `EntityDescription` described above. The second is the type of Id to use for the `Entity`.
An `Entity` needs to be specialized on four generic types. The first is the `EntityDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`.
#### `Meta`
The second generic specialization on `Entity` is `Meta`. This is described in its own section [below](#jsonapimeta). All `Meta` at any level of a JSON API Document follow the same rules.
#### `Links`
The third generic specialization on `Entity` is `Links`. This is described in its own section [below](#jsonnapilinks). All `Links` at any level of a JSON API Document follow the same rules, although the **SPEC** makes different suggestions as to what types of links might live on which parts of the Document.
#### `IdType`
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `EntityDescription` of the type of entity the Id identifies. Having the `EntityDescription` type associated with the Id makes it easy to store all of your entities in a local hash broken out by `EntityDescription`; You can pass Ids around and always know where to look for the `Entity` to which the Id refers. `RawIdType`s are documented below.
The second is the raw type of `Id` to use for the `Entity`. The actual `Id` of the `Entity` will not be a `RawIdType`, though. The `Id` will package a value of `RawIdType` with a specialized reference back to the `Entity` type it identifies. This just looks like `Id<RawIdType, Entity<EntityDescription, RawIdType>>`.
Having the `Entity` type associated with the `Id` makes it easy to store all of your entities in a hash broken out by `Entity` type; You can pass `Ids` around and always know where to look for the `Entity` to which the `Id` refers.
A `RawIdType` is the underlying type that uniquely identifies an `Entity`. This is often a `String` or a `UUID`.
#### `MaybeRawId`
`MaybeRawId` is either a `RawIdType` that can be used to uniquely identify `Entities` or it is `Unidentified` which is used to indicate an `Entity` does not have an `Id` (which is useful when a client is requesting that the server create an `Entity` and assign it a new `Id`).
#### Convenient `typealiases`
Often you can use one `RawIdType` for many if not all of your `Entities`. That means you can save yourself some boilerplate by using `typealias`es like the following:
```
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Id<String, Description>>
public typealias Entity<Description: JSONAPI.EntityDescription, Meta: JSONAPI.Meta, Links: JSONAPI.Links> = JSONAPI.Entity<Description, Meta, Links, String>
public typealias NewEntity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Unidentified>
public typealias NewEntity<Description: JSONAPI.EntityDescription, Meta: JSONAPI.Meta, Links: JSONAPI.Links> = JSONAPI.Entity<Description, Meta, Links, Unidentified>
```
It can also be nice to create a `typealias` for each type of entity you want to work with:
```
typealias Person = Entity<PersonDescription>
typealias Person = Entity<PersonDescription, NoMetadata, NoLinks>
typealias NewPerson = NewEntity<PersonDescription>
typealias NewPerson = NewEntity<PersonDescription, NoMetadata, NoLinks>
```
Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time JSON API spec allows a *Resource Object* to be encoded without an Id is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the Id for that object.
Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object.
### `Relationships`
### `JSONAPI.Relationships`
There are two types of `Relationship`s: `ToOneRelationship` and `ToManyRelationship`. An `EntityDescription`'s `Relationships` type can contain any number of `Relationship`s of either of these types. Do not store anything other than `Relationship`s in the `Relationships` struct of an `EntityDescription`.
There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. An `EntityDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of an `EntityDescription`.
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent no related objects exist with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value might be `null` or it might be a resource identifier) like this:
In addition to identifying entities by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document.
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this:
```
let nullableRelative: ToOneRelationship<Person?>
let nullableRelative: ToOneRelationship<Person?, NoMetadata, NoLinks>
```
An entity that does not have relationships can be described by adding the following to an `EntityDescription`:
```
typealias Relationships = NoRelatives
typealias Relationships = NoRelationships
```
`Relationship`s boil down to Ids of other entities. To access the Id of a related entity, you can use the shorthand `~>` operator with the `KeyPath` of the `Relationship` from which you want the Id. The friends of the above `Person` entity could be accessed as follows (type annotations for clarity):
`Relationship` values boil down to `Ids` of other entities. To access the `Id` of a related `Entity`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `Entity` can be accessed as follows (type annotations for clarity):
```
let friendIds: [Person.Identifier] = person ~> \.friends
```
### `Attributes`
### `JSONAPI.Attributes`
The `Attributes` of an `EntityDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute` or `TransformAttribute` `struct`. This is the place to store all attributes of an entity.
The `Attributes` of an `EntityDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`.
To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional:
```
@@ -214,52 +229,155 @@ let favoriteColor: String = person[\.favoriteColor]
#### `Transformer`
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. To do this, you create a `Transformer`.
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`.
A `Transformer` just provides one static function that transforms one type to another. You might define one for an ISO 8601 compliant `Date` like this:
```
enum ISODateTransformer: Transformer {
public static func transform(_ from: String) throws -> Date {
public static func transform(_ value: String) throws -> Date {
// parse Date out of input and return
}
}
```
Then you define the attribute as a `TransformAttribute` instead of an `Attribute`:
Then you define the attribute as a `TransformedAttribute` instead of an `Attribute`:
```
let date: TransformAttribute<String, ISODateTransformer>
let date: TransformedAttribute<String, ISODateTransformer>
```
Note that the first generic parameter of `TransformAttribute` is the type you expect to decode from JSON, not the type you want to end up with after transformation.
### `JSONAPIDocument`
If you make your `Transformer` a `ReversibleTransformer` then your life will be a bit easier when you construct `TransformedAttributes` because you have access to initializers for both the pre- and post-transformed value types. Continuing with the above example of a `ISODateTransformer`:
```
extension ISODateTransformer: ReversibleTransformer {
public static func reverse(_ value: Date) throws -> String {
// serialize Date to a String
}
}
The entirety of a JSON API request or response is encoded or decoded from- or to a `JSONAPIDocument`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
let exampleAttribute = try? TransformedAttribute<String, ISODateTransformer>(transformedValue: Date())
let otherAttribute = try? TransformedAttribute<String, ISODateTransformer>(rawValue: "2018-12-01 09:06:41 +0000")
```
#### `Validator`
You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid.
#### Computed `Attribute`
You can add computed properties to your `EntityDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties should still have the type `Attribute` because that way you can take advantage of the quick access provided by `Entity`'s subscript operator. Here's an example of how you might take the `Person[\.name]` attribute from the example above and create a `fullName` computed property.
```
public var fullName: Attribute<String> {
return name.map { $0.joined(separator: " ") }
}
```
### Copying `Entities`
`Entity` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying an `Entity`:
1. Assigning a new `Identifier` to the copy of an identified `Entity`.
2. Assigning a new `Identifier` to the copy of an unidentified `Entity`.
The above can be accomplished with code like the following:
```
// use case 1
let person1 = person.withNewIdentifier()
// use case 2
let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self)
let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232")
```
### `JSONAPI.Document`
The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
```
let decoder = JSONDecoder()
let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoIncludes, BasicJSONAPIError>.self
let responseStructure = JSONAPI.Document<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self
let document = try decoder.decode(responseStructure, from: data)
```
A JSON API Document is guaranteed by the **SPEC** to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata."
#### `ResourceBody`
The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `Entity` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `Entity` values in a JSON API document that has a `ManyResourceBody`.
The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody<PrimaryResource>` or a `ManyResourceBody<PrimaryResource>`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `Entity`. These `Poly` types work in the same way as the `Include` types described below.
#### `IncludeDecoder`
If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead.
The second generic type of a `JSONAPIDocument` is an `IncludeDecoder`. This type controls which types of `Entity` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included entities to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `IncludeDecoder`s for up to six types of included entities. These are named `Include1`, `Include2`, `Include3`, and so on.
##### nullable `PrimaryResource`
If you expect a `SingleResourceBody` to sometimes come back `null`, you should make your `PrimaryResource` optional. If you do not make your `PrimaryResource` optional then a `null` primary resource will be considered an error when parsing the JSON.
You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody` because the **SPEC** requires that an empty document in that case be represented by an empty array rather than `null`.
#### `MetaType`
The second generic type of a `JSONAPIDocument` is a `Meta`. This `Meta` follows the same rules as `Meta` at any other part of a JSON API Document. It is described below in its own section, but as an example, the JSON API document could contain the following pagination info in its meta entry:
```
{
"meta": {
"total": 100,
"limit": 50,
"offset": 50
}
}
```
You would then create the following `Meta` type:
```
struct PageMetadata: JSONAPI.Meta {
let total: Int
let limit: Int
let offset: Int
}
```
You can always use `NoMetadata` if this JSON API feature is not needed.
#### `LinksType`
The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are described in their own section [below](#jsonapilinks).
#### `IncludeType`
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `Entity` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included entities to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to six types of included entities. These are named `Include1`, `Include2`, `Include3`, and so on.
**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included entities, it indicates a number of _types_ of included entities. `Include1` can be used to decode any number of included entities as long as all the entities are of the same _type_.
To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1<Person>` instead of `NoIncludes`.
#### `APIDescriptionType`
The fifth generic type of a `JSONAPIDocument` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API.
You can specify this is not part of the document by using the `NoAPIDescription` type.
You can describe the API by a version with no metadata by using `APIDescription<NoMetadata>`.
You can supply any `JSONAPI.Meta` type as the metadata type of the API description.
#### `Error`
The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the JSON API Spec, these errors will be found in the root document member `errors`.
The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the **SPEC**, these errors will be found in the root document member `errors`.
### `RawIdType`
### `JSONAPI.Meta`
A `Meta` struct is totally open-ended. It is described by the **SPEC** as a place to put any information that does not fit into the standard JSON API Document structure anywhere else.
You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`.
### `JSONAPI.Links`
A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included.
You can specify `NoLinks` if the part of the document being described should not contain any `Links`.
### `JSONAPI.RawIdType`
If you want to create new `JSONAPI.Entity` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`):
```
@@ -275,3 +393,6 @@ extension String: CreatableRawIdType {
}
}
```
# JSONAPITestLib
The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITestLib` in action in the Playground included with the `JSONAPI` repository.
@@ -0,0 +1,57 @@
//
// 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 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)
}
}
}
+263 -19
View File
@@ -5,6 +5,19 @@
// Created by Mathew Polzin on 11/5/18.
//
public protocol JSONAPIDocument: Codable, Equatable {
associatedtype PrimaryResourceBody: JSONAPI.ResourceBody
associatedtype MetaType: JSONAPI.Meta
associatedtype LinksType: JSONAPI.Links
associatedtype IncludeType: JSONAPI.Include
associatedtype APIDescription: APIDescriptionType
associatedtype Error: JSONAPIError
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
var body: Body { get }
}
/// A JSON API Document represents the entire body
/// of a JSON API request or the entire body of
/// a JSON API response.
@@ -12,29 +25,171 @@
/// API uses snake case, you will want to use
/// a conversion such as the one offerred by the
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError>: Equatable {
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: JSONAPIDocument {
public typealias Include = IncludeType
/// The JSON API Spec calls this the JSON:API Object. It contains version
/// and metadata information about the API itself.
public let apiDescription: APIDescription
/// The Body of the Document. This body is either one or more errors
/// with links and metadata attempted to parse but not guaranteed or
/// it is a successful data struct containing all the primary and
/// included resources, the metadata, and the links that this
/// document type specifies.
public let body: Body
// public let meta: Meta?
// public let jsonApi: APIDescription?
// public let links: Links?
public enum Body: Equatable {
case errors([Error])
case data(primary: ResourceBody, included: Includes<Include>)
case errors([Error], meta: MetaType?, links: LinksType?)
case data(Data)
public struct Data: Equatable {
public let primary: PrimaryResourceBody
public let includes: Includes<Include>
public let meta: MetaType
public let links: LinksType
public init(primary: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.primary = primary
self.includes = includes
self.meta = meta
self.links = links
}
}
public var isError: Bool {
guard case .errors = self else { return false }
return true
}
public var data: (primary: ResourceBody, included: Includes<Include>)? {
guard case let .data(primary: body, included: includes) = self else { return nil }
return (primary: body, included: includes)
public var errors: [Error]? {
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
return errors
}
public var data: Data? {
guard case let .data(data) = self else { return nil }
return data
}
public var primaryResource: PrimaryResourceBody? {
guard case let .data(data) = self else { return nil }
return data.primary
}
public var includes: Includes<Include>? {
guard case let .data(data) = self else { return nil }
return data.includes
}
public var meta: MetaType? {
switch self {
case .data(let data):
return data.meta
case .errors(_, meta: let metadata?, links: _):
return metadata
default:
return nil
}
}
public var links: LinksType? {
switch self {
case .data(let data):
return data.links
case .errors(_, meta: _, links: let links?):
return links
default:
return nil
}
}
}
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
body = .errors(errors, meta: meta, links: links)
self.apiDescription = apiDescription
}
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
self.apiDescription = apiDescription
}
}
/*
extension Document where IncludeType == NoIncludes {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, includes: .none, meta: meta, links: links)
}
}
extension JSONAPIDocument: Codable {
extension Document where MetaType == NoMetadata {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: .none, links: links)
}
}
extension Document where LinksType == NoLinks {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none)
}
}
extension Document where APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links)
}
}
extension Document where IncludeType == NoIncludes, LinksType == NoLinks {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType) {
self.init(apiDescription: apiDescription, body: body, meta: meta, links: .none)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, meta: .none, links: links)
}
}
extension Document where IncludeType == NoIncludes, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
self.init(apiDescription: .none, body: body, meta: meta, links: links)
}
}
extension Document where MetaType == NoMetadata, LinksType == NoLinks {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>) {
self.init(apiDescription: apiDescription, body: body, includes: includes, links: .none)
}
}
extension Document where MetaType == NoMetadata, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
self.init(apiDescription: .none, body: body, includes: includes, links: links)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
public init(apiDescription: APIDescription, body: PrimaryResourceBody) {
self.init(apiDescription: apiDescription, body: body, includes: .none)
}
}
extension Document where MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
self.init(apiDescription: .none, body: body, includes: includes)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody) {
self.init(apiDescription: .none, body: body)
}
}
*/
extension Document {
private enum RootCodingKeys: String, CodingKey {
case data
case errors
@@ -47,38 +202,127 @@ extension JSONAPIDocument: Codable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootCodingKeys.self)
if let noData = NoAPIDescription() as? APIDescription {
apiDescription = noData
} else {
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
}
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
let meta: MetaType?
if let noMeta = NoMetadata() as? MetaType {
meta = noMeta
} else {
do {
meta = try container.decode(MetaType.self, forKey: .meta)
} catch {
meta = nil
}
}
let links: LinksType?
if let noLinks = NoLinks() as? LinksType {
links = noLinks
} else {
do {
links = try container.decode(LinksType.self, forKey: .links)
} catch {
links = nil
}
}
// If there are errors, there cannot be a body. Return errors and any metadata found.
if let errors = errors {
body = .errors(errors)
body = .errors(errors, meta: meta, links: links)
return
}
let data = try container.decode(ResourceBody.self, forKey: .data)
let data: PrimaryResourceBody
if let noData = NoResourceBody() as? PrimaryResourceBody {
data = noData
} else {
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
}
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
// TODO come back to this and make robust
guard let metaVal = meta else {
throw JSONAPIEncodingError.missingOrMalformedMetadata
}
guard let linksVal = links else {
throw JSONAPIEncodingError.missingOrMalformedLinks
}
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none)
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: RootCodingKeys.self)
switch body {
case .errors(let errors):
case .errors(let errors, meta: let meta, links: let links):
var errContainer = container.nestedUnkeyedContainer(forKey: .errors)
for error in errors {
try errContainer.encode(error)
}
case .data(primary: let resourceBody, included: let includes):
try container.encode(resourceBody, forKey: .data)
if MetaType.self != NoMetadata.self,
let metaVal = meta {
try container.encode(metaVal, forKey: .meta)
}
if LinksType.self != NoLinks.self,
let linksVal = links {
try container.encode(linksVal, forKey: .links)
}
case .data(let data):
try container.encode(data.primary, forKey: .data)
if Include.self != NoIncludes.self {
try container.encode(includes, forKey: .included)
try container.encode(data.includes, forKey: .included)
}
if MetaType.self != NoMetadata.self {
try container.encode(data.meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(data.links, forKey: .links)
}
}
if APIDescription.self != NoAPIDescription.self {
try container.encode(apiDescription, forKey: .jsonapi)
}
}
}
// MARK: - CustomStringConvertible
extension Document: CustomStringConvertible {
public var description: String {
return "Document(\(String(describing: body)))"
}
}
extension Document.Body: CustomStringConvertible {
public var description: String {
switch self {
case .errors(let errors, meta: let meta, links: let links):
return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))"
case .data(let data):
return String(describing: data)
}
}
}
extension Document.Body.Data: CustomStringConvertible {
public var description: String {
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
}
}
+2 -2
View File
@@ -9,7 +9,7 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
static var unknown: Self { get }
}
public enum BasicJSONAPIError: JSONAPIError {
public enum UnknownJSONAPIError: JSONAPIError {
case unknownError
public init(from decoder: Decoder) throws {
@@ -21,7 +21,7 @@ public enum BasicJSONAPIError: JSONAPIError {
try container.encode("unknown")
}
public static var unknown: BasicJSONAPIError {
public static var unknown: UnknownJSONAPIError {
return .unknownError
}
}
+34 -393
View File
@@ -5,16 +5,14 @@
// Created by Mathew Polzin on 11/10/18.
//
import Result
public typealias Include = Poly
public protocol IncludeDecoder: Codable, Equatable {}
public struct Includes<I: IncludeDecoder>: Codable, Equatable {
public struct Includes<I: Include>: Codable, Equatable {
public static var none: Includes { return .init(values: []) }
let values: [I]
private init(values: [I]) {
public init(values: [I]) {
self.values = values
}
@@ -52,433 +50,76 @@ public struct Includes<I: IncludeDecoder>: Codable, Equatable {
}
}
// MARK: - Decoding
extension Includes: CustomStringConvertible {
public var description: String {
return "Includes(\(String(describing: values))"
}
}
func decode<Entity: JSONAPI.EntityType>(_ type: Entity.Type, from container: SingleValueDecodingContainer) throws -> Result<Entity, EncodingError> {
let ret: Result<Entity, EncodingError>
do {
ret = try .success(container.decode(Entity.self))
} catch (let err as EncodingError) {
ret = .failure(err)
} catch (let err) {
ret = .failure(EncodingError.invalidValue(Entity.Description.self,
.init(codingPath: container.codingPath,
debugDescription: err.localizedDescription,
underlyingError: err)))
extension Includes where I == NoIncludes {
public init() {
values = []
}
return ret
}
// MARK: - 0 includes
public protocol _Include0: IncludeDecoder { }
public struct Include0: _Include0 {
public init(from decoder: Decoder) throws {
}
public func encode(to encoder: Encoder) throws {
throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Include0, which should be represented by the absence of an 'included' entry altogether.")
}
}
public typealias Include0 = Poly0
public typealias NoIncludes = Include0
// MARK: - 1 include
public protocol _Include1: _Include0 {
associatedtype A: EntityType
var a: A? { get }
}
public enum Include1<A: EntityType>: _Include1 {
case a(A)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = .a(try container.decode(A.self))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
}
}
}
extension Includes where I: _Include1 {
public typealias Include1 = Poly1
extension Includes where I: _Poly1 {
public subscript(_ lookup: I.A.Type) -> [I.A] {
return values.compactMap { $0.a }
}
}
// MARK: - 2 includes
public protocol _Include2: _Include1 {
associatedtype B: EntityType
var b: B? { get }
}
public enum Include2<A: EntityType, B: EntityType>: _Include2 {
case a(A)
case b(B)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include2.a($0) },
try decode(B.self, from: container).map { Include2.b($0) } ]
let maybeVal: Include2<A, B>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include2<A, B>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
}
}
}
extension Includes where I: _Include2 {
public typealias Include2 = Poly2
extension Includes where I: _Poly2 {
public subscript(_ lookup: I.B.Type) -> [I.B] {
return values.compactMap { $0.b }
}
}
// MARK: - 3 includes
public protocol _Include3: _Include2 {
associatedtype C: EntityType
var c: C? { get }
}
public enum Include3<A: EntityType, B: EntityType, C: EntityType>: _Include3 {
case a(A)
case b(B)
case c(C)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include3.a($0) },
try decode(B.self, from: container).map { Include3.b($0) },
try decode(C.self, from: container).map { Include3.c($0) }]
let maybeVal: Include3<A, B, C>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include3<A, B, C>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
}
}
}
extension Includes where I: _Include3 {
public typealias Include3 = Poly3
extension Includes where I: _Poly3 {
public subscript(_ lookup: I.C.Type) -> [I.C] {
return values.compactMap { $0.c }
}
}
// MARK: - 4 includes
public protocol _Include4: _Include3 {
associatedtype D: EntityType
var d: D? { get }
}
public enum Include4<A: EntityType, B: EntityType, C: EntityType, D: EntityType>: _Include4 {
case a(A)
case b(B)
case c(C)
case d(D)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: D? {
guard case let .d(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include4.a($0) },
try decode(B.self, from: container).map { Include4.b($0) },
try decode(C.self, from: container).map { Include4.c($0) },
try decode(D.self, from: container).map { Include4.d($0) }]
let maybeVal: Include4<A, B, C, D>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include4<A, B, C, D>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
case .d(let d):
try container.encode(d)
}
}
}
extension Includes where I: _Include4 {
public typealias Include4 = Poly4
extension Includes where I: _Poly4 {
public subscript(_ lookup: I.D.Type) -> [I.D] {
return values.compactMap { $0.d }
}
}
// MARK: - 5 includes
public protocol _Include5: _Include4 {
associatedtype E: EntityType
var e: E? { get }
}
public enum Include5<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType>: _Include5 {
case a(A)
case b(B)
case c(C)
case d(D)
case e(E)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: D? {
guard case let .d(ret) = self else { return nil }
return ret
}
public var e: E? {
guard case let .e(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include5.a($0) },
try decode(B.self, from: container).map { Include5.b($0) },
try decode(C.self, from: container).map { Include5.c($0) },
try decode(D.self, from: container).map { Include5.d($0) },
try decode(E.self, from: container).map { Include5.e($0) }]
let maybeVal: Include5<A, B, C, D, E>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include5<A, B, C, D, E>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
case .d(let d):
try container.encode(d)
case .e(let e):
try container.encode(e)
}
}
}
extension Includes where I: _Include5 {
public typealias Include5 = Poly5
extension Includes where I: _Poly5 {
public subscript(_ lookup: I.E.Type) -> [I.E] {
return values.compactMap { $0.e }
}
}
// MARK: - 6 includes
public protocol _Include6: _Include5 {
associatedtype F: EntityType
var f: F? { get }
}
public enum Include6<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType>: _Include6 {
case a(A)
case b(B)
case c(C)
case d(D)
case e(E)
case f(F)
public var a: A? {
guard case let .a(ret) = self else { return nil }
return ret
}
public var b: B? {
guard case let .b(ret) = self else { return nil }
return ret
}
public var c: C? {
guard case let .c(ret) = self else { return nil }
return ret
}
public var d: D? {
guard case let .d(ret) = self else { return nil }
return ret
}
public var e: E? {
guard case let .e(ret) = self else { return nil }
return ret
}
public var f: F? {
guard case let .f(ret) = self else { return nil }
return ret
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let attempts = [
try decode(A.self, from: container).map { Include6.a($0) },
try decode(B.self, from: container).map { Include6.b($0) },
try decode(C.self, from: container).map { Include6.c($0) },
try decode(D.self, from: container).map { Include6.d($0) },
try decode(E.self, from: container).map { Include6.e($0) },
try decode(F.self, from: container).map { Include6.f($0) }]
let maybeVal: Include6<A, B, C, D, E, F>? = attempts
.compactMap { $0.value }
.first
guard let val = maybeVal else {
throw EncodingError.invalidValue(Include6<A, B, C, D, E, F>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
}
self = val
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a(let a):
try container.encode(a)
case .b(let b):
try container.encode(b)
case .c(let c):
try container.encode(c)
case .d(let d):
try container.encode(d)
case .e(let e):
try container.encode(e)
case .f(let f):
try container.encode(f)
}
}
}
extension Includes where I: _Include6 {
public typealias Include6 = Poly6
extension Includes where I: _Poly6 {
public subscript(_ lookup: I.F.Type) -> [I.F] {
return values.compactMap { $0.f }
}
}
// 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
+49 -8
View File
@@ -1,28 +1,55 @@
//
// ResourceBody.swift
// PrimaryResourceBody.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
//
public protocol MaybePrimaryResource: Equatable, Codable {}
/// A PrimaryResource is a type that can be used in the body of a JSON API
/// document as the primary resource.
public protocol PrimaryResource: MaybePrimaryResource {}
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
/// A ResourceBody is a representation of the body of the JSON API Document.
/// It can either be one resource (which can be specified as optional or not)
/// or it can contain many resources (and array with zero or more entries).
public protocol ResourceBody: Codable, Equatable {
}
public struct SingleResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
public let value: Entity?
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
public let value: Entity
public init(entity: Entity) {
self.value = entity
}
}
public struct ManyResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: ResourceBody {
public let values: [Entity]
public init(entities: [Entity]) {
values = entities
}
}
// MARK: Decodable
/// Use NoResourceBody to indicate you expect a JSON API document to not
/// contain a "data" top-level key.
public struct NoResourceBody: ResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: Codable
extension SingleResourceBody {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if container.decodeNil() {
value = nil
let anyNil: Any? = nil
if container.decodeNil(),
let val = anyNil as? Entity {
value = val
return
}
@@ -32,7 +59,7 @@ extension SingleResourceBody {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if value == nil {
if (value as Any?) == nil {
try container.encodeNil()
return
}
@@ -59,3 +86,17 @@ extension ManyResourceBody {
}
}
}
// MARK: CustomStringConvertible
extension SingleResourceBody: CustomStringConvertible {
public var description: String {
return "PrimaryResourceBody(\(String(describing: value)))"
}
}
extension ManyResourceBody: CustomStringConvertible {
public var description: String {
return "PrimaryResourceBody(\(String(describing: values)))"
}
}
+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
}
@@ -0,0 +1,76 @@
//
// ParsingInvariant.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/24/18.
//
/// An Error produced when an invariant fails.
public enum InvariantError: Swift.Error {
case failure(description: String)
}
/// Set a reaction to a particular type of invariant failure.
/// `Warning` and `Quiet` will take a default strategy for handling
/// the invariant and parsing will not fail. `Error` will cause parsing
/// to fail due to the given invariant failing.
public protocol IReaction {
/// Perform the given Invariant strategy.
static func trigger(invariant: InvariantType.Type) throws
}
public enum Invariant {
/// Used to indicate an invariant failure shoud be considered an error.
/// Parsing will fail if an Error invariant fails.
public enum Error: IReaction {
public static func trigger(invariant: InvariantType.Type) throws {
throw InvariantError.failure(description: String(describing: invariant))
}
}
/// Used to indicate an invariant failure should be considered a warning.
/// The invariant description will be printed to the console but parsing
/// will not fail. A default strategy for addressing the invariant will
/// be taken. See the given invariant's documentation for details.
public enum Warning: IReaction {
public static func trigger(invariant: InvariantType.Type) throws {
print("**WARN** \(String(describing: invariant))")
}
}
/// Used to indicate an invariant failures should be considered inconsequential.
/// Parsing will continue and a default strategy for addressing the invariant
/// will be taken. See the given invariant's documentation for details.
public enum Quiet: IReaction {
public static func trigger(invariant: InvariantType.Type) throws {}
}
/// A strategy pairs an invariant type with the desired reaction.
public enum Strategy<IType: InvariantType, IReaction: JSONAPI.IReaction>: IStrategy {}
}
public protocol InvariantType {}
public protocol IStrategy {
associatedtype IType: InvariantType
associatedtype IReaction: JSONAPI.IReaction
}
// TODO: Does this invariant make sense? I can't actually know whether arbitrary
// Id types are "empty" so I think I started down this path without having fully
// thought through it. Perhaps there are still other obvious invariants to add
// though.
public protocol RelationshipNonEmptyIdInvariant: IStrategy where IType == Invariant.Relationships.NonEmptyId {}
extension Invariant {
public enum Relationships {
/// The Empty Id Invariant holds as long as a relationships's Id
/// is not "empty"
public enum NonEmptyId: InvariantType {}
public enum Strategies<NonEmptyId: RelationshipNonEmptyIdInvariant> {}
}
}
// Would be used like `ToOneRelationship<OtherEntity, Invariant.Relationships.Strategies<Invariant.Strategy<Invariant.Relationships.NonEmptyId, Invariant.Error>>>`
+67
View File
@@ -0,0 +1,67 @@
//
// Links.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/24/18.
//
/// A Links structure should contain nothing but JSONAPI.Link properties.
public protocol Links: Codable, Equatable {}
/// Use NoLinks where no links should belong to a JSON API component
public struct NoLinks: Links, CustomStringConvertible {
public static var none: NoLinks { return NoLinks() }
public init() {}
public var description: String { return "No Links" }
}
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 {
private enum CodingKeys: String, CodingKey {
case href
case meta
}
init(from decoder: Decoder) throws {
guard Meta.self == NoMetadata.self,
let noMeta = NoMetadata() as? Meta else {
let container = try decoder.container(keyedBy: CodingKeys.self)
meta = try container.decode(Meta.self, forKey: .meta)
url = try container.decode(URL.self, forKey: .href)
return
}
let container = try decoder.singleValueContainer()
url = try container.decode(URL.self)
meta = noMeta
}
func encode(to encoder: Encoder) throws {
guard Meta.self == NoMetadata.self else {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(url, forKey: .href)
try container.encode(meta, forKey: .meta)
return
}
var container = encoder.singleValueContainer()
try container.encode(url)
}
}
+28
View File
@@ -0,0 +1,28 @@
//
// Meta.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/21/18.
//
/// Conform a type to this protocol to indicate it can be encoded to or decoded from
/// the meta data attached to a component of a JSON API document. Different meta data
/// can be stored all over the place: On the root document, on a resource object, on
/// link objects, etc.
///
/// JSON API Metadata is totally open ended. It can take whatever JSON-compliant structure
/// the server and client agree upon.
public protocol Meta: Codable, Equatable {
}
// We make Optional a Meta if it wraps a Meta so that Metadata can be specified as
// nullable.
extension Optional: Meta where Wrapped: Meta {}
public struct NoMetadata: Meta, CustomStringConvertible {
public static var none: NoMetadata { return NoMetadata() }
public init() { }
public var description: String { return "No Metadata" }
}
@@ -0,0 +1,21 @@
//
// Attribute+Functor.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/28/18.
//
public extension TransformedAttribute {
/// 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: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
return Attribute<T>(value: try transform(value))
}
}
+40 -27
View File
@@ -5,8 +5,13 @@
// Created by Mathew Polzin on 11/13/18.
//
public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
private let rawValue: RawValue
public protocol AttributeType: Codable {
}
// MARK: TransformedAttribute
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
let rawValue: RawValue
public let value: Transformer.To
@@ -16,12 +21,41 @@ public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transfo
}
}
public typealias Attribute<T: Codable> = TransformAttribute<T, IdentityTransformer<T>>
// 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
extension TransformAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
// 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: ReversibleTransformer {
public init(transformedValue: Transformer.To) throws {
self.value = transformedValue
rawValue = try Transformer.reverse(value)
}
}
extension TransformedAttribute: CustomStringConvertible {
public var description: String {
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
}
}
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.
public init(value: RawValue) {
rawValue = value
self.value = value
}
}
// MARK: - Codable
extension TransformAttribute {
extension TransformedAttribute {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
@@ -46,28 +80,7 @@ extension TransformAttribute {
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)
}
}
// MARK: - Transformers
public protocol Transformer {
associatedtype From
associatedtype To
static func transform(_ from: From) throws -> To
}
public enum IdentityTransformer<T>: Transformer {
public static func transform(_ from: T) throws -> T { return from }
}

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