Compare commits

...

45 Commits

Author SHA1 Message Date
Mathew Polzin f41521e33b Update example indentation 2019-11-16 00:06:49 -08:00
Mathew Polzin 45ec7ba753 Update SwiftPM package manifest example for version 3 2019-11-16 00:03:46 -08:00
Mathew Polzin 6ab4237c97 update podspec 2019-11-15 23:58:01 -08:00
Mathew Polzin 75711648a4 more minor documentation fixes 2019-11-15 23:48:49 -08:00
Mathew Polzin 2b4209ccb1 A couple more clarifications/corrections 2019-11-15 23:41:12 -08:00
Mathew Polzin ebd11df104 Merge pull request #54 from mattpolzin/beta/3x
Beta/3x
2019-11-15 23:30:01 -08:00
Mathew Polzin 96da1b4e21 update documentation 2019-11-15 23:15:32 -08:00
Mathew Polzin c7696d83fa update Playground pages to run 2019-11-15 17:46:53 -08:00
Mathew Polzin 8ee04d8932 generate linuxmain 2019-11-15 17:38:04 -08:00
Mathew Polzin 4a7a14b1b0 Add test coverage for resource object compare(to:) 2019-11-15 17:36:42 -08:00
Mathew Polzin a6b7d7a94a woops, abstract wrapper protocol landed in wrong module by accident 2019-11-15 17:05:45 -08:00
Mathew Polzin 1010489a02 compare(to:) bug fixes and test additions 2019-11-15 16:59:00 -08:00
Mathew Polzin ae855c85ee going through and fleshing out tests. minor adjustments and bug fixes. 2019-11-15 08:30:17 -08:00
Mathew Polzin 440b649577 Merge pull request #49 from mattpolzin/alpha/3x
Alpha/3x
2019-11-12 18:38:31 -08:00
Mathew Polzin 54551617b4 Add more errors for the resource object type property 2019-11-12 18:34:34 -08:00
Mathew Polzin 8c3a82ec23 Merge pull request #53 from mattpolzin/feature/better-error-messages
Feature/better error messages
2019-11-10 23:47:05 -08:00
Mathew Polzin e9a3b35dc7 improve error messages for poly include types 2019-11-10 23:43:35 -08:00
Mathew Polzin 2eecf95995 Adding Document Decoding Errors for some common problems 2019-11-10 23:02:26 -08:00
Mathew Polzin 4dc30ddc1c Rounding out the Resource Object errors 2019-11-10 20:46:35 -08:00
Mathew Polzin 455ff64326 update linuxmain 2019-11-09 00:34:39 -08:00
Mathew Polzin 0b4baf35d5 got some attribute cases added and tested. added some descriptions (custom string convertible) 2019-11-09 00:33:42 -08:00
Mathew Polzin 11ef050d58 most common relationship errors tested. 2019-11-08 18:47:28 -08:00
Mathew Polzin 86344ef93f trivial refactor in sparse fieldset file 2019-11-07 08:08:45 -08:00
Mathew Polzin 7fabe2574e rename some of the new public protocols in JSONAPITesting 2019-11-07 07:56:58 -08:00
Mathew Polzin 19636a47f0 Merge pull request #51 from mattpolzin/feature/primary-resource-body-improvements
Feature/primary resource body improvements
2019-11-06 23:46:13 -08:00
Mathew Polzin 0538de48cb rename PolyWrapped to CodablePolyWrapped (missed when I did other similar renaming 2019-11-06 23:41:02 -08:00
Mathew Polzin 024fe2d452 Down to one sequence of compare functions for all documents 2019-11-06 23:21:22 -08:00
Mathew Polzin 832161628b go from 3 specializations of all document related compare functions down to 2. 2019-11-06 23:05:21 -08:00
Mathew Polzin f37f44cfda add comparable protocol 2019-11-06 21:42:13 -08:00
Mathew Polzin ae7e0f528a abstract away document comparison 2019-11-06 00:19:27 -08:00
Mathew Polzin 9f42e5fa3f Merge pull request #48 from mattpolzin/feature/indentation
Feature/indentation
2019-11-05 23:03:03 -08:00
Mathew Polzin 3e96adf775 merge and fix conflicts 2019-11-05 23:00:18 -08:00
Mathew Polzin 83233a7e1a Merge pull request #46 from mattpolzin/feature/testing-comparisons
Feature/testing comparisons
2019-11-05 22:47:47 -08:00
Mathew Polzin adcc6bfb10 fix error after merge 2019-11-05 22:43:40 -08:00
Mathew Polzin 6f9a2df605 Merge remote-tracking branch 'origin/alpha/3x' into feature/testing-comparisons 2019-11-05 22:41:53 -08:00
Mathew Polzin c24b70b49f Merge pull request #45 from mattpolzin/feature/deprecate-subscript
Feature/deprecate subscript
2019-11-05 22:40:37 -08:00
Mathew Polzin 7b56e35a91 Merge pull request #47 from mattpolzin/feature/document-improvements
Feature/document improvements
2019-11-05 22:38:42 -08:00
Mathew Polzin e6f82c6052 indentation 2019-11-05 22:37:48 -08:00
Mathew Polzin d6f01d6c1d indentation 2019-11-05 22:27:59 -08:00
Mathew Polzin a7f6ed5845 update README 2019-11-05 21:03:51 -08:00
Mathew Polzin 87271b93f9 deprecate subscript attribute accessor in favor of key path dynamic member lookup 2019-11-05 20:48:04 -08:00
Mathew Polzin 706346e3a6 just reformatting and rearranging 2019-11-05 20:04:47 -08:00
Mathew Polzin 33a5ff41a0 beginning to do some breaking change cleanup 2019-11-05 19:50:52 -08:00
Mathew Polzin 0fe5c53ada generate linuxmain 2019-11-05 19:04:47 -08:00
Mathew Polzin f7bfa91ccc lots of comparison code, a few small breaking changes 2019-11-05 19:03:51 -08:00
72 changed files with 6269 additions and 2194 deletions
@@ -37,7 +37,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
// JSON:API Documents for this particular API to have Metadata, Links, // JSON:API Documents for this particular API to have Metadata, Links,
// useful Errors, or an APIDescription (The *SPEC* calls this // useful Errors, or an APIDescription (The *SPEC* calls this
// "API Description" the "JSON:API Object"). // "API Description" the "JSON:API Object").
typealias Document<PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>> typealias Document<PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
// MARK: Entity Definitions // MARK: Entity Definitions
@@ -64,11 +64,11 @@ if case let .data(bodyData) = peopleResponse.body {
// MARK: - Work in the abstract // MARK: - Work in the abstract
print("-----") print("-----")
func process<T: JSONAPIDocument>(document: T) { func process<T: CodableJSONAPIDocument>(document: T) {
guard case let .data(body) = document.body else { guard let body = document.body.data else {
return return
} }
let x: T.Body.Data = body let x: T.BodyData = body
} }
process(document: peopleResponse) process(document: peopleResponse)
+2 -2
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
# #
spec.name = "MP-JSONAPI" spec.name = "MP-JSONAPI"
spec.version = "2.5.0" spec.version = "3.0.0"
spec.summary = "Swift Codable JSON API framework." spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results. # This description is used to generate tags and improve search results.
@@ -136,6 +136,6 @@ See the JSON API Spec here: https://jsonapi.org/format/
# spec.requires_arc = true # spec.requires_arc = true
# spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" } # spec.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
spec.dependency "Poly", "~> 2.2" spec.dependency "Poly", "~> 2.3.1"
end end
+2 -2
View File
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/mattpolzin/Poly.git", "repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": { "state": {
"branch": null, "branch": null,
"revision": "b24fd3b41bf3126d4c6dede3708135182172af60", "revision": "0c9c08204142babc480938d704a23513d11420e5",
"version": "2.2.0" "version": "2.3.1"
} }
} }
] ]
+1 -1
View File
@@ -18,7 +18,7 @@ let package = Package(
targets: ["JSONAPITesting"]) targets: ["JSONAPITesting"])
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.2.0")), .package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.1")),
], ],
targets: [ targets: [
.target( .target(
+60 -640
View File
File diff suppressed because it is too large Load Diff
+238 -138
View File
@@ -1,5 +1,5 @@
// //
// JSONAPIDocument.swift // Document.swift
// JSONAPI // JSONAPI
// //
// Created by Mathew Polzin on 11/5/18. // Created by Mathew Polzin on 11/5/18.
@@ -7,52 +7,157 @@
import Poly import Poly
/// An `EncodableJSONAPIDocument` supports encoding but not decoding. public protocol DocumentBodyDataContext {
/// It is actually more restrictive than `JSONAPIDocument` which supports both
/// encoding and decoding.
public protocol EncodableJSONAPIDocument: Equatable, Encodable {
associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody associatedtype PrimaryResourceBody: JSONAPI.EncodableResourceBody
associatedtype MetaType: JSONAPI.Meta associatedtype MetaType: JSONAPI.Meta
associatedtype LinksType: JSONAPI.Links associatedtype LinksType: JSONAPI.Links
associatedtype IncludeType: JSONAPI.Include associatedtype IncludeType: JSONAPI.Include
associatedtype APIDescription: APIDescriptionType
associatedtype Error: JSONAPIError
typealias Body = Document<PrimaryResourceBody, MetaType, LinksType, IncludeType, APIDescription, Error>.Body
var body: Body { get }
} }
/// A `JSONAPIDocument` supports encoding and decoding of a JSON:API public protocol DocumentBodyContext: DocumentBodyDataContext {
/// compliant Document. associatedtype Error: JSONAPIError
public protocol JSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.ResourceBody, IncludeType: Decodable {} associatedtype BodyData: DocumentBodyData
where
BodyData.PrimaryResourceBody == PrimaryResourceBody,
BodyData.MetaType == MetaType,
BodyData.LinksType == LinksType,
BodyData.IncludeType == IncludeType
}
/// A JSON API Document represents the entire body public protocol DocumentBodyData: DocumentBodyDataContext {
/// of a JSON API request or the entire body of /// The document's primary resource body
/// a JSON API response. /// (contains one or many resource objects)
/// Note that this type uses Camel case. If your var primary: PrimaryResourceBody { get }
/// 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.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 /// The document's included objects
/// and metadata information about the API itself. var includes: Includes<IncludeType> { get }
public let apiDescription: APIDescription var meta: MetaType { get }
var links: LinksType { get }
}
public protocol DocumentBody: DocumentBodyContext {
/// `true` if the document represents one or more errors. `false` if the
/// document represents JSON:API data and/or metadata.
var isError: Bool { get }
/// Get all errors in the document, if any.
///
/// `nil` if the Document is _not_ an error response. Otherwise,
/// an array containing all errors.
var errors: [Error]? { get }
/// Get the document data
///
/// `nil` if the Document is an error response. Otherwise,
/// a structure containing the primary resource, any included
/// resources, metadata, and links.
var data: BodyData? { get }
/// Quick access to the `data`'s primary resource.
///
/// `nil` if the Document is an error document. Otherwise,
/// the primary resource body, which will contain zero/one, one/many
/// resources dependening on the `PrimaryResourceBody` type.
///
/// See `SingleResourceBody` and `ManyResourceBody`.
var primaryResource: PrimaryResourceBody? { get }
/// Quick access to the `data`'s includes.
///
/// `nil` if the Document is an error document. Otherwise,
/// zero or more includes.
var includes: Includes<IncludeType>? { get }
/// The metadata for the error or data document or `nil` if
/// no metadata is found.
var meta: MetaType? { get }
/// The links for the error or data document or `nil` if
/// no links are found.
var links: LinksType? { get }
}
/// 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, DocumentBodyContext {
associatedtype APIDescription: APIDescriptionType
associatedtype Body: DocumentBody
where
Body.PrimaryResourceBody == PrimaryResourceBody,
Body.MetaType == MetaType,
Body.LinksType == LinksType,
Body.IncludeType == IncludeType,
Body.Error == Error,
Body.BodyData == BodyData
/// The Body of the Document. This body is either one or more errors /// The Body of the Document. This body is either one or more errors
/// with links and metadata attempted to parse but not guaranteed or /// with links and metadata attempted to parse but not guaranteed or
/// it is a successful data struct containing all the primary and /// it is a successful data struct containing all the primary and
/// included resources, the metadata, and the links that this /// included resources, the metadata, and the links that this
/// document type specifies. /// document type specifies.
var body: Body { get }
/// The JSON API Spec calls this the JSON:API Object. It contains version
/// and metadata information about the API itself.
var apiDescription: APIDescription { get }
}
/// A `CodableJSONAPIDocument` supports encoding and decoding of a JSON:API
/// compliant Document.
public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: Decodable {}
/// A JSON API Document represents the entire body
/// of a JSON API request or the entire body of
/// a JSON API response.
///
/// Note that this type uses Camel case. If your
/// 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.EncodableResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: EncodableJSONAPIDocument {
public typealias Include = IncludeType
public typealias BodyData = Body.Data
// See `EncodableJSONAPIDocument` for documentation.
public let apiDescription: APIDescription
// See `EncodableJSONAPIDocument` for documentation.
public let body: Body public let body: Body
public enum Body: Equatable { public init(apiDescription: APIDescription,
errors: [Error],
meta: MetaType? = nil,
links: LinksType? = nil) {
body = .errors(errors, meta: meta, links: links)
self.apiDescription = apiDescription
}
public init(apiDescription: APIDescription,
body: PrimaryResourceBody,
includes: Includes<Include>,
meta: MetaType,
links: LinksType) {
self.body = .data(
.init(
primary: body,
includes: includes,
meta: meta,
links: links
)
)
self.apiDescription = apiDescription
}
}
extension Document {
public enum Body: DocumentBody, Equatable {
case errors([Error], meta: MetaType?, links: LinksType?) case errors([Error], meta: MetaType?, links: LinksType?)
case data(Data) case data(Data)
public struct Data: Equatable { public typealias BodyData = Data
public struct Data: DocumentBodyData, Equatable {
/// The document's Primary Resource object(s) /// The document's Primary Resource object(s)
public let primary: PrimaryResourceBody public let primary: PrimaryResourceBody
/// The document's included objects /// The document's included objects
@@ -117,97 +222,9 @@ public struct Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, MetaT
} }
} }
} }
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
body = .errors(errors, meta: meta, links: links)
self.apiDescription = apiDescription
} }
public init(apiDescription: APIDescription, extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable {
body: PrimaryResourceBody,
includes: Includes<Include>,
meta: MetaType,
links: LinksType) {
self.body = .data(.init(primary: body, includes: includes, meta: meta, links: links))
self.apiDescription = apiDescription
}
}
/*
extension Document where IncludeType == NoIncludes {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, includes: .none, meta: meta, links: links)
}
}
extension Document where MetaType == NoMetadata {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: .none, links: links)
}
}
extension Document where LinksType == NoLinks {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType) {
self.init(apiDescription: apiDescription, body: body, includes: includes, meta: meta, links: .none)
}
}
extension Document where APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>, meta: MetaType, links: LinksType) {
self.init(apiDescription: .none, body: body, includes: includes, meta: meta, links: links)
}
}
extension Document where IncludeType == NoIncludes, LinksType == NoLinks {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, meta: MetaType) {
self.init(apiDescription: apiDescription, body: body, meta: meta, links: .none)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, links: LinksType) {
self.init(apiDescription: apiDescription, body: body, meta: .none, links: links)
}
}
extension Document where IncludeType == NoIncludes, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, meta: MetaType, links: LinksType) {
self.init(apiDescription: .none, body: body, meta: meta, links: links)
}
}
extension Document where MetaType == NoMetadata, LinksType == NoLinks {
public init(apiDescription: APIDescription, body: PrimaryResourceBody, includes: Includes<Include>) {
self.init(apiDescription: apiDescription, body: body, includes: includes, links: .none)
}
}
extension Document where MetaType == NoMetadata, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>, links: LinksType) {
self.init(apiDescription: .none, body: body, includes: includes, links: links)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks {
public init(apiDescription: APIDescription, body: PrimaryResourceBody) {
self.init(apiDescription: apiDescription, body: body, includes: .none)
}
}
extension Document where MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody, includes: Includes<Include>) {
self.init(apiDescription: .none, body: body, includes: includes)
}
}
extension Document where IncludeType == NoIncludes, MetaType == NoMetadata, LinksType == NoLinks, APIDescription == NoAPIDescription {
public init(body: PrimaryResourceBody) {
self.init(apiDescription: .none, body: body)
}
}
*/
extension Document.Body.Data where PrimaryResourceBody: Appendable {
public func merging(_ other: Document.Body.Data, public func merging(_ other: Document.Body.Data,
combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType, combiningMetaWith metaMerge: (MetaType, MetaType) -> MetaType,
combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data { combiningLinksWith linksMerge: (LinksType, LinksType) -> LinksType) -> Document.Body.Data {
@@ -218,7 +235,7 @@ extension Document.Body.Data where PrimaryResourceBody: Appendable {
} }
} }
extension Document.Body.Data where PrimaryResourceBody: Appendable, MetaType == NoMetadata, LinksType == NoLinks { extension Document.Body.Data where PrimaryResourceBody: ResourceBodyAppendable, MetaType == NoMetadata, LinksType == NoLinks {
public func merging(_ other: Document.Body.Data) -> Document.Body.Data { public func merging(_ other: Document.Body.Data) -> Document.Body.Data {
return merging(other, return merging(other,
combiningMetaWith: { _, _ in .none }, combiningMetaWith: { _, _ in .none },
@@ -328,12 +345,12 @@ extension Document {
} }
} }
extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody: CodableResourceBody, IncludeType: Decodable {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootCodingKeys.self) let container = try decoder.container(keyedBy: RootCodingKeys.self)
if let noData = NoAPIDescription() as? APIDescription { if let noAPIDescription = NoAPIDescription() as? APIDescription {
apiDescription = noData apiDescription = noAPIDescription
} else { } else {
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi) apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
} }
@@ -372,18 +389,30 @@ extension Document: Decodable, JSONAPIDocument where PrimaryResourceBody: Resour
if let noData = NoResourceBody() as? PrimaryResourceBody { if let noData = NoResourceBody() as? PrimaryResourceBody {
data = noData data = noData
} else { } else {
do {
data = try container.decode(PrimaryResourceBody.self, forKey: .data) data = try container.decode(PrimaryResourceBody.self, forKey: .data)
} catch let error as ResourceObjectDecodingError {
throw DocumentDecodingError(error)
} catch let error as ManyResourceBodyDecodingError {
throw DocumentDecodingError(error)
} catch let error as DecodingError {
throw DocumentDecodingError(error)
?? error
}
} }
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included) let maybeIncludes: Includes<Include>?
do {
// TODO come back to this and make robust maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
} catch let error as IncludesDecodingError {
throw DocumentDecodingError(error)
}
guard let metaVal = meta else { guard let metaVal = meta else {
throw JSONAPIEncodingError.missingOrMalformedMetadata throw JSONAPICodingError.missingOrMalformedMetadata(path: decoder.codingPath)
} }
guard let linksVal = links else { guard let linksVal = links else {
throw JSONAPIEncodingError.missingOrMalformedLinks throw JSONAPICodingError.missingOrMalformedLinks(path: decoder.codingPath)
} }
body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal)) body = .data(.init(primary: data, includes: maybeIncludes ?? Includes<Include>.none, meta: metaVal, links: linksVal))
@@ -420,8 +449,9 @@ extension Document.Body.Data: CustomStringConvertible {
extension Document { extension Document {
/// A Document that only supports error bodies. This is useful if you wish to pass around a /// A Document that only supports error bodies. This is useful if you wish to pass around a
/// Document type but you wish to constrain it to error values. /// Document type but you wish to constrain it to error values.
@dynamicMemberLookup
public struct ErrorDocument: EncodableJSONAPIDocument { public struct ErrorDocument: EncodableJSONAPIDocument {
public typealias BodyData = Document.BodyData
public var body: Document.Body { return document.body } public var body: Document.Body { return document.body }
private let document: Document private let document: Document
@@ -436,19 +466,43 @@ extension Document {
try container.encode(document) try container.encode(document)
} }
public subscript<T>(dynamicMember path: KeyPath<Document, T>) -> T { /// The JSON API Spec calls this the JSON:API Object. It contains version
return document[keyPath: path] /// and metadata information about the API itself.
public var apiDescription: APIDescription {
return document.apiDescription
}
/// Get all errors in the document, if any.
public var errors: [Error] {
return document.body.errors ?? []
}
/// The metadata for the error or data document or `nil` if
/// no metadata is found.
public var meta: MetaType? {
return document.body.meta
}
/// The links for the error or data document or `nil` if
/// no links are found.
public var links: LinksType? {
return document.body.links
} }
public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool { public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool {
return lhs == rhs.document return lhs == rhs.document
} }
public static func ==(lhs: ErrorDocument, rhs: Document) -> Bool {
return lhs.document == rhs
}
} }
/// A Document that only supports success bodies. This is useful if you wish to pass around a /// A Document that only supports success bodies. This is useful if you wish to pass around a
/// Document type but you wish to constrain it to success values. /// Document type but you wish to constrain it to success values.
@dynamicMemberLookup
public struct SuccessDocument: EncodableJSONAPIDocument { public struct SuccessDocument: EncodableJSONAPIDocument {
public typealias BodyData = Document.BodyData
public var body: Document.Body { return document.body } public var body: Document.Body { return document.body }
private let document: Document private let document: Document
@@ -471,38 +525,84 @@ extension Document {
try container.encode(document) try container.encode(document)
} }
public subscript<T>(dynamicMember path: KeyPath<Document, T>) -> T { /// The JSON API Spec calls this the JSON:API Object. It contains version
return document[keyPath: path] /// and metadata information about the API itself.
public var apiDescription: APIDescription {
return document.apiDescription
}
/// Get the document data
///
/// `nil` if the Document is an error response. Otherwise,
/// a structure containing the primary resource, any included
/// resources, metadata, and links.
public var data: BodyData? {
return document.body.data
}
/// Quick access to the `data`'s primary resource.
///
/// `nil` if the Document is an error document. Otherwise,
/// the primary resource body, which will contain zero/one, one/many
/// resources dependening on the `PrimaryResourceBody` type.
///
/// See `SingleResourceBody` and `ManyResourceBody`.
public var primaryResource: PrimaryResourceBody? {
return document.body.primaryResource
}
/// Quick access to the `data`'s includes.
///
/// `nil` if the Document is an error document. Otherwise,
/// zero or more includes.
public var includes: Includes<IncludeType>? {
return document.body.includes
}
/// The metadata for the error or data document or `nil` if
/// no metadata is found.
public var meta: MetaType? {
return document.body.meta
}
/// The links for the error or data document or `nil` if
/// no links are found.
public var links: LinksType? {
return document.body.links
} }
public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool { public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool {
return lhs == rhs.document return lhs == rhs.document
} }
public static func ==(lhs: SuccessDocument, rhs: Document) -> Bool {
return lhs.document == rhs
}
} }
} }
extension Document.ErrorDocument: Decodable, JSONAPIDocument extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument
where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { where PrimaryResourceBody: CodableResourceBody, IncludeType: Decodable {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
document = try container.decode(Document.self) document = try container.decode(Document.self)
guard document.body.isError else { guard document.body.isError else {
throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError throw DocumentDecodingError.foundSuccessDocumentWhenExpectingError
} }
} }
} }
extension Document.SuccessDocument: Decodable, JSONAPIDocument extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument
where PrimaryResourceBody: ResourceBody, IncludeType: Decodable { where PrimaryResourceBody: CodableResourceBody, IncludeType: Decodable {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
document = try container.decode(Document.self) document = try container.decode(Document.self)
guard !document.body.isError else { guard !document.body.isError else {
throw JSONAPIDocumentDecodingError.foundErrorDocumentWhenExpectingSuccess throw DocumentDecodingError.foundErrorDocumentWhenExpectingSuccess
} }
} }
} }
@@ -1,11 +1,73 @@
// //
// DocumentDecodingErro.swift // DocumentDecodingError.swift
// //
// //
// Created by Mathew Polzin on 10/20/19. // Created by Mathew Polzin on 10/20/19.
// //
public enum JSONAPIDocumentDecodingError: Swift.Error { public enum DocumentDecodingError: Swift.Error, Equatable {
case primaryResource(error: ResourceObjectDecodingError, idx: Int?)
case primaryResourceMissing
case primaryResourcesMissing
case includes(error: IncludesDecodingError)
case foundErrorDocumentWhenExpectingSuccess case foundErrorDocumentWhenExpectingSuccess
case foundSuccessDocumentWhenExpectingError case foundSuccessDocumentWhenExpectingError
init(_ decodingError: ResourceObjectDecodingError) {
self = .primaryResource(error: decodingError, idx: nil)
}
init(_ decodingError: ManyResourceBodyDecodingError) {
self = .primaryResource(error: decodingError.error, idx: decodingError.idx)
}
init(_ decodingError: IncludesDecodingError) {
self = .includes(error: decodingError)
}
init?(_ decodingError: DecodingError) {
switch decodingError {
case .valueNotFound(let type, let context) where Location(context) == .data && type is AbstractResourceObject.Type:
self = .primaryResourceMissing
case .valueNotFound(let type, let context) where Location(context) == .data && type == UnkeyedDecodingContainer.self:
self = .primaryResourcesMissing
default:
return nil
}
}
private enum Location: Equatable {
case data
init?(_ context: DecodingError.Context) {
guard context.codingPath.contains(where: { $0.stringValue == "data" }) else {
return nil
}
self = .data
}
}
}
extension DocumentDecodingError: CustomStringConvertible {
public var description: String {
switch self {
case .primaryResource(error: let error, idx: let idx):
let idxString = idx.map { " \($0 + 1)" } ?? ""
return "Primary Resource\(idxString) failed to parse because \(error)"
case .primaryResourceMissing:
return "Primary Resource missing."
case .primaryResourcesMissing:
return "Primary Resources array missing."
case .includes(error: let error):
return "\(error)"
case .foundErrorDocumentWhenExpectingSuccess:
return "Expected a success document with a 'data' property but found an error document."
case .foundSuccessDocumentWhenExpectingError:
return "Expected an error document but found a success document with a 'data' property."
}
}
} }
+60 -4
View File
@@ -14,15 +14,15 @@ public typealias Include = EncodableJSONPoly
/// ///
/// If you have /// If you have
/// ///
/// `let includes: Includes<Include2<Thing1, Thing2>> = ...` /// let includes: Includes<Include2<Thing1, Thing2>> = ...
/// ///
/// then you can access all `Thing1` included resources with /// then you can access all `Thing1` included resources with
/// ///
/// `let includedThings = includes[Thing1.self]` /// let includedThings = includes[Thing1.self]
public struct Includes<I: Include>: Encodable, Equatable { public struct Includes<I: Include>: Encodable, Equatable {
public static var none: Includes { return .init(values: []) } public static var none: Includes { return .init(values: []) }
let values: [I] public let values: [I]
public init(values: [I]) { public init(values: [I]) {
self.values = values self.values = values
@@ -32,7 +32,7 @@ public struct Includes<I: Include>: Encodable, Equatable {
var container = encoder.unkeyedContainer() var container = encoder.unkeyedContainer()
guard I.self != NoIncludes.self else { guard I.self != NoIncludes.self else {
throw JSONAPIEncodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.") throw JSONAPICodingError.illegalEncoding("Attempting to encode Include0, which should be represented by the absense of an 'included' entry altogether.", path: encoder.codingPath)
} }
for value in values { for value in values {
@@ -56,8 +56,35 @@ extension Includes: Decodable where I: Decodable {
} }
var valueAggregator = [I]() var valueAggregator = [I]()
var idx = 0
while !container.isAtEnd { while !container.isAtEnd {
do {
valueAggregator.append(try container.decode(I.self)) valueAggregator.append(try container.decode(I.self))
idx = idx + 1
} catch let error as PolyDecodeNoTypesMatchedError {
let errors: [ResourceObjectDecodingError] = error
.individualTypeFailures
.compactMap { decodingError in
switch decodingError.error {
case .typeMismatch(_, let context),
.valueNotFound(_, let context),
.keyNotFound(_, let context),
.dataCorrupted(let context):
return context.underlyingError as? ResourceObjectDecodingError
@unknown default:
return nil
}
}
guard errors.count == error.individualTypeFailures.count else {
throw IncludesDecodingError(error: error, idx: idx)
}
throw IncludesDecodingError(
error: IncludeDecodingError(failures: errors),
idx: idx
)
} catch let error {
throw IncludesDecodingError(error: error, idx: idx)
}
} }
values = valueAggregator values = valueAggregator
@@ -177,3 +204,32 @@ extension Includes where I: _Poly11 {
return values.compactMap { $0.k } return values.compactMap { $0.k }
} }
} }
// MARK: - DecodingError
public struct IncludesDecodingError: Swift.Error, Equatable {
public let error: Swift.Error
public let idx: Int
public static func ==(lhs: Self, rhs: Self) -> Bool {
return lhs.idx == rhs.idx
&& String(describing: lhs) == String(describing: rhs)
}
}
extension IncludesDecodingError: CustomStringConvertible {
public var description: String {
return "Include \(idx + 1) failed to parse: \(error)"
}
}
public struct IncludeDecodingError: Swift.Error, Equatable, CustomStringConvertible {
public let failures: [ResourceObjectDecodingError]
public var description: String {
return failures
.enumerated()
.map {
"\nCould not have been Include Type \($0.offset + 1) because:\n\($0.element)"
}.joined(separator: "\n")
}
}
+43 -26
View File
@@ -10,60 +10,60 @@
/// array should be used for no results). /// array should be used for no results).
public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {} public protocol OptionalEncodablePrimaryResource: Equatable, Encodable {}
/// An `EncodablePrimaryResource` is a `PrimaryResource` that only supports encoding. /// An `EncodablePrimaryResource` is a `CodablePrimaryResource` that only supports encoding.
/// This is actually more restrictave than `PrimaryResource`, which supports both encoding and
/// decoding.
public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {} public protocol EncodablePrimaryResource: OptionalEncodablePrimaryResource {}
/// This protocol allows for `SingleResourceBody` to contain a `null` /// This protocol allows for `SingleResourceBody` to contain a `null`
/// data object where `ManyResourceBody` cannot (because an empty /// data object where `ManyResourceBody` cannot (because an empty
/// array should be used for no results). /// array should be used for no results).
public protocol OptionalPrimaryResource: OptionalEncodablePrimaryResource, Decodable {} public protocol OptionalCodablePrimaryResource: OptionalEncodablePrimaryResource, Decodable {}
/// A `PrimaryResource` is a type that can be used in the body of a JSON API /// A `CodablePrimaryResource` is a type that can be used in the body of a JSON API
/// document as the primary resource. /// document as the primary resource.
public protocol PrimaryResource: EncodablePrimaryResource, OptionalPrimaryResource {} public protocol CodablePrimaryResource: EncodablePrimaryResource, OptionalCodablePrimaryResource {}
extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {} extension Optional: OptionalEncodablePrimaryResource where Wrapped: EncodablePrimaryResource {}
extension Optional: OptionalPrimaryResource where Wrapped: PrimaryResource {} extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimaryResource {}
/// An `EncodableResourceBody` is a `ResourceBody` that only supports being /// An `EncodableResourceBody` is a `ResourceBody` that only supports being
/// encoded. It is actually weaker than `ResourceBody`, which supports both encoding /// encoded. It is actually weaker than `ResourceBody`, which supports both encoding
/// and decoding. /// and decoding.
public protocol EncodableResourceBody: Equatable, Encodable {} public protocol EncodableResourceBody: Equatable, Encodable {
associatedtype PrimaryResource
}
/// A ResourceBody is a representation of the body of the JSON API Document. /// A `CodableResourceBody` is a representation of the body of the JSON:API Document.
/// It can either be one resource (which can be specified as optional or not) /// 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). /// or it can contain many resources (and array with zero or more entries).
public protocol ResourceBody: Decodable, EncodableResourceBody {} public protocol CodableResourceBody: Decodable, EncodableResourceBody {}
/// A `ResourceBody` that has the ability to take on more primary /// A `ResourceBody` that has the ability to take on more primary
/// resources by appending another similarly typed `ResourceBody`. /// resources by appending another similarly typed `ResourceBody`.
public protocol Appendable { public protocol ResourceBodyAppendable {
func appending(_ other: Self) -> Self func appending(_ other: Self) -> Self
} }
public func +<R: Appendable>(_ left: R, right: R) -> R { public func +<R: ResourceBodyAppendable>(_ left: R, right: R) -> R {
return left.appending(right) return left.appending(right)
} }
/// A type allowing for a document body containing 1 primary resource. /// A type allowing for a document body containing 1 primary resource.
/// If the `Entity` specialization is an `Optional` type, the body can contain /// If the `Entity` specialization is an `Optional` type, the body can contain
/// 0 or 1 primary resources. /// 0 or 1 primary resources.
public struct SingleResourceBody<Entity: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody { public struct SingleResourceBody<PrimaryResource: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
public let value: Entity public let value: PrimaryResource
public init(resourceObject: Entity) { public init(resourceObject: PrimaryResource) {
self.value = resourceObject self.value = resourceObject
} }
} }
/// A type allowing for a document body containing 0 or more primary resources. /// A type allowing for a document body containing 0 or more primary resources.
public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, Appendable { public struct ManyResourceBody<PrimaryResource: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
public let values: [Entity] public let values: [PrimaryResource]
public init(resourceObjects: [Entity]) { public init(resourceObjects: [PrimaryResource]) {
values = resourceObjects values = resourceObjects
} }
@@ -74,7 +74,9 @@ public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: Encoda
/// Use NoResourceBody to indicate you expect a JSON API document to not /// Use NoResourceBody to indicate you expect a JSON API document to not
/// contain a "data" top-level key. /// contain a "data" top-level key.
public struct NoResourceBody: ResourceBody { public struct NoResourceBody: CodableResourceBody {
public typealias PrimaryResource = Void
public static var none: NoResourceBody { return NoResourceBody() } public static var none: NoResourceBody { return NoResourceBody() }
} }
@@ -84,7 +86,7 @@ extension SingleResourceBody {
var container = encoder.singleValueContainer() var container = encoder.singleValueContainer()
let anyNil: Any? = nil let anyNil: Any? = nil
let nilValue = anyNil as? Entity let nilValue = anyNil as? PrimaryResource
guard value != nilValue else { guard value != nilValue else {
try container.encodeNil() try container.encodeNil()
return return
@@ -94,18 +96,18 @@ extension SingleResourceBody {
} }
} }
extension SingleResourceBody: Decodable, ResourceBody where Entity: OptionalPrimaryResource { extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let anyNil: Any? = nil let anyNil: Any? = nil
if container.decodeNil(), if container.decodeNil(),
let val = anyNil as? Entity { let val = anyNil as? PrimaryResource {
value = val value = val
return return
} }
value = try container.decode(Entity.self) value = try container.decode(PrimaryResource.self)
} }
} }
@@ -119,12 +121,21 @@ extension ManyResourceBody {
} }
} }
extension ManyResourceBody: Decodable, ResourceBody where Entity: PrimaryResource { extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer() var container = try decoder.unkeyedContainer()
var valueAggregator = [Entity]() var valueAggregator = [PrimaryResource]()
var idx = 0
while !container.isAtEnd { while !container.isAtEnd {
valueAggregator.append(try container.decode(Entity.self)) do {
valueAggregator.append(try container.decode(PrimaryResource.self))
} catch let error as ResourceObjectDecodingError {
throw ManyResourceBodyDecodingError(
error: error,
idx: idx
)
}
idx = idx + 1
} }
values = valueAggregator values = valueAggregator
} }
@@ -143,3 +154,9 @@ extension ManyResourceBody: CustomStringConvertible {
return "PrimaryResourceBody(\(String(describing: values)))" return "PrimaryResourceBody(\(String(describing: values)))"
} }
} }
// MARK: - DecodingError
public struct ManyResourceBodyDecodingError: Swift.Error, Equatable {
public let error: ResourceObjectDecodingError
public let idx: Int
}
-14
View File
@@ -1,14 +0,0 @@
//
// EncodingError.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/7/18.
//
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case illegalDecoding(String)
case missingOrMalformedMetadata
case missingOrMalformedLinks
}
@@ -6,10 +6,12 @@
// //
/// Most of the JSON:API Spec defined Error fields. /// Most of the JSON:API Spec defined Error fields.
public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType { public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Equatable, ErrorDictType, CustomStringConvertible {
/// a unique identifier for this particular occurrence of the problem /// a unique identifier for this particular occurrence of the problem
public let id: IdType? public let id: IdType?
// public let links: Links? // we skip this for now to avoid adding complexity to using this basic type. // public let links: Links? // we skip this for now to avoid adding complexity to using this basic type.
/// the HTTP status code applicable to this problem /// the HTTP status code applicable to this problem
public let status: String? public let status: String?
/// an application-specific error code /// an application-specific error code
@@ -20,6 +22,7 @@ public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Eq
public let detail: String? public let detail: String?
/// an object containing references to the source of the error /// an object containing references to the source of the error
public let source: Source? public let source: Source?
// public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type // public let meta: Meta? // we skip this for now to avoid adding complexity to using this basic type
public init(id: IdType? = nil, public init(id: IdType? = nil,
@@ -61,6 +64,10 @@ public struct BasicJSONAPIErrorPayload<IdType: Codable & Equatable>: Codable, Eq
].compactMap { $0 } ].compactMap { $0 }
return Dictionary(uniqueKeysWithValues: keysAndValues) return Dictionary(uniqueKeysWithValues: keysAndValues)
} }
public var description: String {
return definedFields.map { "\($0.key): \($0.value)" }.sorted().joined(separator: ", ")
}
} }
/// `BasicJSONAPIError` optionally decodes many possible fields /// `BasicJSONAPIError` optionally decodes many possible fields
@@ -8,7 +8,7 @@
/// `GenericJSONAPIError` can be used to specify whatever error /// `GenericJSONAPIError` can be used to specify whatever error
/// payload you expect to need to parse in responses and handle any /// payload you expect to need to parse in responses and handle any
/// other payload structure as `.unknownError`. /// other payload structure as `.unknownError`.
public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError { public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError, CustomStringConvertible {
case unknownError case unknownError
case error(ErrorPayload) case error(ErrorPayload)
@@ -35,6 +35,15 @@ public enum GenericJSONAPIError<ErrorPayload: Codable & Equatable>: JSONAPIError
public static var unknown: Self { public static var unknown: Self {
return .unknownError return .unknownError
} }
public var description: String {
switch self {
case .unknownError:
return "unknown error"
case .error(let payload):
return String(describing: payload)
}
}
} }
public extension GenericJSONAPIError { public extension GenericJSONAPIError {
+27
View File
@@ -0,0 +1,27 @@
//
// JSONAPICodingError.swift
// JSONAPI
//
// Created by Mathew Polzin on 12/7/18.
//
public enum JSONAPICodingError: Swift.Error {
case typeMismatch(expected: String, found: String, path: [CodingKey])
case quantityMismatch(expected: Quantity, path: [CodingKey])
case illegalEncoding(String, path: [CodingKey])
case illegalDecoding(String, path: [CodingKey])
case missingOrMalformedMetadata(path: [CodingKey])
case missingOrMalformedLinks(path: [CodingKey])
public enum Quantity: String, Equatable {
case one
case many
public var other: Quantity {
switch self {
case .one: return .many
case .many: return .one
}
}
}
}
+1 -1
View File
@@ -5,7 +5,7 @@
// Created by Mathew Polzin on 11/24/18. // Created by Mathew Polzin on 11/24/18.
// //
/// A Links structure should contain nothing but JSONAPI.Link properties. /// A Links structure should contain nothing but `JSONAPI.Link` properties.
public protocol Links: Codable, Equatable {} public protocol Links: Codable, Equatable {}
/// Use NoLinks where no links should belong to a JSON API component /// Use NoLinks where no links should belong to a JSON API component
+4 -4
View File
@@ -6,17 +6,17 @@
// //
/// Conform a type to this protocol to indicate it can be encoded to or decoded from /// Conform a type to this protocol to indicate it can be encoded to or decoded from
/// the meta data attached to a component of a JSON API document. Different meta data /// the meta data attached to a component of a JSON:API document. Different meta data
/// can be stored all over the place: On the root document, on a resource object, on /// can be stored all over the place: On the root document, on a resource object, on
/// link objects, etc. /// link objects, etc.
/// ///
/// JSON API Metadata is totally open ended. It can take whatever JSON-compliant structure /// JSON:API Metadata is totally open ended. It can take whatever JSON-compliant structure
/// the server and client agree upon. /// the server and client agree upon.
public protocol Meta: Codable, Equatable { public protocol Meta: Codable, Equatable {
} }
// We make Optional a Meta if it wraps a Meta so that Metadata can be specified as // We make Optional a Meta if it wraps a Meta so that
// nullable. // Metadata can be specified as nullable.
extension Optional: Meta where Wrapped: Meta {} extension Optional: Meta where Wrapped: Meta {}
/// Use this type when you want to specify not to encode or decode any metadata /// Use this type when you want to specify not to encode or decode any metadata
+11 -2
View File
@@ -5,13 +5,21 @@
// Created by Mathew Polzin on 11/13/18. // Created by Mathew Polzin on 11/13/18.
// //
public protocol AttributeType: Codable { public protocol AbstractAttributeType {
var rawValueType: Any.Type { get }
}
public protocol AttributeType: Codable, AbstractAttributeType {
associatedtype RawValue: Codable associatedtype RawValue: Codable
associatedtype ValueType associatedtype ValueType
var value: ValueType { get } var value: ValueType { get }
} }
extension AttributeType {
public var rawValueType: Any.Type { return RawValue.self }
}
// MARK: TransformedAttribute // MARK: TransformedAttribute
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type. /// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
@@ -141,7 +149,8 @@ extension Attribute {
// MARK: Attribute decoding and encoding defaults // MARK: Attribute decoding and encoding defaults
extension AttributeType { extension AttributeType {
public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container, forKey key: Container.Key) throws -> Self { public static func defaultDecoding<Container: KeyedDecodingContainerProtocol>(from container: Container,
forKey key: Container.Key) throws -> Self {
return try container.decode(Self.self, forKey: key) return try container.decode(Self.self, forKey: key)
} }
} }
+5 -2
View File
@@ -45,7 +45,10 @@ public protocol OptionalId: Codable {
init(rawValue: RawType) init(rawValue: RawType)
} }
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {} /// marker protocol
public protocol AbstractId {}
public protocol IdType: AbstractId, OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {} extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
extension Optional: OptionalId where Wrapped: IdType { extension Optional: OptionalId where Wrapped: IdType {
@@ -94,7 +97,7 @@ public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equa
} }
} }
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType { extension Id: Hashable, CustomStringConvertible, AbstractId, IdType where RawType: RawIdType {
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> { public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
return Id(rawValue: rawValue) return Id(rawValue: rawValue)
} }
@@ -18,69 +18,147 @@ import Poly
public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource
public typealias EncodablePolyWrapped = Encodable & Equatable public typealias EncodablePolyWrapped = Encodable & Equatable
public typealias PolyWrapped = EncodablePolyWrapped & Decodable public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable
extension Poly0: PrimaryResource { extension Poly0: CodablePrimaryResource {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
throw JSONAPIEncodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.") throw JSONAPICodingError.illegalDecoding("Attempted to decode Poly0, which should represent a thing that is not expected to be found in a document.", path: decoder.codingPath)
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
throw JSONAPIEncodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.") throw JSONAPICodingError.illegalEncoding("Attempted to encode Poly0, which should represent a thing that is not expected to be found in a document.", path: encoder.codingPath)
} }
} }
// MARK: - 1 type // MARK: - 1 type
extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped {} extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped {}
extension Poly1: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped {} extension Poly1: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped {}
// MARK: - 2 types // MARK: - 2 types
extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped {} extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped, B: EncodablePolyWrapped {}
extension Poly2: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped {} extension Poly2: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped {}
// MARK: - 3 types // MARK: - 3 types
extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {} extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {}
extension Poly3: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {} extension Poly3: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped {}
// MARK: - 4 types // MARK: - 4 types
extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {} 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 {} extension Poly4: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped {}
// MARK: - 5 types // MARK: - 5 types
extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {} 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 {} extension Poly5: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped {}
// MARK: - 6 types // MARK: - 6 types
extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {} 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 {} extension Poly6: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped {}
// MARK: - 7 types // MARK: - 7 types
extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped {} 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 {} extension Poly7: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped {}
// MARK: - 8 types // MARK: - 8 types
extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped {} 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 {} extension Poly8: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped {}
// MARK: - 9 types // MARK: - 9 types
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: 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 {} extension Poly9: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped {}
// MARK: - 10 types // MARK: - 10 types
extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped {} extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where
A: EncodablePolyWrapped,
B: EncodablePolyWrapped,
C: EncodablePolyWrapped,
D: EncodablePolyWrapped,
E: EncodablePolyWrapped,
F: EncodablePolyWrapped,
G: EncodablePolyWrapped,
H: EncodablePolyWrapped,
I: EncodablePolyWrapped,
J: EncodablePolyWrapped {}
extension Poly10: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {} extension Poly10: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped, J: CodablePolyWrapped {}
// MARK: - 11 types // MARK: - 11 types
extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped, G: EncodablePolyWrapped, H: EncodablePolyWrapped, I: EncodablePolyWrapped, J: EncodablePolyWrapped, K: EncodablePolyWrapped {} extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where
A: EncodablePolyWrapped,
B: EncodablePolyWrapped,
C: EncodablePolyWrapped,
D: EncodablePolyWrapped,
E: EncodablePolyWrapped,
F: EncodablePolyWrapped,
G: EncodablePolyWrapped,
H: EncodablePolyWrapped,
I: EncodablePolyWrapped,
J: EncodablePolyWrapped,
K: EncodablePolyWrapped {}
extension Poly11: PrimaryResource, OptionalPrimaryResource where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped, K: PolyWrapped {} extension Poly11: CodablePrimaryResource, OptionalCodablePrimaryResource
where
A: CodablePolyWrapped,
B: CodablePolyWrapped,
C: CodablePolyWrapped,
D: CodablePolyWrapped,
E: CodablePolyWrapped,
F: CodablePolyWrapped,
G: CodablePolyWrapped,
H: CodablePolyWrapped,
I: CodablePolyWrapped,
J: CodablePolyWrapped,
K: CodablePolyWrapped {}
+40 -6
View File
@@ -170,18 +170,36 @@ extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
// succeeds and then attempt to coerce nil to a Identifier // succeeds and then attempt to coerce nil to a Identifier
// type at which point we can store nil in `id`. // type at which point we can store nil in `id`.
let anyNil: Any? = nil let anyNil: Any? = nil
if try container.decodeNil(forKey: .data), if try container.decodeNil(forKey: .data) {
let val = anyNil as? Identifiable.Identifier { guard let val = anyNil as? Identifiable.Identifier else {
throw DecodingError.valueNotFound(
Self.self,
DecodingError.Context(
codingPath: decoder.codingPath,
debugDescription: "Expected non-null relationship data."
)
)
}
id = val id = val
return return
} }
let identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data) let identifier: KeyedDecodingContainer<ResourceIdentifierCodingKeys>
do {
identifier = try container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
} catch let error as DecodingError {
guard case let .typeMismatch(type, context) = error,
type is _DictionaryType.Type else {
throw error
}
throw JSONAPICodingError.quantityMismatch(expected: .one,
path: context.codingPath)
}
let type = try identifier.decode(String.self, forKey: .entityType) let type = try identifier.decode(String.self, forKey: .entityType)
guard type == Identifiable.jsonType else { guard type == Identifiable.jsonType else {
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.jsonType, found: type) throw JSONAPICodingError.typeMismatch(expected: Identifiable.jsonType, found: type, path: decoder.codingPath)
} }
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id)) id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
@@ -230,7 +248,17 @@ extension ToManyRelationship: Codable {
links = try container.decode(LinksType.self, forKey: .links) links = try container.decode(LinksType.self, forKey: .links)
} }
var identifiers = try container.nestedUnkeyedContainer(forKey: .data) var identifiers: UnkeyedDecodingContainer
do {
identifiers = try container.nestedUnkeyedContainer(forKey: .data)
} catch let error as DecodingError {
guard case let .typeMismatch(type, context) = error,
type is _ArrayType.Type else {
throw error
}
throw JSONAPICodingError.quantityMismatch(expected: .many,
path: context.codingPath)
}
var newIds = [Relatable.Identifier]() var newIds = [Relatable.Identifier]()
while !identifiers.isAtEnd { while !identifiers.isAtEnd {
@@ -239,7 +267,7 @@ extension ToManyRelationship: Codable {
let type = try identifier.decode(String.self, forKey: .entityType) let type = try identifier.decode(String.self, forKey: .entityType)
guard type == Relatable.jsonType else { guard type == Relatable.jsonType else {
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.jsonType, found: type) throw JSONAPICodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath)
} }
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id))) newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
@@ -277,3 +305,9 @@ extension ToOneRelationship: CustomStringConvertible {
extension ToManyRelationship: CustomStringConvertible { extension ToManyRelationship: CustomStringConvertible {
public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" } public var description: String { return "Relationship([\(ids.map(String.init(describing:)).joined(separator: ", "))])" }
} }
private protocol _DictionaryType {}
extension Dictionary: _DictionaryType {}
private protocol _ArrayType {}
extension Array: _ArrayType {}

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