mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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,9 +11,9 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>
|
||||
typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let singleDogDocument = SingleDogDocument(body: SingleResourceBody(entity: dogFromCode))
|
||||
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none)
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
@@ -24,13 +24,13 @@ let dogFromData = dogResponse.body.primaryData?.value
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
||||
let houses = [House(), House()]
|
||||
let houses = [House(attributes: .none, relationships: .none, meta: .none, links: .none), House(attributes: .none, relationships: .none, meta: .none, links: .none)]
|
||||
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
|
||||
|
||||
typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, UnknownJSONAPIError>
|
||||
typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
|
||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||
let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes))
|
||||
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
@@ -23,8 +23,10 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Entity typealias for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, String>
|
||||
// MARK: - typealiases for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
|
||||
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
|
||||
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
|
||||
|
||||
// MARK: - A few resource objects (entities)
|
||||
public enum PersonDescription: EntityDescription {
|
||||
@@ -46,11 +48,11 @@ public enum PersonDescription: EntityDescription {
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let friends: ToManyRelationship<Person>
|
||||
public let dogs: ToManyRelationship<Dog>
|
||||
public let home: ToOneRelationship<House>
|
||||
public let friends: ToMany<Person>
|
||||
public let dogs: ToMany<Dog>
|
||||
public let home: ToOne<House>
|
||||
|
||||
public init(friends: ToManyRelationship<Person>, dogs: ToManyRelationship<Dog>, home: ToOneRelationship<House>) {
|
||||
public init(friends: ToMany<Person>, dogs: ToMany<Dog>, home: ToOne<House>) {
|
||||
self.friends = friends
|
||||
self.dogs = dogs
|
||||
self.home = home
|
||||
@@ -60,9 +62,9 @@ public enum PersonDescription: EntityDescription {
|
||||
|
||||
public typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
public extension Entity where Description == PersonDescription, EntityRawIdType == String {
|
||||
public extension Entity where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||
self = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
|
||||
self = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,9 +81,9 @@ public enum DogDescription: EntityDescription {
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let owner: ToOneRelationship<Person?>
|
||||
public let owner: ToOne<Person?>
|
||||
|
||||
public init(owner: ToOneRelationship<Person?>) {
|
||||
public init(owner: ToOne<Person?>) {
|
||||
self.owner = owner
|
||||
}
|
||||
}
|
||||
@@ -89,13 +91,13 @@ public enum DogDescription: EntityDescription {
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, EntityRawIdType == String {
|
||||
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
public init(name: String, owner: Person?) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Id) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: 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"]),
|
||||
|
||||
@@ -29,17 +29,17 @@ To create an Xcode project for JSONAPI, run
|
||||
### 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.
|
||||
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
|
||||
@@ -47,71 +47,40 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
- [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).
|
||||
- [x] Make `TransformedAttribute` a Functor.
|
||||
|
||||
### JSONAPITestLib
|
||||
#### Entity Validator
|
||||
- [x] Disallow optional array in `Attribute` (should be empty array, not `null`).
|
||||
- [x] Only allow `TransformedAttribute` and its derivatives as stored properties within `Attributes` struct. Computed properties can still be any type because they do not get encoded or decoded.
|
||||
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
|
||||
|
||||
### Potential Improvements
|
||||
- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources.
|
||||
- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies.
|
||||
- [ ] Property-based testing (using `SwiftCheck`).
|
||||
- [ ] Error or warning if an included entity is not related to a primary entity or another included entity (Turned off or at least not throwing by default).
|
||||
|
||||
## Usage
|
||||
|
||||
In this documentation, in order to draw attention to the difference between the `JSONAPI` framework (this Swift library) and the **JSON API Spec** (the specification this library helps you follow), the specification will consistently be referred to below as simply the **SPEC**.
|
||||
|
||||
### `EntityDescription`
|
||||
### `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:
|
||||
|
||||
@@ -167,45 +136,61 @@ This readme doesn't go into detail on the **SPEC**, but the following *Resource
|
||||
}
|
||||
```
|
||||
|
||||
### `Entity`
|
||||
### `JSONAPI.Entity`
|
||||
|
||||
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 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 `Entity` needs to be specialized on two generic types. The first is the `EntityDescription` described above. 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>>`.
|
||||
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, String>
|
||||
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 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 `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 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 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`:
|
||||
@@ -218,7 +203,7 @@ typealias Relationships = NoRelationships
|
||||
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`, `ValidatedAttribute`, or `TransformedAttribute` `struct`.
|
||||
|
||||
@@ -288,13 +273,30 @@ public var fullName: Attribute<String> {
|
||||
}
|
||||
```
|
||||
|
||||
### `JSONAPIDocument`
|
||||
### 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 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:
|
||||
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)
|
||||
```
|
||||
@@ -315,7 +317,7 @@ You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody`
|
||||
|
||||
#### `MetaType`
|
||||
|
||||
The second generic type of a `JSONAPIDocument` is a `Meta`. This structure is entirely open-ended. As an example, the JSON API document could, as an example, 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": {
|
||||
@@ -339,9 +341,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`
|
||||
|
||||
@@ -351,11 +351,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 **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`):
|
||||
```
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// APIDescription.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/1/18.
|
||||
//
|
||||
|
||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||
public protocol APIDescriptionType: Codable, Equatable {
|
||||
associatedtype Meta
|
||||
}
|
||||
|
||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
|
||||
public let version: String
|
||||
public let meta: Meta
|
||||
}
|
||||
|
||||
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
|
||||
public typealias Meta = NoMetadata
|
||||
|
||||
public init() {}
|
||||
|
||||
public static var none: NoAPIDescription { return .init() }
|
||||
|
||||
public var description: String { return "No JSON:API Object" }
|
||||
}
|
||||
|
||||
extension APIDescription {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
|
||||
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
|
||||
|
||||
if let metaVal = NoMetadata() as? Meta {
|
||||
meta = metaVal
|
||||
} else {
|
||||
meta = try container.decode(Meta.self, forKey: .meta)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(version, forKey: .version)
|
||||
|
||||
if Meta.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,10 @@ public protocol JSONAPIDocument: Codable, Equatable {
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype APIDescription: APIDescriptionType
|
||||
associatedtype Error: JSONAPIError
|
||||
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, Error>.Body
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
|
||||
var body: Body { get }
|
||||
}
|
||||
@@ -24,11 +25,19 @@ public protocol JSONAPIDocument: Codable, Equatable {
|
||||
/// API uses snake case, you will want to use
|
||||
/// a conversion such as the one offerred by the
|
||||
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
/// The JSON API Spec calls this the JSON:API Object. It contains version
|
||||
/// and metadata information about the API itself.
|
||||
public let apiDescription: APIDescription
|
||||
|
||||
/// The Body of the Document. This body is either one or more errors
|
||||
/// with links and metadata attempted to parse but not guaranteed or
|
||||
/// it is a successful data struct containing all the primary and
|
||||
/// included resources, the metadata, and the links that this
|
||||
/// document type specifies.
|
||||
public let body: Body
|
||||
// public let jsonApi: APIDescription?
|
||||
|
||||
public enum Body: Equatable {
|
||||
case errors([Error], meta: MetaType?, links: LinksType?)
|
||||
@@ -57,8 +66,13 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
|
||||
return errors
|
||||
}
|
||||
|
||||
public var data: Data? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data
|
||||
}
|
||||
|
||||
public var primaryData: PrimaryResourceBody? {
|
||||
public var primaryResource: PrimaryResourceBody? {
|
||||
guard case let .data(data) = self else { return nil }
|
||||
return data.primary
|
||||
}
|
||||
@@ -91,57 +105,90 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
}
|
||||
}
|
||||
|
||||
public init(errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
|
||||
self.apiDescription = apiDescription
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
extension Document where IncludeType == NoIncludes {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: links))
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: .none, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: links))
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where LinksType == NoLinks {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: .none))
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, LinksType == NoLinks {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: meta, links: .none))
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType) {
|
||||
self.init(apiDescription: apiDescription, body: body, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata {
|
||||
public init(body: PrimaryResourceBody, links: LinksType) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: links))
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, links: LinksType) {
|
||||
self.init(apiDescription: apiDescription, body: body, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.body = .data(.init(primary: body, includes: includes, meta: .none, links: .none))
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: includes, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: PrimaryResourceBody) {
|
||||
self.body = .data(.init(primary: body, includes: .none, meta: .none, links: .none))
|
||||
public init(apiDescription: APIDescription, body: PrimaryResourceBody) {
|
||||
self.init(apiDescription: apiDescription, body: body, includes: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
|
||||
self.init(apiDescription: .none, body: body, includes: includes)
|
||||
}
|
||||
}
|
||||
|
||||
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
|
||||
public init(body: PrimaryResourceBody) {
|
||||
self.init(apiDescription: .none, body: body)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
extension Document {
|
||||
private enum RootCodingKeys: String, CodingKey {
|
||||
case data
|
||||
@@ -155,6 +202,12 @@ extension Document {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
@@ -242,6 +295,10 @@ extension Document {
|
||||
try container.encode(data.links, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
if APIDescription.self != NoAPIDescription.self {
|
||||
try container.encode(apiDescription, forKey: .jsonapi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -115,4 +115,11 @@ extension Includes where I: _Poly6 {
|
||||
}
|
||||
|
||||
// MARK: - 7 includes
|
||||
public typealias Include7 = Poly7
|
||||
extension Includes where I: _Poly7 {
|
||||
public subscript(_ lookup: I.G.Type) -> [I.G] {
|
||||
return values.compactMap { $0.g }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 8 includes
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -21,6 +21,17 @@ public protocol JSONAPIURL: Codable, Equatable {}
|
||||
public struct Link<URL: JSONAPI.JSONAPIURL, Meta: JSONAPI.Meta>: Equatable, Codable {
|
||||
public let url: URL
|
||||
public let meta: Meta
|
||||
|
||||
public init(url: URL, meta: Meta) {
|
||||
self.url = url
|
||||
self.meta = meta
|
||||
}
|
||||
}
|
||||
|
||||
extension Link where Meta == NoMetadata {
|
||||
public init(url: URL) {
|
||||
self.init(url: url, meta: .none)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Link {
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
public protocol AttributeType: Codable {
|
||||
}
|
||||
|
||||
// MARK: TransformedAttribute
|
||||
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
|
||||
private let rawValue: RawValue
|
||||
let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
|
||||
@@ -19,6 +21,15 @@ public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Trans
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ValidatedAttribute
|
||||
/// A ValidatedAttribute does not transform its raw value, but it throws
|
||||
/// an error if the raw value does not match expectations.
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
|
||||
// MARK: Attribute
|
||||
/// An Attribute simply represents a type that can be encoded and decoded.
|
||||
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
|
||||
|
||||
extension TransformedAttribute where Transformer: ReversibleTransformer {
|
||||
public init(transformedValue: Transformer.To) throws {
|
||||
self.value = transformedValue
|
||||
@@ -43,10 +54,6 @@ extension TransformedAttribute where Transformer == IdentityTransformer<RawValue
|
||||
}
|
||||
}
|
||||
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
|
||||
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformedAttribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -73,15 +80,7 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,23 +26,31 @@ public struct NoAttributes: Attributes {
|
||||
public static var none: NoAttributes { return .init() }
|
||||
}
|
||||
|
||||
/// Something that is JSONTyped provides a String representation
|
||||
/// of its type.
|
||||
public protocol JSONTyped {
|
||||
static var type: String { get }
|
||||
}
|
||||
|
||||
/// An `EntityProxyDescription` is an `EntityDescription`
|
||||
/// without Codable conformance.
|
||||
public protocol EntityProxyDescription: JSONTyped {
|
||||
associatedtype Attributes: Equatable
|
||||
associatedtype Relationships: Equatable
|
||||
}
|
||||
|
||||
/// An `EntityDescription` describes a JSON API
|
||||
/// Resource Object. The Resource Object
|
||||
/// itself is encoded and decoded as an
|
||||
/// `Entity`, which gets specialized on an
|
||||
/// `EntityDescription`.
|
||||
public protocol EntityDescription {
|
||||
associatedtype Attributes: JSONAPI.Attributes
|
||||
associatedtype Relationships: JSONAPI.Relationships
|
||||
|
||||
static var type: String { get }
|
||||
}
|
||||
public protocol EntityDescription: EntityProxyDescription where Attributes: JSONAPI.Attributes, Relationships: JSONAPI.Relationships {}
|
||||
|
||||
/// EntityProxy is a protocol that can be used to create
|
||||
/// types that _act_ like Entities but cannot be encoded
|
||||
/// or decoded as Entities.
|
||||
public protocol EntityProxy: Equatable {
|
||||
associatedtype Description: EntityDescription
|
||||
public protocol EntityProxy: Equatable, JSONTyped {
|
||||
associatedtype Description: EntityProxyDescription
|
||||
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
|
||||
|
||||
typealias Id = JSONAPI.Id<EntityRawIdType, Self>
|
||||
@@ -71,7 +79,9 @@ extension EntityProxy {
|
||||
/// EntityType is the protocol that Entity conforms to. This
|
||||
/// protocol lets other types accept any Entity as a generic
|
||||
/// specialization.
|
||||
public protocol EntityType: EntityProxy, PrimaryResource {
|
||||
public protocol EntityType: EntityProxy, PrimaryResource where Description: EntityDescription {
|
||||
associatedtype Meta: JSONAPI.Meta
|
||||
associatedtype Links: JSONAPI.Links
|
||||
}
|
||||
|
||||
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
|
||||
@@ -80,7 +90,11 @@ public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdT
|
||||
/// encoded to or decoded from a JSON API
|
||||
/// "Resource Object."
|
||||
/// See https://jsonapi.org/format/#document-resource-objects
|
||||
public struct Entity<Description: JSONAPI.EntityDescription, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
|
||||
public struct Entity<Description: JSONAPI.EntityDescription, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
|
||||
|
||||
public typealias Meta = MetaType
|
||||
public typealias Links = LinksType
|
||||
|
||||
/// The `Entity`'s Id. This can be of type `Unidentified` if
|
||||
/// the entity is being created clientside and the
|
||||
/// server is being asked to create a unique Id. Otherwise,
|
||||
@@ -92,17 +106,24 @@ public struct Entity<Description: JSONAPI.EntityDescription, EntityRawIdType: JS
|
||||
|
||||
/// The JSON API compliant relationships of this `Entity`.
|
||||
public let relationships: Description.Relationships
|
||||
|
||||
/// Any additional metadata packaged with the entity.
|
||||
public let meta: MetaType
|
||||
|
||||
/// Links related to the entity.
|
||||
public let links: LinksType
|
||||
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) {
|
||||
self.id = id
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity: IdentifiableEntityType, Relatable, WrappedRelatable where EntityRawIdType: JSONAPI.RawIdType {
|
||||
extension Entity: Identifiable, IdentifiableEntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {
|
||||
public typealias Identifier = Entity.Id
|
||||
public typealias WrappedIdentifier = Identifier
|
||||
}
|
||||
|
||||
extension Entity: CustomStringConvertible {
|
||||
@@ -113,69 +134,297 @@ extension Entity: CustomStringConvertible {
|
||||
|
||||
// MARK: Convenience initializers
|
||||
extension Entity where EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) {
|
||||
self.id = Entity.Id()
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) {
|
||||
self.id = .unidentified
|
||||
self.attributes = attributes
|
||||
self.relationships = relationships
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
extension Entity where Description.Attributes == NoAttributes {
|
||||
public init(id: Entity.Id, relationships: Description.Relationships, meta: MetaType, links: LinksType) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, MetaType == NoMetadata {
|
||||
public init(id: Entity.Id, relationships: Description.Relationships, links: LinksType) {
|
||||
self.init(id: id, relationships: relationships, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, LinksType == NoLinks {
|
||||
public init(id: Entity.Id, relationships: Description.Relationships, meta: MetaType) {
|
||||
self.init(id: id, relationships: relationships, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(id: Entity.Id, relationships: Description.Relationships) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: relationships)
|
||||
self.init(id: id, relationships: relationships, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType {
|
||||
public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) {
|
||||
self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType {
|
||||
public init(relationships: Description.Relationships, links: LinksType) {
|
||||
self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType {
|
||||
public init(relationships: Description.Relationships, meta: MetaType) {
|
||||
self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType {
|
||||
public init(relationships: Description.Relationships) {
|
||||
self.init(attributes: NoAttributes(), relationships: relationships)
|
||||
self.init(attributes: NoAttributes(), relationships: relationships, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, EntityRawIdType == Unidentified {
|
||||
public init(relationships: Description.Relationships, meta: MetaType, links: LinksType) {
|
||||
self.init(attributes: NoAttributes(), relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, meta: MetaType, links: LinksType) {
|
||||
self.init(id: id, attributes: attributes, relationships: NoRelationships(), meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, MetaType == NoMetadata {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, links: LinksType) {
|
||||
self.init(id: id, attributes: attributes, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, LinksType == NoLinks {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, meta: MetaType) {
|
||||
self.init(id: id, attributes: attributes, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes) {
|
||||
self.init(id: id, attributes: attributes, relationships: NoRelationships())
|
||||
self.init(id: id, attributes: attributes, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, links: LinksType) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, meta: MetaType) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships())
|
||||
self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, meta: MetaType, links: LinksType) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, links: LinksType) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, meta: MetaType) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships(), meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes) {
|
||||
self.init(attributes: attributes, relationships: NoRelationships())
|
||||
self.init(attributes: attributes, relationships: NoRelationships(), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships {
|
||||
public init(id: Entity.Id, meta: MetaType, links: LinksType) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata {
|
||||
public init(id: Entity.Id, links: LinksType) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks {
|
||||
public init(id: Entity.Id, meta: MetaType) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(id: Entity.Id) {
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships())
|
||||
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
|
||||
public init() {
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelationships())
|
||||
public init(meta: MetaType, links: LinksType) {
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType {
|
||||
public init(links: LinksType) {
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType {
|
||||
public init(meta: MetaType) {
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType {
|
||||
public init() {
|
||||
self.init(attributes: NoAttributes(), relationships: NoRelationships(), meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) {
|
||||
self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, links: LinksType) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where LinksType == NoLinks {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) {
|
||||
self.init(id: id, attributes: attributes, relationships: relationships, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where LinksType == NoLinks, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where LinksType == NoLinks, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.init(id: id, attributes: attributes, relationships: relationships, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity where MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == Unidentified {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
self.init(attributes: attributes, relationships: relationships, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// MARK: Pointer for Relationships use.
|
||||
public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
|
||||
|
||||
/// An Entity.Pointer is a `ToOneRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference an Entity so that
|
||||
/// other Entities' Relationships can be built up from it.
|
||||
public typealias Pointer = ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
/// Entity.Pointers is a `ToManyRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a bunch of Entities so
|
||||
/// that other Entities' Relationships can be built up from them.
|
||||
public typealias Pointers = ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
/// Get a pointer to this entity that can be used as a
|
||||
/// relationship to another entity.
|
||||
public var pointer: ToOneRelationship<Entity> {
|
||||
return ToOneRelationship(entity: self)
|
||||
public var pointer: Pointer {
|
||||
return Pointer(entity: self)
|
||||
}
|
||||
|
||||
public func pointer<MType: JSONAPI.Meta, LType: JSONAPI.Links>(withMeta meta: MType, links: LType) -> ToOneRelationship<Entity, MType, LType> {
|
||||
return ToOneRelationship(entity: self, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Identifying Unidentified Entities
|
||||
public extension Entity where EntityRawIdType == Unidentified {
|
||||
/// Create a new Entity from this one with a newly created
|
||||
/// unique Id of the given type.
|
||||
public func identified<RawIdType: CreatableRawIdType>(byType: RawIdType.Type) -> Entity<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
|
||||
/// Create a new Entity from this one with the given Id.
|
||||
public func identified<RawIdType: JSONAPI.RawIdType>(by id: RawIdType) -> Entity<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(id: Entity<Description, MetaType, LinksType, RawIdType>.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
public extension Entity where EntityRawIdType: CreatableRawIdType {
|
||||
/// Create a copy of this Entity with a new unique Id.
|
||||
public func withNewIdentifier() -> Entity {
|
||||
return Entity(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,8 +448,19 @@ public extension EntityProxy {
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
|
||||
// Implementation Note: Handles Transform that returns optional
|
||||
// type.
|
||||
return attributes[keyPath: path].flatMap { $0.value }
|
||||
}
|
||||
|
||||
/// Access the computed attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T>(_ path: KeyPath<Description.Attributes, T>) -> T {
|
||||
// Implementation Note: Handles attributes that are not
|
||||
// AttributeType. These should only exist as computed properties.
|
||||
return attributes[keyPath: path]
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Relationship Access
|
||||
@@ -208,16 +468,41 @@ public extension EntityProxy {
|
||||
/// Access to an Id of a `ToOneRelationship`.
|
||||
/// This allows you to write `entity ~> \.other` instead
|
||||
/// of `entity.relationships.other.id`.
|
||||
public static func ~><OtherEntity: OptionalRelatable>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity>>) -> OtherEntity.WrappedIdentifier {
|
||||
public static func ~><OtherEntity: Identifiable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>>) -> OtherEntity.Identifier {
|
||||
return entity.relationships[keyPath: path].id
|
||||
}
|
||||
|
||||
/// Access to an Id of an optional `ToOneRelationship`.
|
||||
/// This allows you to write `entity ~> \.other` instead
|
||||
/// of `entity.relationships.other?.id`.
|
||||
public static func ~><OtherEntity: OptionalRelatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier {
|
||||
// Implementation Note: This signature applies to `ToOneRelationship<E?, _, _>?`
|
||||
// whereas the one below applies to `ToOneRelationship<E, _, _>?`
|
||||
return entity.relationships[keyPath: path]?.id
|
||||
}
|
||||
|
||||
/// Access to an Id of an optional `ToOneRelationship`.
|
||||
/// This allows you to write `entity ~> \.other` instead
|
||||
/// of `entity.relationships.other?.id`.
|
||||
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier? {
|
||||
// Implementation Note: This signature applies to `ToOneRelationship<E, _, _>?`
|
||||
// whereas the one above applies to `ToOneRelationship<E?, _, _>?`
|
||||
return entity.relationships[keyPath: path]?.id
|
||||
}
|
||||
|
||||
/// Access to all Ids of a `ToManyRelationship`.
|
||||
/// This allows you to write `entity ~> \.others` instead
|
||||
/// of `entity.relationships.others.ids`.
|
||||
public static func ~><OtherEntity: Relatable>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity>>) -> [OtherEntity.Identifier] {
|
||||
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>>) -> [OtherEntity.Identifier] {
|
||||
return entity.relationships[keyPath: path].ids
|
||||
}
|
||||
|
||||
/// Access to all Ids of an optional `ToManyRelationship`.
|
||||
/// This allows you to write `entity ~> \.others` instead
|
||||
/// of `entity.relationships.others?.ids`.
|
||||
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>?>) -> [OtherEntity.Identifier]? {
|
||||
return entity.relationships[keyPath: path]?.ids
|
||||
}
|
||||
}
|
||||
|
||||
infix operator ~>
|
||||
@@ -228,6 +513,8 @@ private enum ResourceObjectCodingKeys: String, CodingKey {
|
||||
case id = "id"
|
||||
case attributes = "attributes"
|
||||
case relationships = "relationships"
|
||||
case meta = "meta"
|
||||
case links = "links"
|
||||
}
|
||||
|
||||
public extension Entity {
|
||||
@@ -247,6 +534,14 @@ public extension Entity {
|
||||
if Description.Relationships.self != NoRelationships.self {
|
||||
try container.encode(relationships, forKey: .relationships)
|
||||
}
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -265,5 +560,9 @@ public extension Entity {
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
relationships = try (NoRelationships() as? Description.Relationships) ?? container.decode(Description.Relationships.self, forKey: .relationships)
|
||||
|
||||
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
|
||||
|
||||
links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,13 +34,31 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible {
|
||||
public var description: String { return "Unidentified" }
|
||||
}
|
||||
|
||||
public protocol MaybeId: Codable {
|
||||
associatedtype EntityType: JSONAPI.EntityProxy
|
||||
public protocol OptionalId: Codable {
|
||||
associatedtype IdentifiableType: JSONAPI.JSONTyped
|
||||
associatedtype RawType: MaybeRawId
|
||||
|
||||
var rawValue: RawType { get }
|
||||
init(rawValue: RawType)
|
||||
}
|
||||
|
||||
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
|
||||
var rawValue: RawType { get }
|
||||
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
|
||||
|
||||
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
|
||||
extension Optional: OptionalId where Wrapped: IdType {
|
||||
public typealias IdentifiableType = Wrapped.IdentifiableType
|
||||
public typealias RawType = Wrapped.RawType?
|
||||
|
||||
public var rawValue: Wrapped.RawType? {
|
||||
guard case .some(let value) = self else {
|
||||
return nil
|
||||
}
|
||||
return value.rawValue
|
||||
}
|
||||
|
||||
public init(rawValue: Wrapped.RawType?) {
|
||||
self = rawValue.map { Wrapped(rawValue: $0) }
|
||||
}
|
||||
}
|
||||
|
||||
public extension IdType {
|
||||
@@ -53,7 +71,7 @@ public protocol CreatableIdType: IdType {
|
||||
|
||||
/// An Entity ID. These IDs can be encoded to or decoded from
|
||||
/// JSON API IDs.
|
||||
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable, Equatable, MaybeId {
|
||||
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
|
||||
|
||||
public let rawValue: RawType
|
||||
|
||||
@@ -63,7 +81,8 @@ public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable,
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
rawValue = try container.decode(RawType.self)
|
||||
let rawValue = try container.decode(RawType.self)
|
||||
self.init(rawValue: rawValue)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
@@ -72,7 +91,11 @@ public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityProxy>: Codable,
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
|
||||
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {
|
||||
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
|
||||
return Id(rawValue: rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Id: CreatableIdType where RawType: CreatableRawIdType {
|
||||
public init() {
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
// Created by Mathew Polzin on 11/22/18.
|
||||
//
|
||||
|
||||
import Result
|
||||
|
||||
/// Poly is a protocol to which types that
|
||||
/// are polymorphic belong to. Specifically,
|
||||
/// Poly1, Poly2, Poly3, etc. types conform
|
||||
@@ -28,7 +26,7 @@ private func decode<Entity: JSONAPI.EntityType>(_ type: Entity.Type, from contai
|
||||
} catch (let err) {
|
||||
ret = .failure(DecodingError.typeMismatch(Entity.Description.self,
|
||||
.init(codingPath: container.codingPath,
|
||||
debugDescription: err.localizedDescription,
|
||||
debugDescription: String(describing: err),
|
||||
underlyingError: err)))
|
||||
}
|
||||
return ret
|
||||
@@ -646,3 +644,158 @@ extension Poly6: CustomStringConvertible {
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 7 types
|
||||
public protocol _Poly7: _Poly6 {
|
||||
associatedtype G: EntityType
|
||||
var g: G? { get }
|
||||
|
||||
init(_ g: G)
|
||||
}
|
||||
|
||||
public extension _Poly7 {
|
||||
subscript(_ lookup: G.Type) -> G? {
|
||||
return g
|
||||
}
|
||||
}
|
||||
|
||||
public enum Poly7<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType, G: EntityType>: _Poly7 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
case c(C)
|
||||
case d(D)
|
||||
case e(E)
|
||||
case f(F)
|
||||
case g(G)
|
||||
|
||||
public var a: A? {
|
||||
guard case let .a(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ a: A) {
|
||||
self = .a(a)
|
||||
}
|
||||
|
||||
public var b: B? {
|
||||
guard case let .b(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ b: B) {
|
||||
self = .b(b)
|
||||
}
|
||||
|
||||
public var c: C? {
|
||||
guard case let .c(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ c: C) {
|
||||
self = .c(c)
|
||||
}
|
||||
|
||||
public var d: D? {
|
||||
guard case let .d(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ d: D) {
|
||||
self = .d(d)
|
||||
}
|
||||
|
||||
public var e: E? {
|
||||
guard case let .e(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ e: E) {
|
||||
self = .e(e)
|
||||
}
|
||||
|
||||
public var f: F? {
|
||||
guard case let .f(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ f: F) {
|
||||
self = .f(f)
|
||||
}
|
||||
|
||||
public var g: G? {
|
||||
guard case let .g(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(_ g: G) {
|
||||
self = .g(g)
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let attempts = [
|
||||
try decode(A.self, from: container).map { Poly7.a($0) },
|
||||
try decode(B.self, from: container).map { Poly7.b($0) },
|
||||
try decode(C.self, from: container).map { Poly7.c($0) },
|
||||
try decode(D.self, from: container).map { Poly7.d($0) },
|
||||
try decode(E.self, from: container).map { Poly7.e($0) },
|
||||
try decode(F.self, from: container).map { Poly7.f($0) },
|
||||
try decode(G.self, from: container).map { Poly7.g($0) }]
|
||||
|
||||
let maybeVal: Poly7<A, B, C, D, E, F, G>? = attempts
|
||||
.compactMap { $0.value }
|
||||
.first
|
||||
|
||||
guard let val = maybeVal else {
|
||||
throw EncodingError.invalidValue(Poly7<A, B, C, D, E, F, G>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
|
||||
}
|
||||
|
||||
self = val
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .a(let a):
|
||||
try container.encode(a)
|
||||
case .b(let b):
|
||||
try container.encode(b)
|
||||
case .c(let c):
|
||||
try container.encode(c)
|
||||
case .d(let d):
|
||||
try container.encode(d)
|
||||
case .e(let e):
|
||||
try container.encode(e)
|
||||
case .f(let f):
|
||||
try container.encode(f)
|
||||
case .g(let g):
|
||||
try container.encode(g)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Poly7: CustomStringConvertible {
|
||||
public var description: String {
|
||||
let str: String
|
||||
switch self {
|
||||
case .a(let a):
|
||||
str = String(describing: a)
|
||||
case .b(let b):
|
||||
str = String(describing: b)
|
||||
case .c(let c):
|
||||
str = String(describing: c)
|
||||
case .d(let d):
|
||||
str = String(describing: d)
|
||||
case .e(let e):
|
||||
str = String(describing: e)
|
||||
case .f(let f):
|
||||
str = String(describing: f)
|
||||
case .g(let g):
|
||||
str = String(describing: g)
|
||||
}
|
||||
|
||||
return "Poly(\(str))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,30 +5,59 @@
|
||||
// Created by Mathew Polzin on 8/31/18.
|
||||
//
|
||||
|
||||
public protocol RelationshipType: Codable {}
|
||||
public protocol RelationshipType {
|
||||
associatedtype LinksType
|
||||
associatedtype MetaType
|
||||
|
||||
var links: LinksType { get }
|
||||
var meta: MetaType { get }
|
||||
}
|
||||
|
||||
/// An Entity relationship that can be encoded to or decoded from
|
||||
/// a JSON API "Resource Linkage."
|
||||
/// See https://jsonapi.org/format/#document-resource-object-linkage
|
||||
/// A convenient typealias might make your code much more legible: `One<EntityDescription>`
|
||||
public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: RelationshipType, Equatable {
|
||||
public struct ToOneRelationship<Identifiable: JSONAPI.Identifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
|
||||
|
||||
public let id: Relatable.WrappedIdentifier
|
||||
public let id: Identifiable.Identifier
|
||||
|
||||
public init(id: Relatable.WrappedIdentifier) {
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
|
||||
public init(id: Identifiable.Identifier, meta: MetaType, links: LinksType) {
|
||||
self.id = id
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
|
||||
public init<E: EntityType>(entity: E) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
id = entity.id
|
||||
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(id: Identifiable.Identifier) {
|
||||
self.init(id: id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier? {
|
||||
public init<E: EntityType>(entity: E?) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
id = entity?.id
|
||||
extension ToOneRelationship {
|
||||
public init<E: EntityType>(entity: E, meta: MetaType, links: LinksType) where E.Id == Identifiable.Identifier {
|
||||
self.init(id: entity.id, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init<E: EntityType>(entity: E) where E.Id == Identifiable.Identifier {
|
||||
self.init(id: entity.id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Identifiable: OptionalRelatable {
|
||||
public init<E: EntityType>(entity: E?, meta: MetaType, links: LinksType) where E.Id == Identifiable.Wrapped.Identifier {
|
||||
self.init(id: entity?.id, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init<E: EntityType>(entity: E?) where E.Id == Identifiable.Wrapped.Identifier {
|
||||
self.init(id: entity?.id, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,73 +65,105 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Ident
|
||||
/// a JSON API "Resource Linkage."
|
||||
/// See https://jsonapi.org/format/#document-resource-object-linkage
|
||||
/// A convenient typealias might make your code much more legible: `Many<EntityDescription>`
|
||||
public struct ToManyRelationship<Relatable: JSONAPI.Relatable>: RelationshipType, Equatable {
|
||||
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
|
||||
|
||||
public let ids: [Relatable.Identifier]
|
||||
|
||||
public init(ids: [Relatable.Identifier]) {
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
|
||||
public init(ids: [Relatable.Identifier], meta: MetaType, links: LinksType) {
|
||||
self.ids = ids
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
|
||||
public init<T: JSONAPI.Relatable>(relationships: [ToOneRelationship<T>]) where T.WrappedIdentifier == Relatable.Identifier {
|
||||
ids = relationships.map { $0.id }
|
||||
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.Identifier == Relatable.Identifier {
|
||||
ids = pointers.map { $0.id }
|
||||
self.meta = meta
|
||||
self.links = links
|
||||
}
|
||||
|
||||
private init() {
|
||||
ids = []
|
||||
public init<E: EntityType>(entities: [E], meta: MetaType, links: LinksType) where E.Id == Relatable.Identifier {
|
||||
self.init(ids: entities.map { $0.id }, meta: meta, links: links)
|
||||
}
|
||||
|
||||
|
||||
private init(meta: MetaType, links: LinksType) {
|
||||
self.init(ids: [], meta: meta, links: links)
|
||||
}
|
||||
|
||||
public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship {
|
||||
return ToManyRelationship(meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
|
||||
public init(ids: [Relatable.Identifier]) {
|
||||
self.init(ids: ids, meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init<T: JSONAPI.Identifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>]) where T.Identifier == Relatable.Identifier {
|
||||
self.init(pointers: pointers, meta: .none, links: .none)
|
||||
}
|
||||
|
||||
public static var none: ToManyRelationship {
|
||||
return .init()
|
||||
return .none(withMeta: .none, links: .none)
|
||||
}
|
||||
|
||||
public init<E: EntityType>(entities: [E]) where E.Id == Relatable.Identifier {
|
||||
self.init(entities: entities, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship {
|
||||
public init<E: EntityType>(entities: [E]) where E.Description == Relatable.Description, E.Id == Relatable.Identifier {
|
||||
ids = entities.map { $0.id }
|
||||
}
|
||||
public protocol Identifiable: JSONTyped {
|
||||
associatedtype Identifier: Equatable
|
||||
}
|
||||
|
||||
/// The WrappedRelatable (a.k.a OptionalRelatable) protocol
|
||||
/// describes Optional<T: Relatable> and Relatable types.
|
||||
public protocol WrappedRelatable: Codable, Equatable {
|
||||
associatedtype Description: EntityDescription
|
||||
associatedtype Identifier: JSONAPI.IdType
|
||||
associatedtype WrappedIdentifier: Codable, Equatable
|
||||
}
|
||||
public typealias OptionalRelatable = WrappedRelatable
|
||||
|
||||
/// The Relatable protocol describes anything that
|
||||
/// has an IdType Identifier
|
||||
public protocol Relatable: WrappedRelatable {}
|
||||
public protocol Relatable: Identifiable where Identifier: JSONAPI.IdType {
|
||||
}
|
||||
|
||||
extension Optional: OptionalRelatable where Wrapped: Relatable {
|
||||
public typealias Description = Wrapped.Description
|
||||
public typealias Identifier = Wrapped.Identifier
|
||||
public typealias WrappedIdentifier = Identifier?
|
||||
/// OptionalRelatable just describes an Optional
|
||||
/// with a Reltable Wrapped type.
|
||||
public protocol OptionalRelatable: Identifiable where Identifier == Wrapped.Identifier? {
|
||||
associatedtype Wrapped: JSONAPI.Relatable
|
||||
}
|
||||
|
||||
extension Optional: Identifiable, OptionalRelatable, JSONTyped where Wrapped: JSONAPI.Relatable {
|
||||
public typealias Identifier = Wrapped.Identifier?
|
||||
|
||||
public static var 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// Result.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/5/18.
|
||||
//
|
||||
|
||||
enum Result<T, E: Swift.Error> {
|
||||
case success(T)
|
||||
case failure(E)
|
||||
|
||||
var value: T? {
|
||||
guard case .success(let val) = self else {
|
||||
return nil
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
var error: E? {
|
||||
guard case .failure(let err) = self else {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func map<U>(_ transform: (T) -> U) -> Result<U, E> {
|
||||
switch self {
|
||||
case .failure(let err):
|
||||
return .failure(err)
|
||||
case .success(let val):
|
||||
return .success(transform(val))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Result: CustomStringConvertible where T: CustomStringConvertible, E: CustomStringConvertible {
|
||||
var description: String {
|
||||
switch self {
|
||||
case .success(let val):
|
||||
return String(describing: val)
|
||||
case .failure(let err):
|
||||
return String(describing: err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,14 @@ extension TransformedAttribute: ExpressibleByFloatLiteral where RawValue: Expres
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: ExpressibleByFloatLiteral where Wrapped: ExpressibleByFloatLiteral {
|
||||
public typealias FloatLiteralType = Wrapped.FloatLiteralType
|
||||
|
||||
public init(floatLiteral value: FloatLiteralType) {
|
||||
self = .some(Wrapped(floatLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByBooleanLiteral where RawValue: ExpressibleByBooleanLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
public typealias BooleanLiteralType = RawValue.BooleanLiteralType
|
||||
|
||||
@@ -47,6 +55,14 @@ extension TransformedAttribute: ExpressibleByBooleanLiteral where RawValue: Expr
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: ExpressibleByBooleanLiteral where Wrapped: ExpressibleByBooleanLiteral {
|
||||
public typealias BooleanLiteralType = Wrapped.BooleanLiteralType
|
||||
|
||||
public init(booleanLiteral value: BooleanLiteralType) {
|
||||
self = .some(Wrapped(booleanLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: ExpressibleByIntegerLiteral where RawValue: ExpressibleByIntegerLiteral, Transformer == IdentityTransformer<RawValue> {
|
||||
public typealias IntegerLiteralType = RawValue.IntegerLiteralType
|
||||
|
||||
@@ -55,6 +71,14 @@ extension TransformedAttribute: ExpressibleByIntegerLiteral where RawValue: Expr
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: ExpressibleByIntegerLiteral where Wrapped: ExpressibleByIntegerLiteral {
|
||||
public typealias IntegerLiteralType = Wrapped.IntegerLiteralType
|
||||
|
||||
public init(integerLiteral value: IntegerLiteralType) {
|
||||
self = .some(Wrapped(integerLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
// regretably, array and dictionary literals are not so easy because Dictionaries and Arrays
|
||||
// cannot be turned back into variadic arguments to pass onto the RawValue type's constructor.
|
||||
|
||||
@@ -79,6 +103,16 @@ extension TransformedAttribute: ExpressibleByDictionaryLiteral where RawValue: D
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: DictionaryType where Wrapped: DictionaryType {
|
||||
public typealias Key = Wrapped.Key
|
||||
|
||||
public typealias Value = Wrapped.Value
|
||||
|
||||
public init<S>(_ keysAndValues: S, uniquingKeysWith combine: (Dictionary<Key, Value>.Value, Dictionary<Key, Value>.Value) throws -> Dictionary<Key, Value>.Value) rethrows where S : Sequence, S.Element == (Key, Value) {
|
||||
self = try .some(Wrapped(keysAndValues, uniquingKeysWith: combine))
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ArrayType {
|
||||
associatedtype Element
|
||||
|
||||
@@ -94,3 +128,11 @@ extension TransformedAttribute: ExpressibleByArrayLiteral where RawValue: ArrayT
|
||||
self.init(value: RawValue(elements))
|
||||
}
|
||||
}
|
||||
|
||||
extension Optional: ArrayType where Wrapped: ArrayType {
|
||||
public typealias Element = Wrapped.Element
|
||||
|
||||
public init<S>(_ s: S) where Element == S.Element, S : Sequence {
|
||||
self = .some(Wrapped(s))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,10 @@ extension TransformedAttribute: AttributeTypeWithOptionalArray where RawValue: O
|
||||
private protocol OptionalRelationshipType {}
|
||||
extension Optional: OptionalRelationshipType where Wrapped: RelationshipType {}
|
||||
|
||||
private protocol _RelationshipType {}
|
||||
extension ToOneRelationship: _RelationshipType {}
|
||||
extension ToManyRelationship: _RelationshipType {}
|
||||
|
||||
public extension Entity {
|
||||
public static func check(_ entity: Entity) throws {
|
||||
var problems = [EntityCheckError]()
|
||||
@@ -72,7 +76,8 @@ public extension Entity {
|
||||
}
|
||||
|
||||
for relationship in relationshipsMirror.children {
|
||||
if relationship.value as? RelationshipType == nil {
|
||||
if relationship.value as? _RelationshipType == nil,
|
||||
relationship.value as? OptionalRelationshipType == nil {
|
||||
problems.append(.nonRelationship(named: relationship.label ?? "unnamed"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,38 +7,38 @@
|
||||
|
||||
import JSONAPI
|
||||
|
||||
extension ToOneRelationship: ExpressibleByNilLiteral where Relatable.WrappedIdentifier: ExpressibleByNilLiteral {
|
||||
extension ToOneRelationship: ExpressibleByNilLiteral where Identifiable.Identifier: ExpressibleByNilLiteral, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(nilLiteral: ()) {
|
||||
|
||||
self.init(id: Relatable.WrappedIdentifier(nilLiteral: ()))
|
||||
self.init(id: Identifiable.Identifier(nilLiteral: ()))
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Relatable.WrappedIdentifier: ExpressibleByUnicodeScalarLiteral {
|
||||
public typealias UnicodeScalarLiteralType = Relatable.WrappedIdentifier.UnicodeScalarLiteralType
|
||||
extension ToOneRelationship: ExpressibleByUnicodeScalarLiteral where Identifiable.Identifier: ExpressibleByUnicodeScalarLiteral, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public typealias UnicodeScalarLiteralType = Identifiable.Identifier.UnicodeScalarLiteralType
|
||||
|
||||
public init(unicodeScalarLiteral value: UnicodeScalarLiteralType) {
|
||||
self.init(id: Relatable.WrappedIdentifier(unicodeScalarLiteral: value))
|
||||
self.init(id: Identifiable.Identifier(unicodeScalarLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Relatable.WrappedIdentifier: ExpressibleByExtendedGraphemeClusterLiteral {
|
||||
public typealias ExtendedGraphemeClusterLiteralType = Relatable.WrappedIdentifier.ExtendedGraphemeClusterLiteralType
|
||||
extension ToOneRelationship: ExpressibleByExtendedGraphemeClusterLiteral where Identifiable.Identifier: ExpressibleByExtendedGraphemeClusterLiteral, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public typealias ExtendedGraphemeClusterLiteralType = Identifiable.Identifier.ExtendedGraphemeClusterLiteralType
|
||||
|
||||
public init(extendedGraphemeClusterLiteral value: ExtendedGraphemeClusterLiteralType) {
|
||||
self.init(id: Relatable.WrappedIdentifier(extendedGraphemeClusterLiteral: value))
|
||||
self.init(id: Identifiable.Identifier(extendedGraphemeClusterLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship: ExpressibleByStringLiteral where Relatable.WrappedIdentifier: ExpressibleByStringLiteral {
|
||||
public typealias StringLiteralType = Relatable.WrappedIdentifier.StringLiteralType
|
||||
extension ToOneRelationship: ExpressibleByStringLiteral where Identifiable.Identifier: ExpressibleByStringLiteral, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public typealias StringLiteralType = Identifiable.Identifier.StringLiteralType
|
||||
|
||||
public init(stringLiteral value: StringLiteralType) {
|
||||
self.init(id: Relatable.WrappedIdentifier(stringLiteral: value))
|
||||
self.init(id: Identifiable.Identifier(stringLiteral: value))
|
||||
}
|
||||
}
|
||||
|
||||
extension ToManyRelationship: ExpressibleByArrayLiteral {
|
||||
extension ToManyRelationship: ExpressibleByArrayLiteral where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public typealias ArrayLiteralElement = Relatable.Identifier
|
||||
|
||||
public init(arrayLiteral elements: ArrayLiteralElement...) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user