mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
48 Commits
2.2.0
...
3.0.0-alpha.4
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c3a82ec23 | |||
| e9a3b35dc7 | |||
| 2eecf95995 | |||
| 4dc30ddc1c | |||
| 455ff64326 | |||
| 0b4baf35d5 | |||
| 11ef050d58 | |||
| 86344ef93f | |||
| 7fabe2574e | |||
| 19636a47f0 | |||
| 0538de48cb | |||
| 024fe2d452 | |||
| 832161628b | |||
| f37f44cfda | |||
| ae7e0f528a | |||
| 9f42e5fa3f | |||
| 3e96adf775 | |||
| 83233a7e1a | |||
| adcc6bfb10 | |||
| 6f9a2df605 | |||
| c24b70b49f | |||
| 7b56e35a91 | |||
| e6f82c6052 | |||
| d6f01d6c1d | |||
| a7f6ed5845 | |||
| 87271b93f9 | |||
| 706346e3a6 | |||
| 33a5ff41a0 | |||
| 0fe5c53ada | |||
| f7bfa91ccc | |||
| 1a15ab6f9d | |||
| eb85620379 | |||
| 57f85476c0 | |||
| ea5c0b8601 | |||
| b46429a0ad | |||
| dbc6ecc6d8 | |||
| dbbacab105 | |||
| 4eb36d4594 | |||
| 43e02351de | |||
| 0b307bd3bc | |||
| 662a84ccf0 | |||
| 44f9bca7dc | |||
| 774b53b9b6 | |||
| 654d3bfd2b | |||
| c26d6d99c0 | |||
| 43dcc4fb12 | |||
| abae975f59 | |||
| 0e6b2a7771 |
@@ -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)!)
|
||||
@@ -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)
|
||||
@@ -141,4 +164,6 @@ public typealias House = ExampleEntity<HouseDescription>
|
||||
|
||||
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
|
||||
|
||||
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.2.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": "0c9c08204142babc480938d704a23513d11420e5",
|
||||
"version": "2.3.1"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+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.3.1")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -5,21 +5,24 @@ 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)
|
||||
- [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
|
||||
@@ -74,6 +77,9 @@ This library works well when used by both the server responsible for serializati
|
||||
- [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)
|
||||
@@ -326,13 +332,11 @@ As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup
|
||||
let favoriteColor: String = person.favoriteColor
|
||||
```
|
||||
|
||||
đź—’ `Attributes` can also be accessed via the older `subscript` operator as follows:
|
||||
:warning: `Attributes` can also be accessed via the older `subscript` operator, but this is a deprecated feature that will be removed in the next major version:
|
||||
```swift
|
||||
let favoriteColor: String = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`.
|
||||
@@ -567,6 +571,35 @@ In order to support sparse fieldsets (which are encode-only), the following comp
|
||||
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
|
||||
There is not anything special going on at the `JSONAPI.Attributes` and `JSONAPI.Relationships` levels, so you can easily provide custom key mappings by taking advantage of `Codable`'s `CodingKeys` pattern. Here are two models that will encode/decode equivalently but offer different naming in your codebase:
|
||||
```swift
|
||||
|
||||
@@ -7,58 +7,58 @@
|
||||
|
||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||
public protocol APIDescriptionType: Codable, Equatable {
|
||||
associatedtype Meta
|
||||
associatedtype Meta
|
||||
}
|
||||
|
||||
/// This is what the JSON API Spec calls the "JSON:API Object"
|
||||
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
|
||||
public let version: String
|
||||
public let meta: Meta
|
||||
public let version: String
|
||||
public let meta: Meta
|
||||
|
||||
public init(version: String, meta: Meta) {
|
||||
self.version = version
|
||||
self.meta = meta
|
||||
}
|
||||
public init(version: String, meta: Meta) {
|
||||
self.version = version
|
||||
self.meta = meta
|
||||
}
|
||||
}
|
||||
|
||||
/// Can be used as `APIDescriptionType` for Documents that do not
|
||||
/// have any API Description (a.k.a. "JSON:API Object").
|
||||
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
|
||||
public typealias Meta = NoMetadata
|
||||
public typealias Meta = NoMetadata
|
||||
|
||||
public init() {}
|
||||
public init() {}
|
||||
|
||||
public static var none: NoAPIDescription { return .init() }
|
||||
public static var none: NoAPIDescription { return .init() }
|
||||
|
||||
public var description: String { return "No JSON:API Object" }
|
||||
public var description: String { return "No JSON:API Object" }
|
||||
}
|
||||
|
||||
extension APIDescription {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case meta
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case meta
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
|
||||
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
|
||||
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
|
||||
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
|
||||
|
||||
if let metaVal = NoMetadata() as? Meta {
|
||||
meta = metaVal
|
||||
} else {
|
||||
meta = try container.decode(Meta.self, forKey: .meta)
|
||||
}
|
||||
}
|
||||
if let metaVal = NoMetadata() as? Meta {
|
||||
meta = metaVal
|
||||
} else {
|
||||
meta = try container.decode(Meta.self, forKey: .meta)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(version, forKey: .version)
|
||||
try container.encode(version, forKey: .version)
|
||||
|
||||
if Meta.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
}
|
||||
if Meta.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// DocumentDecodingError.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 10/20/19.
|
||||
//
|
||||
|
||||
public enum DocumentDecodingError: Swift.Error, Equatable {
|
||||
case primaryResource(error: ResourceObjectDecodingError, idx: Int?)
|
||||
case primaryResourceMissing
|
||||
case primaryResourcesMissing
|
||||
|
||||
case includes(error: IncludesDecodingError)
|
||||
|
||||
case foundErrorDocumentWhenExpectingSuccess
|
||||
case foundSuccessDocumentWhenExpectingError
|
||||
|
||||
init(_ decodingError: ResourceObjectDecodingError) {
|
||||
self = .primaryResource(error: decodingError, idx: nil)
|
||||
}
|
||||
|
||||
init(_ decodingError: ManyResourceBodyDecodingError) {
|
||||
self = .primaryResource(error: decodingError.error, idx: decodingError.idx)
|
||||
}
|
||||
|
||||
init(_ decodingError: IncludesDecodingError) {
|
||||
self = .includes(error: decodingError)
|
||||
}
|
||||
|
||||
init?(_ decodingError: DecodingError) {
|
||||
switch decodingError {
|
||||
case .valueNotFound(let type, let context) where Location(context) == .data && type is AbstractResourceObject.Type:
|
||||
self = .primaryResourceMissing
|
||||
case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self:
|
||||
self = .primaryResourcesMissing
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private enum Location: Equatable {
|
||||
case data
|
||||
case other
|
||||
|
||||
init(_ context: DecodingError.Context) {
|
||||
if context.codingPath.contains(where: { $0.stringValue == "data" }) {
|
||||
self = .data
|
||||
} else {
|
||||
self = .other
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DocumentDecodingError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .primaryResource(error: let error, idx: let idx):
|
||||
let idxString = idx.map { " \($0 + 1)" } ?? ""
|
||||
return "Primary Resource\(idxString) failed to parse because \(error)"
|
||||
case .primaryResourceMissing:
|
||||
return "Primary Resource missing."
|
||||
case .primaryResourcesMissing:
|
||||
return "Primary Resources array missing."
|
||||
|
||||
case .includes(error: let error):
|
||||
return "\(error)"
|
||||
|
||||
case .foundErrorDocumentWhenExpectingSuccess:
|
||||
return "Expected a success document with a 'data' property but found an error document."
|
||||
case .foundSuccessDocumentWhenExpectingError:
|
||||
return "Expected an error document but found a success document with a 'data' property."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,35 +14,35 @@ public typealias Include = EncodableJSONPoly
|
||||
///
|
||||
/// If you have
|
||||
///
|
||||
/// `let includes: Includes<Include2<Thing1, Thing2>> = ...`
|
||||
/// let includes: Includes<Include2<Thing1, Thing2>> = ...
|
||||
///
|
||||
/// then you can access all `Thing1` included resources with
|
||||
///
|
||||
/// `let includedThings = includes[Thing1.self]`
|
||||
/// let includedThings = includes[Thing1.self]
|
||||
public struct Includes<I: Include>: Encodable, Equatable {
|
||||
public static var none: Includes { return .init(values: []) }
|
||||
|
||||
let values: [I]
|
||||
|
||||
public init(values: [I]) {
|
||||
self.values = values
|
||||
}
|
||||
public static var none: Includes { return .init(values: []) }
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
public let values: [I]
|
||||
|
||||
guard I.self != NoIncludes.self else {
|
||||
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.")
|
||||
}
|
||||
public init(values: [I]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
for value in values {
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
return values.count
|
||||
}
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
|
||||
guard I.self != NoIncludes.self else {
|
||||
throw JSONAPICodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath)
|
||||
}
|
||||
|
||||
for value in values {
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
return values.count
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes: Decodable where I: Decodable {
|
||||
@@ -56,8 +56,35 @@ extension Includes: Decodable where I: Decodable {
|
||||
}
|
||||
|
||||
var valueAggregator = [I]()
|
||||
var idx = 0
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
do {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
idx = idx + 1
|
||||
} catch let error as PolyDecodeNoTypesMatchedError {
|
||||
let errors: [ResourceObjectDecodingError] = error
|
||||
.individualTypeFailures
|
||||
.compactMap { decodingError in
|
||||
switch decodingError.error {
|
||||
case .typeMismatch(_, let context),
|
||||
.valueNotFound(_, let context),
|
||||
.keyNotFound(_, let context),
|
||||
.dataCorrupted(let context):
|
||||
return context.underlyingError as? ResourceObjectDecodingError
|
||||
@unknown default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
guard errors.count == error.individualTypeFailures.count else {
|
||||
throw IncludesDecodingError(error: error, idx: idx)
|
||||
}
|
||||
throw IncludesDecodingError(
|
||||
error: IncludeDecodingError(failures: errors),
|
||||
idx: idx
|
||||
)
|
||||
} catch let error {
|
||||
throw IncludesDecodingError(error: error, idx: idx)
|
||||
}
|
||||
}
|
||||
|
||||
values = valueAggregator
|
||||
@@ -65,25 +92,25 @@ extension Includes: Decodable where I: Decodable {
|
||||
}
|
||||
|
||||
extension Includes {
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
}
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
|
||||
return left.appending(right)
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
extension Includes: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Includes(\(String(describing: values))"
|
||||
}
|
||||
public var description: String {
|
||||
return "Includes(\(String(describing: values))"
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes where I == NoIncludes {
|
||||
public init() {
|
||||
values = []
|
||||
}
|
||||
public init() {
|
||||
values = []
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 0 includes
|
||||
@@ -93,73 +120,73 @@ public typealias NoIncludes = Include0
|
||||
// MARK: - 1 include
|
||||
public typealias Include1 = Poly1
|
||||
extension Includes where I: _Poly1 {
|
||||
public subscript(_ lookup: I.A.Type) -> [I.A] {
|
||||
return values.compactMap { $0.a }
|
||||
}
|
||||
public subscript(_ lookup: I.A.Type) -> [I.A] {
|
||||
return values.compactMap { $0.a }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 2 includes
|
||||
public typealias Include2 = Poly2
|
||||
extension Includes where I: _Poly2 {
|
||||
public subscript(_ lookup: I.B.Type) -> [I.B] {
|
||||
return values.compactMap { $0.b }
|
||||
}
|
||||
public subscript(_ lookup: I.B.Type) -> [I.B] {
|
||||
return values.compactMap { $0.b }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 3 includes
|
||||
public typealias Include3 = Poly3
|
||||
extension Includes where I: _Poly3 {
|
||||
public subscript(_ lookup: I.C.Type) -> [I.C] {
|
||||
return values.compactMap { $0.c }
|
||||
}
|
||||
public subscript(_ lookup: I.C.Type) -> [I.C] {
|
||||
return values.compactMap { $0.c }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 4 includes
|
||||
public typealias Include4 = Poly4
|
||||
extension Includes where I: _Poly4 {
|
||||
public subscript(_ lookup: I.D.Type) -> [I.D] {
|
||||
return values.compactMap { $0.d }
|
||||
}
|
||||
public subscript(_ lookup: I.D.Type) -> [I.D] {
|
||||
return values.compactMap { $0.d }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 5 includes
|
||||
public typealias Include5 = Poly5
|
||||
extension Includes where I: _Poly5 {
|
||||
public subscript(_ lookup: I.E.Type) -> [I.E] {
|
||||
return values.compactMap { $0.e }
|
||||
}
|
||||
public subscript(_ lookup: I.E.Type) -> [I.E] {
|
||||
return values.compactMap { $0.e }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 6 includes
|
||||
public typealias Include6 = Poly6
|
||||
extension Includes where I: _Poly6 {
|
||||
public subscript(_ lookup: I.F.Type) -> [I.F] {
|
||||
return values.compactMap { $0.f }
|
||||
}
|
||||
public subscript(_ lookup: I.F.Type) -> [I.F] {
|
||||
return values.compactMap { $0.f }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 7 includes
|
||||
public typealias Include7 = Poly7
|
||||
extension Includes where I: _Poly7 {
|
||||
public subscript(_ lookup: I.G.Type) -> [I.G] {
|
||||
return values.compactMap { $0.g }
|
||||
}
|
||||
public subscript(_ lookup: I.G.Type) -> [I.G] {
|
||||
return values.compactMap { $0.g }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 8 includes
|
||||
public typealias Include8 = Poly8
|
||||
extension Includes where I: _Poly8 {
|
||||
public subscript(_ lookup: I.H.Type) -> [I.H] {
|
||||
return values.compactMap { $0.h }
|
||||
}
|
||||
public subscript(_ lookup: I.H.Type) -> [I.H] {
|
||||
return values.compactMap { $0.h }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 9 includes
|
||||
public typealias Include9 = Poly9
|
||||
extension Includes where I: _Poly9 {
|
||||
public subscript(_ lookup: I.I.Type) -> [I.I] {
|
||||
return values.compactMap { $0.i }
|
||||
}
|
||||
public subscript(_ lookup: I.I.Type) -> [I.I] {
|
||||
return values.compactMap { $0.i }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 10 includes
|
||||
@@ -169,3 +196,40 @@ 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 }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DecodingError
|
||||
public struct IncludesDecodingError: Swift.Error, Equatable {
|
||||
public let error: Swift.Error
|
||||
public let idx: Int
|
||||
|
||||
public static func ==(lhs: Self, rhs: Self) -> Bool {
|
||||
return lhs.idx == rhs.idx
|
||||
&& String(describing: lhs) == String(describing: rhs)
|
||||
}
|
||||
}
|
||||
|
||||
extension IncludesDecodingError: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Include \(idx + 1) failed to parse: \(error)"
|
||||
}
|
||||
}
|
||||
|
||||
public struct IncludeDecodingError: Swift.Error, Equatable, CustomStringConvertible {
|
||||
public let failures: [ResourceObjectDecodingError]
|
||||
|
||||
public var description: String {
|
||||
return failures
|
||||
.enumerated()
|
||||
.map {
|
||||
"\nCould not have been Include Type \($0.offset + 1) because:\n\($0.element)"
|
||||
}.joined(separator: "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,121 +10,132 @@
|
||||
/// array should be used for no results).
|
||||
public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {}
|
||||
|
||||
/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding.
|
||||
/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and
|
||||
/// decoding.
|
||||
/// An `EncodablePrimaryResource` is a `CodablePrimaryResource` that only supports encoding.
|
||||
public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {}
|
||||
|
||||
/// This protocol allows for `SingleResourceBody` to contain a `null`
|
||||
/// data object where `ManyResourceBody` cannot (because an empty
|
||||
/// array should be used for no results).
|
||||
public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
|
||||
public protocol OptionalCodablePrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
|
||||
|
||||
/// A `PrimaryResource` is a type that can be used in the body of a JSON API
|
||||
/// A `CodablePrimaryResource` is a type that can be used in the body of a JSON API
|
||||
/// document as the primary resource.
|
||||
public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {}
|
||||
public protocol CodablePrimaryResource: EncodablePrimaryResource, OptionalCodablePrimaryResource {}
|
||||
|
||||
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
|
||||
|
||||
extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {}
|
||||
extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimaryResource {}
|
||||
|
||||
/// An `EncodableResourceBody` is a `ResourceBody` that only supports being
|
||||
/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding
|
||||
/// and decoding.
|
||||
public protocol EncodableResourceBody: Equatable, Encodable {}
|
||||
public protocol EncodableResourceBody: Equatable, Encodable {
|
||||
associatedtype PrimaryResource
|
||||
}
|
||||
|
||||
/// A ResourceBody is a representation of the body of the JSON API Document.
|
||||
/// A `CodableResourceBody` is a representation of the body of the JSON:API Document.
|
||||
/// It can either be one resource (which can be specified as optional or not)
|
||||
/// or it can contain many resources (and array with zero or more entries).
|
||||
public protocol ResourceBody: Decodable, EncodableResourceBody {}
|
||||
public protocol CodableResourceBody: Decodable, EncodableResourceBody {}
|
||||
|
||||
/// A `ResourceBody` that has the ability to take on more primary
|
||||
/// resources by appending another similarly typed `ResourceBody`.
|
||||
public protocol Appendable {
|
||||
func appending(_ other: Self) -> Self
|
||||
public protocol ResourceBodyAppendable {
|
||||
func appending(_ other: Self) -> Self
|
||||
}
|
||||
|
||||
public func +<R: Appendable>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
public func +<R: ResourceBodyAppendable>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
/// A type allowing for a document body containing 1 primary resource.
|
||||
/// If the `Entity` specialization is an `Optional` type, the body can contain
|
||||
/// 0 or 1 primary resources.
|
||||
public struct SingleResourceBody<Entity: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
|
||||
public let value: Entity
|
||||
public struct SingleResourceBody<PrimaryResource: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
|
||||
public let value: PrimaryResource
|
||||
|
||||
public init(resourceObject: Entity) {
|
||||
self.value = resourceObject
|
||||
}
|
||||
public init(resourceObject: PrimaryResource) {
|
||||
self.value = resourceObject
|
||||
}
|
||||
}
|
||||
|
||||
/// A type allowing for a document body containing 0 or more primary resources.
|
||||
public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, Appendable {
|
||||
public let values: [Entity]
|
||||
public struct ManyResourceBody<PrimaryResource: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
|
||||
public let values: [PrimaryResource]
|
||||
|
||||
public init(resourceObjects: [Entity]) {
|
||||
values = resourceObjects
|
||||
}
|
||||
public init(resourceObjects: [PrimaryResource]) {
|
||||
values = resourceObjects
|
||||
}
|
||||
|
||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||
return ManyResourceBody(resourceObjects: values + other.values)
|
||||
}
|
||||
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
|
||||
return ManyResourceBody(resourceObjects: values + other.values)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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() }
|
||||
public struct NoResourceBody: CodableResourceBody {
|
||||
public typealias PrimaryResource = Void
|
||||
|
||||
public static var none: NoResourceBody { return NoResourceBody() }
|
||||
}
|
||||
|
||||
// MARK: Codable
|
||||
extension SingleResourceBody {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
let nilValue = anyNil as? Entity
|
||||
let nilValue = anyNil as? PrimaryResource
|
||||
guard value != nilValue else {
|
||||
try container.encodeNil()
|
||||
return
|
||||
}
|
||||
|
||||
try container.encode(value)
|
||||
}
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource {
|
||||
extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
let val = anyNil as? PrimaryResource {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
value = try container.decode(Entity.self)
|
||||
value = try container.decode(PrimaryResource.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
|
||||
for value in values {
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
for value in values {
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource {
|
||||
extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
var valueAggregator = [Entity]()
|
||||
var valueAggregator = [PrimaryResource]()
|
||||
var idx = 0
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(Entity.self))
|
||||
do {
|
||||
valueAggregator.append(try container.decode(PrimaryResource.self))
|
||||
} catch let error as ResourceObjectDecodingError {
|
||||
throw ManyResourceBodyDecodingError(
|
||||
error: error,
|
||||
idx: idx
|
||||
)
|
||||
}
|
||||
idx = idx + 1
|
||||
}
|
||||
values = valueAggregator
|
||||
}
|
||||
@@ -133,13 +144,19 @@ extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResourc
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
extension SingleResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "PrimaryResourceBody(\(String(describing: value)))"
|
||||
}
|
||||
public var description: String {
|
||||
return "PrimaryResourceBody(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "PrimaryResourceBody(\(String(describing: values)))"
|
||||
}
|
||||
public var description: String {
|
||||
return "PrimaryResourceBody(\(String(describing: values)))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - DecodingError
|
||||
public struct ManyResourceBodyDecodingError: Swift.Error, Equatable {
|
||||
public let error: ResourceObjectDecodingError
|
||||
public let idx: Int
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// EncodingError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/7/18.
|
||||
//
|
||||
|
||||
public enum JSONAPIEncodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String)
|
||||
case illegalEncoding(String)
|
||||
case illegalDecoding(String)
|
||||
case missingOrMalformedMetadata
|
||||
case missingOrMalformedLinks
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
//
|
||||
// BasicError.swift
|
||||
// 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 {
|
||||
public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType, CustomStringConvertible {
|
||||
/// 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.
|
||||
|
||||
// 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
|
||||
@@ -20,7 +22,8 @@ public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Eq
|
||||
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 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,
|
||||
@@ -61,6 +64,10 @@ public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Eq
|
||||
].compactMap { $0 }
|
||||
return Dictionary(uniqueKeysWithValues: keysAndValues)
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
return definedFields.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
/// `BasicJSONAPIError` optionally decodes many possible fields
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// GenericError.swift
|
||||
// GenericJSONAPIError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 9/29/19.
|
||||
@@ -8,7 +8,7 @@
|
||||
/// `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 {
|
||||
public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError, CustomStringConvertible {
|
||||
case unknownError
|
||||
case error(ErrorPayload)
|
||||
|
||||
@@ -35,6 +35,15 @@ public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError
|
||||
public static var unknown: Self {
|
||||
return .unknownError
|
||||
}
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .unknownError:
|
||||
return "unknown error"
|
||||
case .error(let payload):
|
||||
return String(describing: payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension GenericJSONAPIError {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
//
|
||||
// Error.swift
|
||||
// JSONAPIError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
public protocol JSONAPIError: Swift.Error, Equatable, Codable {
|
||||
static var unknown: Self { get }
|
||||
static var unknown: Self { get }
|
||||
}
|
||||
|
||||
/// `UnknownJSONAPIError` can actually be used in any sitaution
|
||||
@@ -16,18 +16,18 @@ public protocol JSONAPIError: Swift.Error, Equatable, Codable {
|
||||
/// information the server might be providing in the error payload,
|
||||
/// use `BasicJSONAPIError` instead.
|
||||
public enum UnknownJSONAPIError: JSONAPIError {
|
||||
case unknownError
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
self = .unknown
|
||||
}
|
||||
case unknownError
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode("unknown")
|
||||
}
|
||||
|
||||
public static var unknown: Self {
|
||||
return .unknownError
|
||||
}
|
||||
public init(from decoder: Decoder) throws {
|
||||
self = .unknown
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
try container.encode("unknown")
|
||||
}
|
||||
|
||||
public static var unknown: Self {
|
||||
return .unknownError
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// JSONAPICodingError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 12/7/18.
|
||||
//
|
||||
|
||||
public enum JSONAPICodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String, path: [CodingKey])
|
||||
case quantityMismatch(expected: Quantity, path: [CodingKey])
|
||||
case illegalEncoding(String, path: [CodingKey])
|
||||
case illegalDecoding(String, path: [CodingKey])
|
||||
case missingOrMalformedMetadata(path: [CodingKey])
|
||||
case missingOrMalformedLinks(path: [CodingKey])
|
||||
|
||||
public enum Quantity: String, Equatable {
|
||||
case one
|
||||
case many
|
||||
|
||||
public var other: Quantity {
|
||||
switch self {
|
||||
case .one: return .many
|
||||
case .many: return .one
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,58 +10,58 @@ public protocol Links: Codable, Equatable {}
|
||||
|
||||
/// Use NoLinks where no links should belong to a JSON API component
|
||||
public struct NoLinks: Links, CustomStringConvertible {
|
||||
public static var none: NoLinks { return NoLinks() }
|
||||
public init() {}
|
||||
|
||||
public var description: String { return "No Links" }
|
||||
public static var none: NoLinks { return NoLinks() }
|
||||
public init() {}
|
||||
|
||||
public var description: String { return "No Links" }
|
||||
}
|
||||
|
||||
public protocol JSONAPIURL: Codable, Equatable {}
|
||||
|
||||
public struct Link<URL: JSONAPI.JSONAPIURL, Meta: JSONAPI.Meta>: Equatable, Codable {
|
||||
public let url: URL
|
||||
public let meta: Meta
|
||||
|
||||
public init(url: URL, meta: Meta) {
|
||||
self.url = url
|
||||
self.meta = meta
|
||||
}
|
||||
public let url: URL
|
||||
public let meta: Meta
|
||||
|
||||
public init(url: URL, meta: Meta) {
|
||||
self.url = url
|
||||
self.meta = meta
|
||||
}
|
||||
}
|
||||
|
||||
extension Link where Meta == NoMetadata {
|
||||
public init(url: URL) {
|
||||
self.init(url: url, meta: .none)
|
||||
}
|
||||
public init(url: URL) {
|
||||
self.init(url: url, meta: .none)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,25 +6,25 @@
|
||||
//
|
||||
|
||||
/// 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
|
||||
/// 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
|
||||
/// 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.
|
||||
// We make Optional a Meta if it wraps a Meta so that
|
||||
// Metadata can be specified as nullable.
|
||||
extension Optional: Meta where Wrapped: Meta {}
|
||||
|
||||
/// Use this type when you want to specify not to encode or decode any metadata
|
||||
/// for a type.
|
||||
public struct NoMetadata: Meta, CustomStringConvertible {
|
||||
public static var none: NoMetadata { return NoMetadata() }
|
||||
public static var none: NoMetadata { return NoMetadata() }
|
||||
|
||||
public init() { }
|
||||
public init() { }
|
||||
|
||||
public var description: String { return "No Metadata" }
|
||||
public var description: String { return "No Metadata" }
|
||||
}
|
||||
|
||||
@@ -6,31 +6,31 @@
|
||||
//
|
||||
|
||||
public extension TransformedAttribute {
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
func map<T: Codable>(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
func map<T: Codable>(_ transform: (Transformer.To) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
|
||||
public extension Attribute {
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
/// Map an Attribute to a new wrapped type.
|
||||
/// Note that the resulting Attribute will have no transformer, even if the
|
||||
/// source Attribute has a transformer.
|
||||
/// You are mapping the output of the source transform into
|
||||
/// the RawValue of a new transformerless Attribute.
|
||||
///
|
||||
/// Generally, this is the most useful operation. The transformer gives you
|
||||
/// control over the decoding of the Attribute, but once the Attribute exists,
|
||||
/// mapping on it is most useful for creating computed Attribute properties.
|
||||
func map<T: Codable>(_ transform: (ValueType) throws -> T) rethrows -> Attribute<T> {
|
||||
return Attribute<T>(value: try transform(value))
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user