From 0b307bd3bcd9dbb03d83c5cb412012d3bdf0b0ef Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 12 Oct 2019 17:54:28 -0700 Subject: [PATCH 1/2] Add replacement and tapping functions for attributes and relationships. --- .../PATCHing.xcplaygroundpage/Contents.swift | 29 +--- .../ResourceObject+Replacing.swift | 74 +++++++++ .../ResourceObject.swift | 0 .../ResourceObject+ReplacingTests.swift | 155 ++++++++++++++++++ 4 files changed, 232 insertions(+), 26 deletions(-) create mode 100644 Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift rename Sources/JSONAPI/Resource/{ => Resource Object}/ResourceObject.swift (100%) create mode 100644 Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift diff --git a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift index 937b873..f916054 100644 --- a/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift +++ b/JSONAPI.playground/Pages/PATCHing.xcplaygroundpage/Contents.swift @@ -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"))) } diff --git a/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.swift new file mode 100644 index 0000000..98ef37c --- /dev/null +++ b/Sources/JSONAPI/Resource/Resource Object/ResourceObject+Replacing.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) + } +} diff --git a/Sources/JSONAPI/Resource/ResourceObject.swift b/Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift similarity index 100% rename from Sources/JSONAPI/Resource/ResourceObject.swift rename to Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift diff --git a/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift b/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift new file mode 100644 index 0000000..337a5b6 --- /dev/null +++ b/Tests/JSONAPITests/ResourceObject/ResourceObject+ReplacingTests.swift @@ -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 + } + + struct Relationships: JSONAPI.Relationships { + var other: ToOneRelationship + } +} + +private typealias MutableTestType = JSONAPI.ResourceObject + +private enum ImmutableTestDescription: JSONAPI.ResourceObjectDescription { + static let jsonType: String = "test2" + + struct Attributes: JSONAPI.Attributes { + let name: Attribute + } + + struct Relationships: JSONAPI.Relationships { + let other: ToOneRelationship + } +} + +private typealias ImmutableTestType = JSONAPI.ResourceObject From 43e02351deef20425154b9b8e634ebe1a58e4a6d Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 12 Oct 2019 18:22:48 -0700 Subject: [PATCH 2/2] Add documentation on replacing and tapping attributes and relationships --- README.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/README.md b/README.md index 1120097..c4d238c 100644 --- a/README.md +++ b/README.md @@ -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 = JSONAPI.Document> ``` +### 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