mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-07-10 12:18:40 -07:00
Compare commits
84 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e28cd2606 | |||
| cf6fa39548 | |||
| a24f15dc4e | |||
| 87e9ee0606 | |||
| 5ed45078a1 | |||
| 34dc351810 | |||
| 3ff1b867ca | |||
| ae51be20d8 | |||
| c142e93fec | |||
| e99c6625f8 | |||
| e9048a4861 | |||
| ff06c36b36 | |||
| 8eb4c14bb3 | |||
| baee85fb1b | |||
| 44f21b4430 | |||
| f7f36c6e7a | |||
| af7eaeb684 | |||
| 0cc2368331 | |||
| 6c54845525 | |||
| 51b981c108 | |||
| b33c7f4ab5 | |||
| bf8245c7b8 | |||
| 7bd4021790 | |||
| 771641b82c | |||
| 198e5a9800 | |||
| 9db534dea0 | |||
| 2b409e464e | |||
| 7824bd8f68 | |||
| 89217f7187 | |||
| 86a9345fdd | |||
| 8f9ec11f27 | |||
| 9a9403bd98 | |||
| 453ce4b3a8 | |||
| 9a0bba8d07 | |||
| d5b4aa70c7 | |||
| 1df891ce06 | |||
| 6ba217f553 | |||
| e23ec090ed | |||
| ed23349351 | |||
| 6c8646a1b4 | |||
| 3a60ac5fe2 | |||
| 045e88f4d4 | |||
| fe1f4c6c19 | |||
| 83f7a7b60e | |||
| 61f2edb59a | |||
| 32d584099f | |||
| 9a07cf7066 | |||
| 0c7c7edcab | |||
| a596ecaecc | |||
| 265cffe8f0 | |||
| b98fb08353 | |||
| 61e00c2de5 | |||
| e0b53236bb | |||
| 2f3a619284 | |||
| 5aef44c3b3 | |||
| 99e97aadcc | |||
| 17e2ce3138 | |||
| 049dc1f7b5 | |||
| cf7b68c026 | |||
| 223c39cec0 | |||
| 5335f24e9b | |||
| 7ec185bfe8 | |||
| e0e6723222 | |||
| 4b4ebb4214 | |||
| 60cd515fd6 | |||
| 396f8453d1 | |||
| f5eb343bd4 | |||
| c4f96c0376 | |||
| 4d3597ef0e | |||
| fb72817de6 | |||
| 569cec05cf | |||
| 78b9b12592 | |||
| 9143281290 | |||
| badfa30668 | |||
| 0144a2ee80 | |||
| 880894d026 | |||
| 7b5b17918c | |||
| c996e7447c | |||
| 61fa56e84b | |||
| a6bf3e0ae0 | |||
| 620f6989d2 | |||
| 715db33138 | |||
| e99abd26b3 | |||
| c7b97567a9 |
+72
-71
@@ -4,65 +4,66 @@ import Poly
|
||||
|
||||
// MARK: - Preamble (setup)
|
||||
|
||||
// We make String a CreatableRawIdType. This is actually done in
|
||||
// Make String a CreatableRawIdType. This is actually done in
|
||||
// this Playground's Entities.swift file, so it is commented out here.
|
||||
/*
|
||||
var GlobalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
var globalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
globalStringId += 1
|
||||
return String(globalStringId)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Create a typealias because we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// Similarly, we create a typealias for unidentified entities. JSON:API
|
||||
// Similarly, create a typealias for unidentified entities. JSON:API
|
||||
// only allows unidentified entities (i.e. no "id" field) for client
|
||||
// requests that create new entities. In these situations, the server
|
||||
// is expected to assign the new entity a unique ID.
|
||||
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
// Create relationship typealiases because we do not expect
|
||||
// JSON:API Relationships for this particular API to have
|
||||
// Metadata or Links associated with them.
|
||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// We create a typealias for a Document given that we do not expect
|
||||
// Create a typealias for a Document because we do not expect
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
||||
// 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>
|
||||
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "authors" }
|
||||
public static var jsonType: String { return "authors" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let name: Attribute<String>
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
public static var jsonType: String { return "articles" }
|
||||
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
public struct Attributes: JSONAPI.Attributes {
|
||||
public let title: Attribute<String>
|
||||
public let abstract: Attribute<String>
|
||||
}
|
||||
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
public struct Relationships: JSONAPI.Relationships {
|
||||
public let author: ToOneRelationship<Author>
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
@@ -83,38 +84,38 @@ typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoInclud
|
||||
// that creates a document. Note that this document is the entirety
|
||||
// of a JSON:API response body.
|
||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// Let's pretend all of this is coming from a database:
|
||||
// Let's pretend all of this is coming from a database:
|
||||
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .a(document)
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .init(document)
|
||||
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
|
||||
return .b(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
return .init(document.including(includes))
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
@@ -124,8 +125,8 @@ encoder.outputFormatting = .prettyPrinted
|
||||
let responseBody = articleDocument(includeAuthor: true)
|
||||
let responseData = try! encoder.encode(responseBody)
|
||||
|
||||
// Next step would be encoding and setting as the HTTP body of a response.
|
||||
// we will just print it out instead:
|
||||
// Next step would be setting the HTTP body of a response.
|
||||
// We will just print it out instead:
|
||||
print("-----")
|
||||
print(String(data: responseData, encoding: .utf8)!)
|
||||
|
||||
@@ -139,31 +140,31 @@ print(String(data: otherResponseData, encoding: .utf8)!)
|
||||
// MARK: - Client Pseudo-example
|
||||
|
||||
enum NetworkError: Swift.Error {
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
}
|
||||
|
||||
// Skipping over all the API stuff, here's a chunk of code that will
|
||||
// decode a document. We will assume we have made a request for a
|
||||
// single article including the author.
|
||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
}
|
||||
|
||||
let response = try! docode(articleResponseData: responseData)
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
|
||||
import JSONAPI
|
||||
import Foundation
|
||||
|
||||
// MARK: - Resource Object
|
||||
|
||||
enum ThingWithPropertiesDescription: JSONAPI.ResourceObjectDescription {
|
||||
static let jsonType: String = "thing"
|
||||
|
||||
//
|
||||
// NOTE: `JSONAPI.SparsableAttributes` as opposed to `JSONAPI.Attributes`
|
||||
//
|
||||
struct Attributes: JSONAPI.SparsableAttributes {
|
||||
let stringThing: Attribute<String>
|
||||
let numberThing: Attribute<Double>
|
||||
let boolThing: Attribute<Bool?>
|
||||
|
||||
//
|
||||
// NOTE: Special implementation of `CodingKeys`
|
||||
//
|
||||
enum CodingKeys: String, JSONAPI.SparsableCodingKey {
|
||||
case stringThing
|
||||
case numberThing
|
||||
case boolThing
|
||||
}
|
||||
}
|
||||
|
||||
typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias ThingWithProperties = JSONAPI.ResourceObject<ThingWithPropertiesDescription, NoMetadata, NoLinks, String>
|
||||
|
||||
// MARK: - Document
|
||||
|
||||
//
|
||||
// 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>
|
||||
|
||||
//
|
||||
// NOTE: Using `JSONAPI.EncodablePrimaryResource` which means the `ResourceBody` will be `Encodable` but not `Decodable.
|
||||
//
|
||||
typealias SingleDocument<T: JSONAPI.EncodablePrimaryResource> = Document<SingleResourceBody<T>, NoIncludes>
|
||||
|
||||
// MARK: - Resource Initialization
|
||||
|
||||
let resource = ThingWithProperties(id: .init(rawValue: "1234"),
|
||||
attributes: .init(stringThing: .init(value: "hello world"),
|
||||
numberThing: .init(value: 10),
|
||||
boolThing: .init(value: nil)),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
//
|
||||
// NOTE: Creating a sparse resource that will only encode
|
||||
// the attribute named "stringThing"
|
||||
//
|
||||
let sparseResource = resource.sparse(with: [.stringThing])
|
||||
|
||||
// MARK: - Encoding
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let sparseResourceDoc = SingleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: sparseResource),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let data = try! encoder.encode(sparseResourceDoc)
|
||||
|
||||
print(String(data: data, encoding: .utf8)!)
|
||||
@@ -8,6 +8,7 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
|
||||
|
||||
********/
|
||||
|
||||
|
||||
// MARK: - Create a request or response body with one Dog in it
|
||||
let dogFromCode = try! Dog(name: "Buddy", owner: nil)
|
||||
|
||||
@@ -15,17 +16,20 @@ let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(res
|
||||
|
||||
let singleDogData = try! JSONEncoder().encode(singleDogDocument)
|
||||
|
||||
|
||||
// MARK: - Parse a request or response body with one Dog in it
|
||||
let dogResponse = try! JSONDecoder().decode(SingleDogDocument.self, from: singleDogData)
|
||||
let dogFromData = dogResponse.body.primaryResource?.value
|
||||
let dogOwner: Person.Identifier? = dogFromData.flatMap { $0 ~> \.owner }
|
||||
|
||||
// MARKL - Parse a request or response body with one Dog in it using an alternative model
|
||||
|
||||
// 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>
|
||||
let altDogResponse = try! JSONDecoder().decode(AltSingleDogDocument.self, from: singleDogData)
|
||||
let altDogFromData = altDogResponse.body.primaryResource?.value
|
||||
let altDogHuman: Person.Identifier? = altDogFromData.flatMap { $0 ~> \.human }
|
||||
|
||||
|
||||
// MARK: - Create a request or response with multiple people and dogs and houses included
|
||||
let personIds = [Person.Identifier(), Person.Identifier()]
|
||||
let dogs = try! [Dog(name: "Buddy", owner: personIds[0]), Dog(name: "Joy", owner: personIds[0]), Dog(name: "Travis", owner: personIds[1])]
|
||||
@@ -36,6 +40,7 @@ let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { Batch
|
||||
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(resourceObjects: people), includes: .init(values: includes), meta: .none, links: .none)
|
||||
let batchPeopleData = try! JSONEncoder().encode(batchPeopleDocument)
|
||||
|
||||
|
||||
// MARK: - Parse a request or response body with multiple people in it and dogs and houses included
|
||||
|
||||
let peopleResponse = try! JSONDecoder().decode(BatchPeopleDocument.self, from: batchPeopleData)
|
||||
@@ -47,17 +52,15 @@ print("-----")
|
||||
print(peopleResponse)
|
||||
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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
<page name='Usage'/>
|
||||
<page name='Full Client & Server Example'/>
|
||||
<page name='Full Document Verbose Generation'/>
|
||||
<page name='Sparse Fieldsets Example'/>
|
||||
</pages>
|
||||
</playground>
|
||||
+6
-6
@@ -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 = "0.31.0"
|
||||
spec.name = "MP-JSONAPI"
|
||||
spec.version = "2.1.0"
|
||||
spec.summary = "Swift Codable JSON API framework."
|
||||
|
||||
# This description is used to generate tags and improve search results.
|
||||
@@ -93,7 +93,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
# Not including the public_header_files will make all headers public.
|
||||
#
|
||||
|
||||
spec.source_files = "Sources", "Sources/**/*.{swift}"
|
||||
spec.source_files = "Sources/JSONAPI/**/*.{swift}"
|
||||
# spec.exclude_files = "Classes/Exclude"
|
||||
|
||||
# spec.public_header_files = "Classes/**/*.h"
|
||||
@@ -119,7 +119,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
# the lib prefix of their name.
|
||||
#
|
||||
|
||||
spec.framework = "Poly"
|
||||
# spec.frameworks = "SomeFramework", "AnotherFramework"
|
||||
|
||||
# spec.library = "iconv"
|
||||
@@ -132,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.1"
|
||||
|
||||
end
|
||||
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "38051821d7ef49e590e26e819a2fe447e50be9ff",
|
||||
"version": "2.0.1"
|
||||
"revision": "4a08517b24f8e9f6dd8c02ec7da316aac5c00e2e",
|
||||
"version": "2.1.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
+11
-11
@@ -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.1.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]
|
||||
)
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
# JSONAPI
|
||||
[](http://opensource.org/licenses/MIT) [](https://swift.org) [](https://app.bitrise.io/app/c8295b9589aa401e)
|
||||
[](http://opensource.org/licenses/MIT) [](https://swift.org) [](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: Although I find the type-safety of this framework appealing, the Swift compiler currently has enough trouble with it that it can become difficult to reason about errors produced by small typos. Similarly, auto-complete fails to provide reasonable suggestions much of the time. If you get the code right, everything compiles, otherwise it can suck to figure out what is wrong. This is mostly a concern when creating resource objects in-code (servers and test suites 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. :warning:
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 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)
|
||||
|
||||
### Serverside
|
||||
- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX)
|
||||
- [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ)
|
||||
|
||||
### 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
|
||||
<!-- TOC depthFrom:1 depthTo:6 withLinks:1 updateOnSave:1 orderedList:0 -->
|
||||
@@ -16,6 +31,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [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)
|
||||
@@ -42,7 +58,7 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [`Transformer`](#transformer)
|
||||
- [`Validator`](#validator)
|
||||
- [Computed `Attribute`](#computed-attribute)
|
||||
- [Copying `ResourceObjects`](#copying-resourceobjects)
|
||||
- [Copying/Mutating `ResourceObjects`](#copyingmutating-resourceobjects)
|
||||
- [`JSONAPI.Document`](#jsonapidocument)
|
||||
- [`ResourceBody`](#resourcebody)
|
||||
- [nullable `PrimaryResource`](#nullable-primaryresource)
|
||||
@@ -54,6 +70,9 @@ See the JSON API Spec here: https://jsonapi.org/format/
|
||||
- [`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)
|
||||
- [Custom Attribute or Relationship Key Mapping](#custom-attribute-or-relationship-key-mapping)
|
||||
- [Custom Attribute Encode/Decode](#custom-attribute-encodedecode)
|
||||
- [Meta-Attributes](#meta-attributes)
|
||||
@@ -83,14 +102,20 @@ 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.0.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
|
||||
@@ -106,52 +131,34 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
|
||||
### JSON:API
|
||||
#### Document
|
||||
- `data`
|
||||
- [x] Encoding/Decoding
|
||||
- `included`
|
||||
- [x] Encoding/Decoding
|
||||
- `errors`
|
||||
- [x] Encoding/Decoding
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- `jsonapi` (i.e. API Information)
|
||||
- [x] Encoding/Decoding
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] `data`
|
||||
- [x] `included`
|
||||
- [x] `errors`
|
||||
- [x] `meta`
|
||||
- [x] `jsonapi` (i.e. API Information)
|
||||
- [x] `links`
|
||||
|
||||
#### Resource Object
|
||||
- `id`
|
||||
- [x] Encoding/Decoding
|
||||
- `type`
|
||||
- [x] Encoding/Decoding
|
||||
- `attributes`
|
||||
- [x] Encoding/Decoding
|
||||
- `relationships`
|
||||
- [x] Encoding/Decoding
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] `id`
|
||||
- [x] `type`
|
||||
- [x] `attributes`
|
||||
- [x] `relationships`
|
||||
- [x] `links`
|
||||
- [x] `meta`
|
||||
|
||||
#### Relationship Object
|
||||
- `data`
|
||||
- [x] Encoding/Decoding
|
||||
- `links`
|
||||
- [x] Encoding/Decoding
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] `data`
|
||||
- [x] `links`
|
||||
- [x] `meta`
|
||||
|
||||
#### Links Object
|
||||
- `href`
|
||||
- [x] Encoding/Decoding
|
||||
- `meta`
|
||||
- [x] Encoding/Decoding
|
||||
- [x] `href`
|
||||
- [x] `meta`
|
||||
|
||||
### Misc
|
||||
- [x] Support transforms on `Attributes` values (e.g. to support different representations of `Date`)
|
||||
- [x] Support validation on `Attributes`.
|
||||
- [ ] Support sparse fieldsets. At the moment, not sure what this support will look like. A client can likely just define a new model to represent a sparse population of another model in a very specific use case. On the server side, it becomes much more appealing to be able to support arbitrary combinations of omitted fields.
|
||||
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
|
||||
- [x] Support sparse fieldsets (encoding only). A client can likely just define a new model to represent a sparse population of another model in a very specific use case for decoding purposes. On the server side, sparse fieldsets of Resource Objects can be encoded without creating one model for every possible sparse fieldset.
|
||||
|
||||
### Testing
|
||||
#### Resource Object Validator
|
||||
@@ -160,6 +167,8 @@ Note that Playground support for importing non-system Frameworks is still a bit
|
||||
- [x] Only allow `ToManyRelationship` and `ToOneRelationship` within `Relationships` struct.
|
||||
|
||||
### Potential Improvements
|
||||
These ideas could be implemented in future versions.
|
||||
|
||||
- [ ] (Maybe) Use `KeyPath` to specify `Includes` thus creating type safety around the relationship between a primary resource type and the types of included resources.
|
||||
- [ ] (Maybe) Replace `SingleResourceBody` and `ManyResourceBody` with support at the `Document` level to just interpret `PrimaryResource`, `PrimaryResource?`, or `[PrimaryResource]` as the same decoding/encoding strategies.
|
||||
- [ ] Support sideposting. JSONAPI spec might become opinionated in the future (https://github.com/json-api/json-api/pull/1197, https://github.com/json-api/json-api/issues/1215, https://github.com/json-api/json-api/issues/1216) but there is also an existing implementation to consider (https://jsonapi-suite.github.io/jsonapi_suite/ruby/writes/nested-writes). At this time, any sidepost implementation would be an awesome tertiary library to be used alongside the primary JSONAPI library. Maybe `JSONAPISideloading`.
|
||||
@@ -311,11 +320,18 @@ 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
|
||||
```
|
||||
|
||||
đź—’ `Attributes` can also be accessed via the older `subscript` operator as follows:
|
||||
```swift
|
||||
let favoriteColor: String = person[\.favoriteColor]
|
||||
```
|
||||
|
||||
In both cases you retain type-safety, although neither plays particularly nicely with code autocompletion. It is best practice to pick an attribute access syntax and stick with it. At some point in the future the syntax deemed less desirable may be deprecated.
|
||||
|
||||
#### `Transformer`
|
||||
|
||||
Sometimes you need to use a type that does not encode or decode itself in the way you need to represent it as a serialized JSON object. For example, the Swift `Foundation` type `Date` can encode/decode itself to `Double` out of the box, but you might want to represent dates as ISO 8601 compliant `String`s instead. The Foundation library `JSONDecoder` has a setting to make this adjustment, but for the sake of an example, you could create a `Transformer`.
|
||||
@@ -354,7 +370,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> {
|
||||
@@ -362,7 +378,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`:
|
||||
@@ -462,6 +478,8 @@ A `Meta` struct is totally open-ended. It is described by the **SPEC** as a plac
|
||||
|
||||
You can specify `NoMetadata` if the part of the document being described should not contain any `Meta`.
|
||||
|
||||
If you need to support metadata with structure that is not pre-determined, consider an "Any Codable" type such as that found at https://github.com/Flight-School/AnyCodable.
|
||||
|
||||
### `JSONAPI.Links`
|
||||
|
||||
A `Links` struct must contain only `Link` properties. Each `Link` property can either be a `URL` or a `URL` and some `Meta`. Each part of the document has some suggested common `Links` to include but generally any link can be included.
|
||||
@@ -485,6 +503,31 @@ extension String: CreatableRawIdType {
|
||||
}
|
||||
```
|
||||
|
||||
### Sparse Fieldsets
|
||||
Sparse Fieldsets are currently supported when encoding only. When decoding, Sparse Fieldsets become tricker to support under the current types this library uses and it is assumed that clients will request one or maybe two sparse fieldset combinations for any given model at most so it can simply define the `JSONAPI` models needed to decode those subsets of all possible fields. A server, on the other hand, likely needs to support arbitrary combinations of sparse fieldsets and this library provides a mechanism for encoding those sparse fieldsets without too much extra footwork.
|
||||
|
||||
You can use sparse fieldsets on the primary resources(s) _and_ includes of a `JSONAPI.Document`.
|
||||
|
||||
There is a sparse fieldsets example included with this repository as a Playground page.
|
||||
|
||||
#### Supporting Sparse Fieldset Encoding
|
||||
1. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must conform to `JSONAPI.SparsableAttributes` rather than `JSONAPI.Attributes`.
|
||||
2. The `JSONAPI` `ResourceObjectDescription`'s `Attributes` struct must contain a `CodingKeys` enum that conforms to `JSONAPI.SparsableCodingKey` instead of `Swift.CodingKey`.
|
||||
3. `typealiases` you may have created for `JSONAPI.Document` that allow you to decode Documents will not support the "encode-only" nature of sparse fieldsets. See the next section for `typealias` comparisons.
|
||||
4. To create a sparse fieldset from a `ResourceObject` just call its `sparse(with: fields)` method and pass an array of `Attributes.CodingKeys` values you would like included in the encoding.
|
||||
5. Initialize and encode a `Document` containing one or more sparse or full primary resource(s) and any number of sparse or full includes.
|
||||
|
||||
#### 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>
|
||||
```
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
### 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
|
||||
@@ -508,6 +551,8 @@ public enum ResourceObjectDescription2: JSONAPI.ResourceObjectDescription {
|
||||
case wholeOtherThing = "coolProperty"
|
||||
}
|
||||
}
|
||||
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
```
|
||||
|
||||
@@ -595,7 +640,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.
|
||||
@@ -616,7 +661,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -638,38 +683,39 @@ The following serves as a sort of pseudo-example. It skips server/client impleme
|
||||
|
||||
### Preamble (Setup shared by server and client)
|
||||
```swift
|
||||
// We make String a CreatableRawIdType.
|
||||
var GlobalStringId: Int = 0
|
||||
// Make String a CreatableRawIdType.
|
||||
var globalStringId: Int = 0
|
||||
extension String: CreatableRawIdType {
|
||||
public static func unique() -> String {
|
||||
GlobalStringId += 1
|
||||
return String(GlobalStringId)
|
||||
globalStringId += 1
|
||||
return String(globalStringId)
|
||||
}
|
||||
}
|
||||
|
||||
// We create a typealias given that we do not expect JSON:API Resource
|
||||
// Create a typealias because we do not expect JSON:API Resource
|
||||
// Objects for this particular API to have Metadata or Links associated
|
||||
// with them. We also expect them to have String Identifiers.
|
||||
typealias JSONResourceObject<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||
typealias JSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, String>
|
||||
|
||||
// Similarly, we create a typealias for unidentified resource objects. JSON:API
|
||||
// only allows unidentified resource objects (i.e. no "id" field) for client
|
||||
// requests that create new resource objects. In these situations, the server
|
||||
// is expected to assign the new resource object a unique ID.
|
||||
typealias UnidentifiedJSONResourceObject<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||
// Similarly, create a typealias for unidentified entities. JSON:API
|
||||
// only allows unidentified entities (i.e. no "id" field) for client
|
||||
// requests that create new entities. In these situations, the server
|
||||
// is expected to assign the new entity a unique ID.
|
||||
typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONAPI.ResourceObject<Description, NoMetadata, NoLinks, Unidentified>
|
||||
|
||||
// We create typealiases given that we do not expect JSON:API Relationships
|
||||
// for this particular API to have Metadata or Links associated
|
||||
// with them.
|
||||
typealias ToOneRelationship<ResourceObject: Identifiable> = JSONAPI.ToOneRelationship<ResourceObject, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<ResourceObject: Relatable> = JSONAPI.ToManyRelationship<ResourceObject, NoMetadata, NoLinks>
|
||||
// Create relationship typealiases because we do not expect
|
||||
// JSON:API Relationships for this particular API to have
|
||||
// Metadata or Links associated with them.
|
||||
typealias ToOneRelationship<Entity: Identifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
|
||||
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
|
||||
|
||||
// We create a typealias for a Document given that we do not expect
|
||||
// Create a typealias for a Document because we do not expect
|
||||
// JSON:API Documents for this particular API to have Metadata, Links,
|
||||
// useful Errors, or a JSON:API Object (i.e. APIDescription).
|
||||
// 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>
|
||||
|
||||
// MARK: ResourceObject Definitions
|
||||
// MARK: Entity Definitions
|
||||
|
||||
enum AuthorDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "authors" }
|
||||
@@ -681,7 +727,7 @@ enum AuthorDescription: ResourceObjectDescription {
|
||||
public typealias Relationships = NoRelationships
|
||||
}
|
||||
|
||||
typealias Author = JSONResourceObject<AuthorDescription>
|
||||
typealias Author = JSONEntity<AuthorDescription>
|
||||
|
||||
enum ArticleDescription: ResourceObjectDescription {
|
||||
public static var jsonType: String { return "articles" }
|
||||
@@ -696,7 +742,7 @@ enum ArticleDescription: ResourceObjectDescription {
|
||||
}
|
||||
}
|
||||
|
||||
typealias Article = JSONResourceObject<ArticleDescription>
|
||||
typealias Article = JSONEntity<ArticleDescription>
|
||||
|
||||
// MARK: Document Definitions
|
||||
|
||||
@@ -705,47 +751,48 @@ typealias Article = JSONResourceObject<ArticleDescription>
|
||||
typealias SingleArticleDocumentWithIncludes = Document<SingleResourceBody<Article>, Include1<Author>>
|
||||
|
||||
// ... and a typealias to represent a document containing one Article and
|
||||
// not including any related resource objects.
|
||||
// not including any related entities.
|
||||
typealias SingleArticleDocument = Document<SingleResourceBody<Article>, NoIncludes>
|
||||
```
|
||||
|
||||
### Server Pseudo-example
|
||||
```swift
|
||||
// Skipping over all the API and database stuff, here's a chunk of code
|
||||
// that creates a document. Note that this document is the entirety
|
||||
// of a JSON:API response body.
|
||||
func articleDocument(includeAuthor: Bool) -> Either<SingleArticleDocument, SingleArticleDocumentWithIncludes> {
|
||||
// Let's pretend all of this is coming from a database:
|
||||
// Let's pretend all of this is coming from a database:
|
||||
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
let authorId = Author.Identifier(rawValue: "1234")
|
||||
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let article = Article(id: .init(rawValue: "5678"),
|
||||
attributes: .init(title: .init(value: "JSON:API in Swift"),
|
||||
abstract: .init(value: "Not yet written")),
|
||||
relationships: .init(author: .init(id: authorId)),
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
let document = SingleArticleDocument(apiDescription: .none,
|
||||
body: .init(resourceObject: article),
|
||||
includes: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .a(document)
|
||||
switch includeAuthor {
|
||||
case false:
|
||||
return .init(document)
|
||||
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
case true:
|
||||
let author = Author(id: authorId,
|
||||
attributes: .init(name: .init(value: "Janice Bluff")),
|
||||
relationships: .none,
|
||||
meta: .none,
|
||||
links: .none)
|
||||
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
let includes: Includes<SingleArticleDocumentWithIncludes.Include> = .init(values: [.init(author)])
|
||||
|
||||
return .b(document.including(.init(values: [.init(author)])))
|
||||
}
|
||||
return .init(document.including(includes))
|
||||
}
|
||||
}
|
||||
|
||||
let encoder = JSONEncoder()
|
||||
@@ -755,8 +802,8 @@ encoder.outputFormatting = .prettyPrinted
|
||||
let responseBody = articleDocument(includeAuthor: true)
|
||||
let responseData = try! encoder.encode(responseBody)
|
||||
|
||||
// Next step would be encoding and setting as the HTTP body of a response.
|
||||
// we will just print it out instead:
|
||||
// Next step would be setting the HTTP body of a response.
|
||||
// We will just print it out instead:
|
||||
print("-----")
|
||||
print(String(data: responseData, encoding: .utf8)!)
|
||||
|
||||
@@ -771,31 +818,31 @@ print(String(data: otherResponseData, encoding: .utf8)!)
|
||||
### Client Pseudo-example
|
||||
```swift
|
||||
enum NetworkError: Swift.Error {
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
case serverError
|
||||
case quantityMismatch
|
||||
}
|
||||
|
||||
// Skipping over all the API stuff, here's a chunk of code that will
|
||||
// decode a document. We will assume we have made a request for a
|
||||
// single article including the author.
|
||||
func docode(articleResponseData: Data) throws -> (article: Article, author: Author) {
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
let articleDocument = try decoder.decode(SingleArticleDocumentWithIncludes.self, from: articleResponseData)
|
||||
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
switch articleDocument.body {
|
||||
case .data(let data):
|
||||
let authors = data.includes[Author.self]
|
||||
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
guard authors.count == 1 else {
|
||||
throw NetworkError.quantityMismatch
|
||||
}
|
||||
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
return (article: data.primary.value, author: authors[0])
|
||||
case .errors(let errors, meta: _, links: _):
|
||||
throw NetworkError.serverError
|
||||
}
|
||||
}
|
||||
|
||||
let response = try! docode(articleResponseData: responseData)
|
||||
@@ -807,10 +854,18 @@ print(response.author)
|
||||
```
|
||||
|
||||
# JSONAPI+Testing
|
||||
The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration. The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `ResourceObject` values into your unit tests. It also provides a `check()` function for each `ResourceObject` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system. You can see the `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository.
|
||||
The `JSONAPI` framework is packaged with a test library to help you test your `JSONAPI` integration.
|
||||
|
||||
The test library is called `JSONAPITesting`. It provides literal expressibility for `Attribute`, `ToOneRelationship`, and `Id` in many situations so that you can easily write test `ResourceObject` values into your unit tests. It also provides a `check()` function for each `ResourceObject` type that can be used to catch problems with your `JSONAPI` structures that are not caught by Swift's type system.
|
||||
|
||||
You can see the `JSONAPITesting` in action in the Playground included with the `JSONAPI` repository.
|
||||
|
||||
# JSONAPI+Arbitrary
|
||||
This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-Arbitrary
|
||||
This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-Arbitrary for more information.
|
||||
|
||||
# JSONAPI+OpenAPI
|
||||
This library has moved into its own Package. See https://github.com/mattpolzin/JSONAPI-OpenAPI
|
||||
The `JSONAPI+OpenAPI` library generates OpenAPI compliant JSON Schema for models built with the `JSONAPI` library. If your Swift code is your preferred source of truth for API information, this is an easy way to document the response schemas of your API.
|
||||
|
||||
`JSONAPI+OpenAPI` also has experimental support for generating `JSONAPI` Swift code from Open API documentation (this currently lives on the `feature/gen-swift` branch).
|
||||
|
||||
See https://github.com/mattpolzin/JSONAPI-OpenAPI for more information.
|
||||
|
||||
@@ -7,19 +7,26 @@
|
||||
|
||||
import Poly
|
||||
|
||||
public protocol JSONAPIDocument: Codable, Equatable {
|
||||
associatedtype PrimaryResourceBody: JSONAPI.ResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype APIDescription: APIDescriptionType
|
||||
associatedtype Error: JSONAPIError
|
||||
/// An `EncodableJSONAPIDocument` supports encoding but not decoding.
|
||||
/// It is actually more restrictive than `JSONAPIDocument` which supports both
|
||||
/// encoding and decoding.
|
||||
public protocol EncodableJSONAPIDocument: Equatable, Encodable {
|
||||
associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody
|
||||
associatedtype MetaType: JSONAPI.Meta
|
||||
associatedtype LinksType: JSONAPI.Links
|
||||
associatedtype IncludeType: JSONAPI.Include
|
||||
associatedtype APIDescription: APIDescriptionType
|
||||
associatedtype Error: JSONAPIError
|
||||
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
|
||||
|
||||
var body: Body { get }
|
||||
var body: Body { get }
|
||||
}
|
||||
|
||||
/// A `JSONAPIDocument` supports encoding and decoding of a JSON:API
|
||||
/// compliant Document.
|
||||
public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {}
|
||||
|
||||
/// A JSON API Document represents the entire body
|
||||
/// of a JSON API request or the entire body of
|
||||
/// a JSON API response.
|
||||
@@ -27,7 +34,7 @@ public protocol JSONAPIDocument: Codable, Equatable {
|
||||
/// API uses snake case, you will want to use
|
||||
/// a conversion such as the one offerred by the
|
||||
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: JSONAPIDocument {
|
||||
public struct Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: EncodableJSONAPIDocument {
|
||||
public typealias Include = IncludeType
|
||||
|
||||
/// The JSON API Spec calls this the JSON:API Object. It contains version
|
||||
@@ -46,7 +53,9 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
case data(Data)
|
||||
|
||||
public struct Data: Equatable {
|
||||
/// The document's Primary Resource object(s)
|
||||
public let primary: PrimaryResourceBody
|
||||
/// The document's included objects
|
||||
public let includes: Includes<Include>
|
||||
public let meta: MetaType
|
||||
public let links: LinksType
|
||||
@@ -59,6 +68,8 @@ public struct Document<PrimaryResourceBody: JSONAPI.ResourceBody, MetaType: JSON
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` if the document represents one or more errors. `false` if the
|
||||
/// document represents JSON:API data and/or metadata.
|
||||
public var isError: Bool {
|
||||
guard case .errors = self else { return false }
|
||||
return true
|
||||
@@ -196,7 +207,7 @@ extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, Link
|
||||
}
|
||||
*/
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
extension Document.Body.Data where PrimaryResourceBody: Appendable {
|
||||
public func merging(_ other: Document.Body.Data,
|
||||
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
|
||||
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
|
||||
@@ -207,7 +218,7 @@ extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody {
|
||||
}
|
||||
}
|
||||
|
||||
extension Document.Body.Data where PrimaryResourceBody: AppendableResourceBody, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
extension Document.Body.Data where PrimaryResourceBody: Appendable, MetaType == NoMetadata, LinksType == NoLinks {
|
||||
public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
|
||||
return merging(other,
|
||||
combiningMetaWith: { _, _ in .none },
|
||||
@@ -273,66 +284,6 @@ extension Document {
|
||||
case links
|
||||
case jsonapi
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
do {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
} catch {
|
||||
meta = nil
|
||||
}
|
||||
}
|
||||
|
||||
let links: LinksType?
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
do {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
} catch {
|
||||
links = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there are errors, there cannot be a body. Return errors and any metadata found.
|
||||
if let errors = errors {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
return
|
||||
}
|
||||
|
||||
let data: PrimaryResourceBody
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
|
||||
// TODO come back to this and make robust
|
||||
|
||||
guard let metaVal = meta else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedMetadata
|
||||
}
|
||||
guard let linksVal = links else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
}
|
||||
|
||||
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: RootCodingKeys.self)
|
||||
@@ -377,6 +328,68 @@ extension Document {
|
||||
}
|
||||
}
|
||||
|
||||
extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: RootCodingKeys.self)
|
||||
|
||||
if let noData = NoAPIDescription() as? APIDescription {
|
||||
apiDescription = noData
|
||||
} else {
|
||||
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
|
||||
}
|
||||
|
||||
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
|
||||
|
||||
let meta: MetaType?
|
||||
if let noMeta = NoMetadata() as? MetaType {
|
||||
meta = noMeta
|
||||
} else {
|
||||
do {
|
||||
meta = try container.decode(MetaType.self, forKey: .meta)
|
||||
} catch {
|
||||
meta = nil
|
||||
}
|
||||
}
|
||||
|
||||
let links: LinksType?
|
||||
if let noLinks = NoLinks() as? LinksType {
|
||||
links = noLinks
|
||||
} else {
|
||||
do {
|
||||
links = try container.decode(LinksType.self, forKey: .links)
|
||||
} catch {
|
||||
links = nil
|
||||
}
|
||||
}
|
||||
|
||||
// If there are errors, there cannot be a body. Return errors and any metadata found.
|
||||
if let errors = errors {
|
||||
body = .errors(errors, meta: meta, links: links)
|
||||
return
|
||||
}
|
||||
|
||||
let data: PrimaryResourceBody
|
||||
if let noData = NoResourceBody() as? PrimaryResourceBody {
|
||||
data = noData
|
||||
} else {
|
||||
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
|
||||
}
|
||||
|
||||
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
|
||||
|
||||
// TODO come back to this and make robust
|
||||
|
||||
guard let metaVal = meta else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedMetadata
|
||||
}
|
||||
guard let linksVal = links else {
|
||||
throw JSONAPIEncodingError.missingOrMalformedLinks
|
||||
}
|
||||
|
||||
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Document: CustomStringConvertible {
|
||||
|
||||
@@ -9,6 +9,9 @@ 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
|
||||
|
||||
|
||||
@@ -7,9 +7,19 @@
|
||||
|
||||
import Poly
|
||||
|
||||
public typealias Include = JSONPoly
|
||||
public typealias Include = EncodableJSONPoly
|
||||
|
||||
public struct Includes<I: Include>: Codable, Equatable {
|
||||
/// A structure holding zero or more included Resource Objects.
|
||||
/// The resources are accessed by their type using a subscript.
|
||||
///
|
||||
/// If you have
|
||||
///
|
||||
/// `let includes: Includes<Include2<Thing1, Thing2>> = ...`
|
||||
///
|
||||
/// then you can access all `Thing1` included resources with
|
||||
///
|
||||
/// `let includedThings = includes[Thing1.self]`
|
||||
public struct Includes<I: Include>: Encodable, Equatable {
|
||||
public static var none: Includes { return .init(values: []) }
|
||||
|
||||
let values: [I]
|
||||
@@ -17,23 +27,6 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
public init(values: [I]) {
|
||||
self.values = values
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
|
||||
// If not parsing includes, no need to loop over them.
|
||||
guard I.self != NoIncludes.self else {
|
||||
values = []
|
||||
return
|
||||
}
|
||||
|
||||
var valueAggregator = [I]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
}
|
||||
|
||||
values = valueAggregator
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
@@ -52,6 +45,25 @@ public struct Includes<I: Include>: Codable, Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes: Decodable where I: Decodable {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
|
||||
// If not parsing includes, no need to loop over them.
|
||||
guard I.self != NoIncludes.self else {
|
||||
values = []
|
||||
return
|
||||
}
|
||||
|
||||
var valueAggregator = [I]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(I.self))
|
||||
}
|
||||
|
||||
values = valueAggregator
|
||||
}
|
||||
}
|
||||
|
||||
extension Includes {
|
||||
public func appending(_ other: Includes<I>) -> Includes {
|
||||
return Includes(values: values + other.values)
|
||||
@@ -149,3 +161,11 @@ extension Includes where I: _Poly9 {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,29 +5,53 @@
|
||||
// Created by Mathew Polzin on 11/10/18.
|
||||
//
|
||||
|
||||
public protocol MaybePrimaryResource: Equatable, Codable {}
|
||||
/// This protocol allows for a `SingleResourceBody` to contain a `null`
|
||||
/// data object where `ManyResourceBody` cannot (because an empty
|
||||
/// array should be used for no results).
|
||||
public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {}
|
||||
|
||||
/// A PrimaryResource is a type that can be used in the body of a JSON API
|
||||
/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding.
|
||||
/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and
|
||||
/// decoding.
|
||||
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 {}
|
||||
|
||||
/// A `PrimaryResource` is a type that can be used in the body of a JSON API
|
||||
/// document as the primary resource.
|
||||
public protocol PrimaryResource: MaybePrimaryResource {}
|
||||
public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {}
|
||||
|
||||
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
|
||||
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
|
||||
|
||||
extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {}
|
||||
|
||||
/// 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.
|
||||
/// 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: Codable, Equatable {
|
||||
}
|
||||
public protocol ResourceBody: Decodable, EncodableResourceBody {}
|
||||
|
||||
public protocol AppendableResourceBody: ResourceBody {
|
||||
/// 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 func +<R: AppendableResourceBody>(_ left: R, right: R) -> R {
|
||||
public func +<R: Appendable>(_ left: R, right: R) -> R {
|
||||
return left.appending(right)
|
||||
}
|
||||
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
|
||||
/// 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 init(resourceObject: Entity) {
|
||||
@@ -35,7 +59,8 @@ public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: Resource
|
||||
}
|
||||
}
|
||||
|
||||
public struct ManyResourceBody<Entity: JSONAPI.PrimaryResource>: AppendableResourceBody {
|
||||
/// 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 init(resourceObjects: [Entity]) {
|
||||
@@ -55,41 +80,36 @@ public struct NoResourceBody: ResourceBody {
|
||||
|
||||
// MARK: Codable
|
||||
extension SingleResourceBody {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
value = try container.decode(Entity.self)
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.singleValueContainer()
|
||||
|
||||
if (value as Any?) == nil {
|
||||
try container.encodeNil()
|
||||
return
|
||||
}
|
||||
let anyNil: Any? = nil
|
||||
let nilValue = anyNil as? Entity
|
||||
guard value != nilValue else {
|
||||
try container.encodeNil()
|
||||
return
|
||||
}
|
||||
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
var valueAggregator = [Entity]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(Entity.self))
|
||||
}
|
||||
values = valueAggregator
|
||||
}
|
||||
extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
let anyNil: Any? = nil
|
||||
if container.decodeNil(),
|
||||
let val = anyNil as? Entity {
|
||||
value = val
|
||||
return
|
||||
}
|
||||
|
||||
value = try container.decode(Entity.self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.unkeyedContainer()
|
||||
|
||||
@@ -99,6 +119,17 @@ extension ManyResourceBody {
|
||||
}
|
||||
}
|
||||
|
||||
extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
var container = try decoder.unkeyedContainer()
|
||||
var valueAggregator = [Entity]()
|
||||
while !container.isAtEnd {
|
||||
valueAggregator.append(try container.decode(Entity.self))
|
||||
}
|
||||
values = valueAggregator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: CustomStringConvertible
|
||||
|
||||
extension SingleResourceBody: CustomStringConvertible {
|
||||
|
||||
@@ -19,6 +19,8 @@ public protocol Meta: Codable, Equatable {
|
||||
// nullable.
|
||||
extension Optional: Meta where Wrapped: Meta {}
|
||||
|
||||
/// Use this type when you want to specify not to encode or decode any metadata
|
||||
/// for a type.
|
||||
public struct NoMetadata: Meta, CustomStringConvertible {
|
||||
public static var none: NoMetadata { return NoMetadata() }
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ public protocol AttributeType: Codable {
|
||||
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
|
||||
public struct TransformedAttribute<RawValue: Codable, Transformer: JSONAPI.Transformer>: AttributeType where Transformer.From == RawValue {
|
||||
public let rawValue: RawValue
|
||||
|
||||
|
||||
public let value: Transformer.To
|
||||
|
||||
|
||||
public init(rawValue: RawValue) throws {
|
||||
self.rawValue = rawValue
|
||||
value = try Transformer.transform(rawValue)
|
||||
|
||||
@@ -28,6 +28,9 @@ public protocol CreatableRawIdType: RawIdType {
|
||||
|
||||
extension String: RawIdType {}
|
||||
|
||||
/// A type that can be used as the `MaybeRawId` for a `ResourceObject` that does not
|
||||
/// have an Id (most likely because it was created by a client and the server will be responsible
|
||||
/// for assigning it an Id).
|
||||
public struct Unidentified: MaybeRawId, CustomStringConvertible {
|
||||
public init() {}
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ import Poly
|
||||
/// disparate types under one roof for
|
||||
/// the purposes of JSON API compliant
|
||||
/// encoding or decoding.
|
||||
public typealias JSONPoly = Poly & PrimaryResource
|
||||
public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource
|
||||
|
||||
public typealias PolyWrapped = Codable & Equatable
|
||||
public typealias EncodablePolyWrapped = Encodable & Equatable
|
||||
public typealias PolyWrapped = EncodablePolyWrapped & Decodable
|
||||
|
||||
extension Poly0: PrimaryResource {
|
||||
public init(from decoder: Decoder) throws {
|
||||
@@ -30,28 +31,51 @@ extension Poly0: PrimaryResource {
|
||||
}
|
||||
|
||||
// MARK: - 1 type
|
||||
extension Poly1: PrimaryResource, MaybePrimaryResource where A: PolyWrapped {}
|
||||
extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {}
|
||||
|
||||
// MARK: - 2 types
|
||||
extension Poly2: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped {}
|
||||
extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {}
|
||||
|
||||
// MARK: - 3 types
|
||||
extension Poly3: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {}
|
||||
extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {}
|
||||
|
||||
// MARK: - 4 types
|
||||
extension Poly4: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {}
|
||||
extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly4: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {}
|
||||
|
||||
// MARK: - 5 types
|
||||
extension Poly5: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {}
|
||||
extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly5: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {}
|
||||
|
||||
// MARK: - 6 types
|
||||
extension Poly6: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {}
|
||||
extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly6: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {}
|
||||
|
||||
// MARK: - 7 types
|
||||
extension Poly7: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {}
|
||||
extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly7: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {}
|
||||
|
||||
// MARK: - 8 types
|
||||
extension Poly8: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {}
|
||||
extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly8: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {}
|
||||
|
||||
// MARK: - 9 types
|
||||
extension Poly9: PrimaryResource, MaybePrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}
|
||||
extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped {}
|
||||
|
||||
extension Poly9: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}
|
||||
|
||||
// MARK: - 10 types
|
||||
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 {}
|
||||
|
||||
@@ -190,10 +190,6 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ResourceLinkageCodingKeys.self)
|
||||
|
||||
if (id as Any?) == nil {
|
||||
try container.encodeNil(forKey: .data)
|
||||
}
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,16 @@ public protocol Relationships: Codable & Equatable {}
|
||||
/// properties of any types that are JSON encodable.
|
||||
public protocol Attributes: Codable & Equatable {}
|
||||
|
||||
/// CodingKeys must be `CodingKey` and `Equatable` in order
|
||||
/// to support Sparse Fieldsets.
|
||||
public typealias SparsableCodingKey = CodingKey & Equatable
|
||||
|
||||
/// Attributes containing publicly accessible and `Equatable`
|
||||
/// CodingKeys are required to support Sparse Fieldsets.
|
||||
public protocol SparsableAttributes: Attributes {
|
||||
associatedtype CodingKeys: SparsableCodingKey
|
||||
}
|
||||
|
||||
/// Can be used as `Relationships` Type for Entities that do not
|
||||
/// have any Relationships.
|
||||
public struct NoRelationships: Relationships {
|
||||
@@ -48,7 +58,7 @@ public protocol ResourceObjectProxyDescription: JSONTyped {
|
||||
associatedtype Relationships: Equatable
|
||||
}
|
||||
|
||||
/// An `ResourceObjectDescription` describes a JSON API
|
||||
/// A `ResourceObjectDescription` describes a JSON API
|
||||
/// Resource Object. The Resource Object
|
||||
/// itself is encoded and decoded as an
|
||||
/// `ResourceObject`, which gets specialized on an
|
||||
@@ -56,8 +66,9 @@ public protocol ResourceObjectProxyDescription: JSONTyped {
|
||||
public protocol ResourceObjectDescription: ResourceObjectProxyDescription where Attributes: JSONAPI.Attributes, Relationships: JSONAPI.Relationships {}
|
||||
|
||||
/// ResourceObjectProxy is a protocol that can be used to create
|
||||
/// types that _act_ like Entities but cannot be encoded
|
||||
/// or decoded as Entities.
|
||||
/// types that _act_ like ResourceObject but cannot be encoded
|
||||
/// or decoded as ResourceObjects.
|
||||
@dynamicMemberLookup
|
||||
public protocol ResourceObjectProxy: Equatable, JSONTyped {
|
||||
associatedtype Description: ResourceObjectProxyDescription
|
||||
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
|
||||
@@ -81,7 +92,7 @@ public protocol ResourceObjectProxy: Equatable, JSONTyped {
|
||||
}
|
||||
|
||||
extension ResourceObjectProxy {
|
||||
/// The JSON API compliant "type" of this `Entity`.
|
||||
/// The JSON API compliant "type" of this `ResourceObject`.
|
||||
public static var jsonType: String { return Description.jsonType }
|
||||
}
|
||||
|
||||
@@ -141,7 +152,7 @@ extension ResourceObject: CustomStringConvertible {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Convenience initializers
|
||||
// MARK: - Convenience initializers
|
||||
extension ResourceObject where EntityRawIdType: CreatableRawIdType {
|
||||
public init(attributes: Description.Attributes, relationships: Description.Relationships, meta: MetaType, links: LinksType) {
|
||||
self.id = ResourceObject.Id()
|
||||
@@ -392,17 +403,17 @@ extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, Ent
|
||||
}
|
||||
*/
|
||||
|
||||
// MARK: Pointer for Relationships use.
|
||||
// MARK: - Pointer for Relationships use
|
||||
public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
|
||||
|
||||
/// An ResourceObject.Pointer is a `ToOneRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference an ResourceObject so that
|
||||
/// other Entities' Relationships can be built up from it.
|
||||
/// A `ResourceObject.Pointer` is a `ToOneRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a `ResourceObject` so that
|
||||
/// other ResourceObjects' Relationships can be built up from it.
|
||||
typealias Pointer = ToOneRelationship<ResourceObject, NoMetadata, NoLinks>
|
||||
|
||||
/// ResourceObject.Pointers is a `ToManyRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a bunch of Entities so
|
||||
/// that other Entities' Relationships can be built up from them.
|
||||
/// `ResourceObject.Pointers` is a `ToManyRelationship` with no metadata or links.
|
||||
/// This is just a convenient way to reference a bunch of ResourceObjects so
|
||||
/// that other ResourceObjects' Relationships can be built up from them.
|
||||
typealias Pointers = ToManyRelationship<ResourceObject, NoMetadata, NoLinks>
|
||||
|
||||
/// Get a pointer to this resource object that can be used as a
|
||||
@@ -411,57 +422,83 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
|
||||
return Pointer(resourceObject: self)
|
||||
}
|
||||
|
||||
/// Get a pointer (i.e. `ToOneRelationship`) to this resource
|
||||
/// object with the given metadata and links attached.
|
||||
func pointer<MType: JSONAPI.Meta, LType: JSONAPI.Links>(withMeta meta: MType, links: LType) -> ToOneRelationship<ResourceObject, MType, LType> {
|
||||
return ToOneRelationship(resourceObject: self, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Identifying Unidentified Entities
|
||||
// MARK: - Identifying Unidentified Entities
|
||||
public extension ResourceObject where EntityRawIdType == Unidentified {
|
||||
/// Create a new ResourceObject from this one with a newly created
|
||||
/// Create a new `ResourceObject` from this one with a newly created
|
||||
/// unique Id of the given type.
|
||||
func identified<RawIdType: CreatableRawIdType>(byType: RawIdType.Type) -> ResourceObject<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
|
||||
/// Create a new ResourceObject from this one with the given Id.
|
||||
/// Create a new `ResourceObject` from this one with the given Id.
|
||||
func identified<RawIdType: JSONAPI.RawIdType>(by id: RawIdType) -> ResourceObject<Description, MetaType, LinksType, RawIdType> {
|
||||
return .init(id: ResourceObject<Description, MetaType, LinksType, RawIdType>.Identifier(rawValue: id), attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
public extension ResourceObject where EntityRawIdType: CreatableRawIdType {
|
||||
/// Create a copy of this ResourceObject with a new unique Id.
|
||||
/// Create a copy of this `ResourceObject` with a new unique Id.
|
||||
func withNewIdentifier() -> ResourceObject {
|
||||
return ResourceObject(attributes: attributes, relationships: relationships, meta: meta, links: links)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Attribute Access
|
||||
// MARK: - Attribute Access
|
||||
public extension ResourceObjectProxy {
|
||||
// MARK: Keypath Subscript Lookup
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `resourceObject[\.propertyName]` instead
|
||||
/// of `resourceObject.attributes.propertyName`.
|
||||
/// of `resourceObject.attributes.propertyName.value`.
|
||||
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T>) -> T.ValueType {
|
||||
return attributes[keyPath: path].value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `resourceObject[\.propertyName]` instead
|
||||
/// of `resourceObject.attributes.propertyName`.
|
||||
/// of `resourceObject.attributes.propertyName.value`.
|
||||
subscript<T: AttributeType>(_ path: KeyPath<Description.Attributes, T?>) -> T.ValueType? {
|
||||
return attributes[keyPath: path]?.value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `resourceObject[\.propertyName]` instead
|
||||
/// of `resourceObject.attributes.propertyName`.
|
||||
/// of `resourceObject.attributes.propertyName.value`.
|
||||
subscript<T: AttributeType, U>(_ path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
|
||||
// Implementation Note: Handles Transform that returns optional
|
||||
// type.
|
||||
return attributes[keyPath: path].flatMap { $0.value }
|
||||
}
|
||||
|
||||
// MARK: Dynaminc Member Keypath Lookup
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `resourceObject[\.propertyName]` instead
|
||||
/// of `resourceObject.attributes.propertyName.value`.
|
||||
subscript<T: AttributeType>(dynamicMember path: KeyPath<Description.Attributes, T>) -> T.ValueType {
|
||||
return attributes[keyPath: path].value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `resourceObject[\.propertyName]` instead
|
||||
/// of `resourceObject.attributes.propertyName.value`.
|
||||
subscript<T: AttributeType>(dynamicMember path: KeyPath<Description.Attributes, T?>) -> T.ValueType? {
|
||||
return attributes[keyPath: path]?.value
|
||||
}
|
||||
|
||||
/// Access the attribute at the given keypath. This just
|
||||
/// allows you to write `resourceObject[\.propertyName]` instead
|
||||
/// of `resourceObject.attributes.propertyName.value`.
|
||||
subscript<T: AttributeType, U>(dynamicMember path: KeyPath<Description.Attributes, T?>) -> U? where T.ValueType == U? {
|
||||
return attributes[keyPath: path].flatMap { $0.value }
|
||||
}
|
||||
|
||||
// MARK: Direct Keypath Subscript Lookup
|
||||
/// Access the storage of the attribute at the given keypath. This just
|
||||
/// allows you to write `resourceObject[direct: \.propertyName]` instead
|
||||
/// of `resourceObject.attributes.propertyName`.
|
||||
@@ -475,16 +512,24 @@ public extension ResourceObjectProxy {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Meta-Attribute Access
|
||||
// MARK: - Meta-Attribute Access
|
||||
public extension ResourceObjectProxy {
|
||||
// MARK: Keypath Subscript Lookup
|
||||
/// Access an attribute requiring a transformation on the RawValue _and_
|
||||
/// a secondary transformation on this entity (self).
|
||||
subscript<T>(_ path: KeyPath<Description.Attributes, (Self) -> T>) -> T {
|
||||
return attributes[keyPath: path](self)
|
||||
}
|
||||
|
||||
// MARK: Dynamic Member Keypath Lookup
|
||||
/// Access an attribute requiring a transformation on the RawValue _and_
|
||||
/// a secondary transformation on this entity (self).
|
||||
subscript<T>(dynamicMember path: KeyPath<Description.Attributes, (Self) -> T>) -> T {
|
||||
return attributes[keyPath: path](self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Relationship Access
|
||||
// MARK: - Relationship Access
|
||||
public extension ResourceObjectProxy {
|
||||
/// Access to an Id of a `ToOneRelationship`.
|
||||
/// This allows you to write `resourceObject ~> \.other` instead
|
||||
@@ -526,7 +571,7 @@ public extension ResourceObjectProxy {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Meta-Relationship Access
|
||||
// MARK: - Meta-Relationship Access
|
||||
public extension ResourceObjectProxy {
|
||||
/// Access to an Id of a `ToOneRelationship`.
|
||||
/// This allows you to write `resourceObject ~> \.other` instead
|
||||
@@ -547,62 +592,63 @@ infix operator ~>
|
||||
|
||||
// MARK: - Codable
|
||||
private enum ResourceObjectCodingKeys: String, CodingKey {
|
||||
case type = "type"
|
||||
case id = "id"
|
||||
case attributes = "attributes"
|
||||
case relationships = "relationships"
|
||||
case meta = "meta"
|
||||
case links = "links"
|
||||
case type = "type"
|
||||
case id = "id"
|
||||
case attributes = "attributes"
|
||||
case relationships = "relationships"
|
||||
case meta = "meta"
|
||||
case links = "links"
|
||||
}
|
||||
|
||||
public extension ResourceObject {
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
try container.encode(ResourceObject.jsonType, forKey: .type)
|
||||
|
||||
if EntityRawIdType.self != Unidentified.self {
|
||||
try container.encode(id, forKey: .id)
|
||||
}
|
||||
|
||||
if Description.Attributes.self != NoAttributes.self {
|
||||
try container.encode(attributes, forKey: .attributes)
|
||||
}
|
||||
|
||||
if Description.Relationships.self != NoRelationships.self {
|
||||
try container.encode(relationships, forKey: .relationships)
|
||||
}
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
try container.encode(ResourceObject.jsonType, forKey: .type)
|
||||
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
}
|
||||
if EntityRawIdType.self != Unidentified.self {
|
||||
try container.encode(id, forKey: .id)
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
|
||||
guard ResourceObject.jsonType == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type)
|
||||
}
|
||||
if Description.Attributes.self != NoAttributes.self {
|
||||
let nestedEncoder = container.superEncoder(forKey: .attributes)
|
||||
try attributes.encode(to: nestedEncoder)
|
||||
}
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id)
|
||||
if Description.Relationships.self != NoRelationships.self {
|
||||
try container.encode(relationships, forKey: .relationships)
|
||||
}
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ??
|
||||
container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
if MetaType.self != NoMetadata.self {
|
||||
try container.encode(meta, forKey: .meta)
|
||||
}
|
||||
|
||||
relationships = try (NoRelationships() as? Description.Relationships)
|
||||
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
|
||||
?? Description.Relationships(from: EmptyObjectDecoder())
|
||||
if LinksType.self != NoLinks.self {
|
||||
try container.encode(links, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
|
||||
|
||||
links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
let type = try container.decode(String.self, forKey: .type)
|
||||
|
||||
guard ResourceObject.jsonType == type else {
|
||||
throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type)
|
||||
}
|
||||
|
||||
let maybeUnidentified = Unidentified() as? EntityRawIdType
|
||||
id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id)
|
||||
|
||||
attributes = try (NoAttributes() as? Description.Attributes) ??
|
||||
container.decode(Description.Attributes.self, forKey: .attributes)
|
||||
|
||||
relationships = try (NoRelationships() as? Description.Relationships)
|
||||
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
|
||||
?? Description.Relationships(from: EmptyObjectDecoder())
|
||||
|
||||
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
|
||||
|
||||
links = try (NoLinks() as? LinksType) ?? container.decode(LinksType.self, forKey: .links)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,22 +7,26 @@
|
||||
|
||||
/// A Transformer simply defines a static function that transforms a value.
|
||||
public protocol Transformer {
|
||||
associatedtype From
|
||||
associatedtype To
|
||||
associatedtype From
|
||||
associatedtype To
|
||||
|
||||
static func transform(_ value: From) throws -> To
|
||||
/// Turn value of type `From` into a value of type `To` or
|
||||
/// throw an error on failure.
|
||||
static func transform(_ value: From) throws -> To
|
||||
}
|
||||
|
||||
/// ReversibleTransformers define a function that reverses the transform
|
||||
/// operation.
|
||||
public protocol ReversibleTransformer: Transformer {
|
||||
static func reverse(_ value: To) throws -> From
|
||||
/// Turn a value of type `To` into a value of type `From` or
|
||||
/// throw an error on failure.
|
||||
static func reverse(_ value: To) throws -> From
|
||||
}
|
||||
|
||||
/// The IdentityTransformer does not perform any transformation on a value.
|
||||
public enum IdentityTransformer<T>: ReversibleTransformer {
|
||||
public static func transform(_ value: T) throws -> T { return value }
|
||||
public static func reverse(_ value: T) throws -> T { return value }
|
||||
public static func transform(_ value: T) throws -> T { return value }
|
||||
public static func reverse(_ value: T) throws -> T { return value }
|
||||
}
|
||||
|
||||
// MARK: - Validator
|
||||
@@ -37,13 +41,16 @@ public protocol Validator: ReversibleTransformer where From == To {
|
||||
}
|
||||
|
||||
extension Validator {
|
||||
public static func reverse(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
public static func reverse(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
|
||||
public static func validate(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
/// Validate the given value and then return it if valid.
|
||||
/// throws an erro if invalid.
|
||||
/// - returns: The same value passed in, if it was valid.
|
||||
public static func validate(_ value: To) throws -> To {
|
||||
let _ = try transform(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user