Compare commits

...

28 Commits

Author SHA1 Message Date
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
54 changed files with 3395 additions and 992 deletions
@@ -37,7 +37,7 @@ typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Ent
// JSON:API Documents for this particular API to have Metadata, Links,
// 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, BasicJSONAPIError<String>>
typealias Document<PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: JSONAPI.Include> = JSONAPI.Document<PrimaryResourceBody, NoMetadata, NoLinks, IncludeType, NoAPIDescription, BasicJSONAPIError<String>>
// MARK: Entity Definitions
@@ -64,11 +64,11 @@ if case let .data(bodyData) = peopleResponse.body {
// MARK: - Work in the abstract
print("-----")
func process<T: JSONAPIDocument>(document: T) {
guard case let .data(body) = document.body else {
func process<T: CodableJSONAPIDocument>(document: T) {
guard let body = document.body.data else {
return
}
let x: T.Body.Data = body
let x: T.BodyData = body
}
process(document: peopleResponse)
+2 -2
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
#
spec.name = "MP-JSONAPI"
spec.version = "2.5.0"
spec.version = "3.0.0"
spec.summary = "Swift Codable JSON API framework."
# 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.xcconfig = { "HEADER_SEARCH_PATHS" => "$(SDKROOT)/usr/include/libxml2" }
spec.dependency "Poly", "~> 2.2"
spec.dependency "Poly", "~> 2.3.1"
end
+2 -2
View File
@@ -6,8 +6,8 @@
"repositoryURL": "https://github.com/mattpolzin/Poly.git",
"state": {
"branch": null,
"revision": "18cd995be5c28c4dfdc1464e54ee0efb03e215bf",
"version": "2.3.0"
"revision": "0c9c08204142babc480938d704a23513d11420e5",
"version": "2.3.1"
}
}
]
+1 -1
View File
@@ -18,7 +18,7 @@ let package = Package(
targets: ["JSONAPITesting"])
],
dependencies: [
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.0")),
.package(url: "https://github.com/mattpolzin/Poly.git", .upToNextMajor(from: "2.3.1")),
],
targets: [
.target(
+56 -636
View File
File diff suppressed because it is too large Load Diff
+35 -15
View File
@@ -349,8 +349,8 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody:
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootCodingKeys.self)
if let noData = NoAPIDescription() as? APIDescription {
apiDescription = noData
if let noAPIDescription = NoAPIDescription() as? APIDescription {
apiDescription = noAPIDescription
} else {
apiDescription = try container.decode(APIDescription.self, forKey: .jsonapi)
}
@@ -389,18 +389,30 @@ extension Document: Decodable, CodableJSONAPIDocument where PrimaryResourceBody:
if let noData = NoResourceBody() as? PrimaryResourceBody {
data = noData
} else {
data = try container.decode(PrimaryResourceBody.self, forKey: .data)
do {
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)
// TODO come back to this and make robust
let maybeIncludes: Includes<Include>?
do {
maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
} catch let error as IncludesDecodingError {
throw DocumentDecodingError(error)
}
guard let metaVal = meta else {
throw JSONAPIEncodingError.missingOrMalformedMetadata
throw JSONAPICodingError.missingOrMalformedMetadata(path: decoder.codingPath)
}
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))
@@ -480,6 +492,10 @@ extension Document {
public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool {
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
@@ -520,7 +536,7 @@ extension Document {
/// `nil` if the Document is an error response. Otherwise,
/// a structure containing the primary resource, any included
/// resources, metadata, and links.
var data: BodyData? {
public var data: BodyData? {
return document.body.data
}
@@ -531,7 +547,7 @@ extension Document {
/// resources dependening on the `PrimaryResourceBody` type.
///
/// See `SingleResourceBody` and `ManyResourceBody`.
var primaryResource: PrimaryResourceBody? {
public var primaryResource: PrimaryResourceBody? {
return document.body.primaryResource
}
@@ -539,25 +555,29 @@ extension Document {
///
/// `nil` if the Document is an error document. Otherwise,
/// zero or more includes.
var includes: Includes<IncludeType>? {
public var includes: Includes<IncludeType>? {
return document.body.includes
}
/// The metadata for the error or data document or `nil` if
/// no metadata is found.
var meta: MetaType? {
public var meta: MetaType? {
return document.body.meta
}
/// The links for the error or data document or `nil` if
/// no links are found.
var links: LinksType? {
public var links: LinksType? {
return document.body.links
}
public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool {
return lhs == rhs.document
}
public static func ==(lhs: SuccessDocument, rhs: Document) -> Bool {
return lhs.document == rhs
}
}
}
@@ -569,7 +589,7 @@ extension Document.ErrorDocument: Decodable, CodableJSONAPIDocument
document = try container.decode(Document.self)
guard document.body.isError else {
throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError
throw DocumentDecodingError.foundSuccessDocumentWhenExpectingError
}
}
}
@@ -582,7 +602,7 @@ extension Document.SuccessDocument: Decodable, CodableJSONAPIDocument
document = try container.decode(Document.self)
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.
//
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 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."
}
}
}
+58 -2
View File
@@ -32,7 +32,7 @@ public struct Includes<I: Include>: Encodable, Equatable {
var container = encoder.unkeyedContainer()
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 {
@@ -56,8 +56,35 @@ extension Includes: Decodable where I: Decodable {
}
var valueAggregator = [I]()
var idx = 0
while !container.isAtEnd {
valueAggregator.append(try container.decode(I.self))
do {
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
@@ -177,3 +204,32 @@ extension Includes where I: _Poly11 {
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")
}
}
+33 -14
View File
@@ -29,7 +29,9 @@ extension Optional: OptionalCodablePrimaryResource where Wrapped: CodablePrimary
/// 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 {}
public protocol EncodableResourceBody: Equatable, Encodable {
associatedtype PrimaryResource
}
/// 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)
@@ -49,19 +51,19 @@ public func +<R: ResourceBodyAppendable>(_ left: R, right: R) -> R {
/// 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 struct SingleResourceBody<PrimaryResource: JSONAPI.OptionalEncodablePrimaryResource>: EncodableResourceBody {
public let value: PrimaryResource
public init(resourceObject: Entity) {
public init(resourceObject: PrimaryResource) {
self.value = resourceObject
}
}
/// A type allowing for a document body containing 0 or more primary resources.
public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
public let values: [Entity]
public struct ManyResourceBody<PrimaryResource: JSONAPI.EncodablePrimaryResource>: EncodableResourceBody, ResourceBodyAppendable {
public let values: [PrimaryResource]
public init(resourceObjects: [Entity]) {
public init(resourceObjects: [PrimaryResource]) {
values = resourceObjects
}
@@ -73,6 +75,8 @@ public struct ManyResourceBody<Entity: JSONAPI.EncodablePrimaryResource>: Encoda
/// Use NoResourceBody to indicate you expect a JSON API document to not
/// contain a "data" top-level key.
public struct NoResourceBody: CodableResourceBody {
public typealias PrimaryResource = Void
public static var none: NoResourceBody { return NoResourceBody() }
}
@@ -82,7 +86,7 @@ extension SingleResourceBody {
var container = encoder.singleValueContainer()
let anyNil: Any? = nil
let nilValue = anyNil as? Entity
let nilValue = anyNil as? PrimaryResource
guard value != nilValue else {
try container.encodeNil()
return
@@ -92,18 +96,18 @@ extension SingleResourceBody {
}
}
extension SingleResourceBody: Decodable, CodableResourceBody where Entity: OptionalCodablePrimaryResource {
extension SingleResourceBody: Decodable, CodableResourceBody where PrimaryResource: OptionalCodablePrimaryResource {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let anyNil: Any? = nil
if container.decodeNil(),
let val = anyNil as? Entity {
let val = anyNil as? PrimaryResource {
value = val
return
}
value = try container.decode(Entity.self)
value = try container.decode(PrimaryResource.self)
}
}
@@ -117,12 +121,21 @@ extension ManyResourceBody {
}
}
extension ManyResourceBody: Decodable, CodableResourceBody where Entity: CodablePrimaryResource {
extension ManyResourceBody: Decodable, CodableResourceBody where PrimaryResource: CodablePrimaryResource {
public init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var valueAggregator = [Entity]()
var valueAggregator = [PrimaryResource]()
var idx = 0
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
}
@@ -141,3 +154,9 @@ extension ManyResourceBody: CustomStringConvertible {
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
}
+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.
//
/// 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 {}
/// Use NoLinks where no links should belong to a JSON API component
+9 -1
View File
@@ -5,13 +5,21 @@
// 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 ValueType
var value: ValueType { get }
}
extension AttributeType {
public var rawValueType: Any.Type { return RawValue.self }
}
// MARK: TransformedAttribute
/// A TransformedAttribute takes a Codable type and attempts to turn it into another type.
+5 -2
View File
@@ -45,7 +45,10 @@ public protocol OptionalId: Codable {
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: 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> {
return Id(rawValue: rawValue)
}
@@ -18,15 +18,15 @@ import Poly
public typealias EncodableJSONPoly = Poly & EncodablePrimaryResource
public typealias EncodablePolyWrapped = Encodable & Equatable
public typealias PolyWrapped = EncodablePolyWrapped & Decodable
public typealias CodablePolyWrapped = EncodablePolyWrapped & Decodable
extension Poly0: CodablePrimaryResource {
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 {
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)
}
}
@@ -35,42 +35,42 @@ extension Poly1: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped {}
extension Poly1: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped {}
where A: CodablePolyWrapped {}
// MARK: - 2 types
extension Poly2: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped, B: EncodablePolyWrapped {}
extension Poly2: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped {}
// MARK: - 3 types
extension Poly3: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped {}
extension Poly3: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped {}
// MARK: - 4 types
extension Poly4: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped {}
extension Poly4: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped {}
// MARK: - 5 types
extension Poly5: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped {}
extension Poly5: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped {}
// MARK: - 6 types
extension Poly6: EncodablePrimaryResource, OptionalEncodablePrimaryResource
where A: EncodablePolyWrapped, B: EncodablePolyWrapped, C: EncodablePolyWrapped, D: EncodablePolyWrapped, E: EncodablePolyWrapped, F: EncodablePolyWrapped {}
extension Poly6: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped {}
// MARK: - 7 types
extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource
@@ -84,7 +84,7 @@ extension Poly7: EncodablePrimaryResource, OptionalEncodablePrimaryResource
G: EncodablePolyWrapped {}
extension Poly7: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped {}
// MARK: - 8 types
extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource
@@ -99,7 +99,7 @@ extension Poly8: EncodablePrimaryResource, OptionalEncodablePrimaryResource
H: EncodablePolyWrapped {}
extension Poly8: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped {}
// MARK: - 9 types
extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource
@@ -115,7 +115,7 @@ extension Poly9: EncodablePrimaryResource, OptionalEncodablePrimaryResource
I: EncodablePolyWrapped {}
extension Poly9: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped {}
// MARK: - 10 types
extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource
@@ -132,7 +132,7 @@ extension Poly10: EncodablePrimaryResource, OptionalEncodablePrimaryResource
J: EncodablePolyWrapped {}
extension Poly10: CodablePrimaryResource, OptionalCodablePrimaryResource
where A: PolyWrapped, B: PolyWrapped, C: PolyWrapped, D: PolyWrapped, E: PolyWrapped, F: PolyWrapped, G: PolyWrapped, H: PolyWrapped, I: PolyWrapped, J: PolyWrapped {}
where A: CodablePolyWrapped, B: CodablePolyWrapped, C: CodablePolyWrapped, D: CodablePolyWrapped, E: CodablePolyWrapped, F: CodablePolyWrapped, G: CodablePolyWrapped, H: CodablePolyWrapped, I: CodablePolyWrapped, J: CodablePolyWrapped {}
// MARK: - 11 types
extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource
@@ -151,14 +151,14 @@ extension Poly11: EncodablePrimaryResource, OptionalEncodablePrimaryResource
extension Poly11: CodablePrimaryResource, OptionalCodablePrimaryResource
where
A: PolyWrapped,
B: PolyWrapped,
C: PolyWrapped,
D: PolyWrapped,
E: PolyWrapped,
F: PolyWrapped,
G: PolyWrapped,
H: PolyWrapped,
I: PolyWrapped,
J: PolyWrapped,
K: PolyWrapped {}
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
// type at which point we can store nil in `id`.
let anyNil: Any? = nil
if try container.decodeNil(forKey: .data),
let val = anyNil as? Identifiable.Identifier {
if try container.decodeNil(forKey: .data) {
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
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)
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))
@@ -230,7 +248,17 @@ extension ToManyRelationship: Codable {
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]()
while !identifiers.isAtEnd {
@@ -239,7 +267,7 @@ extension ToManyRelationship: Codable {
let type = try identifier.decode(String.self, forKey: .entityType)
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)))
@@ -277,3 +305,9 @@ extension ToOneRelationship: CustomStringConvertible {
extension ToManyRelationship: CustomStringConvertible {
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 {}
@@ -96,10 +96,13 @@ extension ResourceObjectProxy {
public static var jsonType: String { return Description.jsonType }
}
/// A marker protocol.
public protocol AbstractResourceObject {}
/// ResourceObjectType is the protocol that ResourceObject conforms to. This
/// protocol lets other types accept any ResourceObject as a generic
/// specialization.
public protocol ResourceObjectType: ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription {
public protocol ResourceObjectType: AbstractResourceObject, ResourceObjectProxy, CodablePrimaryResource where Description: ResourceObjectDescription {
associatedtype Meta: JSONAPI.Meta
associatedtype Links: JSONAPI.Links
@@ -411,21 +414,56 @@ public extension ResourceObject {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ResourceObjectCodingKeys.self)
let type = try container.decode(String.self, forKey: .type)
let type: String
do {
type = try container.decode(String.self, forKey: .type)
} catch let error as DecodingError {
throw ResourceObjectDecodingError(error)
?? error
}
guard ResourceObject.jsonType == type else {
throw JSONAPIEncodingError.typeMismatch(expected: Description.jsonType, found: type)
throw ResourceObjectDecodingError(
expectedJSONAPIType: ResourceObject.jsonType,
found: type
)
}
let maybeUnidentified = Unidentified() as? EntityRawIdType
id = try maybeUnidentified.map { ResourceObject.Id(rawValue: $0) } ?? container.decode(ResourceObject.Id.self, forKey: .id)
attributes = try (NoAttributes() as? Description.Attributes) ??
container.decode(Description.Attributes.self, forKey: .attributes)
do {
attributes = try (NoAttributes() as? Description.Attributes)
?? container.decodeIfPresent(Description.Attributes.self, forKey: .attributes)
?? Description.Attributes(from: EmptyObjectDecoder())
} catch let decodingError as DecodingError {
throw ResourceObjectDecodingError(decodingError)
?? decodingError
} catch _ as EmptyObjectDecodingError {
throw ResourceObjectDecodingError(
subjectName: ResourceObjectDecodingError.entireObject,
cause: .keyNotFound,
location: .attributes
)
}
relationships = try (NoRelationships() as? Description.Relationships)
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
?? Description.Relationships(from: EmptyObjectDecoder())
do {
relationships = try (NoRelationships() as? Description.Relationships)
?? container.decodeIfPresent(Description.Relationships.self, forKey: .relationships)
?? Description.Relationships(from: EmptyObjectDecoder())
} catch let decodingError as DecodingError {
throw ResourceObjectDecodingError(decodingError)
?? decodingError
} catch let decodingError as JSONAPICodingError {
throw ResourceObjectDecodingError(decodingError)
?? decodingError
} catch _ as EmptyObjectDecodingError {
throw ResourceObjectDecodingError(
subjectName: ResourceObjectDecodingError.entireObject,
cause: .keyNotFound,
location: .relationships
)
}
meta = try (NoMetadata() as? MetaType) ?? container.decode(MetaType.self, forKey: .meta)
@@ -0,0 +1,135 @@
//
// ResourceObjectDecodingError.swift
//
//
// Created by Mathew Polzin on 11/10/19.
//
public struct ResourceObjectDecodingError: Swift.Error, Equatable {
public let subjectName: String
public let cause: Cause
public let location: Location
static let entireObject = "entire object"
public enum Cause: Equatable {
case keyNotFound
case valueNotFound
case typeMismatch(expectedTypeName: String)
case jsonTypeMismatch(expectedType: String, foundType: String)
case quantityMismatch(expected: JSONAPICodingError.Quantity)
}
public enum Location: String, Equatable {
case attributes
case relationships
case type
var singular: String {
switch self {
case .attributes: return "attribute"
case .relationships: return "relationship"
case .type: return "type"
}
}
}
init?(_ decodingError: DecodingError) {
switch decodingError {
case .typeMismatch(let expectedType, let ctx):
(location, subjectName) = Self.context(ctx)
let typeString = String(describing: expectedType)
cause = .typeMismatch(expectedTypeName: typeString)
case .valueNotFound(_, let ctx):
(location, subjectName) = Self.context(ctx)
cause = .valueNotFound
case .keyNotFound(let missingKey, let ctx):
(location, _) = Self.context(ctx)
subjectName = missingKey.stringValue
cause = .keyNotFound
default:
return nil
}
}
init?(_ jsonAPIError: JSONAPICodingError) {
switch jsonAPIError {
case .typeMismatch(expected: let expected, found: let found, path: let path):
(location, subjectName) = Self.context(path: path)
cause = .jsonTypeMismatch(expectedType: expected, foundType: found)
case .quantityMismatch(expected: let expected, path: let path):
(location, subjectName) = Self.context(path: path)
cause = .quantityMismatch(expected: expected)
default:
return nil
}
}
init(expectedJSONAPIType: String, found: String) {
location = .type
subjectName = "self"
cause = .jsonTypeMismatch(expectedType: expectedJSONAPIType, foundType: found)
}
init(subjectName: String, cause: Cause, location: Location) {
self.subjectName = subjectName
self.cause = cause
self.location = location
}
static func context(_ decodingContext: DecodingError.Context) -> (Location, name: String) {
return context(path: decodingContext.codingPath)
}
static func context(path: [CodingKey]) -> (Location, name: String) {
let location: Location
if path.contains(where: { $0.stringValue == "attributes" }) {
location = .attributes
} else if path.contains(where: { $0.stringValue == "relationships" }) {
location = .relationships
} else {
location = .type
}
return (
location,
name: path.last?.stringValue ?? "unnamed"
)
}
}
extension ResourceObjectDecodingError: CustomStringConvertible {
public var description: String {
switch cause {
case .keyNotFound where subjectName == ResourceObjectDecodingError.entireObject:
return "\(location) object is required and missing."
case .keyNotFound where location == .type:
return "'type' (a.k.a. JSON:API type name) is required and missing."
case .keyNotFound:
return "'\(subjectName)' \(location.singular) is required and missing."
case .valueNotFound where location == .type:
return "'\(location.singular)' (a.k.a. JSON:API type name) is not nullable but null was found."
case .valueNotFound:
return "'\(subjectName)' \(location.singular) is not nullable but null was found."
case .typeMismatch(expectedTypeName: let expected) where location == .type:
return "'\(location.singular)' (a.k.a. the JSON:API type name) is not a \(expected) as expected."
case .typeMismatch(expectedTypeName: let expected):
return "'\(subjectName)' \(location.singular) is not a \(expected) as expected."
case .jsonTypeMismatch(expectedType: let expected, foundType: let found) where location == .type:
return "found JSON:API type \"\(found)\" but expected \"\(expected)\""
case .jsonTypeMismatch(expectedType: let expected, foundType: let found):
return "'\(subjectName)' \(location.singular) is of JSON:API type \"\(found)\" but it was expected to be \"\(expected)\""
case .quantityMismatch(expected: let expected):
let expecation: String = {
switch expected {
case .many:
return "\(expected) values"
case .one:
return "\(expected) value"
}
}()
return "'\(subjectName)' \(location.singular) should contain \(expecation) but found \(expected.other)"
}
}
}
@@ -158,7 +158,7 @@ struct SparseFieldKeyedEncodingContainer<Key, SparseKey>: KeyedEncodingContainer
forKey key: Key) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
guard shouldAllow(key: key) else {
return KeyedEncodingContainer(
// TODO: not needed by JSONAPI library, but for completeness could
// NOTE: not needed by JSONAPI library, but for completeness could
// add an EmptyObjectEncoder that can be returned here so that
// at least nothing gets encoded within the nested container
SparseFieldKeyedEncodingContainer<NestedKey, SparseKey>(wrapping: wrappedContainer.nestedContainer(keyedBy: keyType,
@@ -176,7 +176,7 @@ struct SparseFieldKeyedEncodingContainer<Key, SparseKey>: KeyedEncodingContainer
public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer {
guard shouldAllow(key: key) else {
// TODO: not needed by JSONAPI library, but for completeness could
// NOTE: not needed by JSONAPI library, but for completeness could
// add an EmptyObjectEncoder that can be returned here so that
// at least nothing gets encoded within the nested container
return wrappedContainer.nestedUnkeyedContainer(forKey: key)

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