mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5a24c4adb | |||
| 669d5d1342 | |||
| 923ab7d9f4 | |||
| 72769b6107 | |||
| 109e15d741 | |||
| 72180f64ef | |||
| fc962f9a0d | |||
| 52eb123166 | |||
| 4ef147ec45 | |||
| 61074ecc69 | |||
| 4dbcff6023 | |||
| d6e82fab55 | |||
| 5fa9848f02 | |||
| f38399a1d6 | |||
| 28f664326d | |||
| 3b7ef4aeb9 | |||
| bb1ed30e89 | |||
| dfdc266645 | |||
| 585cad0a83 | |||
| 89abdd4cca | |||
| 67a43be26c | |||
| 8f279ce191 | |||
| c9d388579f | |||
| 1061283905 | |||
| 08949d0a93 | |||
| 3047e2d23a | |||
| 41a2a01788 | |||
| 53f7f55e07 | |||
| d8d030286d | |||
| 005a981bf7 | |||
| fad45203dd | |||
| 3468cb555a | |||
| 8defe82536 | |||
| 1f83216b03 | |||
| 6e6973c170 | |||
| 4f52cc2fd1 | |||
| 35d6cbb440 | |||
| d667e91a6a | |||
| 75c8ce7405 | |||
| 3a8237ec0f | |||
| 49e33baa96 | |||
| f2a9b5a7b9 | |||
| 24baadd7a8 | |||
| 8d18be6154 | |||
| 8aa887b527 | |||
| 7d0a686dd9 | |||
| e4eb7816d7 | |||
| 482174f5b4 | |||
| 339480264e | |||
| 5d666ec998 | |||
| 9cacbe9b8b | |||
| 6663ad042e | |||
| 07402259c4 | |||
| 8274ecb523 | |||
| 853c7e892a | |||
| 04bf96a0cf | |||
| 8668311307 | |||
| 689517c633 | |||
| 65b80ee0eb | |||
| e91a03b396 | |||
| 82088c7852 | |||
| 34a4c8e7fc | |||
| 6aeb859c24 | |||
| 163ac94c51 | |||
| e67b9fc142 | |||
| a628992fcb | |||
| cf47f88a61 | |||
| 0425e2adcb | |||
| d3763ba713 | |||
| fcc1796731 | |||
| 921bcef05d | |||
| 661ff6eca5 | |||
| e36180c9b9 | |||
| abee0c4d0e | |||
| 9c8b2fbebb | |||
| a9ef71f383 |
@@ -13,7 +13,7 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
||||
// MARK: - Literal Expressibility
|
||||
// The JSONAPITestLib provides literal expressibility for key types to
|
||||
// make creating tests easier
|
||||
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil))
|
||||
let dog = Dog(id: "1234", attributes: Dog.Attributes(name: "Buddy"), relationships: Dog.Relationships(owner: nil), meta: .none, links: .none)
|
||||
|
||||
// MARK: - JSON API structure checking
|
||||
// The JSONAPITestLib provides a `check` function for each Entity type
|
||||
|
||||
@@ -11,39 +11,60 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
typealias SingleDogDocument = JSONAPIDocument<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>
|
||||
typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let singleDogDocument = SingleDogDocument(body: SingleResourceBody(entity: dogFromCode))
|
||||
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none)
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.primaryData?.value
|
||||
let dogFromData = dogResponse.body.primaryResource?.value
|
||||
let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
|
||||
|
||||
// MARKL - Parse a request or response body with one Dog in it using an alternative model
|
||||
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
|
||||
let altDogFromData = altDogResponse.body.primaryResource?.value
|
||||
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
||||
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
||||
let houses = [House(), House()]
|
||||
let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)]
|
||||
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
|
||||
|
||||
typealias BatchPeopleDocument = JSONAPIDocument<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, UnknownJSONAPIError>
|
||||
typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||
let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes))
|
||||
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||
let peopleFromData = peopleResponse.body.primaryData?.values
|
||||
let peopleFromData = peopleResponse.body.primaryResource?.values
|
||||
let dogsFromData = peopleResponse.body.includes?[Dog.self]
|
||||
let housesFromData = peopleResponse.body.includes?[House.self]
|
||||
|
||||
print("-----")
|
||||
print(peopleResponse)
|
||||
print("-----")
|
||||
|
||||
// MARK: - Pass successfully parsed body to other parts of the code
|
||||
|
||||
if case let .data(bodyData) = peopleResponse.body {
|
||||
print(bodyData)
|
||||
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)
|
||||
|
||||
@@ -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 {
|
||||
@@ -35,6 +37,10 @@ public enum PersonDescription: EntityDescription {
|
||||
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
|
||||
@@ -42,11 +48,11 @@ public enum PersonDescription: EntityDescription {
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let friends: ToManyRelationship<Person>
|
||||
public let dogs: ToManyRelationship<Dog>
|
||||
public let home: ToOneRelationship<House>
|
||||
public let friends: ToMany<Person>
|
||||
public let dogs: ToMany<Dog>
|
||||
public let home: ToOne<House>
|
||||
|
||||
public init(friends: ToManyRelationship<Person>, dogs: ToManyRelationship<Dog>, home: ToOneRelationship<House>) {
|
||||
public init(friends: ToMany<Person>, dogs: ToMany<Dog>, home: ToOne<House>) {
|
||||
self.friends = friends
|
||||
self.dogs = dogs
|
||||
self.home = home
|
||||
@@ -56,9 +62,9 @@ public enum PersonDescription: EntityDescription {
|
||||
|
||||
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 = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +81,9 @@ public enum DogDescription: EntityDescription {
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let owner: ToOneRelationship<Person?>
|
||||
public let owner: ToOne<Person?>
|
||||
|
||||
public init(owner: ToOneRelationship<Person?>) {
|
||||
public init(owner: ToOne<Person?>) {
|
||||
self.owner = owner
|
||||
}
|
||||
}
|
||||
@@ -85,13 +91,41 @@ public enum DogDescription: EntityDescription {
|
||||
|
||||
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 = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Id) throws {
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: .init(owner: .init(id: owner)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-9
@@ -1,15 +1,7 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "Result",
|
||||
"repositoryURL": "https://github.com/mattpolzin/Result",
|
||||
"state": {
|
||||
"branch": "master",
|
||||
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
|
||||
+1
-3
@@ -14,13 +14,11 @@ let package = Package(
|
||||
targets: ["JSONAPITestLib"])
|
||||
],
|
||||
dependencies: [
|
||||
// antitypical/Result without the Foundation requirement:
|
||||
.package(url: "https://github.com/mattpolzin/Result", .branch("master"))
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "JSONAPI",
|
||||
dependencies: ["Result"]),
|
||||
dependencies: []),
|
||||
.target(
|
||||
name: "JSONAPITestLib",
|
||||
dependencies: ["JSONAPI"]),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# JSONAPI
|
||||
[](http://opensource.org/licenses/MIT) [](https://swift.org) [](https://app.bitrise.io/app/c8295b9589aa401e)
|
||||
|
||||
A Swift package for encoding to- and decoding from *JSON API* compliant requests and responses.
|
||||
A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses.
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
|
||||
## Primary Goals
|
||||
|
||||
The primary goals of this framework are:
|
||||
1. Allow creation of Swift types that are easy to use in code but also can be encoded to- or decoded from *JSON API* compliant payloads without lots of boilerplate code.
|
||||
1. Allow creation of Swift types that are easy to use in code but also can be encoded to- or decoded from **JSON API v1.0 Spec** compliant payloads without lots of boilerplate code.
|
||||
2. Leverage `Codable` to avoid additional outside dependencies and get operability with non-JSON encoders/decoders for free.
|
||||
3. Do not sacrifice type safety.
|
||||
4. Be platform agnostic so that Swift code can be written once and used by both the client and the server.
|
||||
@@ -16,7 +16,7 @@ The primary goals of this framework are:
|
||||
### 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.
|
||||
If you find something wrong with this library and it isn't already mentioned under **Project Status**, let me know! I want to keep working towards a library implementation that is useful in any application.
|
||||
|
||||
## Dev Environment
|
||||
### Prerequisites
|
||||
@@ -26,15 +26,20 @@ If you find that something in the JSON API v1.0 Spec is not explicitly missing f
|
||||
To create an Xcode project for JSONAPI, run
|
||||
`swift package generate-xcodeproj`
|
||||
|
||||
### Running the Playground
|
||||
To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace.
|
||||
|
||||
Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the Entities.swift Playground Source file) can get things working for me when I am getting an error about `JSONAPI` not being found.
|
||||
|
||||
## Project Status
|
||||
|
||||
### Decoding
|
||||
### Encoding/Decoding
|
||||
#### Document
|
||||
- [x] `data`
|
||||
- [x] `included`
|
||||
- [x] `errors`
|
||||
- [x] `meta`
|
||||
- [ ] `jsonapi`
|
||||
- [x] `jsonapi`
|
||||
- [x] `links`
|
||||
|
||||
#### Resource Object
|
||||
@@ -42,70 +47,42 @@ To create an Xcode project for JSONAPI, run
|
||||
- [x] `type`
|
||||
- [x] `attributes`
|
||||
- [x] `relationships`
|
||||
- [ ] `links`
|
||||
- [ ] `meta`
|
||||
|
||||
#### Relationship Object
|
||||
- [x] `data`
|
||||
- [ ] `links`
|
||||
- [ ] `meta`
|
||||
|
||||
#### Links Object
|
||||
- [x] `href`
|
||||
- [x] `meta`
|
||||
|
||||
### Encoding
|
||||
#### Document
|
||||
- [x] `data`
|
||||
- [x] `included`
|
||||
- [x] `errors`
|
||||
- [x] `meta`
|
||||
- [ ] `jsonapi`
|
||||
- [x] `links`
|
||||
|
||||
#### Resource Object
|
||||
- [x] `id`
|
||||
- [x] `type`
|
||||
- [x] `attributes`
|
||||
- [x] `relationships`
|
||||
- [ ] `links`
|
||||
- [ ] `meta`
|
||||
- [x] `meta`
|
||||
|
||||
#### Relationship Object
|
||||
- [x] `data`
|
||||
- [ ] `links`
|
||||
- [ ] `meta`
|
||||
- [x] `links`
|
||||
- [x] `meta`
|
||||
|
||||
#### 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).
|
||||
|
||||
### Misc
|
||||
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
|
||||
- [x] Support ability to distinguish between `Attributes` fields that are optional (i.e. the key might not be there) and `Attributes` values that are optional (i.e. the key is guaranteed to be there but it might be `null`).
|
||||
- [x] Fix `ToOneRelationship` so that it is possible to specify an optional relationship where the value is `null` rather than the key being omitted.
|
||||
- [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`)
|
||||
- [x] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
|
||||
- [x] Support validation on `Attributes`.
|
||||
- [ ] 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).
|
||||
|
||||
### JSONAPITestLib
|
||||
#### Entity Validator
|
||||
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
|
||||
- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded.
|
||||
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
|
||||
|
||||
### Potential Improvements
|
||||
- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources.
|
||||
- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies.
|
||||
- [ ] Property-based testing (using `SwiftCheck`).
|
||||
- [ ] Error or warning if an included entity is not related to a primary entity or another included entity (Turned off or at least not throwing by default).
|
||||
|
||||
## Usage
|
||||
|
||||
### `EntityDescription`
|
||||
In this documentation, in order to draw attention to the difference between the `JSONAPI` framework (this Swift library) and the **JSON API Spec** (the specification this library helps you follow), the specification will consistently be referred to below as simply the **SPEC**.
|
||||
|
||||
An `EntityDescription` is the `JSONAPI` framework's specification for what the JSON API spec calls a *Resource Object*. You might create the following `EntityDescription` to represent a person in a network of friends:
|
||||
### `JSONAPI.EntityDescription`
|
||||
|
||||
An `EntityDescription` is the `JSONAPI` framework's representation of what the **SPEC** calls a *Resource Object*. You might create the following `EntityDescription` to represent a person in a network of friends:
|
||||
|
||||
```
|
||||
enum PersonDescription: IdentifiedEntityDescription {
|
||||
@@ -123,13 +100,13 @@ enum PersonDescription: IdentifiedEntityDescription {
|
||||
```
|
||||
|
||||
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".
|
||||
1. A static `var` "type" that matches the JSON type; The **SPEC** requires every *Resource Object* to have a "type".
|
||||
2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes`
|
||||
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelationships`
|
||||
|
||||
Note that an `enum` type is used here for the `EntityDescription`; it could have been a `struct`, but `EntityDescription`s do not ever need to be created so an `enum` with no `case`s is a nice fit for the job.
|
||||
|
||||
This readme doesn't go into detail on the JSON API Spec, but the following JSON API *Resource Object* would be described by the above `PersonDescription`:
|
||||
This readme doesn't go into detail on the **SPEC**, but the following *Resource Object* would be described by the above `PersonDescription`:
|
||||
|
||||
```
|
||||
{
|
||||
@@ -159,43 +136,61 @@ This readme doesn't go into detail on the JSON API Spec, but the following JSON
|
||||
}
|
||||
```
|
||||
|
||||
### `Entity`
|
||||
### `JSONAPI.Entity`
|
||||
|
||||
Once you have an `EntityDescription`, you _create_, _encode_, and _decode_ `Entity`s that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `Entity`s that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with entities.
|
||||
Once you have an `EntityDescription`, you _create_, _encode_, and _decode_ `Entities` that "fit the description". If you have a `CreatableRawIdType` (see the section on `RawIdType`s below) then you can create new `Entities` that will automatically be given unique Ids, but even without a `CreatableRawIdType` you can encode, decode and work with entities.
|
||||
|
||||
The `Entity` and `EntityDescription` together embody the rules and properties of a JSON API *Resource Object*.
|
||||
The `Entity` and `EntityDescription` together with a `JSONAPI.Meta` type and a `JSONAPI.Links` type embody the rules and properties of a JSON API *Resource Object*.
|
||||
|
||||
An `Entity` needs to be specialized on two generic types. The first is the `EntityDescription` described above. The second is the type of Id to use for the `Entity`.
|
||||
An `Entity` needs to be specialized on four generic types. The first is the `EntityDescription` described above. The others are a `Meta`, `Links`, and `MaybeRawId`.
|
||||
|
||||
#### `Meta`
|
||||
|
||||
The second generic specialization on `Entity` is `Meta`. This is described in its own section [below](#jsonapimeta). All `Meta` at any level of a JSON API Document follow the same rules.
|
||||
|
||||
#### `Links`
|
||||
|
||||
The third generic specialization on `Entity` is `Links`. This is described in its own section [below](#jsonnapilinks). All `Links` at any level of a JSON API Document follow the same rules, although the **SPEC** makes different suggestions as to what types of links might live on which parts of the Document.
|
||||
|
||||
#### `IdType`
|
||||
|
||||
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `EntityDescription` of the type of entity the Id identifies. Having the `EntityDescription` type associated with the Id makes it easy to store all of your entities in a local hash broken out by `EntityDescription`; You can pass Ids around and always know where to look for the `Entity` to which the Id refers. `RawIdType`s are documented below.
|
||||
The second is the raw type of `Id` to use for the `Entity`. The actual `Id` of the `Entity` will not be a `RawIdType`, though. The `Id` will package a value of `RawIdType` with a specialized reference back to the `Entity` type it identifies. This just looks like `Id<RawIdType, Entity<EntityDescription, RawIdType>>`.
|
||||
|
||||
Having the `Entity` type associated with the `Id` makes it easy to store all of your entities in a hash broken out by `Entity` type; You can pass `Ids` around and always know where to look for the `Entity` to which the `Id` refers.
|
||||
|
||||
A `RawIdType` is the underlying type that uniquely identifies an `Entity`. This is often a `String` or a `UUID`.
|
||||
|
||||
#### `MaybeRawId`
|
||||
|
||||
`MaybeRawId` is either a `RawIdType` that can be used to uniquely identify `Entities` or it is `Unidentified` which is used to indicate an `Entity` does not have an `Id` (which is useful when a client is requesting that the server create an `Entity` and assign it a new `Id`).
|
||||
|
||||
#### Convenient `typealiases`
|
||||
|
||||
Often you can use one `RawIdType` for many if not all of your `Entities`. That means you can save yourself some boilerplate by using `typealias`es like the following:
|
||||
```
|
||||
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Id<String, Description>>
|
||||
public typealias Entity<Description: JSONAPI.EntityDescription, Meta: JSONAPI.Meta, Links: JSONAPI.Links> = JSONAPI.Entity<Description, Meta, Links, String>
|
||||
|
||||
public typealias NewEntity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Unidentified>
|
||||
public typealias NewEntity<Description: JSONAPI.EntityDescription, Meta: JSONAPI.Meta, Links: JSONAPI.Links> = JSONAPI.Entity<Description, Meta, Links, Unidentified>
|
||||
```
|
||||
|
||||
It can also be nice to create a `typealias` for each type of entity you want to work with:
|
||||
```
|
||||
typealias Person = Entity<PersonDescription>
|
||||
typealias Person = Entity<PersonDescription, NoMetadata, NoLinks>
|
||||
|
||||
typealias NewPerson = NewEntity<PersonDescription>
|
||||
typealias NewPerson = NewEntity<PersonDescription, NoMetadata, NoLinks>
|
||||
```
|
||||
|
||||
Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time JSON API spec allows a *Resource Object* to be encoded without an Id is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the Id for that object.
|
||||
Note that I am assuming an unidentified person is a "new" person. I suspect that is generally an acceptable conflation because the only time the **SPEC** allows a *Resource Object* to be encoded without an `Id` is when a client is requesting the given *Resource Object* be created by the server and the client wants the server to create the `Id` for that object.
|
||||
|
||||
### `Relationships`
|
||||
### `JSONAPI.Relationships`
|
||||
|
||||
There are two types of `Relationship`s: `ToOneRelationship` and `ToManyRelationship`. An `EntityDescription`'s `Relationships` type can contain any number of `Relationship`s of either of these types. Do not store anything other than `Relationship`s in the `Relationships` struct of an `EntityDescription`.
|
||||
There are two types of `Relationships`: `ToOneRelationship` and `ToManyRelationship`. An `EntityDescription`'s `Relationships` type can contain any number of `Relationship` properties of either of these types. Do not store anything other than `Relationship` properties in the `Relationships` struct of an `EntityDescription`.
|
||||
|
||||
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent no related objects exist with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value might be `null` or it might be a resource identifier) like this:
|
||||
In addition to identifying entities by Id and type, `Relationships` can contain `Meta` or `Links` that follow the same rules as [`Meta`](#jsonapimeta) and [`Links`](#jsonapilinks) elsewhere in the JSON API Document.
|
||||
|
||||
To describe a relationship that may be omitted (i.e. the key is not even present in the JSON object), you make the entire `ToOneRelationship` or `ToManyRelationship` optional. However, this is not recommended because you can also represent optional relationships as nullable which means the key is always present. A `ToManyRelationship` can naturally represent the absence of related values with an empty array, so `ToManyRelationship` does not support nullability at all. A `ToOneRelationship` can be marked as nullable (i.e. the value could be either `null` or a resource identifier) like this:
|
||||
```
|
||||
let nullableRelative: ToOneRelationship<Person?>
|
||||
let nullableRelative: ToOneRelationship<Person?, NoMetadata, NoLinks>
|
||||
```
|
||||
|
||||
An entity that does not have relationships can be described by adding the following to an `EntityDescription`:
|
||||
@@ -203,14 +198,14 @@ An entity that does not have relationships can be described by adding the follow
|
||||
typealias Relationships = NoRelationships
|
||||
```
|
||||
|
||||
`Relationship`s boil down to Ids of other entities. To access the Id of a related entity, you can use the shorthand `~>` operator with the `KeyPath` of the `Relationship` from which you want the Id. The friends of the above `Person` entity could be accessed as follows (type annotations for clarity):
|
||||
`Relationship` values boil down to `Ids` of other entities. To access the `Id` of a related `Entity`, you can use the custom `~>` operator with the `KeyPath` of the `Relationship` from which you want the `Id`. The friends of the above `Person` `Entity` can be accessed as follows (type annotations for clarity):
|
||||
```
|
||||
let friendIds: [Person.Identifier] = person ~> \.friends
|
||||
```
|
||||
|
||||
### `Attributes`
|
||||
### `JSONAPI.Attributes`
|
||||
|
||||
The `Attributes` of an `EntityDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute` or `TransformAttribute` `struct`. This is the place to store all attributes of an entity.
|
||||
The `Attributes` of an `EntityDescription` can contain any JSON encodable/decodable types as long as they are wrapped in an `Attribute`, `ValidatedAttribute`, or `TransformedAttribute` `struct`.
|
||||
|
||||
To describe an attribute that may be omitted (i.e. the key might not even be in the JSON object), you make the entire `Attribute` optional:
|
||||
```
|
||||
@@ -232,14 +227,19 @@ typealias Attributes = NoAttributes
|
||||
let favoriteColor: String = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
NOTE: Because of support for computed properties that are not wrapped in `Attribute`, `TransformedAttribute`, or `ValidatedAttribute`, the compiler cannot always infer the type of thing you want back when using subscript attribute access. The following code is ambiguous about whether it should return a `String` or an `Attribute<String>`:
|
||||
```
|
||||
let favoriteColor = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
#### `Transformer`
|
||||
|
||||
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. To do this, you create a `Transformer`.
|
||||
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`.
|
||||
|
||||
A `Transformer` just provides one static function that transforms one type to another. You might define one for an ISO 8601 compliant `Date` like this:
|
||||
```
|
||||
enum ISODateTransformer: Transformer {
|
||||
public static func transform(_ from: String) throws -> Date {
|
||||
public static func transform(_ value: String) throws -> Date {
|
||||
// parse Date out of input and return
|
||||
}
|
||||
}
|
||||
@@ -252,32 +252,77 @@ 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.
|
||||
|
||||
If you make your `Transformer` a `ReversibleTransformer` then your life will be a bit easier when you construct `TransformedAttributes` because you have access to initializers for both the pre- and post-transformed value types. Continuing with the above example of a `ISODateTransformer`:
|
||||
```
|
||||
extension ISODateTransformer: ReversibleTransformer {
|
||||
public static func reverse(_ value: Date) throws -> String {
|
||||
// serialize Date to a String
|
||||
}
|
||||
}
|
||||
|
||||
let exampleAttribute = try? TransformedAttribute<String, ISODateTransformer>(transformedValue: Date())
|
||||
let otherAttribute = try? TransformedAttribute<String, ISODateTransformer>(rawValue: "2018-12-01 09:06:41 +0000")
|
||||
```
|
||||
|
||||
#### `Validator`
|
||||
|
||||
You can also creator `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.
|
||||
You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid.
|
||||
|
||||
### `JSONAPIDocument`
|
||||
#### Computed `Attribute`
|
||||
|
||||
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:
|
||||
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 do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `Person[\.name]` attribute from the example above and create a `fullName` computed property.
|
||||
|
||||
```
|
||||
public var fullName: Attribute<String> {
|
||||
return name.map { $0.joined(separator: " ") }
|
||||
}
|
||||
```
|
||||
|
||||
### Copying `Entities`
|
||||
`Entity` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying an `Entity`:
|
||||
1. Assigning a new `Identifier` to the copy of an identified `Entity`.
|
||||
2. Assigning a new `Identifier` to the copy of an unidentified `Entity`.
|
||||
|
||||
The above can be accomplished with code like the following:
|
||||
|
||||
```
|
||||
// use case 1
|
||||
let person1 = person.withNewIdentifier()
|
||||
|
||||
// use case 2
|
||||
let newlyIdentifiedPerson1 = unidentifiedPerson.identified(byType: String.self)
|
||||
|
||||
let newlyIdentifiedPerson2 = unidentifiedPerson.identified(by: "2232")
|
||||
```
|
||||
|
||||
### `JSONAPI.Document`
|
||||
|
||||
The entirety of a JSON API request or response is encoded or decoded from- or to a `Document`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
|
||||
```
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self
|
||||
let responseStructure = JSONAPI.Document<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."
|
||||
A JSON API Document is guaranteed by the **SPEC** to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata."
|
||||
|
||||
#### `ResourceBody`
|
||||
|
||||
The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `Entity` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `Entity` values in a JSON API document that has a `ManyResourceBody`. 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.
|
||||
The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody<PrimaryResource>` or a `ManyResourceBody<PrimaryResource>`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `Entity`. These `Poly` types work in the same way as the `Include` types described below.
|
||||
|
||||
If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead.
|
||||
|
||||
##### nullable `PrimaryResource`
|
||||
|
||||
If you expect a `SingleResourceBody` to sometimes come back `null`, you should make your `PrimaryResource` optional. If you do not make your `PrimaryResource` optional then a `null` primary resource will be considered an error when parsing the JSON.
|
||||
|
||||
You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody` because the **SPEC** requires that an empty document in that case be represented by an empty array rather than `null`.
|
||||
|
||||
#### `MetaType`
|
||||
|
||||
The second generic type of a `JSONAPIDocument` is a `Meta`. This structure is entirely open-ended. As an example, the JSON API document may contain the following pagination info in its meta entry:
|
||||
The second generic type of a `JSONAPIDocument` is a `Meta`. This `Meta` follows the same rules as `Meta` at any other part of a JSON API Document. It is described below in its own section, but as an example, the JSON API document could contain the following pagination info in its meta entry:
|
||||
```
|
||||
{
|
||||
"meta": {
|
||||
@@ -291,9 +336,9 @@ The second generic type of a `JSONAPIDocument` is a `Meta`. This structure is en
|
||||
You would then create the following `Meta` type:
|
||||
```
|
||||
struct PageMetadata: JSONAPI.Meta {
|
||||
let total: Int
|
||||
let limit: Int
|
||||
let offset: Int
|
||||
let total: Int
|
||||
let limit: Int
|
||||
let offset: Int
|
||||
}
|
||||
```
|
||||
|
||||
@@ -301,9 +346,7 @@ 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.
|
||||
The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are described in their own section [below](#jsonapilinks).
|
||||
|
||||
#### `IncludeType`
|
||||
|
||||
@@ -313,11 +356,33 @@ The fourth generic type of a `JSONAPIDocument` is an `Include`. This type contro
|
||||
|
||||
To specify that we expect friends of a person to be included in the above example `JSONAPIDocument`, we would use `Include1<Person>` instead of `NoIncludes`.
|
||||
|
||||
#### `APIDescriptionType`
|
||||
|
||||
The fifth generic type of a `JSONAPIDocument` is an `APIDescription`. The type represents the "JSON:API Object" described by the **SPEC**. This type describes the highest version of the **SPEC** supported and can carry additional metadata to describe the API.
|
||||
|
||||
You can specify this is not part of the document by using the `NoAPIDescription` type.
|
||||
|
||||
You can describe the API by a version with no metadata by using `APIDescription<NoMetadata>`.
|
||||
|
||||
You can supply any `JSONAPI.Meta` type as the metadata type of the API description.
|
||||
|
||||
#### `Error`
|
||||
|
||||
The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the JSON API Spec, these errors will be found in the root document member `errors`.
|
||||
The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the **SPEC**, these errors will be found in the root document member `errors`.
|
||||
|
||||
### `RawIdType`
|
||||
### `JSONAPI.Meta`
|
||||
|
||||
A `Meta` struct is totally open-ended. It is described by the **SPEC** as a place to put any information that does not fit into the standard JSON API Document structure anywhere else.
|
||||
|
||||
You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`.
|
||||
|
||||
### `JSONAPI.Links`
|
||||
|
||||
A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included.
|
||||
|
||||
You can specify `NoLinks` if the part of the document being described should not contain any `Links`.
|
||||
|
||||
### `JSONAPI.RawIdType`
|
||||
|
||||
If you want to create new `JSONAPI.Entity` values and assign them Ids then you will need to conform at least one type to `CreatableRawIdType`. Doing so is easy; here are two example conformances for `UUID` and `String` (via `UUID`):
|
||||
```
|
||||
@@ -334,5 +399,75 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Attribute or Relationship Key Mapping
|
||||
There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase:
|
||||
```
|
||||
public enum EntityDescription1: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let coolProperty: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
public enum EntityDescription2: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let wholeOtherThing: Attribute<String>
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case wholeOtherThing = "coolProperty"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Attribute Encode/Decode
|
||||
You can safely provide your own encoding or decoding functions for your Attributes struct if you need to as long as you are careful that your encode operation correctly reverses your decode operation. Although this is generally not necessary, `AttributeType` provides a convenience method to make your decoding a bit less boilerplate ridden. This is what it looks like:
|
||||
```
|
||||
public enum EntityDescription1: JSONAPI.EntityDescription {
|
||||
public static var type: String { return "entity" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let property1: Attribute<String>
|
||||
public let property2: Attribute<Int>
|
||||
public let property3: Attribute<String>
|
||||
|
||||
public let weirdThing: Attribute<String>
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case property1
|
||||
case property2
|
||||
case property3
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
extension EntityDescription1.Attributes {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
property1 = try .defaultDecoding(from: container, forKey: .property1)
|
||||
property2 = try .defaultDecoding(from: container, forKey: .property2)
|
||||
property3 = try .defaultDecoding(from: container, forKey: .property3)
|
||||
|
||||
weirdThing = .init(value: "hello world")
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(property1, forKey: .property1)
|
||||
try container.encode(property2, forKey: .property2)
|
||||
try container.encode(property3, forKey: .property3)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 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.
|
||||
The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITestLib`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `Entity` values into your unit tests. It also provides a `check()` function for each `Entity` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITestLib` in action in the Playground included with the `JSONAPI` repository.
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
|
||||
/// Can be used as `APIDescriptionType` for Documents that do not
|
||||
/// have any API Description (a.k.a. "JSON:API Object").
|
||||
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
|
||||
public typealias Meta = NoMetadata
|
||||
|
||||
public init() {}
|
||||
|
||||
public static var none: NoAPIDescription { return .init() }
|
||||
|
||||
public var description: String { return "No JSON:API Object" }
|
||||
}
|
||||
|
||||
extension APIDescription {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
|
||||
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
|
||||
|
||||
if let metaVal = NoMetadata() as? Meta {
|
||||
meta = metaVal
|
||||
} else {
|
||||
meta = try container.decode(Meta.self, forKey: .meta)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(version, forKey: .version)
|
||||
|
||||
if Meta.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,21 +25,36 @@
|
||||
/// 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(Data)
|
||||
|
||||
public struct Data: Equatable {
|
||||
let primary: ResourceBody
|
||||
let includes: Includes<Include>
|
||||
let meta: MetaType
|
||||
let links: LinksType
|
||||
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 {
|
||||
@@ -38,8 +66,13 @@ 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? {
|
||||
public var primaryResource: PrimaryResourceBody? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.primary
|
||||
}
|
||||
@@ -72,58 +105,111 @@ 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) {
|
||||
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(.init(primary: body, includes: .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(.init(primary: body, includes: includes, meta: .none, links: links))
|
||||
extension Document where LinksType == NoLinks {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where LinksType == NoLinks {
|
||||
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: .none))
|
||||
extension Document where APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes, LinksType == NoLinks {
|
||||
public init(body: ResourceBody, meta: MetaType) {
|
||||
self.body = .data(.init(primary: body, includes: .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(.init(primary: body, includes: .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(.init(primary: body, includes: 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(.init(primary: body, includes: .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.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
public func merging(_ other: Document.Body.Data,
|
||||
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
|
||||
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
|
||||
return Document.Body.Data(primary: primary.appending(other.primary),
|
||||
includes: includes.appending(other.includes),
|
||||
meta: metaMerge(meta, other.meta),
|
||||
links: linksMerge(links, other.links))
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
|
||||
return merging(other,
|
||||
combiningMetaWith: { _, _ in .none },
|
||||
combiningLinksWith: { _, _ in .none })
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
extension Document {
|
||||
private enum RootCodingKeys: String, CodingKey {
|
||||
case data
|
||||
case errors
|
||||
@@ -136,6 +222,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?
|
||||
@@ -166,11 +258,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)
|
||||
@@ -223,13 +315,34 @@ extension JSONAPIDocument: Codable {
|
||||
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))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,16 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes {
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
extension Includes: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Includes(\(String(describing: values))"
|
||||
@@ -115,4 +125,25 @@ extension Includes where I: _Poly6 {
|
||||
}
|
||||
|
||||
// MARK: - 7 includes
|
||||
public typealias Include7 = Poly7
|
||||
extension Includes where I: _Poly7 {
|
||||
public subscript(_ lookup: I.G.Type) -> [I.G] {
|
||||
return values.compactMap { $0.g }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 8 includes
|
||||
public typealias Include8 = Poly8
|
||||
extension Includes where I: _Poly8 {
|
||||
public subscript(_ lookup: I.H.Type) -> [I.H] {
|
||||
return values.compactMap { $0.h }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 9 includes
|
||||
public typealias Include9 = Poly9
|
||||
extension Includes where I: _Poly9 {
|
||||
public subscript(_ lookup: I.I.Type) -> [I.I] {
|
||||
return values.compactMap { $0.i }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
//
|
||||
// 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 protocol AppendableResourceBody: ResourceBody {
|
||||
func appending(_ other: Self) -> Self
|
||||
}
|
||||
|
||||
public init(entity: Entity?) {
|
||||
public func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
||||
public let value: Entity
|
||||
|
||||
public init(entity: Entity) {
|
||||
self.value = entity
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: ResourceBody {
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
|
||||
public let values: [Entity]
|
||||
|
||||
public init(entities: [Entity]) {
|
||||
values = entities
|
||||
}
|
||||
|
||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||
return ManyResourceBody(entities: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
/// Use NoResourceBody to indicate you expect a JSON API document to not
|
||||
@@ -32,13 +53,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 +71,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 +103,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)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -9,9 +9,11 @@
|
||||
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 JSONAPIURL: Codable, Equatable {}
|
||||
@@ -19,6 +21,17 @@ public protocol JSONAPIURL: Codable, Equatable {}
|
||||
public struct Link<URL: JSONAPI.JSONAPIURL, Meta: JSONAPI.Meta>: Equatable, Codable {
|
||||
public let url: URL
|
||||
public let meta: Meta
|
||||
|
||||
public init(url: URL, meta: Meta) {
|
||||
self.url = url
|
||||
self.meta = meta
|
||||
}
|
||||
}
|
||||
|
||||
extension Link where Meta == NoMetadata {
|
||||
public init(url: URL) {
|
||||
self.init(url: url, meta: .none)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Link {
|
||||
|
||||
@@ -19,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,36 @@
|
||||
//
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
|
||||
public extension Attribute {
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
public func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,17 @@
|
||||
//
|
||||
|
||||
public protocol AttributeType: Codable {
|
||||
associatedtype RawValue: Codable
|
||||
associatedtype ValueType
|
||||
|
||||
var value: ValueType { get }
|
||||
}
|
||||
|
||||
// MARK: TransformedAttribute
|
||||
|
||||
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
|
||||
private let rawValue: RawValue
|
||||
let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
|
||||
@@ -19,6 +26,25 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer: ReversibleTransformer {
|
||||
/// Initialize a TransformedAttribute from its transformed value. The
|
||||
/// RawValue, which is what gets encoded/decoded, is determined using
|
||||
/// The Transformer's reverse function.
|
||||
public init(transformedValue: Transformer.To) throws {
|
||||
self.value = transformedValue
|
||||
rawValue = try Transformer.reverse(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
|
||||
@@ -27,18 +53,34 @@ extension TransformedAttribute: CustomStringConvertible {
|
||||
|
||||
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
|
||||
|
||||
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
// MARK: ValidatedAttribute
|
||||
|
||||
/// A ValidatedAttribute does not transform its raw value, but it throws
|
||||
/// an error if the raw value does not match expectations.
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
|
||||
// MARK: Attribute
|
||||
|
||||
/// An Attribute simply represents a type that can be encoded and decoded.
|
||||
public struct Attribute<RawValue: Codable>: AttributeType {
|
||||
let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>>
|
||||
|
||||
public var value: RawValue {
|
||||
return attribute.value
|
||||
}
|
||||
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
attribute = .init(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
extension Attribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
|
||||
extension Attribute: Equatable where RawValue: Equatable {}
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformedAttribute {
|
||||
@@ -66,15 +108,40 @@ extension TransformedAttribute {
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
// See note in decode above about the weirdness
|
||||
// going on here.
|
||||
let anyNil: Any? = nil
|
||||
if let _ = anyNil as? Transformer.From,
|
||||
(rawValue as Any?) == nil {
|
||||
try container.encodeNil()
|
||||
}
|
||||
|
||||
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? RawValue {
|
||||
attribute = .init(value: val)
|
||||
} else {
|
||||
attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Attribute decoding and encoding defaults
|
||||
|
||||
extension AttributeType {
|
||||
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container, forKey key: Container.Key) throws -> Self {
|
||||
return try container.decode(Self.self, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,20 +28,37 @@ public protocol CreatableRawIdType: RawIdType {
|
||||
|
||||
extension String: RawIdType {}
|
||||
|
||||
public protocol Identifier: Codable, Equatable {
|
||||
associatedtype EntityDescription: JSONAPI.EntityDescription
|
||||
}
|
||||
|
||||
public struct Unidentified<EntityDescription: JSONAPI.EntityDescription>: 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 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 {
|
||||
@@ -48,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()) }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,30 +5,59 @@
|
||||
// Created by Mathew Polzin on 8/31/18.
|
||||
//
|
||||
|
||||
public protocol RelationshipType: Codable {}
|
||||
public protocol RelationshipType {
|
||||
associatedtype LinksType
|
||||
associatedtype MetaType
|
||||
|
||||
var links: LinksType { get }
|
||||
var meta: MetaType { get }
|
||||
}
|
||||
|
||||
/// An Entity relationship that can be encoded to or decoded from
|
||||
/// a JSON API "Resource Linkage."
|
||||
/// See https://jsonapi.org/format/#document-resource-object-linkage
|
||||
/// A convenient typealias might make your code much more legible: `One<EntityDescription>`
|
||||
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: RelationshipType, Equatable {
|
||||
public struct ToOneRelationship<Identifiable: JSONAPI.Identifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
|
||||
|
||||
public let id: Relatable.WrappedIdentifier
|
||||
public let id: Identifiable.Identifier
|
||||
|
||||
public init(id: Relatable.WrappedIdentifier) {
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
|
||||
public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) {
|
||||
self.id = id
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
|
||||
public init(entity: Entity<Relatable.Description, Relatable.Identifier>) {
|
||||
id = entity.id
|
||||
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(id: Identifiable.Identifier) {
|
||||
self.init(id: id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Optional<Relatable.Identifier> {
|
||||
public init(entity: Entity<Relatable.Description, Relatable.Identifier>?) {
|
||||
id = entity?.id
|
||||
extension ToOneRelationship {
|
||||
public init<E: EntityType>(entity: E, meta: MetaType, links: LinksType) where E.Id == Identifiable.Identifier {
|
||||
self.init(id: entity.id, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init<E: EntityType>(entity: E) where E.Id == Identifiable.Identifier {
|
||||
self.init(id: entity.id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Identifiable: OptionalRelatable {
|
||||
public init<E: EntityType>(entity: E?, meta: MetaType, links: LinksType) where E.Id == Identifiable.Wrapped.Identifier {
|
||||
self.init(id: entity?.id, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init<E: EntityType>(entity: E?) where E.Id == Identifiable.Wrapped.Identifier {
|
||||
self.init(id: entity?.id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,73 +65,105 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Optional<Relata
|
||||
/// a JSON API "Resource Linkage."
|
||||
/// See https://jsonapi.org/format/#document-resource-object-linkage
|
||||
/// A convenient typealias might make your code much more legible: `Many<EntityDescription>`
|
||||
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType, Equatable {
|
||||
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
|
||||
|
||||
public let ids: [Relatable.Identifier]
|
||||
|
||||
public init<T: JSONAPI.Relatable>(relationships: [ToOneRelationship<T>]) where T.WrappedIdentifier == Relatable.Identifier {
|
||||
ids = relationships.map { $0.id }
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
|
||||
public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) {
|
||||
self.ids = ids
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
|
||||
private init() {
|
||||
ids = []
|
||||
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier {
|
||||
ids = pointers.map { $0.id }
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
|
||||
|
||||
public init<E: EntityType>(entities: [E], meta: MetaType, links: LinksType) where E.Id == Relatable.Identifier {
|
||||
self.init(ids: entities.map { $0.id }, meta: meta, links: links)
|
||||
}
|
||||
|
||||
private init(meta: MetaType, links: LinksType) {
|
||||
self.init(ids: [], meta: meta, links: links)
|
||||
}
|
||||
|
||||
public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship {
|
||||
return ToManyRelationship(meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
|
||||
public init(ids: [Relatable.Identifier]) {
|
||||
self.init(ids: ids, meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>]) where T.Identifier == Relatable.Identifier {
|
||||
self.init(pointers: pointers, meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public static var none: ToManyRelationship {
|
||||
return .init()
|
||||
return .none(withMeta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init<E: EntityType>(entities: [E]) where E.Id == Relatable.Identifier {
|
||||
self.init(entities: entities, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship {
|
||||
public init(entities: [Entity<Relatable.Description, Relatable.Identifier>]) {
|
||||
ids = entities.map { $0.id }
|
||||
}
|
||||
public protocol Identifiable: JSONTyped {
|
||||
associatedtype Identifier: Equatable
|
||||
}
|
||||
|
||||
/// The WrappedRelatable (a.k.a OptionalRelatable) protocol
|
||||
/// describes Optional<T: Relatable> and Relatable types.
|
||||
public protocol WrappedRelatable: Codable, Equatable {
|
||||
associatedtype Description: EntityDescription
|
||||
associatedtype Identifier: JSONAPI.IdType
|
||||
associatedtype WrappedIdentifier: Codable, Equatable
|
||||
}
|
||||
public typealias OptionalRelatable = WrappedRelatable
|
||||
|
||||
/// The Relatable protocol describes anything that
|
||||
/// has an IdType Identifier
|
||||
public protocol Relatable: WrappedRelatable {}
|
||||
|
||||
extension Entity: Relatable, WrappedRelatable where Identifier: JSONAPI.IdType {
|
||||
public typealias WrappedIdentifier = Identifier
|
||||
public protocol Relatable: Identifiable where Identifier: JSONAPI.IdType {
|
||||
}
|
||||
|
||||
extension Optional: OptionalRelatable where Wrapped: Relatable {
|
||||
public typealias Description = Wrapped.Description
|
||||
public typealias Identifier = Wrapped.Identifier
|
||||
public typealias WrappedIdentifier = Identifier?
|
||||
/// OptionalRelatable just describes an Optional
|
||||
/// with a Reltable Wrapped type.
|
||||
public protocol OptionalRelatable: Identifiable where Identifier == Wrapped.Identifier? {
|
||||
associatedtype Wrapped: JSONAPI.Relatable
|
||||
}
|
||||
|
||||
extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable {
|
||||
public typealias Identifier = Wrapped.Identifier?
|
||||
|
||||
public static var type: String { return Wrapped.type }
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
private enum ResourceLinkageCodingKeys: String, CodingKey {
|
||||
case data = "data"
|
||||
case meta = "meta"
|
||||
case links = "links"
|
||||
}
|
||||
private enum ResourceIdentifierCodingKeys: String, CodingKey {
|
||||
case id = "id"
|
||||
case entityType = "type"
|
||||
}
|
||||
|
||||
public enum JSONAPIEncodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String)
|
||||
case illegalEncoding(String)
|
||||
case illegalDecoding(String)
|
||||
case missingOrMalformedMetadata
|
||||
case missingOrMalformedLinks
|
||||
}
|
||||
|
||||
extension ToOneRelationship {
|
||||
extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
|
||||
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
}
|
||||
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
|
||||
// A little trickery follows. If the id is nil, the
|
||||
// container.decode(Identifier.self) will fail even if Identifier
|
||||
// is Optional. However, we can check if decoding nil
|
||||
@@ -110,7 +171,7 @@ extension ToOneRelationship {
|
||||
// type at which point we can store nil in `id`.
|
||||
let anyNil: Any? = nil
|
||||
if try container.decodeNil(forKey: .data),
|
||||
let val = anyNil as? Relatable.WrappedIdentifier {
|
||||
let val = anyNil as? Identifiable.Identifier {
|
||||
id = val
|
||||
return
|
||||
}
|
||||
@@ -119,11 +180,11 @@ extension ToOneRelationship {
|
||||
|
||||
let type = try identifier.decode(String.self, forKey: .entityType)
|
||||
|
||||
guard type == Relatable.Description.type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.Description.type, found: type)
|
||||
guard type == Identifiable.type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.type, found: type)
|
||||
}
|
||||
|
||||
id = try identifier.decode(Relatable.WrappedIdentifier.self, forKey: .id)
|
||||
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@@ -133,17 +194,46 @@ extension ToOneRelationship {
|
||||
try container.encodeNil(forKey: .data)
|
||||
}
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
|
||||
// If id is nil, instead of {id: , type: } we will just
|
||||
// encode `null`
|
||||
let anyNil: Any? = nil
|
||||
let nilId = anyNil as? Identifiable.Identifier
|
||||
guard id != nilId else {
|
||||
try container.encodeNil(forKey: .data)
|
||||
return
|
||||
}
|
||||
|
||||
var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
|
||||
|
||||
try identifier.encode(id, forKey: .id)
|
||||
try identifier.encode(Relatable.Description.type, forKey: .entityType)
|
||||
try identifier.encode(id.rawValue, forKey: .id)
|
||||
try identifier.encode(Identifiable.type, forKey: .entityType)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship {
|
||||
extension ToManyRelationship: Codable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
|
||||
|
||||
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
}
|
||||
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
|
||||
var identifiers = try container.nestedUnkeyedContainer(forKey: .data)
|
||||
|
||||
var newIds = [Relatable.Identifier]()
|
||||
@@ -152,24 +242,33 @@ extension ToManyRelationship {
|
||||
|
||||
let type = try identifier.decode(String.self, forKey: .entityType)
|
||||
|
||||
guard type == Relatable.Description.type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.Description.type, found: type)
|
||||
guard type == Relatable.type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.type, found: type)
|
||||
}
|
||||
|
||||
newIds.append(try identifier.decode(Relatable.Identifier.self, forKey: .id))
|
||||
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
|
||||
}
|
||||
ids = newIds
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self)
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
|
||||
var identifiers = container.nestedUnkeyedContainer(forKey: .data)
|
||||
|
||||
for id in ids {
|
||||
var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
|
||||
|
||||
try identifier.encode(id, forKey: .id)
|
||||
try identifier.encode(Relatable.Description.type, forKey: .entityType)
|
||||
try identifier.encode(id.rawValue, forKey: .id)
|
||||
try identifier.encode(Relatable.type, forKey: .entityType)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,5 +279,5 @@ extension ToOneRelationship: CustomStringConvertible {
|
||||
}
|
||||
|
||||
extension ToManyRelationship: CustomStringConvertible {
|
||||
public var description: String { return "Relationship(\(String(describing: ids)))" }
|
||||
public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" }
|
||||
}
|
||||
|
||||
@@ -5,15 +5,24 @@
|
||||
// Created by Mathew Polzin on 11/17/18.
|
||||
//
|
||||
|
||||
/// A Transformer simply defines a static function that transforms a value.
|
||||
public protocol Transformer {
|
||||
associatedtype From
|
||||
associatedtype To
|
||||
|
||||
static func transform(_ from: From) throws -> To
|
||||
static func transform(_ value: From) throws -> To
|
||||
}
|
||||
|
||||
public enum IdentityTransformer<T>: Transformer {
|
||||
public static func transform(_ from: T) throws -> T { return from }
|
||||
/// ReversibleTransformers define a function that reverses the transform
|
||||
/// operation.
|
||||
public protocol ReversibleTransformer: Transformer {
|
||||
static func reverse(_ value: To) throws -> From
|
||||
}
|
||||
|
||||
/// The IdentityTransformer does not perform any transformation on a value.
|
||||
public enum IdentityTransformer<T>: ReversibleTransformer {
|
||||
public static func transform(_ value: T) throws -> T { return value }
|
||||
public static func reverse(_ value: T) throws -> T { return value }
|
||||
}
|
||||
|
||||
// MARK: - Validator
|
||||
@@ -22,10 +31,19 @@ public enum IdentityTransformer<T>: Transformer {
|
||||
/// is passed to it but it does not change the type of the value. Any
|
||||
/// Transformer will perform validation in one sense so a Validator is
|
||||
/// really just semantic sugar (it can provide clarity in its use).
|
||||
public protocol Validator: Transformer where From == To {}
|
||||
/// To enforce the semantics, any change of the value in your implementation
|
||||
/// of `Validator.transform()` will be ignored.
|
||||
public protocol Validator: ReversibleTransformer where From == To {
|
||||
}
|
||||
|
||||
extension Validator {
|
||||
public static func reverse(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
|
||||
public static func validate(_ value: To) throws -> To {
|
||||
return try transform(value)
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user