Compare commits

..

1 Commits

32 changed files with 328 additions and 889 deletions
@@ -1,124 +0,0 @@
//: [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,29 +119,6 @@ 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)
@@ -164,6 +141,4 @@ 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,6 +6,5 @@
<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.5.0"
spec.version = "2.2.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.2"
spec.dependency "Poly", "~> 2.1"
end
+2 -2
View File
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": {
"branch": null,
"revision": "b24fd3b41bf3126d4c6dede3708135182172af60",
"version": "2.2.0"
"revision": "4a08517b24f8e9f6dd8c02ec7da316aac5c00e2e",
"version": "2.1.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.2.0")),
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.1.0")),
],
targets: [
.target(
+1 -34
View File
@@ -16,13 +16,12 @@ 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)
### Client+Server
### Combined
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
@@ -77,9 +76,6 @@ 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)
@@ -573,35 +569,6 @@ 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
-134
View File
@@ -414,137 +414,3 @@ 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> {
// 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 {
// 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")
}
}
}
@@ -1,11 +0,0 @@
//
// DocumentDecodingErro.swift
//
//
// Created by Mathew Polzin on 10/20/19.
//
public enum JSONAPIDocumentDecodingError: Swift.Error {
case foundErrorDocumentWhenExpectingSuccess
case foundSuccessDocumentWhenExpectingError
}
-8
View File
@@ -169,11 +169,3 @@ 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 @@
//
// BasicJSONAPIError.swift
// BasicError.swift
// JSONAPI
//
// Created by Mathew Polzin on 9/29/19.
@@ -1,5 +1,5 @@
//
// GenericJSONAPIError.swift
// GenericError.swift
// JSONAPI
//
// Created by Mathew Polzin on 9/29/19.
+1 -1
View File
@@ -1,5 +1,5 @@
//
// JSONAPIError.swift
// Error.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
@@ -0,0 +1,141 @@
//
// TransformedAttribute.swift
//
// Created by Mathew Polzin on 9/30/19.
//
public protocol AttrType {
associatedtype SerializedType
}
@propertyWrapper
public struct Attr<Serializer, Deserializer, Value>: AttrType where Serializer: Transformer, Deserializer: Transformer, Serializer.From == Value, Deserializer.To == Value {
public typealias SerializedType = Value
private var _wrappedValue: Value?
public var wrappedValue: Value {
guard let ret = _wrappedValue else {
fatalError("Accessed Transformed Value prior to initializing or decoding it.")
}
return ret
}
public init(wrappedValue: Value) {
_wrappedValue = wrappedValue
}
public init(wrappedValue: Value, serializer: Serializer.Type, deserializer: Deserializer.Type) {
_wrappedValue = wrappedValue
}
public init(serializer: Serializer.Type, deserializer: Deserializer.Type) {
_wrappedValue = nil
}
}
extension Attr where
Serializer == IdentityTransformer<Value>,
Deserializer == IdentityTransformer<Value> {
public init(wrappedValue: Value) {
_wrappedValue = wrappedValue
}
public init() {
_wrappedValue = nil
}
}
extension Attr where
Deserializer == IdentityTransformer<Value>,
Serializer: Transformer, Serializer.From == Value {
public init(wrappedValue: Value, serializer: Serializer.Type) {
_wrappedValue = wrappedValue
}
public init(serializer: Serializer.Type) {
_wrappedValue = nil
}
}
extension Attr where
Serializer == IdentityTransformer<Value>,
Deserializer: Transformer, Deserializer.To == Value {
public init(wrappedValue: Value, deserializer: Deserializer.Type) {
_wrappedValue = wrappedValue
}
public init(deserializer: Deserializer.Type) {
_wrappedValue = nil
}
}
extension Attr: Encodable where
Serializer: Transformer, Serializer.To: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(Serializer.transform(wrappedValue))
}
}
extension Attr: Decodable where
Deserializer: Transformer, Deserializer.From: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let anyNil: Any? = nil
if container.decodeNil(),
let val = anyNil as? Value {
_wrappedValue = val
} else {
_wrappedValue = try Deserializer.transform(container.decode(Deserializer.From.self))
}
}
}
public protocol _Omittable {
static var nilValue: Self { get }
}
@propertyWrapper
public struct Omittable<T>: AttrType, _Omittable {
public typealias SerializedType = T?
public let wrappedValue: T?
public init(wrappedValue: T?) {
self.wrappedValue = wrappedValue
}
public static var nilValue: Omittable<T> { return .init(wrappedValue: nil) }
}
extension Omittable: Encodable where T: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if Optional(wrappedValue) != nil {
try container.encode(wrappedValue)
}
}
}
extension Omittable: Decodable where T: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = .init(wrappedValue: try container.decode(T.self))
}
}
extension KeyedDecodingContainer {
public func decode<T>(_ type: T.Type, forKey key: KeyedDecodingContainer<K>.Key) throws -> T where T : Decodable, T: _Omittable {
return try decodeIfPresent(T.self, forKey: key) ?? T.nilValue
}
}
@@ -29,6 +29,12 @@ public enum IdentityTransformer<T>: ReversibleTransformer {
public static func reverse(_ value: T) throws -> T { return value }
}
extension Optional: Transformer where Wrapped: Transformer {
public static func transform(_ value: Wrapped.From?) throws -> Wrapped.To? {
return try value.map { try Wrapped.transform($0) }
}
}
// MARK: - Validator
/// A Validator is a Transformer that throws an error if an invalid value
@@ -79,8 +79,3 @@ 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 {}
@@ -1,74 +0,0 @@
//
// 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)
}
}

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