mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 372e2de721 | |||
| 1a2ba17f02 | |||
| 9f2c7aa2e4 | |||
| 650937dbb9 | |||
| b08cbdb0ae | |||
| c30a2615f2 | |||
| 86e87ebf5d | |||
| 987bf2b750 | |||
| af1aca9cf4 | |||
| b97649fab6 | |||
| 8114132189 | |||
| b61a41c99b | |||
| 16b83ddbef | |||
| 6dff02bd51 | |||
| 2563edf419 | |||
| 98bb64268f | |||
| 399644b62a | |||
| 3b1e5f7414 | |||
| bf842bd781 | |||
| 7edb631f09 | |||
| 3c5a356b44 | |||
| f526963707 | |||
| c4032eb351 | |||
| 7a3a1b8b65 | |||
| b3a4591a5d | |||
| 23d057c47b | |||
| 3327a93df5 | |||
| 713fd2ba3a | |||
| 072479c160 |
@@ -2,3 +2,4 @@
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
/*.xcworkspace
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
//
|
||||
// Examples.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
|
||||
/*******
|
||||
|
||||
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
|
||||
|
||||
********/
|
||||
|
||||
|
||||
typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
|
||||
|
||||
// MARK: - A few resource objects (entities)
|
||||
enum PersonDescription: EntityDescription {
|
||||
|
||||
static var type: String { return "people" }
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: [String]
|
||||
let favoriteColor: String
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let friends: ToManyRelationship<Person>
|
||||
let dogs: ToManyRelationship<Dog>
|
||||
let home: ToOneRelationship<House>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
enum DogDescription: EntityDescription {
|
||||
|
||||
static var type: String { return "dogs" }
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: String
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let owner: ToOneRelationship<Person?>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
enum HouseDescription: EntityDescription {
|
||||
|
||||
static var type: String { return "houses" }
|
||||
|
||||
typealias Attributes = NoAttributes
|
||||
typealias Relationships = NoRelatives
|
||||
}
|
||||
|
||||
typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
// MARK: - Parse a response body with one Dog in it
|
||||
typealias SingleDogResponse = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
|
||||
|
||||
let dummyData = "".data(using: .utf8)!
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogResponse.self, from: dummyData)
|
||||
let dog = dogResponse.body.data?.primary.value
|
||||
|
||||
// MARK: Parse a response body with multiple people in it and dogs and houses included
|
||||
typealias BatchPeopleResponse = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleResponse.self, from: dummyData)
|
||||
let people = peopleResponse.body.data?.primary.values
|
||||
let dogs = peopleResponse.body.data?.included[Dog.self]
|
||||
let houses = peopleResponse.body.data?.included[House.self]
|
||||
@@ -0,0 +1,41 @@
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
/*******
|
||||
|
||||
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
|
||||
|
||||
********/
|
||||
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
typealias SingleDogDocument = JSONAPIDocument<SingleResourceBody<Dog>, NoIncludes, BasicJSONAPIError>
|
||||
|
||||
let singleDogDocument = SingleDogDocument(body: SingleResourceBody(entity: dogFromCode))
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.data?.primary.value
|
||||
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
||||
let houses = [House(), House()]
|
||||
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
|
||||
|
||||
typealias BatchPeopleDocument = JSONAPIDocument<ManyResourceBody<Person>, Include2<Dog, House>, BasicJSONAPIError>
|
||||
|
||||
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
|
||||
let batchPeopleDocument = BatchPeopleDocument(body: .init(entities: people), includes: .init(values: includes))
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||
let peopleFromData = peopleResponse.body.data?.primary.values
|
||||
let dogsFromData = peopleResponse.body.data?.included[Dog.self]
|
||||
let housesFromData = peopleResponse.body.data?.included[House.self]
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// Examples.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/12/18.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import JSONAPI
|
||||
|
||||
/*******
|
||||
|
||||
Please enjoy these examples, but allow me the forced casting and the lack of error checking for the sake of brevity.
|
||||
|
||||
********/
|
||||
|
||||
// MARK: - String as CreatableRawIdType
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Entity typealias for convenience
|
||||
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
|
||||
|
||||
// MARK: - A few resource objects (entities)
|
||||
public enum PersonDescription: EntityDescription {
|
||||
|
||||
public static var type: String { return "people" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<[String]>
|
||||
public let favoriteColor: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let friends: ToManyRelationship<Person>
|
||||
public let dogs: ToManyRelationship<Dog>
|
||||
public let home: ToOneRelationship<House>
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Person = ExampleEntity<PersonDescription>
|
||||
|
||||
public extension Entity where Description == PersonDescription, Identifier == Id<String, PersonDescription> {
|
||||
public init(id: Person.Identifier? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
|
||||
self = try Person(id: id ?? Person.Identifier(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
|
||||
}
|
||||
}
|
||||
|
||||
public enum DogDescription: EntityDescription {
|
||||
|
||||
public static var type: String { return "dogs" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let owner: ToOneRelationship<Person?>
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Dog = ExampleEntity<DogDescription>
|
||||
|
||||
public extension Entity where Description == DogDescription, Identifier == Id<String, DogDescription> {
|
||||
public init(name: String, owner: Person?) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
|
||||
}
|
||||
|
||||
public init(name: String, owner: Person.Identifier) throws {
|
||||
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
|
||||
}
|
||||
}
|
||||
|
||||
public enum HouseDescription: EntityDescription {
|
||||
|
||||
public static var type: String { return "houses" }
|
||||
|
||||
public typealias Attributes = NoAttributes
|
||||
public typealias Relationships = NoRelatives
|
||||
}
|
||||
|
||||
public typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<playground version='5.0' target-platform='macos' executeOnSourceChanges='false'>
|
||||
<timeline fileName='timeline.xctimeline'/>
|
||||
</playground>
|
||||
+4
-4
@@ -3,11 +3,11 @@
|
||||
"pins": [
|
||||
{
|
||||
"package": "Result",
|
||||
"repositoryURL": "https://github.com/antitypical/Result.git",
|
||||
"repositoryURL": "https://github.com/mattpolzin/Result",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "8fc088dcf72802801efeecba76ea8fb041fb773d",
|
||||
"version": "4.0.0"
|
||||
"branch": "master",
|
||||
"revision": "b98e238da6ea030fa7862ae6fd6500552370019c",
|
||||
"version": null
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ let package = Package(
|
||||
targets: ["JSONAPI"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/antitypical/Result.git", from: "4.0.0")
|
||||
.package(url: "https://github.com/mattpolzin/Result", .branch("master"))
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# JSONAPI
|
||||
[](http://opensource.org/licenses/MIT) [](https://swift.org) [](https://app.bitrise.io/app/c8295b9589aa401e)
|
||||
|
||||
A Swift package for encoding and decoding to *JSON API* compliant requests and responses.
|
||||
A Swift package for encoding to- and decoding from *JSON API* compliant requests and responses.
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
|
||||
@@ -19,10 +19,10 @@ The primary goals of this framework are:
|
||||
#### Document
|
||||
- [x] `data`
|
||||
- [x] `included`
|
||||
- [x] `errors` (untested)
|
||||
- [ ] `meta`
|
||||
- [x] `errors`
|
||||
- [x] `meta`
|
||||
- [ ] `jsonapi`
|
||||
- [ ] `links`
|
||||
- [x] `links`
|
||||
|
||||
#### Resource Object
|
||||
- [x] `id`
|
||||
@@ -37,14 +37,18 @@ The primary goals of this framework are:
|
||||
- [ ] `links`
|
||||
- [ ] `meta`
|
||||
|
||||
#### Links Object
|
||||
- [x] `href`
|
||||
- [x] `meta`
|
||||
|
||||
### Encoding
|
||||
#### Document
|
||||
- [x] `data`
|
||||
- [x] `included`
|
||||
- [x] `errors` (untested)
|
||||
- [ ] `meta`
|
||||
- [x] `errors`
|
||||
- [x] `meta`
|
||||
- [ ] `jsonapi`
|
||||
- [ ] `links`
|
||||
- [x] `links`
|
||||
|
||||
#### Resource Object
|
||||
- [x] `id`
|
||||
@@ -59,6 +63,10 @@ The primary goals of this framework are:
|
||||
- [ ] `links`
|
||||
- [ ] `meta`
|
||||
|
||||
#### Links Object
|
||||
- [x] `href`
|
||||
- [x] `meta`
|
||||
|
||||
### EntityDescription Validator (using reflection)
|
||||
- [ ] Disallow optional array in `Attribute` and `Relationship` (should be empty array, not `null`).
|
||||
- [ ] Only allow `Attribute` and `TransformAttribute` within `Attributes` struct.
|
||||
@@ -71,12 +79,12 @@ The primary goals of this framework are:
|
||||
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
|
||||
- [x] Support ability to distinguish between `Attributes` fields that are optional (i.e. the key might not be there) and `Attributes` values that are optional (i.e. the key is guaranteed to be there but it might be `null`).
|
||||
- [x] Fix `ToOneRelationship` so that it is possible to specify an optional relationship where the value is `null` rather than the key being omitted.
|
||||
- [ ] Conform to `CustomStringConvertible`
|
||||
- [ ] More tests around failing to decode improperly structured JSON (not bad JSON, but JSON that is not to spec)
|
||||
- [x] Conform to `CustomStringConvertible`
|
||||
- [x] More tests around failing to decode improperly structured JSON (not bad JSON, but JSON that is not to spec)
|
||||
- [ ] Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources????
|
||||
- [x] For `NoIncludes`, do not even loop over the "included" JSON API section if it exists.
|
||||
- [ ] Property-based testing (using `SwiftCheck`)
|
||||
- [ ] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
|
||||
- [x] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
|
||||
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
|
||||
|
||||
## Usage
|
||||
@@ -102,7 +110,7 @@ enum PersonDescription: IdentifiedEntityDescription {
|
||||
}
|
||||
```
|
||||
|
||||
To enumerate them, the requirements of an `EntityDescription` are
|
||||
The requirements of an `EntityDescription` are:
|
||||
1. A static `var` "type" that matches the JSON type; The JSON spec requires every *Resource Object* to have a "type".
|
||||
2. A `struct` of `Attributes` **- OR -** `typealias Attributes = NoAttributes`
|
||||
3. A `struct` of `Relationships` **- OR -** `typealias Relationships = NoRelatives`
|
||||
@@ -225,31 +233,69 @@ enum ISODateTransformer: Transformer {
|
||||
}
|
||||
```
|
||||
|
||||
Then you define the attribute as a `TransformAttribute` instead of an `Attribute`:
|
||||
Then you define the attribute as a `TransformedAttribute` instead of an `Attribute`:
|
||||
```
|
||||
let date: TransformAttribute<String, ISODateTransformer>
|
||||
let date: TransformedAttribute<String, ISODateTransformer>
|
||||
```
|
||||
|
||||
Note that the first generic parameter of `TransformAttribute` is the type you expect to decode from JSON, not the type you want to end up with after transformation.
|
||||
|
||||
#### `Validator`
|
||||
|
||||
You can also creator `Validator`s and `ValidatedAttribute`s. A `Validator` is just a `Transformer` that by convention does not perform a transformation. It simply `throws` if an attribute value is invalid.
|
||||
|
||||
### `JSONAPIDocument`
|
||||
|
||||
The entirety of a JSON API request or response is encoded or decoded from- or to a `JSONAPIDocument`. As an example, a JSON API response containing one `Person` and no included entities could be decoded as follows:
|
||||
```
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoIncludes, BasicJSONAPIError>.self
|
||||
let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError>.self
|
||||
|
||||
let document = try decoder.decode(responseStructure, from: data)
|
||||
```
|
||||
|
||||
This document is guaranteed by the JSON API spec to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata."
|
||||
|
||||
#### `ResourceBody`
|
||||
|
||||
The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `Entity` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `Entity` values in a JSON API document that has a `ManyResourceBody`.
|
||||
The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `Entity` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `Entity` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `Entity`. These `Poly` types work in the same way as the `Include` types described below.
|
||||
|
||||
#### `IncludeDecoder`
|
||||
If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead.
|
||||
|
||||
The second generic type of a `JSONAPIDocument` is an `IncludeDecoder`. This type controls which types of `Entity` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included entities to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `IncludeDecoder`s for up to six types of included entities. These are named `Include1`, `Include2`, `Include3`, and so on.
|
||||
#### `MetaType`
|
||||
|
||||
The second generic type of a `JSONAPIDocument` is a `Meta`. This structure is entirely open-ended. As an example, the JSON API document may contain the following pagination info in its meta entry:
|
||||
```
|
||||
{
|
||||
"meta": {
|
||||
"total": 100,
|
||||
"limit": 50,
|
||||
"offset": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You would then create the following `Meta` type:
|
||||
```
|
||||
struct PageMetadata: JSONAPI.Meta {
|
||||
let total: Int
|
||||
let limit: Int
|
||||
let offset: Int
|
||||
}
|
||||
```
|
||||
|
||||
You can always use `NoMetadata` if this JSON API feature is not needed.
|
||||
|
||||
#### `LinksType`
|
||||
|
||||
The third generic type of a `JSONAPIDocument` is a `Links` struct. A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`.
|
||||
|
||||
You can specify `NoLinks` if the document should not contain any links.
|
||||
|
||||
#### `IncludeType`
|
||||
|
||||
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `Entity` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included entities to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to six types of included entities. These are named `Include1`, `Include2`, `Include3`, and so on.
|
||||
|
||||
**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included entities, it indicates a number of _types_ of included entities. `Include1` can be used to decode any number of included entities as long as all the entities are of the same _type_.
|
||||
|
||||
|
||||
@@ -12,25 +12,105 @@
|
||||
/// API uses snake case, you will want to use
|
||||
/// a conversion such as the one offerred by the
|
||||
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
|
||||
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError>: Equatable {
|
||||
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, Error: JSONAPIError>: Equatable {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
public let body: Body
|
||||
// public let meta: Meta?
|
||||
// public let jsonApi: APIDescription?
|
||||
// public let links: Links?
|
||||
|
||||
public enum Body: Equatable {
|
||||
case errors([Error])
|
||||
case data(primary: ResourceBody, included: Includes<Include>)
|
||||
|
||||
case errors([Error], meta: MetaType?, links: LinksType?)
|
||||
case data(primary: ResourceBody, included: Includes<Include>, meta: MetaType, links: LinksType)
|
||||
|
||||
public var isError: Bool {
|
||||
guard case .errors = self else { return false }
|
||||
return true
|
||||
}
|
||||
|
||||
public var data: (primary: ResourceBody, included: Includes<Include>)? {
|
||||
guard case let .data(primary: body, included: includes) = self else { return nil }
|
||||
return (primary: body, included: includes)
|
||||
|
||||
public var errors: [Error]? {
|
||||
guard case let .errors(errors, meta: _, links: _) = self else { return nil }
|
||||
return errors
|
||||
}
|
||||
|
||||
public var primaryData: ResourceBody? {
|
||||
guard case let .data(primary: body, included: _, meta: _, links: _) = self else { return nil }
|
||||
return body
|
||||
}
|
||||
|
||||
public var includes: Includes<Include>? {
|
||||
guard case let .data(primary: _, included: includes, meta: _, links: _) = self else { return nil }
|
||||
return includes
|
||||
}
|
||||
|
||||
public var meta: MetaType? {
|
||||
switch self {
|
||||
case .data(primary: _, included: _, meta: let metadata, links: _),
|
||||
.errors(_, meta: let metadata?, links: _):
|
||||
return metadata
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public var links: LinksType? {
|
||||
switch self {
|
||||
case .data(primary: _, included: _, meta: _, links: let links),
|
||||
.errors(_, meta: _, links: let links?):
|
||||
return links
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
}
|
||||
|
||||
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(primary: body, included: includes, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes {
|
||||
public init(body: ResourceBody, meta: MetaType, links: LinksType) {
|
||||
self.body = .data(primary: body, included: .none, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where MetaType == NoMetadata {
|
||||
public init(body: ResourceBody, includes: Includes<Include>, links: LinksType) {
|
||||
self.body = .data(primary: body, included: includes, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where LinksType == NoLinks {
|
||||
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType) {
|
||||
self.body = .data(primary: body, included: includes, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes, LinksType == NoLinks {
|
||||
public init(body: ResourceBody, meta: MetaType) {
|
||||
self.body = .data(primary: body, included: .none, meta: meta, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata {
|
||||
public init(body: ResourceBody, links: LinksType) {
|
||||
self.body = .data(primary: body, included: .none, meta: .none, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: ResourceBody, includes: Includes<Include>) {
|
||||
self.body = .data(primary: body, included: includes, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public init(body: ResourceBody) {
|
||||
self.body = .data(primary: body, included: .none, meta: .none, links: .none)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,36 +129,98 @@ extension JSONAPIDocument: Codable {
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
do {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
} catch {
|
||||
meta = nil
|
||||
}
|
||||
}
|
||||
|
||||
let links: LinksType?
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
do {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
} catch {
|
||||
links = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there are errors, there cannot be a body. Return errors and any metadata found.
|
||||
if let errors = errors {
|
||||
body = .errors(errors)
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
return
|
||||
}
|
||||
|
||||
let data = try container.decode(ResourceBody.self, forKey: .data)
|
||||
let data: ResourceBody
|
||||
if let noData = NoResourceBody() as? ResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(ResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
|
||||
// TODO come back to this and make robust
|
||||
|
||||
guard let metaVal = meta else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedMetadata
|
||||
}
|
||||
guard let linksVal = links else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
}
|
||||
|
||||
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none)
|
||||
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
switch body {
|
||||
case .errors(let errors):
|
||||
case .errors(let errors, meta: let meta, links: let links):
|
||||
var errContainer = container.nestedUnkeyedContainer(forKey: .errors)
|
||||
|
||||
for error in errors {
|
||||
try errContainer.encode(error)
|
||||
}
|
||||
|
||||
case .data(primary: let resourceBody, included: let includes):
|
||||
if MetaType.self != NoMetadata.self,
|
||||
let metaVal = meta {
|
||||
try container.encode(metaVal, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self,
|
||||
let linksVal = links {
|
||||
try container.encode(linksVal, forKey: .links)
|
||||
}
|
||||
|
||||
case .data(primary: let resourceBody, included: let includes, let meta, links: let links):
|
||||
try container.encode(resourceBody, forKey: .data)
|
||||
|
||||
if Include.self != NoIncludes.self {
|
||||
try container.encode(includes, forKey: .included)
|
||||
}
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension JSONAPIDocument: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "JSONAPIDocument(body: \(String(describing: body))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,16 +5,14 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
import Result
|
||||
public typealias Include = Poly
|
||||
|
||||
public protocol IncludeDecoder: Codable, Equatable {}
|
||||
|
||||
public struct Includes<I: IncludeDecoder>: Codable, Equatable {
|
||||
public struct Includes<I: Include>: Codable, Equatable {
|
||||
public static var none: Includes { return .init(values: []) }
|
||||
|
||||
let values: [I]
|
||||
|
||||
private init(values: [I]) {
|
||||
public init(values: [I]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
@@ -52,433 +50,69 @@ public struct Includes<I: IncludeDecoder>: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Decoding
|
||||
extension Includes: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Includes(\(String(describing: values))"
|
||||
}
|
||||
}
|
||||
|
||||
func decode<Entity: JSONAPI.EntityType>(_ type: Entity.Type, from container: SingleValueDecodingContainer) throws -> Result<Entity, EncodingError> {
|
||||
let ret: Result<Entity, EncodingError>
|
||||
do {
|
||||
ret = try .success(container.decode(Entity.self))
|
||||
} catch (let err as EncodingError) {
|
||||
ret = .failure(err)
|
||||
} catch (let err) {
|
||||
ret = .failure(EncodingError.invalidValue(Entity.Description.self,
|
||||
.init(codingPath: container.codingPath,
|
||||
debugDescription: err.localizedDescription,
|
||||
underlyingError: err)))
|
||||
extension Includes where I == NoIncludes {
|
||||
public init() {
|
||||
values = []
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// MARK: - 0 includes
|
||||
|
||||
public protocol _Include0: IncludeDecoder { }
|
||||
public struct Include0: _Include0 {
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Include0, which should be represented by the absence of an 'included' entry altogether.")
|
||||
}
|
||||
}
|
||||
public typealias Include0 = Poly0
|
||||
public typealias NoIncludes = Include0
|
||||
|
||||
// MARK: - 1 include
|
||||
public protocol _Include1: _Include0 {
|
||||
associatedtype A: EntityType
|
||||
var a: A? { get }
|
||||
}
|
||||
public enum Include1<A: EntityType>: _Include1 {
|
||||
case a(A)
|
||||
|
||||
public var a: A? {
|
||||
guard case let .a(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
self = .a(try container.decode(A.self))
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .a(let a):
|
||||
try container.encode(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes where I: _Include1 {
|
||||
public typealias Include1 = Poly1
|
||||
extension Includes where I: _Poly1 {
|
||||
public subscript(_ lookup: I.A.Type) -> [I.A] {
|
||||
return values.compactMap { $0.a }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 2 includes
|
||||
public protocol _Include2: _Include1 {
|
||||
associatedtype B: EntityType
|
||||
var b: B? { get }
|
||||
}
|
||||
public enum Include2<A: EntityType, B: EntityType>: _Include2 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
|
||||
public var a: A? {
|
||||
guard case let .a(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var b: B? {
|
||||
guard case let .b(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let attempts = [
|
||||
try decode(A.self, from: container).map { Include2.a($0) },
|
||||
try decode(B.self, from: container).map { Include2.b($0) } ]
|
||||
|
||||
let maybeVal: Include2<A, B>? = attempts
|
||||
.compactMap { $0.value }
|
||||
.first
|
||||
|
||||
guard let val = maybeVal else {
|
||||
throw EncodingError.invalidValue(Include2<A, B>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
|
||||
}
|
||||
|
||||
self = val
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .a(let a):
|
||||
try container.encode(a)
|
||||
case .b(let b):
|
||||
try container.encode(b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes where I: _Include2 {
|
||||
public typealias Include2 = Poly2
|
||||
extension Includes where I: _Poly2 {
|
||||
public subscript(_ lookup: I.B.Type) -> [I.B] {
|
||||
return values.compactMap { $0.b }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 3 includes
|
||||
public protocol _Include3: _Include2 {
|
||||
associatedtype C: EntityType
|
||||
var c: C? { get }
|
||||
}
|
||||
public enum Include3<A: EntityType, B: EntityType, C: EntityType>: _Include3 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
case c(C)
|
||||
|
||||
public var a: A? {
|
||||
guard case let .a(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var b: B? {
|
||||
guard case let .b(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var c: C? {
|
||||
guard case let .c(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let attempts = [
|
||||
try decode(A.self, from: container).map { Include3.a($0) },
|
||||
try decode(B.self, from: container).map { Include3.b($0) },
|
||||
try decode(C.self, from: container).map { Include3.c($0) }]
|
||||
|
||||
let maybeVal: Include3<A, B, C>? = attempts
|
||||
.compactMap { $0.value }
|
||||
.first
|
||||
|
||||
guard let val = maybeVal else {
|
||||
throw EncodingError.invalidValue(Include3<A, B, C>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
|
||||
}
|
||||
|
||||
self = val
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .a(let a):
|
||||
try container.encode(a)
|
||||
case .b(let b):
|
||||
try container.encode(b)
|
||||
case .c(let c):
|
||||
try container.encode(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes where I: _Include3 {
|
||||
public typealias Include3 = Poly3
|
||||
extension Includes where I: _Poly3 {
|
||||
public subscript(_ lookup: I.C.Type) -> [I.C] {
|
||||
return values.compactMap { $0.c }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 4 includes
|
||||
public protocol _Include4: _Include3 {
|
||||
associatedtype D: EntityType
|
||||
var d: D? { get }
|
||||
}
|
||||
public enum Include4<A: EntityType, B: EntityType, C: EntityType, D: EntityType>: _Include4 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
case c(C)
|
||||
case d(D)
|
||||
|
||||
public var a: A? {
|
||||
guard case let .a(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var b: B? {
|
||||
guard case let .b(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var c: C? {
|
||||
guard case let .c(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var d: D? {
|
||||
guard case let .d(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let attempts = [
|
||||
try decode(A.self, from: container).map { Include4.a($0) },
|
||||
try decode(B.self, from: container).map { Include4.b($0) },
|
||||
try decode(C.self, from: container).map { Include4.c($0) },
|
||||
try decode(D.self, from: container).map { Include4.d($0) }]
|
||||
|
||||
let maybeVal: Include4<A, B, C, D>? = attempts
|
||||
.compactMap { $0.value }
|
||||
.first
|
||||
|
||||
guard let val = maybeVal else {
|
||||
throw EncodingError.invalidValue(Include4<A, B, C, D>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
|
||||
}
|
||||
|
||||
self = val
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .a(let a):
|
||||
try container.encode(a)
|
||||
case .b(let b):
|
||||
try container.encode(b)
|
||||
case .c(let c):
|
||||
try container.encode(c)
|
||||
case .d(let d):
|
||||
try container.encode(d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes where I: _Include4 {
|
||||
public typealias Include4 = Poly4
|
||||
extension Includes where I: _Poly4 {
|
||||
public subscript(_ lookup: I.D.Type) -> [I.D] {
|
||||
return values.compactMap { $0.d }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 5 includes
|
||||
public protocol _Include5: _Include4 {
|
||||
associatedtype E: EntityType
|
||||
var e: E? { get }
|
||||
}
|
||||
public enum Include5<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType>: _Include5 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
case c(C)
|
||||
case d(D)
|
||||
case e(E)
|
||||
|
||||
public var a: A? {
|
||||
guard case let .a(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var b: B? {
|
||||
guard case let .b(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var c: C? {
|
||||
guard case let .c(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var d: D? {
|
||||
guard case let .d(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var e: E? {
|
||||
guard case let .e(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let attempts = [
|
||||
try decode(A.self, from: container).map { Include5.a($0) },
|
||||
try decode(B.self, from: container).map { Include5.b($0) },
|
||||
try decode(C.self, from: container).map { Include5.c($0) },
|
||||
try decode(D.self, from: container).map { Include5.d($0) },
|
||||
try decode(E.self, from: container).map { Include5.e($0) }]
|
||||
|
||||
let maybeVal: Include5<A, B, C, D, E>? = attempts
|
||||
.compactMap { $0.value }
|
||||
.first
|
||||
|
||||
guard let val = maybeVal else {
|
||||
throw EncodingError.invalidValue(Include5<A, B, C, D, E>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
|
||||
}
|
||||
|
||||
self = val
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .a(let a):
|
||||
try container.encode(a)
|
||||
case .b(let b):
|
||||
try container.encode(b)
|
||||
case .c(let c):
|
||||
try container.encode(c)
|
||||
case .d(let d):
|
||||
try container.encode(d)
|
||||
case .e(let e):
|
||||
try container.encode(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes where I: _Include5 {
|
||||
public typealias Include5 = Poly5
|
||||
extension Includes where I: _Poly5 {
|
||||
public subscript(_ lookup: I.E.Type) -> [I.E] {
|
||||
return values.compactMap { $0.e }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 6 includes
|
||||
public protocol _Include6: _Include5 {
|
||||
associatedtype F: EntityType
|
||||
var f: F? { get }
|
||||
}
|
||||
public enum Include6<A: EntityType, B: EntityType, C: EntityType, D: EntityType, E: EntityType, F: EntityType>: _Include6 {
|
||||
case a(A)
|
||||
case b(B)
|
||||
case c(C)
|
||||
case d(D)
|
||||
case e(E)
|
||||
case f(F)
|
||||
|
||||
public var a: A? {
|
||||
guard case let .a(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var b: B? {
|
||||
guard case let .b(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var c: C? {
|
||||
guard case let .c(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var d: D? {
|
||||
guard case let .d(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var e: E? {
|
||||
guard case let .e(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public var f: F? {
|
||||
guard case let .f(ret) = self else { return nil }
|
||||
return ret
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let attempts = [
|
||||
try decode(A.self, from: container).map { Include6.a($0) },
|
||||
try decode(B.self, from: container).map { Include6.b($0) },
|
||||
try decode(C.self, from: container).map { Include6.c($0) },
|
||||
try decode(D.self, from: container).map { Include6.d($0) },
|
||||
try decode(E.self, from: container).map { Include6.e($0) },
|
||||
try decode(F.self, from: container).map { Include6.f($0) }]
|
||||
|
||||
let maybeVal: Include6<A, B, C, D, E, F>? = attempts
|
||||
.compactMap { $0.value }
|
||||
.first
|
||||
|
||||
guard let val = maybeVal else {
|
||||
throw EncodingError.invalidValue(Include6<A, B, C, D, E, F>.self, .init(codingPath: decoder.codingPath, debugDescription: "Failed to find an include of the expected type. Attempts: \(attempts.map { $0.error }.compactMap { $0 })"))
|
||||
}
|
||||
|
||||
self = val
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
switch self {
|
||||
case .a(let a):
|
||||
try container.encode(a)
|
||||
case .b(let b):
|
||||
try container.encode(b)
|
||||
case .c(let c):
|
||||
try container.encode(c)
|
||||
case .d(let d):
|
||||
try container.encode(d)
|
||||
case .e(let e):
|
||||
try container.encode(e)
|
||||
case .f(let f):
|
||||
try container.encode(f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes where I: _Include6 {
|
||||
public typealias Include6 = Poly6
|
||||
extension Includes where I: _Poly6 {
|
||||
public subscript(_ lookup: I.F.Type) -> [I.F] {
|
||||
return values.compactMap { $0.f }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 7 includes
|
||||
// MARK: - 8 includes
|
||||
|
||||
@@ -5,15 +5,31 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
public protocol PrimaryResource: Equatable, Codable {}
|
||||
|
||||
public protocol ResourceBody: Codable, Equatable {
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
|
||||
public struct SingleResourceBody<Entity: JSONAPI.PrimaryResource>: ResourceBody {
|
||||
public let value: Entity?
|
||||
|
||||
public init(entity: Entity?) {
|
||||
self.value = entity
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
|
||||
public let values: [Entity]
|
||||
|
||||
public init(entities: [Entity]) {
|
||||
values = entities
|
||||
}
|
||||
}
|
||||
|
||||
/// Use NoResourceBody to indicate you expect a JSON API document to not
|
||||
/// contain a "data" top-level key.
|
||||
public struct NoResourceBody: ResourceBody {
|
||||
public static var none: NoResourceBody { return NoResourceBody() }
|
||||
}
|
||||
|
||||
// MARK: Decodable
|
||||
@@ -59,3 +75,17 @@ extension ManyResourceBody {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
extension SingleResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "ResourceBody(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "ResourceBody(\(String(describing: values)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Links.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/24/18.
|
||||
//
|
||||
|
||||
/// A Links structure should contain nothing but JSONAPI.Link properties.
|
||||
public protocol Links: Codable, Equatable {}
|
||||
|
||||
/// Use NoLinks where no links should belong to a JSON API component
|
||||
public struct NoLinks: Links {
|
||||
public static var none: NoLinks { return NoLinks() }
|
||||
public init() {}
|
||||
}
|
||||
|
||||
public protocol URL: Codable, Equatable {}
|
||||
|
||||
public struct Link<URL: JSONAPI.URL, Meta: JSONAPI.Meta>: Equatable, Codable {
|
||||
public let url: URL
|
||||
public let meta: Meta
|
||||
}
|
||||
|
||||
public extension Link {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case href
|
||||
case meta
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
guard Meta.self == NoMetadata.self,
|
||||
let noMeta = NoMetadata() as? Meta else {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
meta = try container.decode(Meta.self, forKey: .meta)
|
||||
url = try container.decode(URL.self, forKey: .href)
|
||||
return
|
||||
}
|
||||
let container = try decoder.singleValueContainer()
|
||||
url = try container.decode(URL.self)
|
||||
meta = noMeta
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
guard Meta.self == NoMetadata.self else {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(url, forKey: .href)
|
||||
try container.encode(meta, forKey: .meta)
|
||||
return
|
||||
}
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(url)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Meta.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/21/18.
|
||||
//
|
||||
|
||||
/// Conform a type to this protocol to indicate it can be encoded to or decoded from
|
||||
/// the meta data attached to a component of a JSON API document. Different meta data
|
||||
/// can be stored all over the place: On the root document, on a resource object, on
|
||||
/// link objects, etc.
|
||||
///
|
||||
/// JSON API Metadata is totally open ended. It can take whatever JSON-compliant structure
|
||||
/// the server and client agree upon.
|
||||
public protocol Meta: Codable, Equatable {
|
||||
}
|
||||
|
||||
// We make Optional a Meta if it wraps a Meta so that Metadata can be specified as
|
||||
// nullable.
|
||||
extension Optional: Meta where Wrapped: Meta {}
|
||||
|
||||
public struct NoMetadata: Meta {
|
||||
public static var none: NoMetadata { return NoMetadata() }
|
||||
|
||||
public init() { }
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
// Created by Mathew Polzin on 11/13/18.
|
||||
//
|
||||
|
||||
public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: Codable where Transformer.From == RawValue {
|
||||
private let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
@@ -16,12 +16,20 @@ public struct TransformAttribute<RawValue: Codable, Transformer: JSONAPI.Transfo
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Attribute<T: Codable> = TransformAttribute<T, IdentityTransformer<T>>
|
||||
extension TransformedAttribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
|
||||
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
|
||||
|
||||
public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Validator> = TransformedAttribute<RawValue, Validator> where RawValue == Validator.From
|
||||
|
||||
public typealias Attribute<T: Codable> = TransformedAttribute<T, IdentityTransformer<T>>
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformAttribute {
|
||||
extension TransformedAttribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
@@ -58,16 +66,3 @@ extension TransformAttribute {
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Transformers
|
||||
|
||||
public protocol Transformer {
|
||||
associatedtype From
|
||||
associatedtype To
|
||||
|
||||
static func transform(_ from: From) throws -> To
|
||||
}
|
||||
|
||||
public enum IdentityTransformer<T>: Transformer {
|
||||
public static func transform(_ from: T) throws -> T { return from }
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ public protocol EntityDescription {
|
||||
/// EntityType is the protocol that Entity conforms to. This
|
||||
/// protocol lets other types accept any Entity as a generic
|
||||
/// specialization.
|
||||
public protocol EntityType: Codable, Equatable {
|
||||
public protocol EntityType: PrimaryResource {
|
||||
associatedtype Description: EntityDescription
|
||||
associatedtype Identifier: Equatable & Codable
|
||||
}
|
||||
@@ -70,6 +70,12 @@ public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI
|
||||
}
|
||||
}
|
||||
|
||||
extension Entity: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Entity<\(Entity.type)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenience initializers
|
||||
extension Entity where Identifier: CreatableIdType {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
|
||||
@@ -129,21 +135,21 @@ public extension Entity {
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>>) -> TFRM.To {
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>>) -> TFRM.To {
|
||||
return attributes[keyPath: path].value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>?>) -> TFRM.To? {
|
||||
subscript<T, TFRM: Transformer>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> TFRM.To? {
|
||||
return attributes[keyPath: path]?.value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `entity[\.propertyName]` instead
|
||||
/// of `entity.relationships.propertyName`.
|
||||
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
|
||||
subscript<T, TFRM: Transformer, U>(_ path: KeyPath<Description.Attributes, TransformedAttribute<T, TFRM>?>) -> U? where TFRM.To == U? {
|
||||
return attributes[keyPath: path].flatMap { $0.value }
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,6 +16,10 @@ public struct ToOneRelationship<Relatable: JSONAPI.OptionalRelatable>: Equatable
|
||||
public var ids: [Relatable.WrappedIdentifier] {
|
||||
return [id]
|
||||
}
|
||||
|
||||
public init(id: Relatable.WrappedIdentifier) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Identifier {
|
||||
@@ -25,8 +29,8 @@ extension ToOneRelationship where Relatable.WrappedIdentifier == Relatable.Ident
|
||||
}
|
||||
|
||||
extension ToOneRelationship where Relatable.WrappedIdentifier == Optional<Relatable.Identifier> {
|
||||
public init(entity: Entity<Relatable.Description, Relatable.Identifier>) {
|
||||
id = entity.id
|
||||
public init(entity: Entity<Relatable.Description, Relatable.Identifier>?) {
|
||||
id = entity?.id
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,6 +96,9 @@ private enum ResourceIdentifierCodingKeys: String, CodingKey {
|
||||
public enum JSONAPIEncodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String)
|
||||
case illegalEncoding(String)
|
||||
case illegalDecoding(String)
|
||||
case missingOrMalformedMetadata
|
||||
case missingOrMalformedLinks
|
||||
}
|
||||
|
||||
extension ToOneRelationship {
|
||||
@@ -170,10 +177,10 @@ extension ToManyRelationship {
|
||||
}
|
||||
|
||||
// MARK: CustomStringDescribable
|
||||
public extension ToOneRelationship {
|
||||
var description: String { return "Relationship(\(String(describing: id)))" }
|
||||
extension ToOneRelationship: CustomStringConvertible {
|
||||
public var description: String { return "Relationship(\(String(describing: id)))" }
|
||||
}
|
||||
|
||||
public extension ToManyRelationship {
|
||||
var description: String { return "Relationship(\(String(describing: ids)))" }
|
||||
extension ToManyRelationship: CustomStringConvertible {
|
||||
public var description: String { return "Relationship(\(String(describing: ids)))" }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Transformer.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/17/18.
|
||||
//
|
||||
|
||||
public protocol Transformer {
|
||||
associatedtype From
|
||||
associatedtype To
|
||||
|
||||
static func transform(_ from: From) throws -> To
|
||||
}
|
||||
|
||||
public enum IdentityTransformer<T>: Transformer {
|
||||
public static func transform(_ from: T) throws -> T { return from }
|
||||
}
|
||||
|
||||
// MARK: - Validator
|
||||
|
||||
/// A Validator is a Transformer that throws an error if an invalid value
|
||||
/// is passed to it but it does not change the type of the value. Any
|
||||
/// Transformer will perform validation in one sense so a Validator is
|
||||
/// really just semantic sugar (it can provide clarity in its use).
|
||||
public protocol Validator: Transformer where From == To {}
|
||||
|
||||
extension Validator {
|
||||
public static func validate(_ value: To) throws -> To {
|
||||
return try transform(value)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,85 @@ let single_document_no_includes = """
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let single_document_no_includes_with_metadata = """
|
||||
{
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "articles",
|
||||
"relationships": {
|
||||
"author": {
|
||||
"data": {
|
||||
"type": "authors",
|
||||
"id": "33"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 70,
|
||||
"limit": 40,
|
||||
"offset": 10
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let single_document_no_includes_with_links = """
|
||||
{
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "articles",
|
||||
"relationships": {
|
||||
"author": {
|
||||
"data": {
|
||||
"type": "authors",
|
||||
"id": "33"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"link": "https://website.com",
|
||||
"link2": {
|
||||
"href": "https://othersite.com",
|
||||
"meta": {
|
||||
"hello": "world"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let single_document_no_includes_with_metadata_with_links = """
|
||||
{
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "articles",
|
||||
"relationships": {
|
||||
"author": {
|
||||
"data": {
|
||||
"type": "authors",
|
||||
"id": "33"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"link": "https://website.com",
|
||||
"link2": {
|
||||
"href": "https://othersite.com",
|
||||
"meta": {
|
||||
"hello": "world"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 70,
|
||||
"limit": 40,
|
||||
"offset": 10
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let single_document_some_includes = """
|
||||
{
|
||||
"data": {
|
||||
@@ -51,6 +130,71 @@ let single_document_some_includes = """
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let single_document_some_includes_with_metadata_with_links = """
|
||||
{
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "articles",
|
||||
"relationships": {
|
||||
"author": {
|
||||
"data": {
|
||||
"type": "authors",
|
||||
"id": "33"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"included": [
|
||||
{
|
||||
"id": "33",
|
||||
"type": "authors"
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"link": "https://website.com",
|
||||
"link2": {
|
||||
"href": "https://othersite.com",
|
||||
"meta": {
|
||||
"hello": "world"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"total": 70,
|
||||
"limit": 40,
|
||||
"offset": 10
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let single_document_some_includes_with_metadata = """
|
||||
{
|
||||
"data": {
|
||||
"id": "1",
|
||||
"type": "articles",
|
||||
"relationships": {
|
||||
"author": {
|
||||
"data": {
|
||||
"type": "authors",
|
||||
"id": "33"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"included": [
|
||||
{
|
||||
"id": "33",
|
||||
"type": "authors"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 70,
|
||||
"limit": 40,
|
||||
"offset": 10
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let many_document_no_includes = """
|
||||
{
|
||||
"data": [
|
||||
@@ -150,3 +294,115 @@ let many_document_some_includes = """
|
||||
]
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let error_document_no_metadata = """
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"description": "Boooo!",
|
||||
"code": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let error_document_with_metadata = """
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"description": "Boooo!",
|
||||
"code": 1
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 70,
|
||||
"limit": 40,
|
||||
"offset": 10
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let error_document_with_links = """
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"description": "Boooo!",
|
||||
"code": 1
|
||||
}
|
||||
],
|
||||
"links": {
|
||||
"link": "https://website.com",
|
||||
"link2": {
|
||||
"href": "https://othersite.com",
|
||||
"meta": {
|
||||
"hello": "world"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let error_document_with_metadata_with_links = """
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"description": "Boooo!",
|
||||
"code": 1
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"total": 70,
|
||||
"limit": 40,
|
||||
"offset": 10
|
||||
},
|
||||
"links": {
|
||||
"link": "https://website.com",
|
||||
"link2": {
|
||||
"href": "https://othersite.com",
|
||||
"meta": {
|
||||
"hello": "world"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let metadata_document = """
|
||||
{
|
||||
"meta": {
|
||||
"total": 100,
|
||||
"limit": 50,
|
||||
"offset": 0
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let metadata_document_with_links = """
|
||||
{
|
||||
"meta": {
|
||||
"total": 100,
|
||||
"limit": 50,
|
||||
"offset": 0
|
||||
},
|
||||
"links": {
|
||||
"link": "https://website.com",
|
||||
"link2": {
|
||||
"href": "https://othersite.com",
|
||||
"meta": {
|
||||
"hello": "world"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let metadata_document_missing_metadata = """
|
||||
{
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let metadata_document_missing_metadata2 = """
|
||||
{
|
||||
"meta": null
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user