mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 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.
|
||||
|
||||
@@ -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)
|
||||
@@ -569,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>.SuccessDocument {
|
||||
// Note that if IncludeType is NoIncludes, then we allow anything
|
||||
// to be included, but if IncludeType already specifies a type
|
||||
// of thing to be expected then we lock that down.
|
||||
// See: Document.including() where IncludeType: _Poly1
|
||||
switch document.body {
|
||||
case .data(let data):
|
||||
return .init(apiDescription: document.apiDescription,
|
||||
body: data.primary,
|
||||
includes: includes,
|
||||
meta: data.meta,
|
||||
links: data.links)
|
||||
case .errors:
|
||||
fatalError("SuccessDocument cannot end up in an error state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// extending where _Poly1 means all non-zero _Poly arities are included
|
||||
extension Document.SuccessDocument where IncludeType: _Poly1 {
|
||||
/// Create a new Document adding the given includes. This does not
|
||||
/// remove existing includes; it is additive.
|
||||
public func including(_ includes: Includes<IncludeType>) -> Document.SuccessDocument {
|
||||
// Note that if IncludeType is NoIncludes, then we allow anything
|
||||
// to be included, but if IncludeType already specifies a type
|
||||
// of thing to be expected then we lock that down.
|
||||
// See: Document.including() where IncludeType == NoIncludes
|
||||
switch document.body {
|
||||
case .data(let data):
|
||||
return .init(apiDescription: document.apiDescription,
|
||||
body: data.primary,
|
||||
includes: data.includes + includes,
|
||||
meta: data.meta,
|
||||
links: data.links)
|
||||
case .errors:
|
||||
fatalError("SuccessDocument cannot end up in an error state")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// DocumentDecodingErro.swift
|
||||
//
|
||||
//
|
||||
// Created by Mathew Polzin on 10/20/19.
|
||||
//
|
||||
|
||||
public enum JSONAPIDocumentDecodingError: Swift.Error {
|
||||
case foundErrorDocumentWhenExpectingSuccess
|
||||
case foundSuccessDocumentWhenExpectingError
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// ResourceObject+Replacing.swift
|
||||
// JSONAPI
|
||||
//
|
||||
// Created by Mathew Polzin on 10/12/19.
|
||||
//
|
||||
|
||||
public extension JSONAPI.ResourceObject {
|
||||
/// Return a new `ResourceObject`, having replaced `self`'s
|
||||
/// `attributes` with the attributes returned by the given
|
||||
/// replacement function.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - replacement: A function that takes the existing `attributes` and returns the replacement.
|
||||
func replacingAttributes(_ replacement: (Description.Attributes) -> Description.Attributes) -> Self {
|
||||
return Self(id: id,
|
||||
attributes: replacement(attributes),
|
||||
relationships: relationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having updated `self`'s
|
||||
/// `attributes` with the tap function given.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - tap: A function that takes a copy of the existing `attributes` and mutates them.
|
||||
func tappingAttributes(_ tap: (inout Description.Attributes) -> Void) -> Self {
|
||||
var newAttributes = attributes
|
||||
tap(&newAttributes)
|
||||
return Self(id: id,
|
||||
attributes: newAttributes,
|
||||
relationships: relationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having replaced `self`'s
|
||||
/// `relationships` with the `relationships` returned by the given
|
||||
/// replacement function.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - replacement: A function that takes the existing relationships and returns the replacement.
|
||||
func replacingRelationships(_ replacement: (Description.Relationships) -> Description.Relationships) -> Self {
|
||||
return Self(id: id,
|
||||
attributes: attributes,
|
||||
relationships: replacement(relationships),
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
/// Return a new `ResourceObject`, having updated `self`'s
|
||||
/// `relationships` with the tap function given.
|
||||
///
|
||||
/// - important: `self` is not mutated. A copy of self is returned.
|
||||
///
|
||||
/// - parameters:
|
||||
/// - tap: A function that takes a copy of the existing `relationships` and mutates them.
|
||||
func tappingRelationships(_ tap: (inout Description.Relationships) -> Void) -> Self {
|
||||
var newRelationships = relationships
|
||||
tap(&newRelationships)
|
||||
return Self(id: id,
|
||||
attributes: attributes,
|
||||
relationships: newRelationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class DocumentTests: XCTestCase {
|
||||
XCTAssert(Doc.Error.self == UnknownJSONAPIError.self)
|
||||
}
|
||||
|
||||
// Document
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
@@ -37,6 +38,35 @@ class DocumentTests: XCTestCase {
|
||||
meta: .none,
|
||||
links: .none
|
||||
))
|
||||
|
||||
// Document.SuccessDocument
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>.SuccessDocument(
|
||||
apiDescription: .none,
|
||||
body: .none,
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none
|
||||
))
|
||||
|
||||
// Document.ErrorDocument
|
||||
test(JSONAPI.Document<
|
||||
NoResourceBody,
|
||||
NoMetadata,
|
||||
NoLinks,
|
||||
NoIncludes,
|
||||
NoAPIDescription,
|
||||
UnknownJSONAPIError
|
||||
>.ErrorDocument(
|
||||
apiDescription: .none,
|
||||
errors: []
|
||||
))
|
||||
}
|
||||
|
||||
func test_singleDocumentNull() {
|
||||
@@ -51,11 +81,34 @@ class DocumentTests: XCTestCase {
|
||||
XCTAssertEqual(document.body.includes?.count, 0)
|
||||
XCTAssertEqual(document.body.links, NoLinks())
|
||||
XCTAssertEqual(document.apiDescription, .none)
|
||||
|
||||
// SuccessDocument
|
||||
let document2 = decoded(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_null)
|
||||
|
||||
XCTAssert(document == document2)
|
||||
|
||||
XCTAssertFalse(document2.body.isError)
|
||||
XCTAssertNil(document2.body.errors)
|
||||
XCTAssertNotNil(document2.body.primaryResource)
|
||||
XCTAssertEqual(document2.body.meta, NoMetadata())
|
||||
XCTAssertNil(document2.body.primaryResource?.value)
|
||||
XCTAssertEqual(document2.body.includes?.count, 0)
|
||||
XCTAssertEqual(document2.body.links, NoLinks())
|
||||
XCTAssertEqual(document2.apiDescription, .none)
|
||||
|
||||
// ErrorDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
from: single_document_null))
|
||||
}
|
||||
|
||||
func test_singleDocumentNull_encode() {
|
||||
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_null)
|
||||
|
||||
// SuccessDocument
|
||||
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_null)
|
||||
}
|
||||
|
||||
func test_singleDocumentNullWithAPIDescription() {
|
||||
@@ -94,6 +147,14 @@ extension DocumentTests {
|
||||
func test_errorDocumentFailsWithNoAPIDescription() {
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// SuccessDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// ErrorDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
}
|
||||
|
||||
func test_unknownErrorDocumentNoMeta() {
|
||||
@@ -115,6 +176,32 @@ extension DocumentTests {
|
||||
XCTAssertEqual(errors.0, document.body.errors)
|
||||
XCTAssertEqual(errors.0[0], .unknown)
|
||||
XCTAssertEqual(errors.meta, NoMetadata())
|
||||
|
||||
// SuccessDocument
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
from: error_document_no_metadata))
|
||||
|
||||
// ErrorDocument
|
||||
let document2 = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
|
||||
data: error_document_no_metadata)
|
||||
|
||||
XCTAssert(document == document2)
|
||||
|
||||
XCTAssertTrue(document2.body.isError)
|
||||
XCTAssertEqual(document2.body.meta, NoMetadata())
|
||||
XCTAssertNil(document2.body.data)
|
||||
XCTAssertNil(document2.body.primaryResource)
|
||||
XCTAssertNil(document2.body.includes)
|
||||
|
||||
guard case let .errors(errors2) = document2.body else {
|
||||
XCTFail("Needed body to be in errors case but it was not.")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(errors2.0.count, 1)
|
||||
XCTAssertEqual(errors2.0, document2.body.errors)
|
||||
XCTAssertEqual(errors2.0[0], .unknown)
|
||||
XCTAssertEqual(errors2.meta, NoMetadata())
|
||||
}
|
||||
|
||||
func test_unknownErrorDocumentAddIncludingType() {
|
||||
@@ -620,6 +707,27 @@ extension DocumentTests {
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
|
||||
}
|
||||
|
||||
func test_singleSuccessDocumentNoIncludesAddIncludingType() {
|
||||
// NOTE distinct from above for being Document.SuccessDocument
|
||||
let author = Author(id: "1",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_no_includes)
|
||||
|
||||
let documentWithIncludes = document.including(Includes<Include1<Author>>(values: [.init(author)]))
|
||||
|
||||
XCTAssert(type(of: documentWithIncludes) == Document<NoResourceBody, NoMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self)
|
||||
XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors)
|
||||
XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta)
|
||||
XCTAssertEqual(document.body.links, documentWithIncludes.body.links)
|
||||
XCTAssertEqual(document.body.includes, Includes<NoIncludes>.none)
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
|
||||
}
|
||||
|
||||
func test_singleDocumentNoIncludesWithAPIDescription() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_no_includes_with_api_description)
|
||||
@@ -848,6 +956,31 @@ extension DocumentTests {
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor])
|
||||
}
|
||||
|
||||
func test_singleSuccessDocumentSomeIncludesAddIncludes() {
|
||||
// NOTE distinct from above for being Document.SuccessDocument
|
||||
let existingAuthor = Author(id: "33",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let newAuthor = Author(id: "1",
|
||||
attributes: .none,
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
|
||||
data: single_document_some_includes)
|
||||
|
||||
let documentWithIncludes = document.including(.init(values: [.init(newAuthor)]))
|
||||
|
||||
XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors)
|
||||
XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta)
|
||||
XCTAssertEqual(document.body.links, documentWithIncludes.body.links)
|
||||
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor])
|
||||
}
|
||||
|
||||
func test_singleDocumentSomeIncludesWithAPIDescription() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, TestAPIDescription, UnknownJSONAPIError>.self,
|
||||
data: single_document_some_includes_with_api_description)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
//
|
||||
// ResourceObject+ReplacingTests.swift
|
||||
// JSONAPITests
|
||||
//
|
||||
// Created by Mathew Polzin on 10/12/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
import JSONAPI
|
||||
|
||||
final class ResourceObjectReplacingTests: XCTestCase {
|
||||
func test_replaceMutableAttributes() {
|
||||
let testResource = MutableTestType(attributes: .init(name: .init(value: "Matt")),
|
||||
relationships: .init(other: .init(id: .init(rawValue: "2"))),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let mutatedResource = testResource
|
||||
.replacingAttributes {
|
||||
var newAttributes = $0
|
||||
newAttributes.name = .init(value: "Matt 2")
|
||||
return newAttributes
|
||||
}
|
||||
|
||||
XCTAssertEqual(testResource.name, "Matt")
|
||||
XCTAssertEqual(mutatedResource.name, "Matt 2")
|
||||
}
|
||||
|
||||
func test_tapMutableAttributes() {
|
||||
let testResource = MutableTestType(attributes: .init(name: .init(value: "Matt")),
|
||||
relationships: .init(other: .init(id: .init(rawValue: "2"))),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let mutatedResource = testResource
|
||||
.tappingAttributes { $0.name = .init(value: "Matt 2") }
|
||||
|
||||
XCTAssertEqual(testResource.name, "Matt")
|
||||
XCTAssertEqual(mutatedResource.name, "Matt 2")
|
||||
}
|
||||
|
||||
func test_replaceImmutableAttributes() {
|
||||
let testResource = ImmutableTestType(attributes: .init(name: .init(value: "Matt")),
|
||||
relationships: .init(other: .init(id: .init(rawValue: "2"))),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let mutatedResource = testResource
|
||||
.replacingAttributes {
|
||||
return .init(name: $0.name.map { $0 + " 2" })
|
||||
}
|
||||
|
||||
XCTAssertEqual(testResource.name, "Matt")
|
||||
XCTAssertEqual(mutatedResource.name, "Matt 2")
|
||||
}
|
||||
|
||||
func test_tapImmutableAttributes() {
|
||||
let testResource = ImmutableTestType(attributes: .init(name: .init(value: "Matt")),
|
||||
relationships: .init(other: .init(id: .init(rawValue: "2"))),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let mutatedResource = testResource
|
||||
.tappingAttributes { $0 = .init(name: $0.name.map { $0 + " 2" }) }
|
||||
|
||||
XCTAssertEqual(testResource.name, "Matt")
|
||||
XCTAssertEqual(mutatedResource.name, "Matt 2")
|
||||
}
|
||||
|
||||
func test_replaceMutableRelationships() {
|
||||
let testResource = MutableTestType(attributes: .init(name: .init(value: "Matt")),
|
||||
relationships: .init(other: .init(id: .init(rawValue: "2"))),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let mutatedResource = testResource
|
||||
.replacingRelationships {
|
||||
var newRelationships = $0
|
||||
newRelationships.other = .init(id: .init(rawValue: "3"))
|
||||
return newRelationships
|
||||
}
|
||||
|
||||
XCTAssertEqual(testResource ~> \.other, "2")
|
||||
XCTAssertEqual(mutatedResource ~> \.other, "3")
|
||||
}
|
||||
|
||||
func test_tapMutableRelationships() {
|
||||
let testResource = MutableTestType(attributes: .init(name: .init(value: "Matt")),
|
||||
relationships: .init(other: .init(id: .init(rawValue: "2"))),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let mutatedResource = testResource
|
||||
.tappingRelationships { $0.other = .init(id: .init(rawValue: "3")) }
|
||||
|
||||
XCTAssertEqual(testResource ~> \.other, "2")
|
||||
XCTAssertEqual(mutatedResource ~> \.other, "3")
|
||||
}
|
||||
|
||||
func test_replaceImmutableRelationships() {
|
||||
let testResource = ImmutableTestType(attributes: .init(name: .init(value: "Matt")),
|
||||
relationships: .init(other: .init(id: .init(rawValue: "2"))),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let mutatedResource = testResource
|
||||
.replacingRelationships { _ in
|
||||
return .init(other: .init(id: .init(rawValue: "3")))
|
||||
}
|
||||
|
||||
XCTAssertEqual(testResource ~> \.other, "2")
|
||||
XCTAssertEqual(mutatedResource ~> \.other, "3")
|
||||
}
|
||||
|
||||
func test_tapImmutableRelationships() {
|
||||
let testResource = ImmutableTestType(attributes: .init(name: .init(value: "Matt")),
|
||||
relationships: .init(other: .init(id: .init(rawValue: "2"))),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let mutatedResource = testResource
|
||||
.tappingRelationships { $0 = .init(other: .init(id: .init(rawValue: "3"))) }
|
||||
|
||||
XCTAssertEqual(testResource ~> \.other, "2")
|
||||
XCTAssertEqual(mutatedResource ~> \.other, "3")
|
||||
}
|
||||
}
|
||||
|
||||
private enum MutableTestDescription: JSONAPI.ResourceObjectDescription {
|
||||
static let jsonType: String = "test"
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
var name: Attribute<String>
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
var other: ToOneRelationship<MutableTestType, NoMetadata, NoLinks>
|
||||
}
|
||||
}
|
||||
|
||||
private typealias MutableTestType = JSONAPI.ResourceObject<MutableTestDescription, NoMetadata, NoLinks, String>
|
||||
|
||||
private enum ImmutableTestDescription: JSONAPI.ResourceObjectDescription {
|
||||
static let jsonType: String = "test2"
|
||||
|
||||
struct Attributes: JSONAPI.Attributes {
|
||||
let name: Attribute<String>
|
||||
}
|
||||
|
||||
struct Relationships: JSONAPI.Relationships {
|
||||
let other: ToOneRelationship<ImmutableTestType, NoMetadata, NoLinks>
|
||||
}
|
||||
}
|
||||
|
||||
private typealias ImmutableTestType = JSONAPI.ResourceObject<ImmutableTestDescription, NoMetadata, NoLinks, String>
|
||||
@@ -42,6 +42,17 @@ extension Attribute_FunctorTests {
|
||||
]
|
||||
}
|
||||
|
||||
extension BasicJSONAPIErrorTests {
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__BasicJSONAPIErrorTests = [
|
||||
("test_decodeAFewExamples", test_decodeAFewExamples),
|
||||
("test_definedFields", test_definedFields),
|
||||
("test_initAndEquality", test_initAndEquality),
|
||||
]
|
||||
}
|
||||
|
||||
extension ComputedPropertiesTests {
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
@@ -145,6 +156,8 @@ extension DocumentTests {
|
||||
("test_singleDocumentSomeIncludesWithMetadata_encode", test_singleDocumentSomeIncludesWithMetadata_encode),
|
||||
("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription),
|
||||
("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode),
|
||||
("test_singleSuccessDocumentNoIncludesAddIncludingType", test_singleSuccessDocumentNoIncludesAddIncludingType),
|
||||
("test_singleSuccessDocumentSomeIncludesAddIncludes", test_singleSuccessDocumentSomeIncludesAddIncludes),
|
||||
("test_sparseIncludeFullPrimaryResource", test_sparseIncludeFullPrimaryResource),
|
||||
("test_sparseIncludeSparsePrimaryResource", test_sparseIncludeSparsePrimaryResource),
|
||||
("test_sparsePrimaryResource", test_sparsePrimaryResource),
|
||||
@@ -196,6 +209,21 @@ extension EmptyObjectDecoderTests {
|
||||
]
|
||||
}
|
||||
|
||||
extension GenericJSONAPIErrorTests {
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__GenericJSONAPIErrorTests = [
|
||||
("test_decodeKnown", test_decodeKnown),
|
||||
("test_decodeUnknown", test_decodeUnknown),
|
||||
("test_definedFields", test_definedFields),
|
||||
("test_encode", test_encode),
|
||||
("test_encodeUnknown", test_encodeUnknown),
|
||||
("test_initAndEquality", test_initAndEquality),
|
||||
("test_payloadAccess", test_payloadAccess),
|
||||
]
|
||||
}
|
||||
|
||||
extension IncludedTests {
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
@@ -205,6 +233,8 @@ extension IncludedTests {
|
||||
("test_ComboSparseAndFullIncludeTypes", test_ComboSparseAndFullIncludeTypes),
|
||||
("test_EightDifferentIncludes", test_EightDifferentIncludes),
|
||||
("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode),
|
||||
("test_ElevenDifferentIncludes", test_ElevenDifferentIncludes),
|
||||
("test_ElevenDifferentIncludes_encode", test_ElevenDifferentIncludes_encode),
|
||||
("test_FiveDifferentIncludes", test_FiveDifferentIncludes),
|
||||
("test_FiveDifferentIncludes_encode", test_FiveDifferentIncludes_encode),
|
||||
("test_FourDifferentIncludes", test_FourDifferentIncludes),
|
||||
@@ -323,6 +353,22 @@ extension ResourceBodyTests {
|
||||
]
|
||||
}
|
||||
|
||||
extension ResourceObjectReplacingTests {
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
// to regenerate.
|
||||
static let __allTests__ResourceObjectReplacingTests = [
|
||||
("test_replaceImmutableAttributes", test_replaceImmutableAttributes),
|
||||
("test_replaceImmutableRelationships", test_replaceImmutableRelationships),
|
||||
("test_replaceMutableAttributes", test_replaceMutableAttributes),
|
||||
("test_replaceMutableRelationships", test_replaceMutableRelationships),
|
||||
("test_tapImmutableAttributes", test_tapImmutableAttributes),
|
||||
("test_tapImmutableRelationships", test_tapImmutableRelationships),
|
||||
("test_tapMutableAttributes", test_tapMutableAttributes),
|
||||
("test_tapMutableRelationships", test_tapMutableRelationships),
|
||||
]
|
||||
}
|
||||
|
||||
extension ResourceObjectTests {
|
||||
// DO NOT MODIFY: This is autogenerated, use:
|
||||
// `swift test --generate-linuxmain`
|
||||
@@ -442,16 +488,19 @@ public func __allTests() -> [XCTestCaseEntry] {
|
||||
testCase(APIDescriptionTests.__allTests__APIDescriptionTests),
|
||||
testCase(AttributeTests.__allTests__AttributeTests),
|
||||
testCase(Attribute_FunctorTests.__allTests__Attribute_FunctorTests),
|
||||
testCase(BasicJSONAPIErrorTests.__allTests__BasicJSONAPIErrorTests),
|
||||
testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests),
|
||||
testCase(CustomAttributesTests.__allTests__CustomAttributesTests),
|
||||
testCase(DocumentTests.__allTests__DocumentTests),
|
||||
testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests),
|
||||
testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests),
|
||||
testCase(IncludedTests.__allTests__IncludedTests),
|
||||
testCase(LinksTests.__allTests__LinksTests),
|
||||
testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests),
|
||||
testCase(PolyProxyTests.__allTests__PolyProxyTests),
|
||||
testCase(RelationshipTests.__allTests__RelationshipTests),
|
||||
testCase(ResourceBodyTests.__allTests__ResourceBodyTests),
|
||||
testCase(ResourceObjectReplacingTests.__allTests__ResourceObjectReplacingTests),
|
||||
testCase(ResourceObjectTests.__allTests__ResourceObjectTests),
|
||||
testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests),
|
||||
testCase(SparseFieldsetTests.__allTests__SparseFieldsetTests),
|
||||
|
||||
Reference in New Issue
Block a user