Compare commits

...

56 Commits

Author SHA1 Message Date
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 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 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 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 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 61fa56e84b bump podspec version after two cocoapods fixes. 2019-07-16 22:19:50 -07:00
Mathew Polzin a6bf3e0ae0 Merge pull request #22 from chrisballinger/fix-podspec
Remove Poly from podspec frameworks
2019-07-16 22:15:04 -07:00
Mathew Polzin 620f6989d2 Merge pull request #23 from chrisballinger/fix-issue-20
Do not include testing source files in podspec
2019-07-16 22:01:29 -07:00
Chris Ballinger 715db33138 Do not include testing source files in podspec 2019-07-16 17:11:36 -07:00
Chris Ballinger e99abd26b3 Remove Poly from podspec frameworks 2019-07-16 16:16:32 -07:00
Mathew Polzin 099d17aa58 Update README.md
Removing check boxes for `Arbitrary` conformance and `OpenAPI` integration. Both of these "features" are maintained in separate libraries.
2019-07-03 07:28:31 -07:00
Mathew Polzin 92d1239a98 Merge pull request #19 from gufo/optional-relationships
Allow omitting `relationships` if all are optional
2019-07-03 07:19:14 -07:00
Janko Luin b7ce3d226e Apply EmptyObjectDecoder rather than empty JSON
Co-Authored-By: Mathew Polzin <matt.polzin@gmail.com>
2019-07-03 08:32:44 +02:00
Janko Luin 5b56d596e2 Merge remote-tracking branch 'upstream/master' into optional-relationships 2019-07-03 08:30:17 +02:00
Mathew Polzin e820f34253 change podspec version in anticipation of next release 2019-07-02 18:12:29 -07:00
Mathew Polzin c75912ab79 switch Poly versioning to 'upToNextMajor' and updated the resolved version. 2019-07-02 18:09:06 -07:00
Mathew Polzin e4481c9e4f noticed the README had some typos I missed when I changed Entity to ResourceObject. 2019-07-02 18:05:22 -07:00
Mathew Polzin e9b9a2bd78 Add Empty Object Decoder to be used in upcoming release. 2019-07-02 17:36:54 -07:00
Janko Luin fc78958f76 Allow omitting relationships if all are optional
When all relationships are optional, the `relationships` key is also
optional and not required in the structure.

