mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a15ab6f9d | |||
| eb85620379 | |||
| 57f85476c0 | |||
| ea5c0b8601 | |||
| b46429a0ad | |||
| dbc6ecc6d8 | |||
| dbbacab105 | |||
| 4eb36d4594 | |||
| 43e02351de | |||
| 0b307bd3bc | |||
| 662a84ccf0 | |||
| 44f9bca7dc | |||
| 774b53b9b6 | |||
| 654d3bfd2b | |||
| c26d6d99c0 | |||
| 43dcc4fb12 | |||
| abae975f59 | |||
| 0e6b2a7771 | |||
| 194a58ae56 | |||
| f1d6b22f61 | |||
| 3057992348 | |||
| d4806ff557 | |||
| b0801f7cee | |||
| 88c5d400aa | |||
| 6cd5aeaba6 |
+1
-1
@@ -37,7 +37,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or an APIDescription (The *SPEC* calls this
|
||||
// "API Description" the "JSON:API Object").
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//: [Previous](@previous)
|
||||
|
||||
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.
|
||||
|
||||
This playground focuses on receiving a resource, making some changes, and then creating a request body for a PATCH request.
|
||||
As with all examples in these playround pages, no actual networking code will be provided.
|
||||
|
||||
********/
|
||||
|
||||
// Mock up a server response
|
||||
let mockDogData = """
|
||||
{
|
||||
"data": {
|
||||
"id": "1234",
|
||||
"type": "dogs",
|
||||
"attributes": {
|
||||
"name": "Sparky"
|
||||
},
|
||||
"relationships": {
|
||||
"owner": {
|
||||
"data": null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
//
|
||||
// MARK: - EXAMPLE 1 (Mutable Attributes)
|
||||
//
|
||||
|
||||
// pretend to have requested a Dog and received the mock data
|
||||
// now parse it.
|
||||
let parsedResponse = try! JSONDecoder().decode(MutableDogDocument.self, from: mockDogData)
|
||||
|
||||
// extract our Dog (skipping over any robustness to handle errors)
|
||||
var dog = parsedResponse.body.primaryResource!.value
|
||||
print("Received dog named: \(dog.name)")
|
||||
|
||||
// change the dog's name
|
||||
let changedDog = dog.tappingAttributes { $0.name = .init(value: "Julia") }
|
||||
|
||||
// create a document to be used as a request body for a PATCH request
|
||||
let patchRequest = MutableDogDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: changedDog),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
// encode and send off to server
|
||||
let encodedPatchRequest = try! JSONEncoder().encode(patchRequest)
|
||||
print("----")
|
||||
print(String(data: encodedPatchRequest, encoding:.utf8)!)
|
||||
|
||||
|
||||
//
|
||||
// MARK: - EXAMPLE 2 (Immutable Attributes)
|
||||
//
|
||||
print()
|
||||
print("####")
|
||||
print()
|
||||
|
||||
// pretend to have requested a Dog and received the mock data
|
||||
// now parse it.
|
||||
let parsedResponse2 = try! JSONDecoder().decode(SingleDogDocument.self, from: mockDogData)
|
||||
|
||||
// extract our Dog (skipping over any robustness to handle errors)
|
||||
var dog2 = parsedResponse2.body.primaryResource!.value
|
||||
print("Received dog named: \(dog2.name)")
|
||||
|
||||
// change the dog's name
|
||||
let changedDog2 = dog2.replacingAttributes { _ in
|
||||
return .init(name: .init(value: "Nigel"))
|
||||
}
|
||||
|
||||
// create a document to be used as a request body for a PATCH request
|
||||
let patchRequest2 = SingleDogDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: changedDog2),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
// encode and send off to server
|
||||
let encodedPatchRequest2 = try! JSONEncoder().encode(patchRequest2)
|
||||
print("----")
|
||||
print(String(data: encodedPatchRequest2, encoding:.utf8)!)
|
||||
|
||||
|
||||
//
|
||||
// MARK: - EXAMPLE 3 (Change relationship)
|
||||
//
|
||||
print()
|
||||
print("####")
|
||||
print()
|
||||
|
||||
// pretend to have requested a Dog and received the mock data
|
||||
// now parse it.
|
||||
let parsedResponse3 = try! JSONDecoder().decode(SingleDogDocument.self, from: mockDogData)
|
||||
|
||||
// extract our Dog (skipping over any robustness to handle errors)
|
||||
var dog3 = parsedResponse2.body.primaryResource!.value
|
||||
print("Received dog with owner: \(dog3 ~> \.owner)")
|
||||
|
||||
// give the dog an owner
|
||||
let changedDog3 = dog3.replacingRelationships { _ in
|
||||
return .init(owner: .init(id: Id(rawValue: "1")))
|
||||
}
|
||||
|
||||
// create a document to be used as a request body for a PATCH request
|
||||
let patchRequest3 = SingleDogDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: changedDog3),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
// encode and send off to server
|
||||
let encodedPatchRequest3 = try! JSONEncoder().encode(patchRequest3)
|
||||
print("----")
|
||||
print(String(data: encodedPatchRequest3, encoding:.utf8)!)
|
||||
@@ -35,7 +35,7 @@ typealias ThingWithProperties = JSONAPI.ResourceObject<ThingWithPropertiesDescri
|
||||
//
|
||||
// NOTE: Using `JSONAPI.EncodableResourceBody` which means the document type will be `Encodable` but not `Decodable`.
|
||||
//
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
//
|
||||
// NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable.
|
||||
|
||||
@@ -24,7 +24,7 @@ let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
|
||||
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it using an alternative model
|
||||
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
|
||||
let altDogFromData = altDogResponse.body.primaryResource?.value
|
||||
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
||||
@@ -63,7 +63,7 @@ if case let .data(bodyData) = peopleResponse.body {
|
||||
|
||||
|
||||
// MARK: - Work in the abstract
|
||||
|
||||
print("-----")
|
||||
func process<T: JSONAPIDocument>(document: T) {
|
||||
guard case let .data(body) = document.body else {
|
||||
return
|
||||
@@ -71,3 +71,29 @@ func process<T: JSONAPIDocument>(document: T) {
|
||||
let x: T.Body.Data = body
|
||||
}
|
||||
process(document: peopleResponse)
|
||||
|
||||
// MARK: - Work with errors
|
||||
typealias ErrorDoc = JSONAPI.Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
let mockErrorData =
|
||||
"""
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"status": "500",
|
||||
"title": "Internal Server Error",
|
||||
"detail": "Server fell over while parsing your request."
|
||||
}
|
||||
]
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData)
|
||||
|
||||
switch errorResponse.body {
|
||||
case .data:
|
||||
print("cool, data!")
|
||||
case .errors(let errors, let meta, let links):
|
||||
let errorDetails = errors.compactMap { $0.payload?.detail }
|
||||
print("error details: \(errorDetails)")
|
||||
}
|
||||
|
||||
@@ -119,6 +119,29 @@ public enum AlternativeDogDescription: ResourceObjectDescription {
|
||||
|
||||
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
|
||||
|
||||
public enum MutableDogDescription: ResourceObjectDescription {
|
||||
|
||||
public static var jsonType: String { return "dogs" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public var name: Attribute<String>
|
||||
|
||||
public init(name: Attribute<String>) {
|
||||
self.name = name
|
||||
}
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public var owner: ToOne<Person?>
|
||||
|
||||
public init(owner: ToOne<Person?>) {
|
||||
self.owner = owner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public typealias MutableDog = ExampleEntity<MutableDogDescription>
|
||||
|
||||
public extension ResourceObject where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
|
||||
init(name: String, owner: Person?) throws {
|
||||
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none)
|
||||
@@ -139,6 +162,8 @@ public enum HouseDescription: ResourceObjectDescription {
|
||||
|
||||
public typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
|
||||
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
|
||||
public typealias MutableDogDocument = JSONAPI.Document<SingleResourceBody<MutableDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
<page name='Full Client & Server Example'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
<page name='Sparse Fieldsets Example'/>
|
||||
<page name='PATCHing'/>
|
||||
</pages>
|
||||
</playground>
|
||||
+2
-2
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
|
||||
#
|
||||
|
||||
spec.name = "MP-JSONAPI"
|
||||
spec.version = "2.1.0"
|
||||
spec.version = "2.5.0"
|
||||
spec.summary = "Swift Codable JSON API framework."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
@@ -136,6 +136,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
# spec.requires_arc = true
|
||||
|
||||
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
|
||||
spec.dependency "Poly", "~> 2.1"
|
||||
spec.dependency "Poly", "~> 2.2"
|
||||
|
||||
end
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "4a08517b24f8e9f6dd8c02ec7da316aac5c00e2e",
|
||||
"version": "2.1.0"
|
||||
"revision": "b24fd3b41bf3126d4c6dede3708135182172af60",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ let package = Package(
|
||||
targets: ["JSONAPITesting"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.1.0")),
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.2.0")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -5,28 +5,29 @@ A Swift package for encoding to- and decoding from **JSON API** compliant reques
|
||||
|
||||
See the JSON API Spec here: https://jsonapi.org/format/
|
||||
|
||||
:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful. :warning:
|
||||
:warning: This library provides well-tested type safety when working with JSON:API 1.0. However, the Swift compiler can sometimes have difficulty tracking down small typos when initializing `ResourceObjects`. Once the code is written correctly, it will compile, but tracking down the source of programmer errors can be an annoyance. This is mostly a concern when creating resource objects in-code (servers and test cases must do this). Writing a client that uses this framework to ingest JSON API Compliant API responses is much less painful.
|
||||
|
||||
## Quick Start
|
||||
|
||||
:warning: The following Google Colab examples have correct code, but there appears to be an bug in the branch of the Swift compiler currently being used by the Google Colab Swift notebooks such that the `JSONAPI` package cannot be pulled in and you cannot run the examples in-browser.
|
||||
|
||||
### Clientside
|
||||
- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te)
|
||||
- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU)
|
||||
- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr)
|
||||
- [Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH)
|
||||
- [Basic Example](https://colab.research.google.com/drive/1IS7lRSBGoiW02Vd1nN_rfdDbZvTDj6Te)
|
||||
- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU)
|
||||
- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr)
|
||||
- [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH)
|
||||
- [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U)
|
||||
|
||||
### Serverside
|
||||
- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX)
|
||||
- [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ)
|
||||
|
||||
### Combined
|
||||
### Client+Server
|
||||
This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README.
|
||||
|
||||
## Table of Contents
|
||||
<!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
|
||||
|
||||
- [JSONAPI](#jsonapi)
|
||||
- [Table of Contents](#table-of-contents)
|
||||
- [Primary Goals](#primary-goals)
|
||||
- [Caveat](#caveat)
|
||||
- [Dev Environment](#dev-environment)
|
||||
@@ -67,12 +68,18 @@ This library works well when used by both the server responsible for serializati
|
||||
- [`IncludeType`](#includetype)
|
||||
- [`APIDescriptionType`](#apidescriptiontype)
|
||||
- [`Error`](#error)
|
||||
- [`UnknownJSONAPIError`](#unknownjsonapierror)
|
||||
- [`BasicJSONAPIError`](#basicjsonapierror)
|
||||
- [`GenericJSONAPIError`](#genericjsonapierror)
|
||||
- [`JSONAPI.Meta`](#jsonapimeta)
|
||||
- [`JSONAPI.Links`](#jsonapilinks)
|
||||
- [`JSONAPI.RawIdType`](#jsonapirawidtype)
|
||||
- [Sparse Fieldsets](#sparse-fieldsets)
|
||||
- [Supporting Sparse Fieldset Encoding](#supporting-sparse-fieldset-encoding)
|
||||
- [Sparse Fieldset `typealias` comparisons](#sparse-fieldset-typealias-comparisons)
|
||||
- [Replacing and Tapping Attributes/Relationships](#replacing-and-tapping-attributesrelationships)
|
||||
- [Tapping](#tapping)
|
||||
- [Replacing](#replacing)
|
||||
- [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping)
|
||||
- [Custom Attribute Encode/Decode](#custom-attribute-encodedecode)
|
||||
- [Meta-Attributes](#meta-attributes)
|
||||
@@ -85,8 +92,6 @@ This library works well when used by both the server responsible for serializati
|
||||
- [JSONAPI+Arbitrary](#jsonapiarbitrary)
|
||||
- [JSONAPI+OpenAPI](#jsonapiopenapi)
|
||||
|
||||
<!-- /TOC -->
|
||||
|
||||
## Primary Goals
|
||||
|
||||
The primary goals of this framework are:
|
||||
@@ -108,7 +113,7 @@ If you find something wrong with this library and it isn't already mentioned und
|
||||
### Swift Package Manager
|
||||
Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets.
|
||||
```
|
||||
.package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.0.0"))
|
||||
.package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.2.0"))
|
||||
```
|
||||
|
||||
### CocoaPods
|
||||
@@ -122,6 +127,8 @@ To use this framework in your project via Cocoapods, add the following dependenc
|
||||
To create an Xcode project for JSONAPI, run
|
||||
`swift package generate-xcodeproj`
|
||||
|
||||
With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -330,7 +337,7 @@ let favoriteColor: String = person.favoriteColor
|
||||
let favoriteColor: String = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
In both cases you retain type-safety, although neither plays particularly nicely with code autocompletion. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated.
|
||||
In both cases you retain type-safety. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated.
|
||||
|
||||
#### `Transformer`
|
||||
|
||||
@@ -403,7 +410,7 @@ The entirety of a JSON API request or response is encoded or decoded from- or to
|
||||
```swift
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let responseStructure = JSONAPI.Document<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self
|
||||
let responseStructure = JSONAPI.Document<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError<String>>.self
|
||||
|
||||
let document = try decoder.decode(responseStructure, from: data)
|
||||
```
|
||||
@@ -452,7 +459,7 @@ The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are d
|
||||
|
||||
#### `IncludeType`
|
||||
|
||||
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to six types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on.
|
||||
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. 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 resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_.
|
||||
|
||||
@@ -470,7 +477,45 @@ You can supply any `JSONAPI.Meta` type as the metadata type of the API descripti
|
||||
|
||||
#### `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`.
|
||||
The final generic type of a `JSONAPIDocument` is the `Error`.
|
||||
|
||||
You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`.
|
||||
|
||||
##### `UnknownJSONAPIError`
|
||||
The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described.
|
||||
|
||||
##### `BasicJSONAPIError`
|
||||
The `BasicJSONAPIError` type will always succeed unless it is faced with an `id` field of an unexpected type, although it still "succeeds" in falling back to its `.unknown` case when that happens. This type extracts _most_ of the fields the **SPEC** describes [here](https://jsonapi.org/format/#error-objects). Because all of these fields are optional in the **SPEC**, they are optional on the `BasicJSONAPIError` type. You will have to create your own error type if you want to define certain fields as non-optional or parse metadata or links out of error objects.
|
||||
|
||||
đź—’Metadata and links are supported at the Document level for error responses, the are just not supported hanging off of the individual errors in the `errors` array of the response when using this error type.
|
||||
|
||||
The `BasicJSONAPIError` type is generic on one thing: The type it expects for the `id` field. If you expect integer `ids` back, you use `BasicJSONAPIError<Int>`. The same can be done for `String` or any other type that is both `Codable` and `Equatable`. You can even employ something like `AnyCodable` from *Flight-School* as your id field type. If you only need to handle a small subset of possible `id` field types, you can also use the `Poly` library that is already a dependency of `JSONAPI`. For example, you might expect a mix of `String` and `Int` ids for some reason: `BasicJSONAPIError<Either<Int, String>>`.
|
||||
|
||||
The two easiest ways to access the available properties of an error response are under the `payload` property of the error (this property is `nil` if the error was parsed as `.unknown`) or by asking the error for its `definedFields` dictionary.
|
||||
|
||||
As an example, let's say you have the following `Document` type that is destined for errors:
|
||||
```swift
|
||||
typealias ErrorDoc = JSONAPI.Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
```
|
||||
And you've parsed an error response
|
||||
```swift
|
||||
let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData)
|
||||
```
|
||||
You can get at the `Document` body and errors in a couple of different ways, but for one you can switch on the body:
|
||||
```swift
|
||||
switch errorResponse.body {
|
||||
case .data:
|
||||
print("cool, data!")
|
||||
|
||||
case .errors(let errors, let meta, let links):
|
||||
let errorDetails = errors.compactMap { $0.payload?.detail }
|
||||
|
||||
print("error details: \(errorDetails)")
|
||||
}
|
||||
```
|
||||
|
||||
##### `GenericJSONAPIError`
|
||||
This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError<YourType>` as the error type for a `Document`.
|
||||
|
||||
### `JSONAPI.Meta`
|
||||
|
||||
@@ -520,12 +565,41 @@ There is a sparse fieldsets example included with this repository as a Playgroun
|
||||
#### Sparse Fieldset `typealias` comparisons
|
||||
You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`):
|
||||
```swift
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
```
|
||||
|
||||
In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`):
|
||||
```swift
|
||||
typealias SparseDocument<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
typealias SparseDocument<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
```
|
||||
|
||||
### Replacing and Tapping Attributes/Relationships
|
||||
When you are working with an immutable Resource Object, it can be useful to replace its attributes or relationships. As a client, you might receive a resource from the server, update something, and then send the server a PATCH request.
|
||||
|
||||
`ResourceObject` is immutable, but you can create a new copy of a `ResourceObject` having updated attributes or relationships.
|
||||
|
||||
#### Tapping
|
||||
If your `Attributes` or `Relationships` struct is mutable (i.e. its properties are `var`s) then you may find `ResourceObject`'s `tappingAttributes()` and `tappingRelationships()` functions useful. For both, you pass a function that takes an `inout` copy of the respective object or value that you can mutate. The mutated value is then used to create a new `ResourceObject`.
|
||||
|
||||
For example, to take a hypothetical `Dog` resource object and change the name attribute:
|
||||
```swift
|
||||
let resourceObject = Dog(...)
|
||||
|
||||
let newResourceObject = resourceObject
|
||||
.tappingAttributes { $0.name = .init(value: "Charlie") }
|
||||
```
|
||||
|
||||
#### Replacing
|
||||
If your `Attributes` or `Relationships` struct is immutable (i.e. its properties are `let`s) then you may find `ResourceObject`'s `replacingAttributes()` and `replacingRelationships()` functions useful. For both, you pass a function that takes the current attributes or relationships and you return a new value. The new value is then used to create a new `ResourceObject`.
|
||||
|
||||
For example, to take a hypothetical `Dog` resource object and change the name attribute:
|
||||
```swift
|
||||
let resourceObject = Dog(...)
|
||||
|
||||
let newResourceObject = resourceObject
|
||||
.replacingAttributes { _ in
|
||||
return Dog.Attributes(name: .init(value: "Charlie"))
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Attribute or Relationship Key Mapping
|
||||
@@ -713,7 +787,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or an APIDescription (The *SPEC* calls this
|
||||
// "API Description" the "JSON:API Object").
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
|
||||
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
|
||||
@@ -414,3 +414,137 @@ extension Document.Body.Data: CustomStringConvertible {
|
||||
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error and Success Document Types
|
||||
|
||||
extension Document {
|
||||
/// A Document that only supports error bodies. This is useful if you wish to pass around a
|
||||
/// Document type but you wish to constrain it to error values.
|
||||
@dynamicMemberLookup
|
||||
public struct ErrorDocument: EncodableJSONAPIDocument {
|
||||
public var body: Document.Body { return document.body }
|
||||
|
||||
private let document: Document
|
||||
|
||||
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
document = .init(apiDescription: apiDescription, errors: errors, meta: meta, links: links)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(document)
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember path: KeyPath<Document, T>) -> T {
|
||||
return document[keyPath: path]
|
||||
}
|
||||
|
||||
public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool {
|
||||
return lhs == rhs.document
|
||||
}
|
||||
}
|
||||
|
||||
/// A Document that only supports success bodies. This is useful if you wish to pass around a
|
||||
/// Document type but you wish to constrain it to success values.
|
||||
@dynamicMemberLookup
|
||||
public struct SuccessDocument: EncodableJSONAPIDocument {
|
||||
public var body: Document.Body { return document.body }
|
||||
|
||||
private let document: Document
|
||||
|
||||
public init(apiDescription: APIDescription,
|
||||
body: PrimaryResourceBody,
|
||||
includes: Includes<Include>,
|
||||
meta: MetaType,
|
||||
links: LinksType) {
|
||||
document = .init(apiDescription: apiDescription,
|
||||
body: body,
|
||||
includes: includes,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(document)
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember path: KeyPath<Document, T>) -> T {
|
||||
return document[keyPath: path]
|
||||
}
|
||||
|
||||
public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool {
|
||||
return lhs == rhs.document
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.ErrorDocument: Decodable, JSONAPIDocument
|
||||
where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
document = try container.decode(Document.self)
|
||||
|
||||
guard document.body.isError else {
|
||||
throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.SuccessDocument: Decodable, JSONAPIDocument
|
||||
where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
document = try container.decode(Document.self)
|
||||
|
||||
guard !document.body.isError else {
|
||||
throw JSONAPIDocumentDecodingError.foundErrorDocumentWhenExpectingSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.SuccessDocument where IncludeType == NoIncludes {
|
||||
/// Create a new Document with the given includes.
|
||||
public func including<I: JSONAPI.Include>(_ includes: Includes<I>) -> Document<PrimaryResourceBody, MetaType, LinksType, I, APIDescription, Error>.SuccessDocument {
|
||||
// Note that if IncludeType is NoIncludes, then we allow anything
|
||||
// to be included, but if IncludeType already specifies a type
|
||||
// of thing to be expected then we lock that down.
|
||||
// See: Document.including() where IncludeType: _Poly1
|
||||
switch document.body {
|
||||
case .data(let data):
|
||||
return .init(apiDescription: document.apiDescription,
|
||||
body: data.primary,
|
||||
includes: includes,
|
||||
meta: data.meta,
|
||||
links: data.links)
|
||||
case .errors:
|
||||
fatalError("SuccessDocument cannot end up in an error state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extending where _Poly1 means all non-zero _Poly arities are included
|
||||
extension Document.SuccessDocument where IncludeType: _Poly1 {
|
||||
/// Create a new Document adding the given includes. This does not
|
||||
/// remove existing includes; it is additive.
|
||||
public func including(_ includes: Includes<IncludeType>) -> Document.SuccessDocument {
|
||||
// Note that if IncludeType is NoIncludes, then we allow anything
|
||||
// to be included, but if IncludeType already specifies a type
|
||||
// of thing to be expected then we lock that down.
|
||||
// See: Document.including() where IncludeType == NoIncludes
|
||||
switch document.body {
|
||||
case .data(let data):
|
||||
return .init(apiDescription: document.apiDescription,
|
||||
body: data.primary,
|
||||
includes: data.includes + includes,
|
||||
meta: data.meta,
|
||||
links: data.links)
|
||||
case .errors:
|
||||
fatalError("SuccessDocument cannot end up in an error state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// DocumentDecodingErro.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 10/20/19.
|
||||
//
|
||||
|
||||
public enum JSONAPIDocumentDecodingError: Swift.Error {
|
||||
case foundErrorDocumentWhenExpectingSuccess
|
||||
case foundSuccessDocumentWhenExpectingError
|
||||
}
|
||||
@@ -169,3 +169,11 @@ extension Includes where I: _Poly10 {
|
||||
return values.compactMap { $0.j }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 11 includes
|
||||
public typealias Include11 = Poly11
|
||||
extension Includes where I: _Poly11 {
|
||||
public subscript(_ lookup: I.K.Type) -> [I.K] {
|
||||
return values.compactMap { $0.k }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// BasicJSONAPIError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 9/29/19.
|
||||
//
|
||||
|
||||
/// Most of the JSON:API Spec defined Error fields.
|
||||
public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType {
|
||||
/// a unique identifier for this particular occurrence of the problem
|
||||
public let id: IdType?
|
||||
// public let links: Links? // we skip this for now to avoid adding complexity to using this basic type.
|
||||
/// the HTTP status code applicable to this problem
|
||||
public let status: String?
|
||||
/// an application-specific error code
|
||||
public let code: String?
|
||||
/// a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization
|
||||
public let title: String?
|
||||
/// a human-readable explanation specific to this occurrence of the problem. Like `title`, this field’s value can be localized
|
||||
public let detail: String?
|
||||
/// an object containing references to the source of the error
|
||||
public let source: Source?
|
||||
// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type
|
||||
|
||||
public init(id: IdType? = nil,
|
||||
status: String? = nil,
|
||||
code: String? = nil,
|
||||
title: String? = nil,
|
||||
detail: String? = nil,
|
||||
source: Source? = nil) {
|
||||
self.id = id
|
||||
self.status = status
|
||||
self.code = code
|
||||
self.title = title
|
||||
self.detail = detail
|
||||
self.source = source
|
||||
}
|
||||
|
||||
public struct Source: Codable, Equatable {
|
||||
/// a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].
|
||||
public let pointer: String?
|
||||
/// which URI query parameter caused the error
|
||||
public let parameter: String?
|
||||
|
||||
public init(pointer: String? = nil,
|
||||
parameter: String? = nil) {
|
||||
self.pointer = pointer
|
||||
self.parameter = parameter
|
||||
}
|
||||
}
|
||||
|
||||
public var definedFields: [String: String] {
|
||||
let keysAndValues = [
|
||||
id.map { ("id", String(describing: $0)) },
|
||||
status.map { ("status", $0) },
|
||||
code.map { ("code", $0) },
|
||||
title.map { ("title", $0) },
|
||||
detail.map { ("detail", $0) },
|
||||
source.flatMap { $0.pointer.map { ("pointer", $0) } },
|
||||
source.flatMap { $0.parameter.map { ("parameter", $0) } }
|
||||
].compactMap { $0 }
|
||||
return Dictionary(uniqueKeysWithValues: keysAndValues)
|
||||
}
|
||||
}
|
||||
|
||||
/// `BasicJSONAPIError` optionally decodes many possible fields
|
||||
/// specified by the JSON:API 1.0 Spec. It gives no type-guarantees of what
|
||||
/// will be non-nil, but could provide good diagnostic information when
|
||||
/// you do not know what error structure to expect.
|
||||
///
|
||||
/// ```
|
||||
/// Fields:
|
||||
/// - id
|
||||
/// - status
|
||||
/// - code
|
||||
/// - title
|
||||
/// - detail
|
||||
/// - source
|
||||
/// - pointer
|
||||
/// - parameter
|
||||
/// ```
|
||||
///
|
||||
/// The JSON:API Spec does not dictate the type of this particular Id field,
|
||||
/// so you must specify whether to expect, for example, an `Int` or a `String`
|
||||
/// in the id field.
|
||||
///
|
||||
/// Something like `AnyCodable` from *Flight-School* could be
|
||||
/// a good option if you do not know what to expect. You could also use
|
||||
/// `Either<Int, String>` (provided by the `Poly` package that is
|
||||
/// already a dependency of `JSONAPI`).
|
||||
///
|
||||
/// - Important: The `definedFields` property will include fields
|
||||
/// with non-nil values in a flattened way. There will be no `source` key
|
||||
/// but there will be `pointer` and `parameter` keys (if those values
|
||||
/// are non-nil).
|
||||
public typealias BasicJSONAPIError<IdType: Codable & Equatable> = GenericJSONAPIError<BasicJSONAPIErrorPayload<IdType>>
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// GenericJSONAPIError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 9/29/19.
|
||||
//
|
||||
|
||||
/// `GenericJSONAPIError` can be used to specify whatever error
|
||||
/// payload you expect to need to parse in responses and handle any
|
||||
/// other payload structure as `.unknownError`.
|
||||
public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError {
|
||||
case unknownError
|
||||
case error(ErrorPayload)
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
do {
|
||||
self = .error(try container.decode(ErrorPayload.self))
|
||||
} catch {
|
||||
self = .unknown
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
switch self {
|
||||
case .error(let payload):
|
||||
try container.encode(payload)
|
||||
case .unknownError:
|
||||
try container.encode("unknown")
|
||||
}
|
||||
}
|
||||
|
||||
public static var unknown: Self {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
|
||||
public extension GenericJSONAPIError {
|
||||
var payload: ErrorPayload? {
|
||||
switch self {
|
||||
case .unknownError:
|
||||
return nil
|
||||
case .error(let payload):
|
||||
return payload
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol ErrorDictType {
|
||||
var definedFields: [String: String] { get }
|
||||
}
|
||||
|
||||
extension GenericJSONAPIError: ErrorDictType where ErrorPayload: ErrorDictType {
|
||||
/// Get a dictionary of all defined fields and their values.
|
||||
public var definedFields: [String: String] {
|
||||
switch self {
|
||||
case .unknownError:
|
||||
return [:]
|
||||
case .error(let basicPayload):
|
||||
return basicPayload.definedFields
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Error.swift
|
||||
// JSONAPIError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
@@ -11,7 +11,10 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
|
||||
|
||||
/// `UnknownJSONAPIError` can actually be used in any sitaution
|
||||
/// where you don't know what errors are possible _or_ you just don't
|
||||
/// care what errors might show up.
|
||||
/// care what errors might show up. If you don't know how the error
|
||||
/// will be structured but you would like to have access to more
|
||||
/// information the server might be providing in the error payload,
|
||||
/// use `BasicJSONAPIError` instead.
|
||||
public enum UnknownJSONAPIError: JSONAPIError {
|
||||
case unknownError
|
||||
|
||||
@@ -24,7 +27,7 @@ public enum UnknownJSONAPIError: JSONAPIError {
|
||||
try container.encode("unknown")
|
||||
}
|
||||
|
||||
public static var unknown: UnknownJSONAPIError {
|
||||
public static var unknown: Self {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
@@ -79,3 +79,8 @@ extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped,
|
||||
extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {}
|
||||
|
||||
// MARK: - 11 types
|
||||
extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ResourceObject+Replacing.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 10/12/19.
|
||||
//
|
||||
|
||||
public extension JSONAPI.ResourceObject {
|
||||
/// Return a new `ResourceObject`, having replaced `self`'s
|
||||
/// `attributes` with the attributes returned by the given
|
||||
/// replacement function.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - replacement: A function that takes the existing `attributes` and returns the replacement.
|
||||
func replacingAttributes(_ replacement: (Description.Attributes) -> Description.Attributes) -> Self {
|
||||
return Self(id: id,
|
||||
attributes: replacement(attributes),
|
||||
relationships: relationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having updated `self`'s
|
||||
/// `attributes` with the tap function given.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - tap: A function that takes a copy of the existing `attributes` and mutates them.
|
||||
func tappingAttributes(_ tap: (inout Description.Attributes) -> Void) -> Self {
|
||||
var newAttributes = attributes
|
||||
tap(&newAttributes)
|
||||
return Self(id: id,
|
||||
attributes: newAttributes,
|
||||
relationships: relationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having replaced `self`'s
|
||||
/// `relationships` with the `relationships` returned by the given
|
||||
/// replacement function.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - replacement: A function that takes the existing relationships and returns the replacement.
|
||||
func replacingRelationships(_ replacement: (Description.Relationships) -> Description.Relationships) -> Self {
|
||||
return Self(id: id,
|
||||
attributes: attributes,
|
||||
relationships: replacement(relationships),
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having updated `self`'s
|
||||
/// `relationships` with the tap function given.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - tap: A function that takes a copy of the existing `relationships` and mutates them.
|
||||
func tappingRelationships(_ tap: (inout Description.Relationships) -> Void) -> Self {
|
||||
var newRelationships = relationships
|
||||
tap(&newRelationships)
|
||||
return Self(id: id,
|
||||
attributes: attributes,
|
||||
relationships: newRelationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class DocumentTests: XCTestCase {
|
||||
XCTAssert(Doc.Error.self == UnknownJSONAPIError.self)
|
||||
}
|
||||
|
||||
// Document
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
@@ -37,6 +38,35 @@ class DocumentTests: XCTestCase {
|
||||
meta: .none,
|
||||
links: .none
|
||||
))
|
||||
|
||||
// Document.SuccessDocument
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>.SuccessDocument(
|
||||
apiDescription: .none,
|
||||
body: .none,
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none
|
||||
))
|
||||
|
||||
// Document.ErrorDocument
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>.ErrorDocument(
|
||||
apiDescription: .none,
|
||||
errors: []
|
||||
))
|
||||
}
|
||||
|
||||
func test_singleDocumentNull() {
|
||||
@@ -51,11 +81,34 @@ class DocumentTests: XCTestCase {
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.links, NoLinks())
|
||||
XCTAssertEqual(document.apiDescription, .none)
|
||||
|
||||
// SuccessDocument
|
||||
let document2 = decoded(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_null)
|
||||
|
||||
XCTAssert(document == document2)
|
||||
|
||||
XCTAssertFalse(document2.body.isError)
|
||||
XCTAssertNil(document2.body.errors)
|
||||
XCTAssertNotNil(document2.body.primaryResource)
|
||||
XCTAssertEqual(document2.body.meta, NoMetadata())
|
||||
XCTAssertNil(document2.body.primaryResource?.value)
|
||||
XCTAssertEqual(document2.body.includes?.count, 0)
|
||||
XCTAssertEqual(document2.body.links, NoLinks())
|
||||
XCTAssertEqual(document2.apiDescription, .none)
|
||||
|
||||
// ErrorDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
from: single_document_null))
|
||||
}
|
||||
|
||||
func test_singleDocumentNull_encode() {
|
||||
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_null)
|
||||
|
||||
// SuccessDocument
|
||||
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_null)
|
||||
}
|
||||
|
||||
func test_singleDocumentNullWithAPIDescription() {
|
||||
@@ -94,6 +147,14 @@ extension DocumentTests {
|
||||
func test_errorDocumentFailsWithNoAPIDescription() {
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// SuccessDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// ErrorDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
}
|
||||
|
||||
func test_unknownErrorDocumentNoMeta() {
|
||||
@@ -115,6 +176,32 @@ extension DocumentTests {
|
||||
XCTAssertEqual(errors.0, document.body.errors)
|
||||
XCTAssertEqual(errors.0[0], .unknown)
|
||||
XCTAssertEqual(errors.meta, NoMetadata())
|
||||
|
||||
// SuccessDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// ErrorDocument
|
||||
let document2 = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
data: error_document_no_metadata)
|
||||
|
||||
XCTAssert(document == document2)
|
||||
|
||||
XCTAssertTrue(document2.body.isError)
|
||||
XCTAssertEqual(document2.body.meta, NoMetadata())
|
||||
XCTAssertNil(document2.body.data)
|
||||
XCTAssertNil(document2.body.primaryResource)
|
||||
XCTAssertNil(document2.body.includes)
|
||||
|
||||
guard case let .errors(errors2) = document2.body else {
|
||||
XCTFail("Needed body to be in errors case but it was not.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(errors2.0.count, 1)
|
||||
XCTAssertEqual(errors2.0, document2.body.errors)
|
||||
XCTAssertEqual(errors2.0[0], .unknown)
|
||||
XCTAssertEqual(errors2.meta, NoMetadata())
|
||||
}
|
||||
|
||||
func test_unknownErrorDocumentAddIncludingType() {
|
||||
@@ -620,6 +707,27 @@ extension DocumentTests {
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
|
||||
}
|
||||
|
||||
func test_singleSuccessDocumentNoIncludesAddIncludingType() {
|
||||
// NOTE distinct from above for being Document.SuccessDocument
|
||||
let author = Author(id: "1",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_no_includes)
|
||||
|
||||
let documentWithIncludes = document.including(Includes<Include1<Author>>(values: [.init(author)]))
|
||||
|
||||
XCTAssert(type(of: documentWithIncludes) == Document<NoResourceBody, NoMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self)
|
||||
XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors)
|
||||
XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta)
|
||||
XCTAssertEqual(document.body.links, documentWithIncludes.body.links)
|
||||
XCTAssertEqual(document.body.includes, Includes<NoIncludes>.none)
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
|
||||
}
|
||||
|
||||
func test_singleDocumentNoIncludesWithAPIDescription() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_no_includes_with_api_description)
|
||||
@@ -848,6 +956,31 @@ extension DocumentTests {
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor])
|
||||
}
|
||||
|
||||
func test_singleSuccessDocumentSomeIncludesAddIncludes() {
|
||||
// NOTE distinct from above for being Document.SuccessDocument
|
||||
let existingAuthor = Author(id: "33",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let newAuthor = Author(id: "1",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_some_includes)
|
||||
|
||||
let documentWithIncludes = document.including(.init(values: [.init(newAuthor)]))
|
||||
|
||||
XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors)
|
||||
XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta)
|
||||
XCTAssertEqual(document.body.links, documentWithIncludes.body.links)
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor])
|
||||
}
|
||||
|
||||
func test_singleDocumentSomeIncludesWithAPIDescription() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_some_includes_with_api_description)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user