Compare commits

...

12 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
Mathew Polzin 44f9bca7dc Merge pull request #39 from mattpolzin/feature/add-poly-11
Add Include11 Type
2019-10-02 20:10:56 -07:00
Mathew Polzin 774b53b9b6 bump podspec version and bump podspec Poly dependency. 2019-10-02 20:05:28 -07:00
Mathew Polzin 654d3bfd2b Update Poly, add Include11 type. 2019-10-02 20:02:11 -07:00
Mathew Polzin c26d6d99c0 Update README.md 2019-09-29 17:21:43 -07:00
Mathew Polzin 43dcc4fb12 Update README.md 2019-09-29 17:21:08 -07:00
Mathew Polzin abae975f59 Update README.md 2019-09-29 17:20:42 -07:00
Mathew Polzin 0e6b2a7771 Update README.md 2019-09-29 17:14:25 -07:00
22 changed files with 565 additions and 94 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>
+2 -2
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
#
spec.name = "MP-JSONAPI"
spec.version = "2.2.0"
spec.version = "2.4.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
View File
@@ -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
View File
@@ -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(
+36 -2
View File
@@ -5,15 +5,17 @@ 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)
### Serverside
- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX)
@@ -74,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)
@@ -567,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
+8
View File
@@ -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 -1
View File
@@ -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)
}
}
@@ -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
}
}
]
@@ -1,30 +0,0 @@
[
{
"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_entity4",
"id": "364B3B69-4DF1-467F-B52E-B0C9E44F666E"
}
]
@@ -1,26 +0,0 @@
[
{
"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"
}
}
}
}
]
@@ -1,18 +0,0 @@
[
{
"type": "test_entity1",
"id": "2DF03B69-4B0A-467F-B52E-B0C9E44FCECF",
"attributes": {
"foo": "Hello",
"bar": 123
}
},
{
"type": "test_entity1",
"id": "90F03B69-4DF1-467F-B52E-B0C9E44FC333",
"attributes": {
"foo": "World",
"bar": 456
}
}
]

Some files were not shown because too many files have changed in this diff Show More