mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Merge pull request #41 from mattpolzin/feature/resource-object-replacing
Resource Object replacing and tapping
This commit is contained in:
@@ -12,25 +12,6 @@ import JSONAPI
|
||||
|
||||
********/
|
||||
|
||||
// Mapping functions (will be included in future version of library)
|
||||
extension JSONAPI.ResourceObject {
|
||||
func mapAttributes(_ transform: (Description.Attributes) -> Description.Attributes) -> Self {
|
||||
return Self(id: id,
|
||||
attributes: transform(attributes),
|
||||
relationships: relationships,
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
|
||||
func mapRelationships(_ transform: (Description.Relationships) -> Description.Relationships) -> Self {
|
||||
return Self(id: id,
|
||||
attributes: attributes,
|
||||
relationships: transform(relationships),
|
||||
meta: meta,
|
||||
links: links)
|
||||
}
|
||||
}
|
||||
|
||||
// Mock up a server response
|
||||
let mockDogData = """
|
||||
{
|
||||
@@ -62,11 +43,7 @@ var dog = parsedResponse.body.primaryResource!.value
|
||||
print("Received dog named: \(dog.name)")
|
||||
|
||||
// change the dog's name
|
||||
let changedDog = dog.mapAttributes { currentAttributes in
|
||||
var ret = currentAttributes
|
||||
ret.name = .init(value: "Julia")
|
||||
return ret
|
||||
}
|
||||
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,
|
||||
@@ -97,7 +74,7 @@ var dog2 = parsedResponse2.body.primaryResource!.value
|
||||
print("Received dog named: \(dog2.name)")
|
||||
|
||||
// change the dog's name
|
||||
let changedDog2 = dog2.mapAttributes { _ in
|
||||
let changedDog2 = dog2.replacingAttributes { _ in
|
||||
return .init(name: .init(value: "Nigel"))
|
||||
}
|
||||
|
||||
@@ -130,7 +107,7 @@ var dog3 = parsedResponse2.body.primaryResource!.value
|
||||
print("Received dog with owner: \(dog3 ~> \.owner)")
|
||||
|
||||
// give the dog an owner
|
||||
let changedDog3 = dog3.mapRelationships { _ in
|
||||
let changedDog3 = dog3.replacingRelationships { _ in
|
||||
return .init(owner: .init(id: Id(rawValue: "1")))
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,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 +572,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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user