mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
26 Commits
2.3.0
...
3.0.0-alpha.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 |
@@ -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>
|
||||
+1
-1
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
|
||||
#
|
||||
|
||||
spec.name = "MP-JSONAPI"
|
||||
spec.version = "2.3.0"
|
||||
spec.version = "2.5.0"
|
||||
spec.summary = "Swift Codable JSON API framework."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "b24fd3b41bf3126d4c6dede3708135182172af60",
|
||||
"version": "2.2.0"
|
||||
"revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf",
|
||||
"version": "2.3.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ let package = Package(
|
||||
targets: ["JSONAPITesting"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.2.0")),
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.0")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -16,12 +16,13 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [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
|
||||
@@ -76,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)
|
||||
@@ -328,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`.
|
||||
@@ -569,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,11 @@
|
||||
//
|
||||
// DocumentDecodingErro.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 10/20/19.
|
||||
//
|
||||
|
||||
public enum JSONAPIDocumentDecodingError: Swift.Error {
|
||||
case foundErrorDocumentWhenExpectingSuccess
|
||||
case foundSuccessDocumentWhenExpectingError
|
||||
}
|
||||
@@ -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 JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.")
|
||||
}
|
||||
|
||||
for value in values {
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
public var count: Int {
|
||||
return values.count
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes: Decodable where I: Decodable {
|
||||
@@ -65,25 +65,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 +93,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
|
||||
|
||||
@@ -10,78 +10,76 @@
|
||||
/// 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 {}
|
||||
|
||||
/// 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 let value: Entity
|
||||
|
||||
public init(resourceObject: Entity) {
|
||||
self.value = resourceObject
|
||||
}
|
||||
public init(resourceObject: Entity) {
|
||||
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<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
|
||||
public let values: [Entity]
|
||||
|
||||
public init(resourceObjects: [Entity]) {
|
||||
values = resourceObjects
|
||||
}
|
||||
public init(resourceObjects: [Entity]) {
|
||||
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 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
|
||||
@@ -90,11 +88,11 @@ extension SingleResourceBody {
|
||||
return
|
||||
}
|
||||
|
||||
try container.encode(value)
|
||||
}
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource {
|
||||
extension SingleResourceBody: Decodable, CodableResourceBody where Entity: OptionalCodablePrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
@@ -110,16 +108,16 @@ extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrim
|
||||
}
|
||||
|
||||
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 Entity: CodablePrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
var valueAggregator = [Entity]()
|
||||
@@ -133,13 +131,13 @@ 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)))"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
//
|
||||
|
||||
public enum JSONAPIEncodingError: Swift.Error {
|
||||
case typeMismatch(expected: String, found: String)
|
||||
case illegalEncoding(String)
|
||||
case illegalDecoding(String)
|
||||
case missingOrMalformedMetadata
|
||||
case missingOrMalformedLinks
|
||||
case typeMismatch(expected: String, found: String)
|
||||
case illegalEncoding(String)
|
||||
case illegalDecoding(String)
|
||||
case missingOrMalformedMetadata
|
||||
case missingOrMalformedLinks
|
||||
}
|
||||
|
||||
@@ -6,10 +6,12 @@
|
||||
//
|
||||
|
||||
/// 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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,49 +6,49 @@
|
||||
//
|
||||
|
||||
public protocol AttributeType: Codable {
|
||||
associatedtype RawValue: Codable
|
||||
associatedtype ValueType
|
||||
associatedtype RawValue: Codable
|
||||
associatedtype ValueType
|
||||
|
||||
var value: ValueType { get }
|
||||
var value: ValueType { get }
|
||||
}
|
||||
|
||||
// MARK: TransformedAttribute
|
||||
|
||||
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
|
||||
public let rawValue: RawValue
|
||||
public let rawValue: RawValue
|
||||
|
||||
public let value: Transformer.To
|
||||
public let value: Transformer.To
|
||||
|
||||
public init(rawValue: RawValue) throws {
|
||||
self.rawValue = rawValue
|
||||
value = try Transformer.transform(rawValue)
|
||||
}
|
||||
public init(rawValue: RawValue) throws {
|
||||
self.rawValue = rawValue
|
||||
value = try Transformer.transform(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer == IdentityTransformer<RawValue> {
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
}
|
||||
// If we are using the identity transform, we can skip the transform and guarantee no
|
||||
// error is thrown.
|
||||
public init(value: RawValue) {
|
||||
rawValue = value
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute where Transformer: ReversibleTransformer {
|
||||
/// Initialize a TransformedAttribute from its transformed value. The
|
||||
/// RawValue, which is what gets encoded/decoded, is determined using
|
||||
/// The Transformer's reverse function.
|
||||
public init(transformedValue: Transformer.To) throws {
|
||||
self.value = transformedValue
|
||||
rawValue = try Transformer.reverse(value)
|
||||
}
|
||||
/// Initialize a TransformedAttribute from its transformed value. The
|
||||
/// RawValue, which is what gets encoded/decoded, is determined using
|
||||
/// The Transformer's reverse function.
|
||||
public init(transformedValue: Transformer.To) throws {
|
||||
self.value = transformedValue
|
||||
rawValue = try Transformer.reverse(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
|
||||
}
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: Transformer.From.self)) -> \(String(describing: Transformer.To.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension TransformedAttribute: Equatable where Transformer.From: Equatable, Transformer.To: Equatable {}
|
||||
@@ -63,85 +63,86 @@ public typealias ValidatedAttribute<RawValue: Codable, Validator: JSONAPI.Valida
|
||||
|
||||
/// An Attribute simply represents a type that can be encoded and decoded.
|
||||
public struct Attribute<RawValue: Codable>: AttributeType {
|
||||
let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>>
|
||||
let attribute: TransformedAttribute<RawValue, IdentityTransformer<RawValue>>
|
||||
|
||||
public var value: RawValue {
|
||||
return attribute.value
|
||||
}
|
||||
public var value: RawValue {
|
||||
return attribute.value
|
||||
}
|
||||
|
||||
public init(value: RawValue) {
|
||||
attribute = .init(value: value)
|
||||
}
|
||||
public init(value: RawValue) {
|
||||
attribute = .init(value: value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: CustomStringConvertible {
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
|
||||
}
|
||||
public var description: String {
|
||||
return "Attribute<\(String(describing: RawValue.self))>(\(String(describing: value)))"
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute: Equatable where RawValue: Equatable {}
|
||||
|
||||
// MARK: - Codable
|
||||
extension TransformedAttribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let rawVal: RawValue
|
||||
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Transformer.From {
|
||||
rawVal = val
|
||||
} else {
|
||||
rawVal = try container.decode(Transformer.From.self)
|
||||
}
|
||||
|
||||
rawValue = rawVal
|
||||
value = try Transformer.transform(rawVal)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
let rawVal: RawValue
|
||||
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Transformer.From {
|
||||
rawVal = val
|
||||
} else {
|
||||
rawVal = try container.decode(Transformer.From.self)
|
||||
}
|
||||
|
||||
rawValue = rawVal
|
||||
value = try Transformer.transform(rawVal)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
extension Attribute {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? RawValue {
|
||||
attribute = .init(value: val)
|
||||
} else {
|
||||
attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self)
|
||||
}
|
||||
}
|
||||
// A little trickery follows. If the value is nil, the
|
||||
// container.decode(Value.self) will fail even if Value
|
||||
// is Optional. However, we can check if decoding nil
|
||||
// succeeds and then attempt to coerce nil to a Value
|
||||
// type at which point we can store nil in `value`.
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? RawValue {
|
||||
attribute = .init(value: val)
|
||||
} else {
|
||||
attribute = try container.decode(TransformedAttribute<RawValue, IdentityTransformer<RawValue>>.self)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(attribute)
|
||||
}
|
||||
try container.encode(attribute)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Attribute decoding and encoding defaults
|
||||
|
||||
extension AttributeType {
|
||||
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container, forKey key: Container.Key) throws -> Self {
|
||||
return try container.decode(Self.self, forKey: key)
|
||||
}
|
||||
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container,
|
||||
forKey key: Container.Key) throws -> Self {
|
||||
return try container.decode(Self.self, forKey: key)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user