mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6aeb859c24 | |||
| 163ac94c51 | |||
| e67b9fc142 | |||
| a628992fcb | |||
| cf47f88a61 | |||
| 0425e2adcb | |||
| d3763ba713 | |||
| fcc1796731 | |||
| 921bcef05d | |||
| 661ff6eca5 | |||
| e36180c9b9 | |||
| abee0c4d0e | |||
| 9c8b2fbebb | |||
| a9ef71f383 | |||
| 232554ec51 | |||
| 8f36bb40e3 | |||
| 55f2f52676 | |||
| dd5f3b1737 | |||
| 7d7b3d7f19 | |||
| aedd5dc29b | |||
| 3964202ea2 | |||
| dcabafd583 | |||
| 9df9efc2dc | |||
| a55a4cbed0 | |||
| 6f6ed87b4c | |||
| 59ff145aa8 | |||
| 20a685e67b | |||
| 3d8d10584b | |||
| b93580c900 | |||
| 8927938d56 | |||
| 372e2de721 | |||
| 1a2ba17f02 | |||
| 9f2c7aa2e4 | |||
| 650937dbb9 | |||
| b08cbdb0ae | |||
| c30a2615f2 | |||
| 86e87ebf5d | |||
| 987bf2b750 | |||
| af1aca9cf4 | |||
| b97649fab6 | |||
| 8114132189 | |||
| b61a41c99b | |||
| 16b83ddbef | |||
| 6dff02bd51 | |||
| 2563edf419 | |||
| 98bb64268f | |||
| 399644b62a | |||
| 3b1e5f7414 | |||
| bf842bd781 | |||
| 7edb631f09 | |||
| 3c5a356b44 | |||
| f526963707 | |||
| c4032eb351 | |||
| 7a3a1b8b65 | |||
| b3a4591a5d | |||
| 23d057c47b | |||
| 3327a93df5 | |||
| 713fd2ba3a | |||
| 072479c160 |
@@ -2,3 +2,4 @@
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
/*.xcworkspace
|
||||
|
||||
@@ -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))
|
||||
|
||||
// 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, UnknownJSONAPIError>
|
||||
|
||||
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.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(), 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 = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, UnknownJSONAPIError>
|
||||
|
||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||
let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes))
|
||||
let 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.BodyData = body
|
||||
}
|
||||
process(document: peopleResponse)
|
||||
@@ -0,0 +1,112 @@
|
||||
//
|
||||
// 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: - Entity typealias for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, String>
|
||||
|
||||
// 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: ToManyRelationship<Person>
|
||||
public let dogs: ToManyRelationship<Dog>
|
||||
public let home: ToOneRelationship<House>
|
||||
|
||||
public init(friends: ToManyRelationship<Person>, dogs: ToManyRelationship<Dog>, home: ToOneRelationship<House>) {
|
||||
self.friends = friends
|
||||
self.dogs = dogs
|
||||
self.home = home
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
public extension Entity where Description == PersonDescription, 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)))
|
||||
}
|
||||
}
|
||||
|
||||
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: ToOneRelationship<Person?>
|
||||
|
||||
public init(owner: ToOneRelationship<Person?>) {
|
||||
self.owner = owner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, 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)))
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Id) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
|
||||
}
|
||||
}
|
||||
|
||||
public enum HouseDescription: EntityDescription {
|
||||
|
||||
public static var type: String { return "houses" }
|
||||
|
||||
public typealias Attributes = NoAttributes
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
public typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
|
||||
@@ -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>
|
||||
+4
-4
@@ -3,11 +3,11 @@
|
||||
"pins": [
|
||||
{
|
||||
"package": "Result",
|
||||
"repositoryURL": "https://github.com/antitypical/Result.git",
|
||||
"repositoryURL": "https://github.com/mattpolzin/Result",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8fc088dcf72802801efeecba76ea8fb041fb773d",
|
||||
"version": "4.0.0"
|
||||
"branch": "master",
|
||||
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+9
-5
@@ -6,23 +6,27 @@ 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")
|
||||
// antitypical/Result without the Foundation requirement:
|
||||
.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"]),
|
||||
.target(
|
||||
name: "JSONAPITestLib",
|
||||
dependencies: ["JSONAPI"]),
|
||||
.testTarget(
|
||||
name: "JSONAPITests",
|
||||
dependencies: ["JSONAPI"]),
|
||||
dependencies: ["JSONAPITestLib"])
|
||||
],
|
||||
swiftLanguageVersions: [.v4_2]
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# JSONAPI
|
||||
[](http://opensource.org/licenses/MIT) [](https://swift.org) [](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/
|
||||
|
||||
@@ -13,16 +13,29 @@ The primary goals of this framework are:
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
If you find that something in the JSON API v1.0 Spec is not explicitly missing from the **Project Status** checklist but this library does not support it, please let me know! I want to keep working towards a library implementation that is useful in any application.
|
||||
|
||||
## Dev Environment
|
||||
### Prerequisites
|
||||
1. Swift 4.2+ and Swift Package Manager
|
||||
|
||||
### Xcode project
|
||||
To create an Xcode project for JSONAPI, run
|
||||
`swift package generate-xcodeproj`
|
||||
|
||||
## Project Status
|
||||
|
||||
### Decoding
|
||||
#### Document
|
||||
- [x] `data`
|
||||
- [x] `included`
|
||||
- [x] `errors` (untested)
|
||||
- [ ] `meta`
|
||||
- [x] `errors`
|
||||
- [x] `meta`
|
||||
- [ ] `jsonapi`
|
||||
- [ ] `links`
|
||||
- [x] `links`
|
||||
|
||||
#### Resource Object
|
||||
- [x] `id`
|
||||
@@ -37,14 +50,18 @@ The primary goals of this framework are:
|
||||
- [ ] `links`
|
||||
- [ ] `meta`
|
||||
|
||||
#### Links Object
|
||||
- [x] `href`
|
||||
- [x] `meta`
|
||||
|
||||
### Encoding
|
||||
#### Document
|
||||
- [x] `data`
|
||||
- [x] `included`
|
||||
- [x] `errors` (untested)
|
||||
- [ ] `meta`
|
||||
- [x] `errors`
|
||||
- [x] `meta`
|
||||
- [ ] `jsonapi`
|
||||
- [ ] `links`
|
||||
- [x] `links`
|
||||
|
||||
#### Resource Object
|
||||
- [x] `id`
|
||||
@@ -59,10 +76,14 @@ The primary goals of this framework are:
|
||||
- [ ] `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.
|
||||
#### Links Object
|
||||
- [x] `href`
|
||||
- [x] `meta`
|
||||
|
||||
### Entity Validator (using reflection)
|
||||
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
|
||||
- [x] Only allow `TransformedAttribute` and its derivatives within `Attributes` struct.
|
||||
- [x] 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).
|
||||
@@ -71,17 +92,17 @@ The primary goals of this framework are:
|
||||
- [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)
|
||||
- [x] Conform to `CustomStringConvertible`
|
||||
- [x] 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`.
|
||||
- [x] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
|
||||
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
|
||||
- [x] Make it easier to construct `Attributes` and `Relationships` values in test cases (literal expressibility).
|
||||
- [x] Make `TransformedAttribute` a Functor.
|
||||
|
||||
## Usage
|
||||
### Prerequisites
|
||||
1. Swift 4.2+ and Swift Package Manager
|
||||
|
||||
### `EntityDescription`
|
||||
|
||||
@@ -102,10 +123,10 @@ enum PersonDescription: IdentifiedEntityDescription {
|
||||
}
|
||||
```
|
||||
|
||||
To enumerate them, the requirements of an `EntityDescription` are
|
||||
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".
|
||||
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.
|
||||
|
||||
@@ -149,13 +170,13 @@ An `Entity` needs to be specialized on two generic types. The first is the `Enti
|
||||
|
||||
#### `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.
|
||||
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `Entity` type that the Id identifies. Having the `Entity` type associated with the Id makes it easy to store all of your entities in a local 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. `RawIdType`s are documented below.
|
||||
|
||||
#### 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> = JSONAPI.Entity<Description, String>
|
||||
|
||||
public typealias NewEntity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Unidentified>
|
||||
```
|
||||
@@ -180,7 +201,7 @@ let nullableRelative: ToOneRelationship<Person?>
|
||||
|
||||
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):
|
||||
@@ -225,31 +246,79 @@ enum ISODateTransformer: Transformer {
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
#### `Validator`
|
||||
|
||||
You can also creator `Validator`s 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: " ") }
|
||||
}
|
||||
```
|
||||
|
||||
### `JSONAPIDocument`
|
||||
|
||||
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 decoder = JSONDecoder()
|
||||
|
||||
let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoIncludes, BasicJSONAPIError>.self
|
||||
let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self
|
||||
|
||||
let document = try decoder.decode(responseStructure, from: data)
|
||||
```
|
||||
|
||||
This document is guaranteed by the JSON API 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` 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`. 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.
|
||||
#### `MetaType`
|
||||
|
||||
The second generic type of a `JSONAPIDocument` is a `Meta`. This structure is entirely open-ended. As an example, the JSON API document may 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. A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`.
|
||||
|
||||
You can specify `NoLinks` if the document should not contain any links.
|
||||
|
||||
#### `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_.
|
||||
|
||||
@@ -275,3 +344,6 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# JSONAPITestLib
|
||||
JSONAPI comes with a test library to help you test your JSON API 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.
|
||||
|
||||
@@ -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 Error: JSONAPIError
|
||||
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, Error>.Body
|
||||
typealias BodyData = Body.Data
|
||||
|
||||
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,118 @@
|
||||
/// 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, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
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 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 primaryData: 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(errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
}
|
||||
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument: Codable {
|
||||
extension Document where IncludeType == NoIncludes {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where LinksType == NoLinks {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: .none))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, LinksType == NoLinks {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: .none))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata {
|
||||
public init(body: PrimaryResourceBody, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: .none))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: PrimaryResourceBody) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: .none))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document {
|
||||
private enum RootCodingKeys: String, CodingKey {
|
||||
case data
|
||||
case errors
|
||||
@@ -49,36 +151,115 @@ extension JSONAPIDocument: Codable {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,69 @@ 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
|
||||
// MARK: - 8 includes
|
||||
|
||||
@@ -1,19 +1,35 @@
|
||||
//
|
||||
// ResourceBody.swift
|
||||
// PrimaryResourceBody.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
public protocol PrimaryResource: Equatable, Codable {}
|
||||
|
||||
public protocol ResourceBody: Codable, Equatable {
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
|
||||
public struct SingleResourceBody<Entity: JSONAPI.PrimaryResource>: 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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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: Decodable
|
||||
@@ -59,3 +75,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)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//
|
||||
// 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 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)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,10 @@
|
||||
// Created by Mathew Polzin on 11/13/18.
|
||||
//
|
||||
|
||||
public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
|
||||
public protocol AttributeType: Codable {
|
||||
}
|
||||
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
|
||||
private let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
@@ -16,12 +19,29 @@ public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transfo
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Attribute<T: Codable> = TransformAttribute<T, IdentityTransformer<T>>
|
||||
extension TransformedAttribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
|
||||
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformAttribute {
|
||||
extension TransformedAttribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
@@ -58,16 +78,3 @@ extension TransformAttribute {
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public typealias Attributes = Codable & Equatable
|
||||
|
||||
/// Can be used as `Relationships` Type for Entities that do not
|
||||
/// have any Relationships.
|
||||
public struct NoRelatives: Relationships {}
|
||||
public struct NoRelationships: Relationships {}
|
||||
|
||||
/// Can be used as `Attributes` Type for Entities that do not
|
||||
/// have any Attributes.
|
||||
@@ -37,16 +37,35 @@ public protocol EntityDescription {
|
||||
/// EntityType is the protocol that Entity conforms to. This
|
||||
/// protocol lets other types accept any Entity as a generic
|
||||
/// specialization.
|
||||
public protocol EntityType: Codable, Equatable {
|
||||
public protocol EntityType: PrimaryResource {
|
||||
associatedtype Description: EntityDescription
|
||||
associatedtype Identifier: Equatable & Codable
|
||||
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
|
||||
|
||||
typealias Id = JSONAPI.Id<EntityRawIdType, Self>
|
||||
|
||||
typealias Attributes = Description.Attributes
|
||||
typealias Relationships = Description.Relationships
|
||||
|
||||
/// The `Entity`'s Id. This can be of type `Unidentified` if
|
||||
/// the entity is being created clientside and the
|
||||
/// server is being asked to create a unique Id. Otherwise,
|
||||
/// this should be of a type conforming to `IdType`.
|
||||
var id: Id { get }
|
||||
|
||||
/// The JSON API compliant attributes of this `Entity`.
|
||||
var attributes: Attributes { get }
|
||||
|
||||
/// The JSON API compliant relationships of this `Entity`.
|
||||
var relationships: Relationships { get }
|
||||
}
|
||||
|
||||
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
|
||||
|
||||
/// An `Entity` is a single model type that can be
|
||||
/// encoded to or decoded from a JSON API
|
||||
/// "Resource Object."
|
||||
/// See https://jsonapi.org/format/#document-resource-objects
|
||||
public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI.Identifier>: EntityType {
|
||||
public struct Entity<Description: JSONAPI.EntityDescription, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
|
||||
|
||||
/// The JSON API compliant "type" of this `Entity`.
|
||||
public static var type: String { return Description.type }
|
||||
@@ -55,7 +74,7 @@ public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI
|
||||
/// the entity is being created clientside and the
|
||||
/// server is being asked to create a unique Id. Otherwise,
|
||||
/// this should be of a type conforming to `IdType`.
|
||||
public let id: Identifier
|
||||
public let id: Entity.Id
|
||||
|
||||
/// The JSON API compliant attributes of this `Entity`.
|
||||
public let attributes: Description.Attributes
|
||||
@@ -63,60 +82,71 @@ public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI
|
||||
/// The JSON API compliant relationships of this `Entity`.
|
||||
public let relationships: Description.Relationships
|
||||
|
||||
public init(id: Identifier, attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.id = id
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity: IdentifiableEntityType, Relatable, WrappedRelatable where EntityRawIdType: JSONAPI.RawIdType {
|
||||
public typealias Identifier = Entity.Id
|
||||
public typealias WrappedIdentifier = Identifier
|
||||
}
|
||||
|
||||
extension Entity: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Entity<\(Entity.type)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenience initializers
|
||||
extension Entity where Identifier: CreatableIdType {
|
||||
extension Entity where EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.id = Identifier()
|
||||
self.id = Entity.Id()
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes {
|
||||
public init(id: Identifier, relationships: Description.Relationships) {
|
||||
public init(id: Entity.Id, relationships: Description.Relationships) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: relationships)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Identifier: CreatableIdType {
|
||||
extension Entity where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType {
|
||||
public init(relationships: Description.Relationships) {
|
||||
self.init(attributes: NoAttributes(), relationships: relationships)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelatives {
|
||||
public init(id: Identifier, attributes: Description.Attributes) {
|
||||
self.init(id: id, attributes: attributes, relationships: NoRelatives())
|
||||
extension Entity where Description.Relationships == NoRelationships {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes) {
|
||||
self.init(id: id, attributes: attributes, relationships: NoRelationships())
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelatives, Identifier: CreatableIdType {
|
||||
extension Entity where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes) {
|
||||
self.init(attributes: attributes, relationships: NoRelatives())
|
||||
self.init(attributes: attributes, relationships: NoRelationships())
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelatives {
|
||||
public init(id: Identifier) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelatives())
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships {
|
||||
public init(id: Entity.Id) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships())
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelatives, Identifier: CreatableIdType {
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
|
||||
public init() {
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelatives())
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelationships())
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Pointer for Relationships use.
|
||||
public extension Entity where Identifier: IdType {
|
||||
public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
|
||||
/// Get a pointer to this entity that can be used as a
|
||||
/// relationship to another entity.
|
||||
public var pointer: ToOneRelationship<Entity> {
|
||||
@@ -129,21 +159,21 @@ public extension Entity {
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>>) -> TFRM.To {
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>>) -> TFRM.To {
|
||||
return attributes[keyPath: path].value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>?>) -> TFRM.To? {
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> TFRM.To? {
|
||||
return attributes[keyPath: path]?.value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
|
||||
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
|
||||
return attributes[keyPath: path].flatMap { $0.value }
|
||||
}
|
||||
}
|
||||
@@ -181,7 +211,7 @@ public extension Entity {
|
||||
|
||||
try container.encode(Entity.type, forKey: .type)
|
||||
|
||||
if Identifier.self != Unidentified.self {
|
||||
if EntityRawIdType.self != Unidentified.self {
|
||||
try container.encode(id, forKey: .id)
|
||||
}
|
||||
|
||||
@@ -189,7 +219,7 @@ public extension Entity {
|
||||
try container.encode(attributes, forKey: .attributes)
|
||||
}
|
||||
|
||||
if Description.Relationships.self != NoRelatives.self {
|
||||
if Description.Relationships.self != NoRelationships.self {
|
||||
try container.encode(relationships, forKey: .relationships)
|
||||
}
|
||||
}
|
||||
@@ -203,11 +233,12 @@ public extension Entity {
|
||||
guard Entity.type == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.type, found: type)
|
||||
}
|
||||
|
||||
id = try (Unidentified() as? Identifier) ?? container.decode(Identifier.self, forKey: .id)
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { Entity.Id(rawValue: $0) } ?? container.decode(Entity.Id.self, forKey: .id)
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
relationships = try (NoRelatives() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
|
||||
relationships = try (NoRelationships() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,18 +28,18 @@ 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 MaybeId: Codable {
|
||||
associatedtype EntityType: JSONAPI.EntityType
|
||||
associatedtype RawType: MaybeRawId
|
||||
}
|
||||
|
||||
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
|
||||
var rawValue: RawType { get }
|
||||
}
|
||||
|
||||
@@ -47,27 +53,33 @@ 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, EntityType: JSONAPI.EntityType>: Codable, Equatable, MaybeId {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
|
||||
|
||||
extension Id: CreatableIdType where RawType: CreatableRawIdType {
|
||||
public init() {
|
||||
rawValue = .unique()
|
||||
}
|
||||
}
|
||||
|
||||
extension Id where RawType == Unidentified {
|
||||
public static var unidentified: Id { return .init(rawValue: Unidentified()) }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user