Compare commits

...

67 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
Mathew Polzin f73ca8bc2c Merge pull request #18 from mattpolzin/feature/match-jsonapi-naming
Feature/match jsonapi naming
2019-06-12 20:59:50 -07:00
Mathew Polzin 176eca3dce Bump podspec version to 0.30.0 2019-06-12 20:57:14 -07:00
Mathew Polzin 4e0e44c45d Update README after renaming. Rename test lib entity checker. 2019-06-12 20:15:07 -07:00
Mathew Polzin e09e3cd8ac Finish renaming all things 'Entity' to 'ResourceObject' 2019-06-12 19:56:33 -07:00
Mathew Polzin 502f82ad14 Rename EntityProxy to ResourceObjectProxy and EntityType to ResourceObjectType. 2019-06-12 19:11:43 -07:00
Mathew Polzin 2bb238b092 Renamed EntityProxyDescription to ResourceObjectProxyDescription and EntityDescription to ResourceObjectDescription. 2019-06-12 19:07:21 -07:00
Mathew Polzin 491fe1fbea clarifying name change in tests. update documentation around entity direct subscript accessor 2019-04-18 22:54:08 -07:00
Mathew Polzin 6b6f40c968 breaking change: change spelling of Entity subscript accessor that returns values of properties within Attributes without digging into the AttributeType. This change will make it possible for the compiler to unambiguously determine the type of subscript access being made before the value is stored, cast, or compared. 2019-04-18 22:40:12 -07:00
Mathew Polzin 14bd29bf42 Update podspec in expectance of breaking change. 2019-04-18 22:37:58 -07:00
Mathew Polzin cf4d8fc378 Update README.md 2019-04-16 22:39:33 -07:00
Mathew Polzin 10b756f6db Update README.md 2019-04-16 22:37:35 -07:00
51 changed files with 3600 additions and 1254 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: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, String>
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: EntityDescription> = JSONAPI.Entity<Description, NoMetadata, NoLinks, Unidentified>
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: EntityDescription {
public static var jsonType: String { return "authors" }
enum AuthorDescription: ResourceObjectDescription {
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: EntityDescription {
public static var jsonType: String { return "articles" }
enum ArticleDescription: ResourceObjectDescription {
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(entity: 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)
@@ -64,7 +64,7 @@ struct ToManyRelationshipLinks: JSONAPI.Links {
}
/// Description of an Author entity.
enum AuthorDescription: EntityDescription {
enum AuthorDescription: ResourceObjectDescription {
static var jsonType: String { return "authors" }
struct Attributes: JSONAPI.Attributes {
@@ -76,10 +76,10 @@ enum AuthorDescription: EntityDescription {
}
}
typealias Author = JSONAPI.Entity<AuthorDescription, EntityMetadata, EntityLinks, String>
typealias Author = JSONAPI.ResourceObject<AuthorDescription, EntityMetadata, EntityLinks, String>
/// Description of an Article entity.
enum ArticleDescription: EntityDescription {
enum ArticleDescription: ResourceObjectDescription {
static var jsonType: String { return "articles" }
struct Attributes: JSONAPI.Attributes {
@@ -96,7 +96,7 @@ enum ArticleDescription: EntityDescription {
}
}
typealias Article = JSONAPI.Entity<ArticleDescription, EntityMetadata, EntityLinks, String>
typealias Article = JSONAPI.ResourceObject<ArticleDescription, EntityMetadata, EntityLinks, String>
/// Metadata associated with the API Description
struct APIDescriptionMetadata: JSONAPI.Meta {
@@ -196,7 +196,7 @@ let documentLinks = SingleArticleDocumentLinks(otherArticlesByPrimaryAuthor: .in
meta: .init(expiry: tomorrow)))
let singleArticleDocument = SingleArticleDocument(apiDescription: apiDescription,
body: .init(entity: article),
body: .init(resourceObject: article),
includes: .init(values: [.init(author1), .init(author2), .init(author3)]),
meta: documentMetadata,
links: documentLinks)
@@ -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,24 +8,28 @@ 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)
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(entity: dogFromCode), includes: .none, meta: .none, links: .none)
let singleDogDocument = SingleDogDocument(apiDescription: .none, body: .init(resourceObject: dogFromCode), includes: .none, meta: .none, links: .none)
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])]
@@ -33,9 +37,10 @@ let houses = [House(attributes: .none, relationships: .none, meta: .none, links:
let people = try! [Person(id: personIds[0], name: ["Gary", "Doe"], favoriteColor: "Orange-Red", friends: [], dogs: [dogs[0], dogs[1]], home: houses[0]), Person(id: personIds[1], name: ["Elise", "Joy"], favoriteColor: "Red", friends: [], dogs: [dogs[2]], home: houses[1])]
let includes = dogs.map { BatchPeopleDocument.Include($0) } + houses.map { BatchPeopleDocument.Include($0) }
let batchPeopleDocument = BatchPeopleDocument(apiDescription: .none, body: .init(entities: people), includes: .init(values: includes), meta: .none, links: .none)
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) {
+9 -9
View File
@@ -24,12 +24,12 @@ extension String: CreatableRawIdType {
}
// MARK: - typealiases for convenience
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, NoMetadata, NoLinks, String>
public typealias ExampleEntity<Description: ResourceObjectDescription> = ResourceObject<Description, NoMetadata, NoLinks, String>
public typealias ToOne<E: Identifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
// MARK: - A few resource objects (entities)
public enum PersonDescription: EntityDescription {
public enum PersonDescription: ResourceObjectDescription {
public static var jsonType: String { return "people" }
@@ -62,13 +62,13 @@ public enum PersonDescription: EntityDescription {
public typealias Person = ExampleEntity<PersonDescription>
public extension Entity where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
public extension ResourceObject where Description == PersonDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)), meta: .none, links: .none)
self = Person(id: id ?? Person.Id(), attributes: .init(name: .init(value: name), favoriteColor: .init(value: favoriteColor)), relationships: .init(friends: .init(resourceObjects: friends), dogs: .init(resourceObjects: dogs), home: .init(resourceObject: home)), meta: .none, links: .none)
}
}
public enum DogDescription: EntityDescription {
public enum DogDescription: ResourceObjectDescription {
public static var jsonType: String { return "dogs" }
@@ -91,7 +91,7 @@ public enum DogDescription: EntityDescription {
public typealias Dog = ExampleEntity<DogDescription>
public enum AlternativeDogDescription: EntityDescription {
public enum AlternativeDogDescription: ResourceObjectDescription {
public static var jsonType: String { return "dogs" }
@@ -119,9 +119,9 @@ public enum AlternativeDogDescription: EntityDescription {
public typealias AlternativeDog = ExampleEntity<AlternativeDogDescription>
public extension Entity where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
public extension ResourceObject where Description == DogDescription, MetaType == NoMetadata, LinksType == NoLinks, EntityRawIdType == String {
init(name: String, owner: Person?) throws {
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)), meta: .none, links: .none)
self = Dog(attributes: .init(name: .init(value: name)), relationships: DogDescription.Relationships(owner: .init(resourceObject: owner)), meta: .none, links: .none)
}
init(name: String, owner: Person.Id) throws {
@@ -129,7 +129,7 @@ public extension Entity where Description == DogDescription, MetaType == NoMetad
}
}
public enum HouseDescription: EntityDescription {
public enum HouseDescription: ResourceObjectDescription {
public static var jsonType: String { return "houses" }
+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.20.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(
+187 -196
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)
+72 -41
View File
@@ -5,45 +5,70 @@
// 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(entity: Entity) {
self.value = entity
public init(resourceObject: Entity) {
self.value = resourceObject
}
}
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(entities: [Entity]) {
values = entities
public init(resourceObjects: [Entity]) {
values = resourceObjects
}
public func appending(_ other: ManyResourceBody) -> ManyResourceBody {
return ManyResourceBody(entities: values + other.values)
return ManyResourceBody(resourceObjects: values + other.values)
}
}
@@ -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() }
File diff suppressed because it is too large Load Diff
+4 -1
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() {}
@@ -69,7 +72,7 @@ public protocol CreatableIdType: IdType {
init()
}
/// An Entity ID. These IDs can be encoded to or decoded from
/// An ResourceObject ID. These IDs can be encoded to or decoded from
/// JSON API IDs.
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
@@ -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 {}
+16 -20
View File
@@ -13,10 +13,10 @@ public protocol RelationshipType {
var meta: MetaType { get }
}
/// An Entity relationship that can be encoded to or decoded from
/// An ResourceObject relationship that can be encoded to or decoded from
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `One<EntityDescription>`
/// A convenient typealias might make your code much more legible: `One<ResourceObjectDescription>`
public struct ToOneRelationship<Identifiable: JSONAPI.Identifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
public let id: Identifiable.Identifier
@@ -38,33 +38,33 @@ extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
}
extension ToOneRelationship {
public init<E: EntityType>(entity: E, meta: MetaType, links: LinksType) where E.Id == Identifiable.Identifier {
self.init(id: entity.id, meta: meta, links: links)
public init<T: ResourceObjectType>(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.Identifier {
self.init(id: resourceObject.id, meta: meta, links: links)
}
}
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
public init<E: EntityType>(entity: E) where E.Id == Identifiable.Identifier {
self.init(id: entity.id, meta: .none, links: .none)
public init<T: ResourceObjectType>(resourceObject: T) where T.Id == Identifiable.Identifier {
self.init(id: resourceObject.id, meta: .none, links: .none)
}
}
extension ToOneRelationship where Identifiable: OptionalRelatable {
public init<E: EntityType>(entity: E?, meta: MetaType, links: LinksType) where E.Id == Identifiable.Wrapped.Identifier {
self.init(id: entity?.id, meta: meta, links: links)
public init<T: ResourceObjectType>(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.Identifier {
self.init(id: resourceObject?.id, meta: meta, links: links)
}
}
extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks {
public init<E: EntityType>(entity: E?) where E.Id == Identifiable.Wrapped.Identifier {
self.init(id: entity?.id, meta: .none, links: .none)
public init<T: ResourceObjectType>(resourceObject: T?) where T.Id == Identifiable.Wrapped.Identifier {
self.init(id: resourceObject?.id, meta: .none, links: .none)
}
}
/// An Entity relationship that can be encoded to or decoded from
/// An ResourceObject relationship that can be encoded to or decoded from
/// a JSON API "Resource Linkage."
/// See https://jsonapi.org/format/#document-resource-object-linkage
/// A convenient typealias might make your code much more legible: `Many<EntityDescription>`
/// A convenient typealias might make your code much more legible: `Many<ResourceObjectDescription>`
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
public let ids: [Relatable.Identifier]
@@ -84,8 +84,8 @@ public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI
self.links = links
}
public init<E: EntityType>(entities: [E], meta: MetaType, links: LinksType) where E.Id == Relatable.Identifier {
self.init(ids: entities.map { $0.id }, meta: meta, links: links)
public init<T: ResourceObjectType>(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.Identifier {
self.init(ids: resourceObjects.map { $0.id }, meta: meta, links: links)
}
private init(meta: MetaType, links: LinksType) {
@@ -111,8 +111,8 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks
return .none(withMeta: .none, links: .none)
}
public init<E: EntityType>(entities: [E]) where E.Id == Relatable.Identifier {
self.init(entities: entities, meta: .none, links: .none)
public init<T: ResourceObjectType>(resourceObjects: [T]) where T.Id == Relatable.Identifier {
self.init(resourceObjects: resourceObjects, meta: .none, links: .none)
}
}
@@ -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)
}

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