Compare commits

..

5 Commits

Author SHA1 Message Date
Mathew Polzin dbbacab105 Bump podspec version 2019-10-12 19:30:24 -07:00
Mathew Polzin 4eb36d4594 Merge pull request #41 from mattpolzin/feature/resource-object-replacing
Resource Object replacing and tapping
2019-10-12 19:27:29 -07:00
Mathew Polzin 43e02351de Add documentation on replacing and tapping attributes and relationships 2019-10-12 18:22:48 -07:00
Mathew Polzin 0b307bd3bc Add replacement and tapping functions for attributes and relationships. 2019-10-12 17:54:28 -07:00
Mathew Polzin 662a84ccf0 Add examples around modifying attributes/relationships on resource object and then preparing a new request (i.e. for PATCHing) 2019-10-12 09:35:01 -07:00
8 changed files with 412 additions and 1 deletions
@@ -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)!)
+25
View File
@@ -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>>
+1
View File
@@ -6,5 +6,6 @@
<page name='Full Client &amp; Server Example'/>
<page name='Full Document Verbose Generation'/>
<page name='Sparse Fieldsets Example'/>
<page name='PATCHing'/>
</pages>
</playground>
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
#
spec.name = "MP-JSONAPI"
spec.version = "2.3.0"
spec.version = "2.4.0"
spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results.
+32
View File
@@ -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>