I'm not super happy with importing Foundation and creating new objects
any time a key is missing, but ultimately none of my attempts at
conditional generics worked out for me.
2019-07-02 17:02:55 +02:00
34 changed files with 2869 additions and 523 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,6 +52,7 @@ print("-----")
print(peopleResponse)
print("-----")
// MARK: - Pass successfully parsed body to other parts of the code
/*
@@ -59,6 +65,7 @@ if case let .data(bodyData) = peopleResponse.body {
}
*/
// MARK: - Work in the abstract
func process<T: JSONAPIDocument>(document: T) {
+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>
+2 -3
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
#
spec.name = "JSONAPI"
spec.version = "0.30.0"
spec.version = "1.0.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"
+2 -2
View File
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": {
"branch": null,
"revision": "d24d4c1214dd05f89eb1182a46592856dd0a0645",
"version": "2.0.0"
"revision": "38051821d7ef49e590e26e819a2fe447e50be9ff",
"version": "2.0.1"
}
}
]
+1 -1
View File
@@ -18,7 +18,7 @@ let package = Package(
targets: ["JSONAPITesting"])
],
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", from: "2.0.0"),
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.0.0")),
],
targets: [
.target(
+144 -150
View File
File diff suppressed because it is too large Load Diff
+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
+31 -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)
+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 {
+215
View File
@@ -0,0 +1,215 @@
//
// EmptyObjectDecoder.swift
// JSONAPI
//
// Created by Mathew Polzin on 7/2/19.
//
/// `EmptyObjectDecoder` exists internally for the sole purpose of
/// allowing certain fallback logic paths to attempt to create `Decodable`
/// types from empty containers (specifically in a way that is agnostic
/// of any given encoding). In other words, this serves the same purpose
/// as `JSONDecoder().decode(Thing.self, from: "{}".data(using: .utf8)!)`
/// without needing to use a third party or `Foundation` library decoder.
struct EmptyObjectDecoder: Decoder {
var codingPath: [CodingKey] = []
var userInfo: [CodingUserInfoKey : Any] = [:]
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
return KeyedDecodingContainer(EmptyKeyedContainer())
}
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
return EmptyUnkeyedContainer()
}
func singleValueContainer() throws -> SingleValueDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotBeSingleValue
}
}
enum EmptyObjectDecodingError: Swift.Error {
case emptyObjectCannotBeSingleValue
case emptyObjectCannotBeUnkeyedValues
case emptyObjectCannotHaveKeyedValues
case emptyObjectCannotHaveNestedContainers
case emptyObjectCannotHaveSuper
}
struct EmptyUnkeyedContainer: UnkeyedDecodingContainer {
var codingPath: [CodingKey] { return [] }
var count: Int? { return 0 }
var isAtEnd: Bool { return true }
var currentIndex: Int { return 0 }
mutating func decodeNil() throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Bool.Type) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: String.Type) throws -> String {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Double.Type) throws -> Double {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Float.Type) throws -> Float {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int.Type) throws -> Int {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int8.Type) throws -> Int8 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int16.Type) throws -> Int16 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int32.Type) throws -> Int32 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: Int64.Type) throws -> Int64 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt.Type) throws -> UInt {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt8.Type) throws -> UInt8 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt16.Type) throws -> UInt16 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt32.Type) throws -> UInt32 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode(_ type: UInt64.Type) throws -> UInt64 {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func decode<T>(_ type: T.Type) throws -> T where T : Decodable {
throw EmptyObjectDecodingError.emptyObjectCannotBeUnkeyedValues
}
mutating func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
mutating func superDecoder() throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
}
struct EmptyKeyedContainer<Key: CodingKey>: KeyedDecodingContainerProtocol {
var codingPath: [CodingKey] { return [] }
var allKeys: [Key] { return [] }
func contains(_ key: Key) -> Bool {
return false
}
func decodeNil(forKey key: Key) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: String.Type, forKey key: Key) throws -> String {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int.Type, forKey key: Key) throws -> Int {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func decode<T>(_ type: T.Type, forKey key: Key) throws -> T where T : Decodable {
throw EmptyObjectDecodingError.emptyObjectCannotHaveKeyedValues
}
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer {
throw EmptyObjectDecodingError.emptyObjectCannotHaveNestedContainers
}
func superDecoder() throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
func superDecoder(forKey key: Key) throws -> Decoder {
throw EmptyObjectDecodingError.emptyObjectCannotHaveSuper
}
}
+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() }
+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,46 @@ 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 {}
@@ -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)
}
+71 -56
View File
@@ -5,6 +5,7 @@
// Created by Mathew Polzin on 7/24/18.
//
/// A JSON API structure within an ResourceObject that contains
/// named properties of types `ToOneRelationship` and
/// `ToManyRelationship`.
@@ -14,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 {
@@ -47,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
@@ -394,14 +405,14 @@ extension ResourceObject where MetaType == NoMetadata, LinksType == NoLinks, Ent
// 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
@@ -410,6 +421,8 @@ 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)
}
@@ -417,20 +430,20 @@ public extension ResourceObject where EntityRawIdType: JSONAPI.RawIdType {
// 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)
}
@@ -546,61 +559,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.decode(Description.Relationships.self, forKey: .relationships)
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
}
}
@@ -0,0 +1,204 @@
//
// SparseEncoder.swift
//
//
// Created by Mathew Polzin on 8/4/19.
//
class SparseFieldEncoder<SparseKey: CodingKey & Equatable>: Encoder {
private let wrappedEncoder: Encoder
private let allowedKeys: [SparseKey]
public var codingPath: [CodingKey] {
return wrappedEncoder.codingPath
}
public var userInfo: [CodingUserInfoKey : Any] {
return wrappedEncoder.userInfo
}
public init(wrapping encoder: Encoder, encoding allowedKeys: [SparseKey]) {
wrappedEncoder = encoder
self.allowedKeys = allowedKeys
}
public func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
let container = SparseFieldKeyedEncodingContainer(wrapping: wrappedEncoder.container(keyedBy: type),
encoding: allowedKeys)
return KeyedEncodingContainer(container)
}
public func unkeyedContainer() -> UnkeyedEncodingContainer {
return wrappedEncoder.unkeyedContainer()
}
public func singleValueContainer() -> SingleValueEncodingContainer {
return wrappedEncoder.singleValueContainer()
}
}
struct SparseFieldKeyedEncodingContainer<Key, SparseKey>: KeyedEncodingContainerProtocol where SparseKey: CodingKey, SparseKey: Equatable, Key: CodingKey {
private var wrappedContainer: KeyedEncodingContainer<Key>
private let allowedKeys: [SparseKey]
public var codingPath: [CodingKey] {
return wrappedContainer.codingPath
}
public init(wrapping container: KeyedEncodingContainer<Key>, encoding allowedKeys: [SparseKey]) {
wrappedContainer = container
self.allowedKeys = allowedKeys
}
/// Ask the container whether the given key should be encoded.
public func shouldAllow(key: Key) -> Bool {
if let key = key as? SparseKey {
return allowedKeys.contains(key)
}
return true
}
public mutating func encodeNil(forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encodeNil(forKey: key)
}
public mutating func encode(_ value: Bool, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: String, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: Double, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: Float, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: Int, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: Int8, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: Int16, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: Int32, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: Int64, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: UInt, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: UInt8, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: UInt16, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: UInt32, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode(_ value: UInt64, forKey key: Key) throws {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func encode<T>(_ value: T, forKey key: Key) throws where T : Encodable {
guard shouldAllow(key: key) else { return }
try wrappedContainer.encode(value, forKey: key)
}
public mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type,
forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
guard shouldAllow(key: key) else {
return KeyedEncodingContainer(
// TODO: not needed by JSONAPI library, but for completeness could
// add an EmptyObjectEncoder that can be returned here so that
// at least nothing gets encoded within the nested container
SparseFieldKeyedEncodingContainer<NestedKey, SparseKey>(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType,
forKey: key),
encoding: [])
)
}
return KeyedEncodingContainer(
SparseFieldKeyedEncodingContainer<NestedKey, SparseKey>(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType,
forKey: key),
encoding: allowedKeys)
)
}
public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
guard shouldAllow(key: key) else {
// TODO: not needed by JSONAPI library, but for completeness could
// add an EmptyObjectEncoder that can be returned here so that
// at least nothing gets encoded within the nested container
return wrappedContainer.nestedUnkeyedContainer(forKey: key)
}
return wrappedContainer.nestedUnkeyedContainer(forKey: key)
}
public mutating func superEncoder() -> Encoder {
return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(),
encoding: allowedKeys)
}
public mutating func superEncoder(forKey key: Key) -> Encoder {
guard shouldAllow(key: key) else {
// NOTE: We are creating a sparse field encoder with no allowed keys
// here because the given key should not be allowed.
return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key),
encoding: [SparseKey]())
}
return SparseFieldEncoder(wrapping: wrappedContainer.superEncoder(forKey: key),
encoding: allowedKeys)
}
}

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