Compare commits

..

79 Commits

Author SHA1 Message Date
Mathew Polzin 7e28cd2606 Merge pull request #35 from mattpolzin/add-include-10
Add include10 Type
2019-09-16 17:13:49 -07:00
Mathew Polzin cf6fa39548 bump Podspec version 2019-09-16 09:58:57 -07:00
Mathew Polzin a24f15dc4e regenerate linuxmain 2019-09-16 09:38:36 -07:00
Mathew Polzin 87e9ee0606 remove tests that are duplicates of those in the Poly package. I just never deleted this file when the Poly stuff moved into its own package. Add support for Include10. 2019-09-16 09:37:34 -07:00
Mathew Polzin 5ed45078a1 Update README.md
Fix incorrect Swift version requirement and suggested SPM dependency version.
2019-09-14 16:44:46 -07:00
Mathew Polzin 34dc351810 Merge pull request #34 from mattpolzin/swift-5.1
Swift 5.1 tools dependency, access to attributes via dynamicMemberLookup
2019-09-14 16:28:24 -07:00
Mathew Polzin 3ff1b867ca Bump spec version to account for breaking changes with configs and swift tools version 2019-09-14 15:53:48 -07:00
Mathew Polzin ae51be20d8 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-09-14 15:51:05 -07:00
Mathew Polzin c142e93fec Merge pull request #33 from chrisballinger/patch-1
Change Pod name to avoid conflict. Resolves #32
2019-09-14 15:49:37 -07:00
Chris Ballinger e99c6625f8 Update README.md 2019-09-14 10:07:53 -07:00
Chris Ballinger e9048a4861 Change Pod name to avoid conflict. Resolves #32 2019-09-13 15:07:37 -07:00
Mathew Polzin ff06c36b36 uncomment out language version declaration in package manifest, 'fix' indentation. 2019-09-05 22:22:55 -07:00
Mathew Polzin 8eb4c14bb3 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-09-05 21:39:27 -07:00
Mathew Polzin baee85fb1b Update README.md 2019-09-02 16:07:10 -07:00
Mathew Polzin 44f21b4430 Update README.md 2019-09-02 16:04:06 -07:00
Mathew Polzin f7f36c6e7a Update README.md 2019-09-02 15:59:24 -07:00
Mathew Polzin af7eaeb684 Update README.md 2019-09-02 15:10:43 -07:00
Mathew Polzin 0cc2368331 Update README.md
Add instructions for adding as Swift Package Manager dependency.
2019-09-02 15:05:23 -07:00
Mathew Polzin 6c54845525 Update README.md 2019-09-02 15:00:07 -07:00
Mathew Polzin 51b981c108 Update README.md 2019-09-02 13:25:57 -07:00
Mathew Polzin b33c7f4ab5 Update README.md 2019-08-29 09:33:03 -07:00
Mathew Polzin bf8245c7b8 Update README.md 2019-08-29 09:30:59 -07:00
Mathew Polzin 7bd4021790 Update README.md 2019-08-28 20:21:50 -07:00
Mathew Polzin 771641b82c Update README.md 2019-08-28 18:55:42 -07:00
Mathew Polzin 198e5a9800 Update README.md 2019-08-28 18:55:11 -07:00
Mathew Polzin 9db534dea0 Update README.md
Add basic example Google Colab.
2019-08-27 00:48:11 -07:00
Mathew Polzin 2b409e464e merge w/ master 2019-08-23 17:53:33 -07:00
Mathew Polzin 7824bd8f68 Merge pull request #30 from mattpolzin/release/1.0
Release/1.0
2019-08-14 17:39:42 -07:00
Mathew Polzin 89217f7187 bump podspec version 2019-08-14 17:31:43 -07:00
Mathew Polzin 86a9345fdd Adding some documentation and making SparseFieldEncoder internal because it does not need to be public. 2019-08-14 09:22:06 -07:00
Mathew Polzin 8f9ec11f27 Housekeeping in the README 2019-08-14 08:57:57 -07:00
Mathew Polzin 9a9403bd98 Merge pull request #28 from mattpolzin/sparse-encoding
Sparse encoding
2019-08-12 07:35:49 -07:00
Mathew Polzin 453ce4b3a8 whitespace changes -- trying to switch from one whitespace standard to another in cohesive chunks to fit Xcode 11 default. 2019-08-06 09:19:06 -07:00
Mathew Polzin 9a0bba8d07 Rename AppendableResourceBody to Appendable. It is used in conjunction with ResourceBody but does not conform to it anymore. 2019-08-06 09:12:02 -07:00
Mathew Polzin d5b4aa70c7 Update linuxmain 2019-08-06 09:04:15 -07:00
Mathew Polzin 1df891ce06 Add test that uses JSONAPIDocument protocol in generic context 2019-08-06 08:55:49 -07:00
Mathew Polzin 6ba217f553 More sparse field encoder tests 2019-08-06 08:27:22 -07:00
Mathew Polzin e23ec090ed Starting to add SparseFieldEncoder tests 2019-08-05 19:44:09 -07:00
Mathew Polzin ed23349351 Update README 2019-08-05 18:45:13 -07:00
Mathew Polzin 6c8646a1b4 whitespace 2019-08-05 18:09:29 -07:00
Mathew Polzin 3a60ac5fe2 Add playground page showing off sparse fieldset encoding 2019-08-05 17:59:38 -07:00
Mathew Polzin 045e88f4d4 More code documentation 2019-08-05 16:28:02 -07:00
Mathew Polzin fe1f4c6c19 A bit of code documentation 2019-08-05 16:15:56 -07:00
Mathew Polzin 83f7a7b60e Add sparse document tests 2019-08-05 16:02:40 -07:00
Mathew Polzin 61f2edb59a Add tests for sparse fieldset includes 2019-08-05 15:21:59 -07:00
Mathew Polzin 32d584099f Update linuxmain 2019-08-05 14:51:48 -07:00
Mathew Polzin 9a07cf7066 Add test class placeholder 2019-08-05 14:51:18 -07:00
Mathew Polzin 0c7c7edcab Add sparse fieldset resource body tests 2019-08-05 14:48:08 -07:00
Mathew Polzin a596ecaecc A stab at separating out decoding enough to make it possible to use encode-only sparse fieldsets with JSONDocument 2019-08-05 09:23:44 -07:00
Mathew Polzin 265cffe8f0 Rename MaybePrimaryResource to OptionalPrimaryResource because I use 'Maybe' elsewhere to indicate a type-level distinction whereas this is a value-level distinction that really is just 'Optional' at play. 2019-08-05 08:17:38 -07:00
Mathew Polzin b98fb08353 Adding a bit of code doc 2019-08-05 08:13:18 -07:00
Mathew Polzin 61e00c2de5 small rename 2019-08-05 08:01:42 -07:00
Mathew Polzin e0b53236bb Add sparse fields method to ResourceObject and test it. 2019-08-04 23:03:56 -07:00
Mathew Polzin 2f3a619284 Spacing difference 2019-08-04 18:47:26 -07:00
Mathew Polzin 5aef44c3b3 Add Sparse Fieldset support for Attributes 2019-08-04 18:44:28 -07:00
Mathew Polzin 99e97aadcc Merge branch 'master' into swift-5.1 2019-08-04 10:34:32 -07:00
Mathew Polzin 17e2ce3138 Merge pull request #27 from mattpolzin/round-off-empty-decoder-tests
Cover the EmptyObjectDecoder unkeyed container with tests
2019-08-04 10:30:57 -07:00
Mathew Polzin 049dc1f7b5 Cover the EmptyObjectDecoder unkeyed container with tests 2019-08-04 10:28:48 -07:00
Mathew Polzin cf7b68c026 Merge w/ master 2019-08-04 09:02:04 -07:00
Mathew Polzin 223c39cec0 Merge pull request #26 from mattpolzin/testcov1
Test coverage PR
2019-08-02 09:34:36 -07:00
Mathew Polzin 5335f24e9b Add test coverage for EmptyObjectDecoder's keyed container 2019-08-02 09:30:30 -07:00
Mathew Polzin 7ec185bfe8 Add a couple tests and a bit of code doc. 2019-08-02 08:45:38 -07:00
Mathew Polzin e0e6723222 Make Xcode Beta happy by explicitly creating optionals where they used to be implied. 2019-08-02 07:27:17 -07:00
Mathew Polzin 4b4ebb4214 merge w/ master 2019-07-30 17:52:42 -07:00
Mathew Polzin 60cd515fd6 just filling in a couple small holes in test coverage 2019-07-30 17:44:06 -07:00
Mathew Polzin 396f8453d1 swap out broken nil checks (not harming anything, but not working as evidenced by not getting hit by any test cases) 2019-07-29 21:00:44 -07:00
Mathew Polzin f5eb343bd4 Complete test coverage of APIDescription 2019-07-29 20:41:22 -07:00
Mathew Polzin c4f96c0376 Updated example in playground/documentation after realizing I was not using a variable I had created. 2019-07-28 13:10:42 -07:00
Mathew Polzin 4d3597ef0e Update README.md 2019-07-27 21:30:14 -07:00
Mathew Polzin fb72817de6 bump swift version badge to swift 5.1 2019-07-25 09:36:35 -07:00
Mathew Polzin 569cec05cf ditch language version argument in package file that is not accepted by swift test. update linuxmain. 2019-07-25 09:27:56 -07:00
Mathew Polzin 78b9b12592 Update README 2019-07-25 09:23:01 -07:00
Mathew Polzin 9143281290 Introduce dynamic member keypath lookup as alternative to existing subscript keypath lookup for attributes. also did a bit of tidying up. 2019-07-25 09:12:17 -07:00
Mathew Polzin badfa30668 Merge remote-tracking branch 'origin/master' into swift-5.1 2019-07-25 07:49:22 -07:00
Mathew Polzin 0144a2ee80 Remove prior experimentation with property wrappers -- feature was not baked when that experimentation was done. 2019-07-25 07:49:17 -07:00
Mathew Polzin 880894d026 small fix for example in README 2019-07-25 07:38:21 -07:00
Mathew Polzin 7b5b17918c Update linuxmain, slightly update wording and indentation on example at bottom of README. 2019-07-24 19:43:11 -07:00
Mathew Polzin c996e7447c Update README.md 2019-07-23 21:11:59 -07:00
Mathew Polzin c7b97567a9 update swift tools version in package file, add some property wrappers, add some tests for wrappers. its all broken but worth holding onto for now. 2019-06-20 22:35:55 -07:00
41 changed files with 3031 additions and 1369 deletions
@@ -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
+3 -3
View File
@@ -15,11 +15,11 @@ Please enjoy these examples, but allow me the forced casting and the lack of err
********/
// MARK: - String as CreatableRawIdType
var GlobalStringId: Int = 0
var globalStringId: Int = 0
extension String: CreatableRawIdType {
public static func unique() -> String {
GlobalStringId += 1
return String(GlobalStringId)
globalStringId += 1
return String(globalStringId)
}
}
+1
View File
@@ -5,5 +5,6 @@
<page name='Usage'/>
<page name='Full Client &amp; Server Example'/>
<page name='Full Document Verbose Generation'/>
<page name='Sparse Fieldsets Example'/>
</pages>
</playground>
+5 -4
View File
@@ -15,8 +15,8 @@ Pod::Spec.new do |spec|
# summary should be tweet-length, and the description more in depth.
#
spec.name = "JSONAPI"
spec.version = "0.31.1"
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.
@@ -131,10 +131,11 @@ See the JSON API Spec here: https://jsonapi.org/format/
# where they will only apply to your library. If you depend on other Podspecs
# you can include multiple dependencies to ensure it works.
spec.swift_version = "5.0"
spec.swift_version = "5.1"
spec.module_name = "JSONAPI"
# spec.requires_arc = true
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
spec.dependency "Poly", "~> 2.0"
spec.dependency "Poly", "~> 2.1"
end
+2 -2
View File
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": {
"branch": null,
"revision": "38051821d7ef49e590e26e819a2fe447e50be9ff",
"version": "2.0.1"
"revision": "4a08517b24f8e9f6dd8c02ec7da316aac5c00e2e",
"version": "2.1.0"
}
}
]
+11 -11
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.0
// swift-tools-version:5.1
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@@ -6,27 +6,27 @@ import PackageDescription
let package = Package(
name: "JSONAPI",
platforms: [
.macOS(.v10_10),
.iOS(.v10)
.macOS(.v10_10),
.iOS(.v10)
],
products: [
.library(
name: "JSONAPI",
targets: ["JSONAPI"]),
.library(
name: "JSONAPITesting",
targets: ["JSONAPITesting"])
.library(
name: "JSONAPITesting",
targets: ["JSONAPITesting"])
],
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.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]
)
+169 -114
View File
@@ -1,11 +1,26 @@
# JSONAPI
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.0](http://img.shields.io/badge/Swift-5.0-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
[![MIT license](http://img.shields.io/badge/license-MIT-lightgrey.svg)](http://opensource.org/licenses/MIT) [![Swift 5.1](http://img.shields.io/badge/Swift-5.1-blue.svg)](https://swift.org) [![Build Status](https://app.bitrise.io/app/c8295b9589aa401e/status.svg?token=vzcyqWD5bQ4xqQfZsaVzNw&branch=master)](https://app.bitrise.io/app/c8295b9589aa401e)
A Swift package for encoding to- and decoding from **JSON API** compliant requests and responses.
See the JSON API Spec here: https://jsonapi.org/format/
:warning: 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.
+85 -72
View File
@@ -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 {
+3
View File
@@ -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
+39 -19
View File
@@ -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 }
}
}
+67 -36
View File
@@ -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 {
+2
View File
@@ -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() }
+2 -2
View File
@@ -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)
+3
View File
@@ -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)
}
+116 -70
View File
@@ -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)
}
}
+21 -14
View File
@@ -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