Compare commits

...

78 Commits

Author SHA1 Message Date
Mathew Polzin ae7e0f528a abstract away document comparison 2019-11-06 00:19:27 -08:00
Mathew Polzin 9f42e5fa3f Merge pull request #48 from mattpolzin/feature/indentation
Feature/indentation
2019-11-05 23:03:03 -08:00
Mathew Polzin 3e96adf775 merge and fix conflicts 2019-11-05 23:00:18 -08:00
Mathew Polzin 83233a7e1a Merge pull request #46 from mattpolzin/feature/testing-comparisons
Feature/testing comparisons
2019-11-05 22:47:47 -08:00
Mathew Polzin adcc6bfb10 fix error after merge 2019-11-05 22:43:40 -08:00
Mathew Polzin 6f9a2df605 Merge remote-tracking branch 'origin/alpha/3x' into feature/testing-comparisons 2019-11-05 22:41:53 -08:00
Mathew Polzin c24b70b49f Merge pull request #45 from mattpolzin/feature/deprecate-subscript
Feature/deprecate subscript
2019-11-05 22:40:37 -08:00
Mathew Polzin 7b56e35a91 Merge pull request #47 from mattpolzin/feature/document-improvements
Feature/document improvements
2019-11-05 22:38:42 -08:00
Mathew Polzin e6f82c6052 indentation 2019-11-05 22:37:48 -08:00
Mathew Polzin d6f01d6c1d indentation 2019-11-05 22:27:59 -08:00
Mathew Polzin a7f6ed5845 update README 2019-11-05 21:03:51 -08:00
Mathew Polzin 87271b93f9 deprecate subscript attribute accessor in favor of key path dynamic member lookup 2019-11-05 20:48:04 -08:00
Mathew Polzin 706346e3a6 just reformatting and rearranging 2019-11-05 20:04:47 -08:00
Mathew Polzin 33a5ff41a0 beginning to do some breaking change cleanup 2019-11-05 19:50:52 -08:00
Mathew Polzin 0fe5c53ada generate linuxmain 2019-11-05 19:04:47 -08:00
Mathew Polzin f7bfa91ccc lots of comparison code, a few small breaking changes 2019-11-05 19:03:51 -08:00
Mathew Polzin 1a15ab6f9d Fix return type of success document after adding includes 2019-10-27 16:21:41 -07:00
Mathew Polzin eb85620379 regenerate swift test manifest 2019-10-20 22:30:15 -07:00
Mathew Polzin 57f85476c0 Bump podspec version 2019-10-20 22:29:22 -07:00
Mathew Polzin ea5c0b8601 Add Document.ErrorDocument and Document.SuccessDocument types 2019-10-20 22:23:52 -07:00
Mathew Polzin b46429a0ad Update README.md 2019-10-15 23:37:02 -07:00
Mathew Polzin dbc6ecc6d8 Update README.md 2019-10-12 21:39:29 -07:00
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
Mathew Polzin 194a58ae56 Merge pull request #37 from mattpolzin/feature/basic-error
Feature/basic error
2019-09-29 17:01:25 -07:00
Mathew Polzin f1d6b22f61 Add playground example, add/update documentation, correct visibility of new error payload properties to public. 2019-09-29 16:49:38 -07:00
Mathew Polzin 3057992348 bump podspec version, update Playground examples 2019-09-29 15:57:54 -07:00
Mathew Polzin d4806ff557 Add a few decode examples 2019-09-29 15:36:16 -07:00
Mathew Polzin b0801f7cee Add tests for BasicJSONAPIError and tweak documentation 2019-09-29 15:20:08 -07:00
Mathew Polzin 88c5d400aa Add generic and basic error types. add tests for generic type. 2019-09-29 14:56:04 -07:00
Mathew Polzin 6cd5aeaba6 Update README.md
Update max number of `Include` types in documentation.
2019-09-16 17:16:42 -07:00
Mathew Polzin 7e28cd2606 Merge pull request #35 from mattpolzin/add-include-10
Add include10 Type
2019-09-16 17:13:49 -07:00
Mathew Polzin cf6fa39548 bump Podspec version 2019-09-16 09:58:57 -07:00
Mathew Polzin a24f15dc4e regenerate linuxmain 2019-09-16 09:38:36 -07:00
Mathew Polzin 87e9ee0606 remove tests that are duplicates of those in the Poly package. I just never deleted this file when the Poly stuff moved into its own package. Add support for Include10. 2019-09-16 09:37:34 -07:00
Mathew Polzin 5ed45078a1 Update README.md
Fix incorrect Swift version requirement and suggested SPM dependency version.
2019-09-14 16:44:46 -07:00
Mathew Polzin 34dc351810 Merge pull request #34 from mattpolzin/swift-5.1
Swift 5.1 tools dependency, access to attributes via dynamicMemberLookup
2019-09-14 16:28:24 -07:00
Mathew Polzin 3ff1b867ca Bump spec version to account for breaking changes with configs and swift tools version 2019-09-14 15:53:48 -07:00
Mathew Polzin ae51be20d8 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-09-14 15:51:05 -07:00
Mathew Polzin c142e93fec Merge pull request #33 from chrisballinger/patch-1
Change Pod name to avoid conflict. Resolves #32
2019-09-14 15:49:37 -07:00
Chris Ballinger e99c6625f8 Update README.md 2019-09-14 10:07:53 -07:00
Chris Ballinger e9048a4861 Change Pod name to avoid conflict. Resolves #32 2019-09-13 15:07:37 -07:00
Mathew Polzin ff06c36b36 uncomment out language version declaration in package manifest, 'fix' indentation. 2019-09-05 22:22:55 -07:00
Mathew Polzin 8eb4c14bb3 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-09-05 21:39:27 -07:00
Mathew Polzin baee85fb1b Update README.md 2019-09-02 16:07:10 -07:00
Mathew Polzin 44f21b4430 Update README.md 2019-09-02 16:04:06 -07:00
Mathew Polzin f7f36c6e7a Update README.md 2019-09-02 15:59:24 -07:00
Mathew Polzin af7eaeb684 Update README.md 2019-09-02 15:10:43 -07:00
Mathew Polzin 0cc2368331 Update README.md
Add instructions for adding as Swift Package Manager dependency.
2019-09-02 15:05:23 -07:00
Mathew Polzin 6c54845525 Update README.md 2019-09-02 15:00:07 -07:00
Mathew Polzin 51b981c108 Update README.md 2019-09-02 13:25:57 -07:00
Mathew Polzin b33c7f4ab5 Update README.md 2019-08-29 09:33:03 -07:00
Mathew Polzin bf8245c7b8 Update README.md 2019-08-29 09:30:59 -07:00
Mathew Polzin 7bd4021790 Update README.md 2019-08-28 20:21:50 -07:00
Mathew Polzin 771641b82c Update README.md 2019-08-28 18:55:42 -07:00
Mathew Polzin 198e5a9800 Update README.md 2019-08-28 18:55:11 -07:00
Mathew Polzin 9db534dea0 Update README.md
Add basic example Google Colab.
2019-08-27 00:48:11 -07:00
Mathew Polzin 2b409e464e merge w/ master 2019-08-23 17:53:33 -07:00
Mathew Polzin 99e97aadcc Merge branch 'master' into swift-5.1 2019-08-04 10:34:32 -07:00
Mathew Polzin cf7b68c026 Merge w/ master 2019-08-04 09:02:04 -07:00
Mathew Polzin 4b4ebb4214 merge w/ master 2019-07-30 17:52:42 -07:00
Mathew Polzin fb72817de6 bump swift version badge to swift 5.1 2019-07-25 09:36:35 -07:00
Mathew Polzin 569cec05cf ditch language version argument in package file that is not accepted by swift test. update linuxmain. 2019-07-25 09:27:56 -07:00
Mathew Polzin 78b9b12592 Update README 2019-07-25 09:23:01 -07:00
Mathew Polzin 9143281290 Introduce dynamic member keypath lookup as alternative to existing subscript keypath lookup for attributes. also did a bit of tidying up. 2019-07-25 09:12:17 -07:00
Mathew Polzin badfa30668 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-07-25 07:49:22 -07:00
Mathew Polzin 0144a2ee80 Remove prior experimentation with property wrappers -- feature was not baked when that experimentation was done. 2019-07-25 07:49:17 -07:00
Mathew Polzin c7b97567a9 update swift tools version in package file, add some property wrappers, add some tests for wrappers. its all broken but worth holding onto for now. 2019-06-20 22:35:55 -07:00
71 changed files with 5256 additions and 2720 deletions
@@ -37,7 +37,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
// JSON:API Documents for this particular API to have Metadata, Links,
// useful Errors, or an APIDescription (The *SPEC* calls this
// "API Description" the "JSON:API Object").
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
// MARK: Entity Definitions
@@ -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)!)
@@ -35,7 +35,7 @@ typealias ThingWithProperties = JSONAPI.ResourceObject<ThingWithPropertiesDescri
//
// NOTE: Using `JSONAPI.EncodableResourceBody` which means the document type will be `Encodable` but not `Decodable`.
//
typealias Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
typealias Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
//
// NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable.
@@ -24,7 +24,7 @@ let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
// MARK: - Parse a request or response body with one Dog in it using an alternative model
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
typealias AltSingleDogDocument = JSONAPI.Document<SingleResourceBody<AlternativeDog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
let altDogFromData = altDogResponse.body.primaryResource?.value
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
@@ -55,19 +55,15 @@ print("-----")
// MARK: - Pass successfully parsed body to other parts of the code
/*
---- CRASHING IN XCODE 10.2 PLAYGROUND ----
if case let .data(bodyData) = peopleResponse.body {
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
print("first person's name: \(bodyData.primary.values[0].fullName)")
} else {
print("no body data")
}
*/
// MARK: - Work in the abstract
print("-----")
func process<T: JSONAPIDocument>(document: T) {
guard case let .data(body) = document.body else {
return
@@ -75,3 +71,29 @@ func process<T: JSONAPIDocument>(document: T) {
let x: T.Body.Data = body
}
process(document: peopleResponse)
// MARK: - Work with errors
typealias ErrorDoc = JSONAPI.Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
let mockErrorData =
"""
{
"errors": [
{
"status": "500",
"title": "Internal Server Error",
"detail": "Server fell over while parsing your request."
}
]
}
""".data(using: .utf8)!
let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData)
switch errorResponse.body {
case .data:
print("cool, data!")
case .errors(let errors, let meta, let links):
let errorDetails = errors.compactMap { $0.payload?.detail }
print("error details: \(errorDetails)")
}
+30 -5
View File
@@ -15,11 +15,11 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
********/
// MARK: - String as CreatableRawIdType
var GlobalStringId: Int = 0
var globalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
GlobalStringId += 1
return String(GlobalStringId)
globalStringId += 1
return String(globalStringId)
}
}
@@ -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)
@@ -139,6 +162,8 @@ public enum HouseDescription: ResourceObjectDescription {
public typealias House = ExampleEntity<HouseDescription>
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>
public typealias SingleDogDocument = JSONAPI.Document<SingleResourceBody<Dog>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
public typealias BatchPeopleDocument = JSONAPI.Document<ManyResourceBody<Person>, NoMetadata, NoLinks, Include2<Dog, House>, NoAPIDescription, UnknownJSONAPIError>
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>
+5 -4
View File
@@ -15,8 +15,8 @@ Pod::Spec.new do |spec|
# summary should be tweet-length, and the description more in depth.
#
spec.name = "JSONAPI"
spec.version = "1.0.0"
spec.name = "MP-JSONAPI"
spec.version = "2.5.0"
spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results.
@@ -131,10 +131,11 @@ See the JSON API Spec here: https://jsonapi.org/format/
# where they will only apply to your library. If you depend on other Podspecs
# you can include multiple dependencies to ensure it works.
spec.swift_version = "5.0"
spec.swift_version = "5.1"
spec.module_name = "JSONAPI"
# spec.requires_arc = true
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
spec.dependency "Poly", "~> 2.0"
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": "38051821d7ef49e590e26e819a2fe447e50be9ff",
"version": "2.0.1"
"revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf",
"version": "2.3.0"
}
}
]
+11 -11
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.0
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,27 +6,27 @@ import PackageDescription
let package = Package(
name: "JSONAPI",
platforms: [
.macOS(.v10_10),
.iOS(.v10)
.macOS(.v10_10),
.iOS(.v10)
],
products: [
.library(
name: "JSONAPI",
targets: ["JSONAPI"]),
.library(
name: "JSONAPITesting",
targets: ["JSONAPITesting"])
.library(
name: "JSONAPITesting",
targets: ["JSONAPITesting"])
],
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.0")),
],
targets: [
.target(
name: "JSONAPI",
dependencies: ["Poly"]),
.target(
name: "JSONAPITesting",
dependencies: ["JSONAPI"]),
.target(
name: "JSONAPITesting",
dependencies: ["JSONAPI"]),
.testTarget(
name: "JSONAPITests",
dependencies: ["JSONAPI", "JSONAPITesting"]),
@@ -34,5 +34,5 @@ let package = Package(
name: "JSONAPITestingTests",
dependencies: ["JSONAPI", "JSONAPITesting"])
],
swiftLanguageVersions: [.v5]
swiftLanguageVersions: [.v5]
)
+121 -20
View File
@@ -1,21 +1,38 @@
# JSONAPI
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.0](http://img.shields.io/badge/Swift-5.0-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.1](http://img.shields.io/badge/Swift-5.1-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses.
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)
- [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
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
<!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
- [JSONAPI](#jsonapi)
- [Table of Contents](#table-of-contents)
- [Primary Goals](#primary-goals)
- [Caveat](#caveat)
- [Dev Environment](#dev-environment)
- [Prerequisites](#prerequisites)
- [Swift Package Manager](#swift-package-manager)
- [CocoaPods](#cocoapods)
- [Xcode project](#xcode-project)
- [Running the Playground](#running-the-playground)
@@ -51,12 +68,18 @@ See the JSON API Spec here: https://jsonapi.org/format/
- [`IncludeType`](#includetype)
- [`APIDescriptionType`](#apidescriptiontype)
- [`Error`](#error)
- [`UnknownJSONAPIError`](#unknownjsonapierror)
- [`BasicJSONAPIError`](#basicjsonapierror)
- [`GenericJSONAPIError`](#genericjsonapierror)
- [`JSONAPI.Meta`](#jsonapimeta)
- [`JSONAPI.Links`](#jsonapilinks)
- [`JSONAPI.RawIdType`](#jsonapirawidtype)
- [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)
@@ -69,8 +92,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
- [JSONAPI+Arbitrary](#jsonapiarbitrary)
- [JSONAPI+OpenAPI](#jsonapiopenapi)
<!-- /TOC -->
## Primary Goals
The primary goals of this framework are:
@@ -86,20 +107,28 @@ If you find something wrong with this library and it isn't already mentioned und
## Dev Environment
### Prerequisites
1. Swift 4.2+
1. Swift 5.1+
2. Swift Package Manager *OR* Cocoapods
### Swift Package Manager
Just include the following in your package's dependencies and add `JSONAPI` to the dependencies for any of your targets.
```
.package(url: "https://github.com/mattpolzin/JSONAPI.git", .upToNextMajor(from: "2.2.0"))
```
### CocoaPods
To use this framework in your project via Cocoapods instead of Swift Package Manager, add the following dependencies to your Podfile.
To use this framework in your project via Cocoapods, add the following dependencies to your Podfile.
```
pod 'Poly', :git => 'https://github.com/mattpolzin/Poly.git'
pod 'JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git'
pod 'MP-JSONAPI', :git => 'https://github.com/mattpolzin/JSONAPI.git'
```
### Xcode project
To create an Xcode project for JSONAPI, run
`swift package generate-xcodeproj`
With Xcode 11+ you can also just open the folder containing your clone of this repository and begin working.
### Running the Playground
To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace.
@@ -298,7 +327,12 @@ A resource object that does not have attributes can be described by adding the f
typealias Attributes = NoAttributes
```
`Attributes` can be accessed via the `subscript` operator of the `ResourceObject` type as follows:
As of Swift 5.1, `Attributes` can be accessed via dynamic member keypath lookup as follows:
```swift
let favoriteColor: String = person.favoriteColor
```
:warning: `Attributes` can also be accessed via the older `subscript` operator, but this is a deprecated feature that will be removed in the next major version:
```swift
let favoriteColor: String = person[\.favoriteColor]
```
@@ -341,7 +375,7 @@ You can also creator `Validators` and `ValidatedAttribute`s. A `Validator` is ju
#### Computed `Attribute`
You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person[\.name]` attribute from the example above and create a `fullName` computed property.
You can add computed properties to your `ResourceObjectDescription.Attributes` struct if you would like to expose attributes that are not explicitly represented by the JSON. These computed properties do not have to be wrapped in `Attribute`, `ValidatedAttribute`, or `TransformedAttribute`. This allows computed attributes to be of types that are not `Codable`. Here's an example of how you might take the `person.name` attribute from the example above and create a `fullName` computed property.
```swift
public var fullName: Attribute<String> {
@@ -349,7 +383,7 @@ public var fullName: Attribute<String> {
}
```
If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person[\.fullName]` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result.
If your computed property is wrapped in a `AttributeType` then you can still use the default subscript operator to access it (as would be the case with the `person.fullName` example above). However, if you add a property to the `Attributes` `struct` that is not wrapped in an `AttributeType`, you must either access it from its full path (`person.attributes.newThing`) or with the "direct" subscript accessor (`person[direct: \.newThing]`). This keeps the subscript access unambiguous enough for the compiler to be helpful prior to explicitly casting, comparing, or storing the result.
### Copying/Mutating `ResourceObjects`
`ResourceObject` is a value type, so copying is its default behavior. There are two common mutations you might want to make when copying a `ResourceObject`:
@@ -374,7 +408,7 @@ The entirety of a JSON API request or response is encoded or decoded from- or to
```swift
let decoder = JSONDecoder()
let responseStructure = JSONAPI.Document<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self
let responseStructure = JSONAPI.Document<SingleResourceBody<Person>, NoMetadata, NoLinks, NoIncludes, BasicJSONAPIError<String>>.self
let document = try decoder.decode(responseStructure, from: data)
```
@@ -423,7 +457,7 @@ The third generic type of a `JSONAPIDocument` is a `Links` struct. `Links` are d
#### `IncludeType`
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to six types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on.
The fourth generic type of a `JSONAPIDocument` is an `Include`. This type controls which types of `ResourceObject` are looked for when decoding the "included" part of the JSON API document. If you do not expect any included resource objects to be in the document, `NoIncludes` is the way to go. The `JSONAPI` framework provides `Include`s for up to 10 types of included resource objects. These are named `Include1`, `Include2`, `Include3`, and so on.
**IMPORTANT**: The number trailing "Include" in these type names does not indicate a number of included resource objects, it indicates a number of _types_ of included resource objects. `Include1` can be used to decode any number of included resource objects as long as all the resource objects are of the same _type_.
@@ -441,7 +475,45 @@ You can supply any `JSONAPI.Meta` type as the metadata type of the API descripti
#### `Error`
The final generic type of a `JSONAPIDocument` is the `Error`. You should create an error type that can decode all the errors you expect your `JSONAPIDocument` to be able to decode. As prescribed by the **SPEC**, these errors will be found in the root document member `errors`.
The final generic type of a `JSONAPIDocument` is the `Error`.
You can either create an error type that can handle all the errors you expect your `JSONAPIDocument` to be able to encode/decode or use an out-of-box error type described here. As prescribed by the **SPEC**, these errors will be found under the root document key `errors`.
##### `UnknownJSONAPIError`
The `UnknownJSONAPIError` type will always succeed in parsing errors but it will not give you any information about what error occurred. You will generally get more bang for your buck out of the next error type described.
##### `BasicJSONAPIError`
The `BasicJSONAPIError` type will always succeed unless it is faced with an `id` field of an unexpected type, although it still "succeeds" in falling back to its `.unknown` case when that happens. This type extracts _most_ of the fields the **SPEC** describes [here](https://jsonapi.org/format/#error-objects). Because all of these fields are optional in the **SPEC**, they are optional on the `BasicJSONAPIError` type. You will have to create your own error type if you want to define certain fields as non-optional or parse metadata or links out of error objects.
đź—’Metadata and links are supported at the Document level for error responses, the are just not supported hanging off of the individual errors in the `errors` array of the response when using this error type.
The `BasicJSONAPIError` type is generic on one thing: The type it expects for the `id` field. If you expect integer `ids` back, you use `BasicJSONAPIError<Int>`. The same can be done for `String` or any other type that is both `Codable` and `Equatable`. You can even employ something like `AnyCodable` from *Flight-School* as your id field type. If you only need to handle a small subset of possible `id` field types, you can also use the `Poly` library that is already a dependency of `JSONAPI`. For example, you might expect a mix of `String` and `Int` ids for some reason: `BasicJSONAPIError<Either<Int, String>>`.
The two easiest ways to access the available properties of an error response are under the `payload` property of the error (this property is `nil` if the error was parsed as `.unknown`) or by asking the error for its `definedFields` dictionary.
As an example, let's say you have the following `Document` type that is destined for errors:
```swift
typealias ErrorDoc = JSONAPI.Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, BasicJSONAPIError<String>>
```
And you've parsed an error response
```swift
let errorResponse = try! JSONDecoder().decode(ErrorDoc.self, from: mockErrorData)
```
You can get at the `Document` body and errors in a couple of different ways, but for one you can switch on the body:
```swift
switch errorResponse.body {
case .data:
print("cool, data!")
case .errors(let errors, let meta, let links):
let errorDetails = errors.compactMap { $0.payload?.detail }
print("error details: \(errorDetails)")
}
```
##### `GenericJSONAPIError`
This type makes it simple to use your own error payload structures as `JSONAPIError` types. Simply define a `Codable` and `Equatable` struct and then use `GenericJSONAPIError<YourType>` as the error type for a `Document`.
### `JSONAPI.Meta`
@@ -491,12 +563,41 @@ There is a sparse fieldsets example included with this repository as a Playgroun
#### Sparse Fieldset `typealias` comparisons
You might have found a `typealias` like the following for encoding/decoding `JSONAPI.Document`s (note the primary resource body is a `JSONAPI.ResourceBody`):
```swift
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
```
In order to support sparse fieldsets (which are encode-only), the following companion `typealias` would be useful (note the primary resource body is a `JSONAPI.EncodableResourceBody`):
```swift
typealias SparseDocument<PrimaryResourceBody: JSONAPI.EncodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
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
@@ -611,7 +712,7 @@ typealias User = JSONAPI.ResourceObject<UserDescription, NoMetadata, NoLinks, St
Given a value `user` of the above resource object type, you can access the `createdAt` attribute just like you would any other:
```swift
let createdAt = user[\.createdAt]
let createdAt = user.createdAt
```
This works because `createdAt` is defined in the form: `var {name}: ({ResourceObject}) -> {Value}` where `{ResourceObject}` is the `JSONAPI.ResourceObject` described by the `ResourceObjectDescription` containing the meta-attribute.
@@ -632,7 +733,7 @@ enum UserDescription: ResourceObjectDescription {
struct Relationships: JSONAPI.Relationships {
public var friend: (User) -> User.Identifier {
return { user in
return User.Identifier(rawValue: user[\.friend_id])
return User.Identifier(rawValue: user.friend_id)
}
}
}
@@ -684,7 +785,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
// JSON:API Documents for this particular API to have Metadata, Links,
// useful Errors, or an APIDescription (The *SPEC* calls this
// "API Description" the "JSON:API Object").
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, UnknownJSONAPIError>
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
// MARK: Entity Definitions
+32 -32
View File
@@ -7,58 +7,58 @@
/// This is what the JSON API Spec calls the "JSON:API Object"
public protocol APIDescriptionType: Codable, Equatable {
associatedtype Meta
associatedtype Meta
}
/// This is what the JSON API Spec calls the "JSON:API Object"
public struct APIDescription<Meta: JSONAPI.Meta>: APIDescriptionType {
public let version: String
public let meta: Meta
public let version: String
public let meta: Meta
public init(version: String, meta: Meta) {
self.version = version
self.meta = meta
}
public init(version: String, meta: Meta) {
self.version = version
self.meta = meta
}
}
/// Can be used as `APIDescriptionType` for Documents that do not
/// have any API Description (a.k.a. "JSON:API Object").
public struct NoAPIDescription: APIDescriptionType, CustomStringConvertible {
public typealias Meta = NoMetadata
public typealias Meta = NoMetadata
public init() {}
public init() {}
public static var none: NoAPIDescription { return .init() }
public static var none: NoAPIDescription { return .init() }
public var description: String { return "No JSON:API Object" }
public var description: String { return "No JSON:API Object" }
}
extension APIDescription {
private enum CodingKeys: String, CodingKey {
case version
case meta
}
private enum CodingKeys: String, CodingKey {
case version
case meta
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
// The spec says that if a version is not specified, it should be assumed to be at least 1.0
version = (try? container.decode(String.self, forKey: .version)) ?? "1.0"
if let metaVal = NoMetadata() as? Meta {
meta = metaVal
} else {
meta = try container.decode(Meta.self, forKey: .meta)
}
}
if let metaVal = NoMetadata() as? Meta {
meta = metaVal
} else {
meta = try container.decode(Meta.self, forKey: .meta)
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(version, forKey: .version)
try container.encode(version, forKey: .version)
if Meta.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
}
if Meta.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
//
// DocumentDecodingErro.swift
//
//
// Created by Mathew Polzin on 10/20/19.
//
public enum JSONAPIDocumentDecodingError: Swift.Error {
case foundErrorDocumentWhenExpectingSuccess
case foundSuccessDocumentWhenExpectingError
}
-30
View File
@@ -1,30 +0,0 @@
//
// Error.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
//
public protocol JSONAPIError: Swift.Error, Equatable, Codable {
static var unknown: Self { get }
}
/// `UnknownJSONAPIError` can actually be used in any sitaution
/// where you don't know what errors are possible _or_ you just don't
/// care what errors might show up.
public enum UnknownJSONAPIError: JSONAPIError {
case unknownError
public init(from decoder: Decoder) throws {
self = .unknown
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("unknown")
}
public static var unknown: UnknownJSONAPIError {
return .unknownError
}
}
+75 -59
View File
@@ -14,35 +14,35 @@ public typealias Include = EncodableJSONPoly
///
/// If you have
///
/// `let includes: Includes<Include2<Thing1, Thing2>> = ...`
/// let includes: Includes<Include2<Thing1, Thing2>> = ...
///
/// then you can access all `Thing1` included resources with
///
/// `let includedThings = includes[Thing1.self]`
/// let includedThings = includes[Thing1.self]
public struct Includes<I: Include>: Encodable, Equatable {
public static var none: Includes { return .init(values: []) }
let values: [I]
public init(values: [I]) {
self.values = values
}
public static var none: Includes { return .init(values: []) }
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
public let values: [I]
guard I.self != NoIncludes.self else {
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.")
}
public init(values: [I]) {
self.values = values
}
for value in values {
try container.encode(value)
}
}
public var count: Int {
return values.count
}
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
guard I.self != NoIncludes.self else {
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.")
}
for value in values {
try container.encode(value)
}
}
public var count: Int {
return values.count
}
}
extension Includes: Decodable where I: Decodable {
@@ -65,25 +65,25 @@ extension Includes: Decodable where I: Decodable {
}
extension Includes {
public func appending(_ other: Includes<I>) -> Includes {
return Includes(values: values + other.values)
}
public func appending(_ other: Includes<I>) -> Includes {
return Includes(values: values + other.values)
}
}
public func +<I: Include>(_ left: Includes<I>, _ right: Includes<I>) -> Includes<I> {
return left.appending(right)
return left.appending(right)
}
extension Includes: CustomStringConvertible {
public var description: String {
return "Includes(\(String(describing: values))"
}
public var description: String {
return "Includes(\(String(describing: values))"
}
}
extension Includes where I == NoIncludes {
public init() {
values = []
}
public init() {
values = []
}
}
// MARK: - 0 includes
@@ -93,71 +93,87 @@ public typealias NoIncludes = Include0
// MARK: - 1 include
public typealias Include1 = Poly1
extension Includes where I: _Poly1 {
public subscript(_ lookup: I.A.Type) -> [I.A] {
return values.compactMap { $0.a }
}
public subscript(_ lookup: I.A.Type) -> [I.A] {
return values.compactMap { $0.a }
}
}
// MARK: - 2 includes
public typealias Include2 = Poly2
extension Includes where I: _Poly2 {
public subscript(_ lookup: I.B.Type) -> [I.B] {
return values.compactMap { $0.b }
}
public subscript(_ lookup: I.B.Type) -> [I.B] {
return values.compactMap { $0.b }
}
}
// MARK: - 3 includes
public typealias Include3 = Poly3
extension Includes where I: _Poly3 {
public subscript(_ lookup: I.C.Type) -> [I.C] {
return values.compactMap { $0.c }
}
public subscript(_ lookup: I.C.Type) -> [I.C] {
return values.compactMap { $0.c }
}
}
// MARK: - 4 includes
public typealias Include4 = Poly4
extension Includes where I: _Poly4 {
public subscript(_ lookup: I.D.Type) -> [I.D] {
return values.compactMap { $0.d }
}
public subscript(_ lookup: I.D.Type) -> [I.D] {
return values.compactMap { $0.d }
}
}
// MARK: - 5 includes
public typealias Include5 = Poly5
extension Includes where I: _Poly5 {
public subscript(_ lookup: I.E.Type) -> [I.E] {
return values.compactMap { $0.e }
}
public subscript(_ lookup: I.E.Type) -> [I.E] {
return values.compactMap { $0.e }
}
}
// MARK: - 6 includes
public typealias Include6 = Poly6
extension Includes where I: _Poly6 {
public subscript(_ lookup: I.F.Type) -> [I.F] {
return values.compactMap { $0.f }
}
public subscript(_ lookup: I.F.Type) -> [I.F] {
return values.compactMap { $0.f }
}
}
// MARK: - 7 includes
public typealias Include7 = Poly7
extension Includes where I: _Poly7 {
public subscript(_ lookup: I.G.Type) -> [I.G] {
return values.compactMap { $0.g }
}
public subscript(_ lookup: I.G.Type) -> [I.G] {
return values.compactMap { $0.g }
}
}
// MARK: - 8 includes
public typealias Include8 = Poly8
extension Includes where I: _Poly8 {
public subscript(_ lookup: I.H.Type) -> [I.H] {
return values.compactMap { $0.h }
}
public subscript(_ lookup: I.H.Type) -> [I.H] {
return values.compactMap { $0.h }
}
}
// MARK: - 9 includes
public typealias Include9 = Poly9
extension Includes where I: _Poly9 {
public subscript(_ lookup: I.I.Type) -> [I.I] {
return values.compactMap { $0.i }
}
public subscript(_ lookup: I.I.Type) -> [I.I] {
return values.compactMap { $0.i }
}
}
// MARK: - 10 includes
public typealias Include10 = Poly10
extension Includes where I: _Poly10 {
public subscript(_ lookup: I.J.Type) -> [I.J] {
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 }
}
}
+43 -45
View File
@@ -10,78 +10,76 @@
/// array should be used for no results).
public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {}
/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding.
/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and
/// decoding.
/// An `EncodablePrimaryResource` is a `CodablePrimaryResource` that only supports encoding.
public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {}
/// This protocol allows for `SingleResourceBody` to contain a `null`
/// data object where `ManyResourceBody` cannot (because an empty
/// array should be used for no results).
public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
public protocol OptionalCodablePrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
/// A `PrimaryResource` is a type that can be used in the body of a JSON API
/// A `CodablePrimaryResource` is a type that can be used in the body of a JSON API
/// document as the primary resource.
public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {}
public protocol CodablePrimaryResource: EncodablePrimaryResource, OptionalCodablePrimaryResource {}
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {}
extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimaryResource {}
/// An `EncodableResourceBody` is a `ResourceBody` that only supports being
/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding
/// and decoding.
public protocol EncodableResourceBody: Equatable, Encodable {}
/// A ResourceBody is a representation of the body of the JSON API Document.
/// A `CodableResourceBody` is a representation of the body of the JSON:API Document.
/// It can either be one resource (which can be specified as optional or not)
/// or it can contain many resources (and array with zero or more entries).
public protocol ResourceBody: Decodable, EncodableResourceBody {}
public protocol CodableResourceBody: Decodable, EncodableResourceBody {}
/// A `ResourceBody` that has the ability to take on more primary
/// resources by appending another similarly typed `ResourceBody`.
public protocol Appendable {
func appending(_ other: Self) -> Self
public protocol ResourceBodyAppendable {
func appending(_ other: Self) -> Self
}
public func +<R: Appendable>(_ left: R, right: R) -> R {
return left.appending(right)
public func +<R: ResourceBodyAppendable>(_ left: R, right: R) -> R {
return left.appending(right)
}
/// A type allowing for a document body containing 1 primary resource.
/// If the `Entity` specialization is an `Optional` type, the body can contain
/// 0 or 1 primary resources.
public struct SingleResourceBody<Entity: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
public let value: Entity
public let value: Entity
public init(resourceObject: Entity) {
self.value = resourceObject
}
public init(resourceObject: Entity) {
self.value = resourceObject
}
}
/// A type allowing for a document body containing 0 or more primary resources.
public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, Appendable {
public let values: [Entity]
public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
public let values: [Entity]
public init(resourceObjects: [Entity]) {
values = resourceObjects
}
public init(resourceObjects: [Entity]) {
values = resourceObjects
}
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
return ManyResourceBody(resourceObjects: values + other.values)
}
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
return ManyResourceBody(resourceObjects: values + other.values)
}
}
/// Use NoResourceBody to indicate you expect a JSON API document to not
/// contain a "data" top-level key.
public struct NoResourceBody: ResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
public struct NoResourceBody: CodableResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: Codable
extension SingleResourceBody {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
let anyNil: Any? = nil
let nilValue = anyNil as? Entity
@@ -90,11 +88,11 @@ extension SingleResourceBody {
return
}
try container.encode(value)
}
try container.encode(value)
}
}
extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource {
extension SingleResourceBody: Decodable, CodableResourceBody where Entity: OptionalCodablePrimaryResource {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
@@ -110,16 +108,16 @@ extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrim
}
extension ManyResourceBody {
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
public func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for value in values {
try container.encode(value)
}
}
for value in values {
try container.encode(value)
}
}
}
extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource {
extension ManyResourceBody: Decodable, CodableResourceBody where Entity: CodablePrimaryResource {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var valueAggregator = [Entity]()
@@ -133,13 +131,13 @@ extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResourc
// MARK: CustomStringConvertible
extension SingleResourceBody: CustomStringConvertible {
public var description: String {
return "PrimaryResourceBody(\(String(describing: value)))"
}
public var description: String {
return "PrimaryResourceBody(\(String(describing: value)))"
}
}
extension ManyResourceBody: CustomStringConvertible {
public var description: String {
return "PrimaryResourceBody(\(String(describing: values)))"
}
public var description: String {
return "PrimaryResourceBody(\(String(describing: values)))"
}
}
+5 -5
View File
@@ -6,9 +6,9 @@
//
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
}
@@ -0,0 +1,103 @@
//
// BasicJSONAPIError.swift
// JSONAPI
//
// Created by Mathew Polzin on 9/29/19.
//
/// Most of the JSON:API Spec defined Error fields.
public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType, CustomStringConvertible {
/// a unique identifier for this particular occurrence of the problem
public let id: IdType?
// public let links: Links? // we skip this for now to avoid adding complexity to using this basic type.
/// the HTTP status code applicable to this problem
public let status: String?
/// an application-specific error code
public let code: String?
/// a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization
public let title: String?
/// a human-readable explanation specific to this occurrence of the problem. Like `title`, this field’s value can be localized
public let detail: String?
/// an object containing references to the source of the error
public let source: Source?
// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type
public init(id: IdType? = nil,
status: String? = nil,
code: String? = nil,
title: String? = nil,
detail: String? = nil,
source: Source? = nil) {
self.id = id
self.status = status
self.code = code
self.title = title
self.detail = detail
self.source = source
}
public struct Source: Codable, Equatable {
/// a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].
public let pointer: String?
/// which URI query parameter caused the error
public let parameter: String?
public init(pointer: String? = nil,
parameter: String? = nil) {
self.pointer = pointer
self.parameter = parameter
}
}
public var definedFields: [String: String] {
let keysAndValues = [
id.map { ("id", String(describing: $0)) },
status.map { ("status", $0) },
code.map { ("code", $0) },
title.map { ("title", $0) },
detail.map { ("detail", $0) },
source.flatMap { $0.pointer.map { ("pointer", $0) } },
source.flatMap { $0.parameter.map { ("parameter", $0) } }
].compactMap { $0 }
return Dictionary(uniqueKeysWithValues: keysAndValues)
}
public var description: String {
return definedFields.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ")
}
}
/// `BasicJSONAPIError` optionally decodes many possible fields
/// specified by the JSON:API 1.0 Spec. It gives no type-guarantees of what
/// will be non-nil, but could provide good diagnostic information when
/// you do not know what error structure to expect.
///
/// ```
/// Fields:
/// - id
/// - status
/// - code
/// - title
/// - detail
/// - source
/// - pointer
/// - parameter
/// ```
///
/// The JSON:API Spec does not dictate the type of this particular Id field,
/// so you must specify whether to expect, for example, an `Int` or a `String`
/// in the id field.
///
/// Something like `AnyCodable` from *Flight-School* could be
/// a good option if you do not know what to expect. You could also use
/// `Either<Int, String>` (provided by the `Poly` package that is
/// already a dependency of `JSONAPI`).
///
/// - Important: The `definedFields` property will include fields
/// with non-nil values in a flattened way. There will be no `source` key
/// but there will be `pointer` and `parameter` keys (if those values
/// are non-nil).
public typealias BasicJSONAPIError<IdType: Codable & Equatable> = GenericJSONAPIError<BasicJSONAPIErrorPayload<IdType>>
@@ -0,0 +1,74 @@
//
// GenericJSONAPIError.swift
// JSONAPI
//
// Created by Mathew Polzin on 9/29/19.
//
/// `GenericJSONAPIError` can be used to specify whatever error
/// payload you expect to need to parse in responses and handle any
/// other payload structure as `.unknownError`.
public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError, CustomStringConvertible {
case unknownError
case error(ErrorPayload)
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = .error(try container.decode(ErrorPayload.self))
} catch {
self = .unknown
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .error(let payload):
try container.encode(payload)
case .unknownError:
try container.encode("unknown")
}
}
public static var unknown: Self {
return .unknownError
}
public var description: String {
switch self {
case .unknownError:
return "unknown error"
case .error(let payload):
return String(describing: payload)
}
}
}
public extension GenericJSONAPIError {
var payload: ErrorPayload? {
switch self {
case .unknownError:
return nil
case .error(let payload):
return payload
}
}
}
public protocol ErrorDictType {
var definedFields: [String: String] { get }
}
extension GenericJSONAPIError: ErrorDictType where ErrorPayload: ErrorDictType {
/// Get a dictionary of all defined fields and their values.
public var definedFields: [String: String] {
switch self {
case .unknownError:
return [:]
case .error(let basicPayload):
return basicPayload.definedFields
}
}
}
+33
View File
@@ -0,0 +1,33 @@
//
// JSONAPIError.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/10/18.
//
public protocol JSONAPIError: Swift.Error, Equatable, Codable {
static var unknown: Self { get }
}
/// `UnknownJSONAPIError` can actually be used in any sitaution
/// where you don't know what errors are possible _or_ you just don't
/// care what errors might show up. If you don't know how the error
/// will be structured but you would like to have access to more
/// information the server might be providing in the error payload,
/// use `BasicJSONAPIError` instead.
public enum UnknownJSONAPIError: JSONAPIError {
case unknownError
public init(from decoder: Decoder) throws {
self = .unknown
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("unknown")
}
public static var unknown: Self {
return .unknownError
}
}

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