mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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": "b24fd3b41bf3126d4c6dede3708135182172af60",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ let package = Package(
|
||||
targets: ["JSONAPITesting"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.1.0")),
|
||||
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.2.0")),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
||||
@@ -5,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)
|
||||
@@ -567,6 +573,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
|
||||
|
||||
@@ -414,3 +414,137 @@ extension Document.Body.Data: CustomStringConvertible {
|
||||
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error and Success Document Types
|
||||
|
||||
extension Document {
|
||||
/// A Document that only supports error bodies. This is useful if you wish to pass around a
|
||||
/// Document type but you wish to constrain it to error values.
|
||||
@dynamicMemberLookup
|
||||
public struct ErrorDocument: EncodableJSONAPIDocument {
|
||||
public var body: Document.Body { return document.body }
|
||||
|
||||
private let document: Document
|
||||
|
||||
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
|
||||
document = .init(apiDescription: apiDescription, errors: errors, meta: meta, links: links)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(document)
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember path: KeyPath<Document, T>) -> T {
|
||||
return document[keyPath: path]
|
||||
}
|
||||
|
||||
public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool {
|
||||
return lhs == rhs.document
|
||||
}
|
||||
}
|
||||
|
||||
/// A Document that only supports success bodies. This is useful if you wish to pass around a
|
||||
/// Document type but you wish to constrain it to success values.
|
||||
@dynamicMemberLookup
|
||||
public struct SuccessDocument: EncodableJSONAPIDocument {
|
||||
public var body: Document.Body { return document.body }
|
||||
|
||||
private let document: Document
|
||||
|
||||
public init(apiDescription: APIDescription,
|
||||
body: PrimaryResourceBody,
|
||||
includes: Includes<Include>,
|
||||
meta: MetaType,
|
||||
links: LinksType) {
|
||||
document = .init(apiDescription: apiDescription,
|
||||
body: body,
|
||||
includes: includes,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
try container.encode(document)
|
||||
}
|
||||
|
||||
public subscript<T>(dynamicMember path: KeyPath<Document, T>) -> T {
|
||||
return document[keyPath: path]
|
||||
}
|
||||
|
||||
public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool {
|
||||
return lhs == rhs.document
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.ErrorDocument: Decodable, JSONAPIDocument
|
||||
where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
document = try container.decode(Document.self)
|
||||
|
||||
guard document.body.isError else {
|
||||
throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.SuccessDocument: Decodable, JSONAPIDocument
|
||||
where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
document = try container.decode(Document.self)
|
||||
|
||||
guard !document.body.isError else {
|
||||
throw JSONAPIDocumentDecodingError.foundErrorDocumentWhenExpectingSuccess
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.SuccessDocument where IncludeType == NoIncludes {
|
||||
/// Create a new Document with the given includes.
|
||||
public func including<I: JSONAPI.Include>(_ includes: Includes<I>) -> Document<PrimaryResourceBody, MetaType, LinksType, I, APIDescription, Error> {
|
||||
// Note that if IncludeType is NoIncludes, then we allow anything
|
||||
// to be included, but if IncludeType already specifies a type
|
||||
// of thing to be expected then we lock that down.
|
||||
// See: Document.including() where IncludeType: _Poly1
|
||||
switch document.body {
|
||||
case .data(let data):
|
||||
return .init(apiDescription: document.apiDescription,
|
||||
body: data.primary,
|
||||
includes: includes,
|
||||
meta: data.meta,
|
||||
links: data.links)
|
||||
case .errors:
|
||||
fatalError("SuccessDocument cannot end up in an error state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extending where _Poly1 means all non-zero _Poly arities are included
|
||||
extension Document.SuccessDocument where IncludeType: _Poly1 {
|
||||
/// Create a new Document adding the given includes. This does not
|
||||
/// remove existing includes; it is additive.
|
||||
public func including(_ includes: Includes<IncludeType>) -> Document {
|
||||
// Note that if IncludeType is NoIncludes, then we allow anything
|
||||
// to be included, but if IncludeType already specifies a type
|
||||
// of thing to be expected then we lock that down.
|
||||
// See: Document.including() where IncludeType == NoIncludes
|
||||
switch document.body {
|
||||
case .data(let data):
|
||||
return .init(apiDescription: document.apiDescription,
|
||||
body: data.primary,
|
||||
includes: data.includes + includes,
|
||||
meta: data.meta,
|
||||
links: data.links)
|
||||
case .errors:
|
||||
fatalError("SuccessDocument cannot end up in an error state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// DocumentDecodingErro.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 10/20/19.
|
||||
//
|
||||
|
||||
public enum JSONAPIDocumentDecodingError: Swift.Error {
|
||||
case foundErrorDocumentWhenExpectingSuccess
|
||||
case foundSuccessDocumentWhenExpectingError
|
||||
}
|
||||
@@ -169,3 +169,11 @@ extension Includes where I: _Poly10 {
|
||||
return values.compactMap { $0.j }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 11 includes
|
||||
public typealias Include11 = Poly11
|
||||
extension Includes where I: _Poly11 {
|
||||
public subscript(_ lookup: I.K.Type) -> [I.K] {
|
||||
return values.compactMap { $0.k }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// BasicError.swift
|
||||
// BasicJSONAPIError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 9/29/19.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// GenericError.swift
|
||||
// GenericJSONAPIError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 9/29/19.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//
|
||||
// Error.swift
|
||||
// JSONAPIError.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
|
||||
@@ -79,3 +79,8 @@ extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped,
|
||||
extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {}
|
||||
|
||||
// MARK: - 11 types
|
||||
extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ResourceObject+Replacing.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 10/12/19.
|
||||
//
|
||||
|
||||
public extension JSONAPI.ResourceObject {
|
||||
/// Return a new `ResourceObject`, having replaced `self`'s
|
||||
/// `attributes` with the attributes returned by the given
|
||||
/// replacement function.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - replacement: A function that takes the existing `attributes` and returns the replacement.
|
||||
func replacingAttributes(_ replacement: (Description.Attributes) -> Description.Attributes) -> Self {
|
||||
return Self(id: id,
|
||||
attributes: replacement(attributes),
|
||||
relationships: relationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having updated `self`'s
|
||||
/// `attributes` with the tap function given.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - tap: A function that takes a copy of the existing `attributes` and mutates them.
|
||||
func tappingAttributes(_ tap: (inout Description.Attributes) -> Void) -> Self {
|
||||
var newAttributes = attributes
|
||||
tap(&newAttributes)
|
||||
return Self(id: id,
|
||||
attributes: newAttributes,
|
||||
relationships: relationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having replaced `self`'s
|
||||
/// `relationships` with the `relationships` returned by the given
|
||||
/// replacement function.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - replacement: A function that takes the existing relationships and returns the replacement.
|
||||
func replacingRelationships(_ replacement: (Description.Relationships) -> Description.Relationships) -> Self {
|
||||
return Self(id: id,
|
||||
attributes: attributes,
|
||||
relationships: replacement(relationships),
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having updated `self`'s
|
||||
/// `relationships` with the tap function given.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - tap: A function that takes a copy of the existing `relationships` and mutates them.
|
||||
func tappingRelationships(_ tap: (inout Description.Relationships) -> Void) -> Self {
|
||||
var newRelationships = relationships
|
||||
tap(&newRelationships)
|
||||
return Self(id: id,
|
||||
attributes: attributes,
|
||||
relationships: newRelationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class DocumentTests: XCTestCase {
|
||||
XCTAssert(Doc.Error.self == UnknownJSONAPIError.self)
|
||||
}
|
||||
|
||||
// Document
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
@@ -37,6 +38,35 @@ class DocumentTests: XCTestCase {
|
||||
meta: .none,
|
||||
links: .none
|
||||
))
|
||||
|
||||
// Document.SuccessDocument
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>.SuccessDocument(
|
||||
apiDescription: .none,
|
||||
body: .none,
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none
|
||||
))
|
||||
|
||||
// Document.ErrorDocument
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>.ErrorDocument(
|
||||
apiDescription: .none,
|
||||
errors: []
|
||||
))
|
||||
}
|
||||
|
||||
func test_singleDocumentNull() {
|
||||
@@ -51,11 +81,34 @@ class DocumentTests: XCTestCase {
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.links, NoLinks())
|
||||
XCTAssertEqual(document.apiDescription, .none)
|
||||
|
||||
// SuccessDocument
|
||||
let document2 = decoded(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_null)
|
||||
|
||||
XCTAssert(document == document2)
|
||||
|
||||
XCTAssertFalse(document2.body.isError)
|
||||
XCTAssertNil(document2.body.errors)
|
||||
XCTAssertNotNil(document2.body.primaryResource)
|
||||
XCTAssertEqual(document2.body.meta, NoMetadata())
|
||||
XCTAssertNil(document2.body.primaryResource?.value)
|
||||
XCTAssertEqual(document2.body.includes?.count, 0)
|
||||
XCTAssertEqual(document2.body.links, NoLinks())
|
||||
XCTAssertEqual(document2.apiDescription, .none)
|
||||
|
||||
// ErrorDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
from: single_document_null))
|
||||
}
|
||||
|
||||
func test_singleDocumentNull_encode() {
|
||||
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_null)
|
||||
|
||||
// SuccessDocument
|
||||
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_null)
|
||||
}
|
||||
|
||||
func test_singleDocumentNullWithAPIDescription() {
|
||||
@@ -94,6 +147,14 @@ extension DocumentTests {
|
||||
func test_errorDocumentFailsWithNoAPIDescription() {
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// SuccessDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// ErrorDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
}
|
||||
|
||||
func test_unknownErrorDocumentNoMeta() {
|
||||
@@ -115,6 +176,32 @@ extension DocumentTests {
|
||||
XCTAssertEqual(errors.0, document.body.errors)
|
||||
XCTAssertEqual(errors.0[0], .unknown)
|
||||
XCTAssertEqual(errors.meta, NoMetadata())
|
||||
|
||||
// SuccessDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// ErrorDocument
|
||||
let document2 = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
data: error_document_no_metadata)
|
||||
|
||||
XCTAssert(document == document2)
|
||||
|
||||
XCTAssertTrue(document2.body.isError)
|
||||
XCTAssertEqual(document2.body.meta, NoMetadata())
|
||||
XCTAssertNil(document2.body.data)
|
||||
XCTAssertNil(document2.body.primaryResource)
|
||||
XCTAssertNil(document2.body.includes)
|
||||
|
||||
guard case let .errors(errors2) = document2.body else {
|
||||
XCTFail("Needed body to be in errors case but it was not.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(errors2.0.count, 1)
|
||||
XCTAssertEqual(errors2.0, document2.body.errors)
|
||||
XCTAssertEqual(errors2.0[0], .unknown)
|
||||
XCTAssertEqual(errors2.meta, NoMetadata())
|
||||
}
|
||||
|
||||
func test_unknownErrorDocumentAddIncludingType() {
|
||||
@@ -620,6 +707,26 @@ extension DocumentTests {
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
|
||||
}
|
||||
|
||||
func test_singleSuccessDocumentNoIncludesAddIncludingType() {
|
||||
// NOTE distinct from above for being Document.SuccessDocument
|
||||
let author = Author(id: "1",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_no_includes)
|
||||
|
||||
let documentWithIncludes = document.including(Includes<Include1<Author>>(values: [.init(author)]))
|
||||
|
||||
XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors)
|
||||
XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta)
|
||||
XCTAssertEqual(document.body.links, documentWithIncludes.body.links)
|
||||
XCTAssertEqual(document.body.includes, Includes<NoIncludes>.none)
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
|
||||
}
|
||||
|
||||
func test_singleDocumentNoIncludesWithAPIDescription() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_no_includes_with_api_description)
|
||||
@@ -848,6 +955,31 @@ extension DocumentTests {
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor])
|
||||
}
|
||||
|
||||
func test_singleSuccessDocumentSomeIncludesAddIncludes() {
|
||||
// NOTE distinct from above for being Document.SuccessDocument
|
||||
let existingAuthor = Author(id: "33",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let newAuthor = Author(id: "1",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_some_includes)
|
||||
|
||||
let documentWithIncludes = document.including(.init(values: [.init(newAuthor)]))
|
||||
|
||||
XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors)
|
||||
XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta)
|
||||
XCTAssertEqual(document.body.links, documentWithIncludes.body.links)
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor])
|
||||
}
|
||||
|
||||
func test_singleDocumentSomeIncludesWithAPIDescription() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_some_includes_with_api_description)
|
||||
|
||||
@@ -197,6 +197,28 @@ class IncludedTests: XCTestCase {
|
||||
test_DecodeEncodeEquality(type: Includes<Include10<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6, TestEntity7, TestEntity8, TestEntity9, TestEntity10>>.self,
|
||||
data: ten_different_type_includes)
|
||||
}
|
||||
|
||||
func test_ElevenDifferentIncludes() {
|
||||
let includes = decoded(type: Includes<Include11<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6, TestEntity7, TestEntity8, TestEntity9, TestEntity10, TestEntity11>>.self,
|
||||
data: eleven_different_type_includes)
|
||||
|
||||
XCTAssertEqual(includes[TestEntity.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity2.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity3.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity4.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity5.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity6.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity7.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity8.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity9.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity10.self].count, 1)
|
||||
XCTAssertEqual(includes[TestEntity11.self].count, 1)
|
||||
}
|
||||
|
||||
func test_ElevenDifferentIncludes_encode() {
|
||||
test_DecodeEncodeEquality(type: Includes<Include11<TestEntity, TestEntity2, TestEntity3, TestEntity4, TestEntity5, TestEntity6, TestEntity7, TestEntity8, TestEntity9, TestEntity10, TestEntity11>>.self,
|
||||
data: eleven_different_type_includes)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Appending
|
||||
@@ -503,4 +525,15 @@ extension IncludedTests {
|
||||
}
|
||||
|
||||
typealias TestEntity10 = BasicEntity<TestEntityType10>
|
||||
|
||||
enum TestEntityType11: ResourceObjectDescription {
|
||||
|
||||
typealias Attributes = NoAttributes
|
||||
|
||||
public static var jsonType: String { return "test_entity11" }
|
||||
|
||||
typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias TestEntity11 = BasicEntity<TestEntityType11>
|
||||
}
|
||||
|
||||
@@ -597,3 +597,92 @@ let ten_different_type_includes = """
|
||||
}
|
||||
]
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let eleven_different_type_includes = """
|
||||
[
|
||||
{
|
||||
"type": "test_entity1",
|
||||
"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF",
|
||||
"attributes": {
|
||||
"foo": "Hello",
|
||||
"bar": 123
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "test_entity2",
|
||||
"id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333",
|
||||
"attributes": {
|
||||
"foo": "World",
|
||||
"bar": 456
|
||||
},
|
||||
"relationships": {
|
||||
"entity1": {
|
||||
"data": {
|
||||
"type": "test_entity1",
|
||||
"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "test_entity3",
|
||||
"id": "11223B69-4DF1-467F-B52E-B0C9E44FC443",
|
||||
"relationships": {
|
||||
"entity1": {
|
||||
"data": {
|
||||
"type": "test_entity1",
|
||||
"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF"
|
||||
}
|
||||
},
|
||||
"entity2": {
|
||||
"data": [
|
||||
{
|
||||
"type": "test_entity2",
|
||||
"id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "test_entity6",
|
||||
"id": "11113B69-4DF1-467F-B52E-B0C9E44FC444",
|
||||
"relationships": {
|
||||
"entity4": {
|
||||
"data": {
|
||||
"type": "test_entity4",
|
||||
"id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "test_entity5",
|
||||
"id": "A24B3B69-4DF1-467F-B52E-B0C9E44F436A"
|
||||
},
|
||||
{
|
||||
"type": "test_entity4",
|
||||
"id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E"
|
||||
},
|
||||
{
|
||||
"type": "test_entity7",
|
||||
"id": "364B3B69-4DF1-222F-B52E-B0C9E44F666E"
|
||||
},
|
||||
{
|
||||
"type": "test_entity8",
|
||||
"id": "364B3B69-4DF1-222F-B52E-B0C9E44F266F"
|
||||
},
|
||||
{
|
||||
"type": "test_entity9",
|
||||
"id": "364B3B69-4DF1-218F-B52E-B0C9E44F2661"
|
||||
},
|
||||
{
|
||||
"type": "test_entity10",
|
||||
"id": "264B3B69-4DF1-212F-B52E-B0C9E44F2660"
|
||||
},
|
||||
{
|
||||
"type": "test_entity11",
|
||||
"id": "264B3B69-4DF3-212F-B32E-A0C9E44F26C0B"
|
||||
}
|
||||
]
|
||||
""".data(using: .utf8)!
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
[
|
||||
{
|
||||
"type": "test_entity1",
|
||||
"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF",
|
||||
"attributes": {
|
||||
"foo": "Hello",
|
||||
"bar": 123
|
||||
}
|
||||
}
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user