Compare commits

..

88 Commits

Author SHA1 Message Date
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
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
61 changed files with 5553 additions and 619 deletions
-41
View File
@@ -1,41 +0,0 @@
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 = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
let singleDogDocument = SingleDogDocument(body: SingleResourceBody(entity: dogFromCode))
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.data?.primary.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(), House()]
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 = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes))
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.data?.primary.values
let dogsFromData = peopleResponse.body.data?.included[Dog.self]
let housesFromData = 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,70 @@
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.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(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.primaryResource?.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)
+68 -15
View File
@@ -23,8 +23,10 @@ extension String: CreatableRawIdType {
}
}
// MARK: - Entity typealias for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
// 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 {
@@ -34,20 +36,35 @@ public enum PersonDescription: EntityDescription {
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: 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: 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, Identifier == Id<String, PersonDescription> {
public init(id: Person.Identifier? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
self = try Person(id: id ?? Person.Identifier(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
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)
}
}
@@ -57,22 +74,58 @@ public enum DogDescription: EntityDescription {
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: ToOneRelationship<Person?>
public let owner: ToOne<Person?>
public init(owner: ToOne<Person?>) {
self.owner = owner
}
}
}
public typealias Dog = ExampleEntity<DogDescription>
public extension Entity where Description == DogDescription, Identifier == Id<String, DogDescription> {
public init(name: String, owner: Person?) throws {
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
public enum AlternativeDogDescription: EntityDescription {
public static var type: String { return "dogs" }
public struct Attributes: JSONAPI.Attributes {
public let name: Attribute<String>
public init(name: Attribute<String>) {
self.name = name
}
}
public init(name: String, owner: Person.Identifier) throws {
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
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)), 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)
}
}
@@ -81,7 +134,7 @@ public enum HouseDescription: EntityDescription {
public static var type: String { return "houses" }
public typealias Attributes = NoAttributes
public typealias Relationships = NoRelatives
public typealias Relationships = NoRelationships
}
public typealias House = ExampleEntity<HouseDescription>
+5 -2
View File
@@ -1,4 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<playground version='5.0' target-platform='macos' executeOnSourceChanges='false'>
<timeline fileName='timeline.xctimeline'/>
<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/mattpolzin/Result",
"state": {
"branch": "master",
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
"version": null
}
}
]
},
"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/mattpolzin/Result", .branch("master"))
],
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]
)
+265 -115
View File
File diff suppressed because it is too large Load Diff
@@ -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)
}
}
}
+150 -48
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,15 +25,37 @@
/// 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, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, 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 jsonApi: APIDescription?
public enum Body: Equatable {
case errors([Error], meta: MetaType?, links: LinksType?)
case data(primary: ResourceBody, included: Includes<Include>, 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 }
@@ -31,21 +66,27 @@ public struct JSONAPIDocument<ResourceBody: 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: ResourceBody? {
guard case let .data(primary: body, included: _, meta: _, links: _) = self else { return nil }
return body
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(primary: _, included: includes, meta: _, links: _) = self else { return nil }
return includes
guard case let .data(data) = self else { return nil }
return data.includes
}
public var meta: MetaType? {
switch self {
case .data(primary: _, included: _, meta: let metadata, links: _),
.errors(_, meta: let metadata?, links: _):
case .data(let data):
return data.meta
case .errors(_, meta: let metadata?, links: _):
return metadata
default:
return nil
@@ -54,8 +95,9 @@ public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, MetaType: JSON
public var links: LinksType? {
switch self {
case .data(primary: _, included: _, meta: _, links: let links),
.errors(_, meta: _, links: let links?):
case .data(let data):
return data.links
case .errors(_, meta: _, links: let links?):
return links
default:
return nil
@@ -63,58 +105,91 @@ public struct JSONAPIDocument<ResourceBody: 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: ResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.body = .data(primary: body, included: includes, meta: meta, links: links)
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 where IncludeType == NoIncludes {
public init(body: ResourceBody, meta: MetaType, links: LinksType) {
self.body = .data(primary: body, included: .none, meta: meta, links: links)
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 JSONAPIDocument where MetaType == NoMetadata {
public init(body: ResourceBody, includes: Includes<Include>, links: LinksType) {
self.body = .data(primary: body, included: 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 JSONAPIDocument where LinksType == NoLinks {
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType) {
self.body = .data(primary: body, included: 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 JSONAPIDocument where IncludeType == NoIncludes, LinksType == NoLinks {
public init(body: ResourceBody, meta: MetaType) {
self.body = .data(primary: body, included: .none, meta: meta, links: .none)
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 JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata {
public init(body: ResourceBody, links: LinksType) {
self.body = .data(primary: body, included: .none, meta: .none, links: links)
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 JSONAPIDocument where MetaType == NoMetadata, LinksType == NoLinks {
public init(body: ResourceBody, includes: Includes<Include>) {
self.body = .data(primary: body, included: includes, meta: .none, links: .none)
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 JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
public init(body: ResourceBody) {
self.body = .data(primary: body, included: .none, meta: .none, links: .none)
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 JSONAPIDocument: Codable {
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
@@ -127,6 +202,12 @@ 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?
@@ -157,11 +238,11 @@ extension JSONAPIDocument: Codable {
return
}
let data: ResourceBody
if let noData = NoResourceBody() as? ResourceBody {
let data: PrimaryResourceBody
if let noData = NoResourceBody() as? PrimaryResourceBody {
data = noData
} else {
data = try container.decode(ResourceBody.self, forKey: .data)
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
}
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
@@ -175,7 +256,7 @@ extension JSONAPIDocument: Codable {
throw JSONAPIEncodingError.missingOrMalformedLinks
}
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal)
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
}
public func encode(to encoder: Encoder) throws {
@@ -199,28 +280,49 @@ extension JSONAPIDocument: Codable {
try container.encode(linksVal, forKey: .links)
}
case .data(primary: let resourceBody, included: let includes, let meta, links: let links):
try container.encode(resourceBody, forKey: .data)
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(meta, forKey: .meta)
try container.encode(data.meta, forKey: .meta)
}
if LinksType.self != NoLinks.self {
try container.encode(links, forKey: .links)
try container.encode(data.links, forKey: .links)
}
}
if APIDescription.self != NoAPIDescription.self {
try container.encode(apiDescription, forKey: .jsonapi)
}
}
}
// MARK: - CustomStringConvertible
extension JSONAPIDocument: CustomStringConvertible {
extension Document: CustomStringConvertible {
public var description: String {
return "JSONAPIDocument(body: \(String(describing: body))"
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
}
}
+7
View File
@@ -115,4 +115,11 @@ 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
+23 -12
View File
@@ -1,24 +1,33 @@
//
// ResourceBody.swift
// PrimaryResourceBody.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
//
public protocol PrimaryResource: Equatable, Codable {}
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.PrimaryResource>: ResourceBody {
public let value: Entity?
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
public let value: Entity
public init(entity: 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]) {
@@ -32,13 +41,15 @@ public struct NoResourceBody: ResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: Decodable
// 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
}
@@ -48,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
}
@@ -80,12 +91,12 @@ extension ManyResourceBody {
extension SingleResourceBody: CustomStringConvertible {
public var description: String {
return "ResourceBody(\(String(describing: value)))"
return "PrimaryResourceBody(\(String(describing: value)))"
}
}
extension ManyResourceBody: CustomStringConvertible {
public var description: String {
return "ResourceBody(\(String(describing: values)))"
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
}
+16 -3
View File
@@ -9,16 +9,29 @@
public protocol Links: Codable, Equatable {}
/// Use NoLinks where no links should belong to a JSON API component
public struct NoLinks: Links {
public struct NoLinks: Links, CustomStringConvertible {
public static var none: NoLinks { return NoLinks() }
public init() {}
public var description: String { return "No Links" }
}
public protocol URL: Codable, Equatable {}
public protocol JSONAPIURL: Codable, Equatable {}
public struct Link<URL: JSONAPI.URL, Meta: JSONAPI.Meta>: Equatable, Codable {
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 {
+3 -1
View File
@@ -19,8 +19,10 @@ public protocol Meta: Codable, Equatable {
// nullable.
extension Optional: Meta where Wrapped: Meta {}
public struct NoMetadata: 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 -14
View File
@@ -5,8 +5,13 @@
// Created by Mathew Polzin on 11/13/18.
//
public struct TransformedAttribute<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,6 +21,22 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
}
}
// MARK: ValidatedAttribute
/// A ValidatedAttribute does not transform its raw value, but it throws
/// an error if the raw value does not match expectations.
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
// MARK: Attribute
/// An Attribute simply represents a type that can be encoded and decoded.
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
extension TransformedAttribute where Transformer: 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)))"
@@ -24,9 +45,14 @@ extension TransformedAttribute: CustomStringConvertible {
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
// If we are using the identity transform, we can skip the transform and guarantee no
// error is thrown.
public init(value: RawValue) {
rawValue = value
self.value = value
}
}
// MARK: - Codable
extension TransformedAttribute {
@@ -54,15 +80,15 @@ 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)
}
}
// 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
+48 -13
View File
@@ -5,10 +5,16 @@
// Created by Mathew Polzin on 7/24/18.
//
/// All types that are RawIdType and additionally
/// Unidentified conform to this protocol. You
/// should not add conformance to this protocol
/// directly.
public protocol MaybeRawId: Codable, Equatable {}
/// Any type that you would like to be encoded to and
/// decoded from JSON API ids should conform to this
/// protocol. Conformance for `String` is given.
public protocol RawIdType: Codable, Equatable {}
public protocol RawIdType: MaybeRawId, Hashable {}
/// If you would like to be able to create new
/// Entities with Ids backed by a RawIdType then
@@ -22,19 +28,37 @@ public protocol CreatableRawIdType: RawIdType {
extension String: RawIdType {}
public protocol Identifier: Codable, Equatable {}
public struct Unidentified: Identifier, CustomStringConvertible {
public struct Unidentified: MaybeRawId, CustomStringConvertible {
public init() {}
public var description: String { return "Id(Unidentified)" }
public var description: String { return "Unidentified" }
}
public protocol IdType: Identifier, CustomStringConvertible {
associatedtype EntityDescription: JSONAPI.EntityDescription
associatedtype RawType: RawIdType
public protocol OptionalId: Codable {
associatedtype IdentifiableType: JSONAPI.JSONTyped
associatedtype RawType: MaybeRawId
var rawValue: RawType { get }
init(rawValue: RawType)
}
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 {
@@ -47,27 +71,38 @@ public protocol CreatableIdType: IdType {
/// An Entity ID. These IDs can be encoded to or decoded from
/// JSON API IDs.
public struct Id<RawType: RawIdType, EntityDescription: JSONAPI.EntityDescription>: IdType {
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
public let rawValue: RawType
public init(rawValue: RawType) {
self.rawValue = rawValue
}
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 {
var container = encoder.singleValueContainer()
try container.encode(rawValue)
}
}
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() {
rawValue = .unique()
}
}
extension Id where RawType == Unidentified {
public static var unidentified: Id { return .init(rawValue: Unidentified()) }
}

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