Add Document.ErrorDocument and Document.SuccessDocument types

This commit is contained in:
Mathew Polzin
2019-10-20 22:23:52 -07:00
parent b46429a0ad
commit ea5c0b8601
3 changed files with 277 additions and 0 deletions
+134
View File
@@ -414,3 +414,137 @@ extension Document.Body.Data: CustomStringConvertible {
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
}
}
// MARK: - Error and Success Document Types
extension Document {
/// 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.
@dynamicMemberLookup
public struct ErrorDocument: EncodableJSONAPIDocument {
public var body: Document.Body { return document.body }
private let document: Document
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
document = .init(apiDescription: apiDescription, errors: errors, meta: meta, links: links)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(document)
}
public subscript<T>(dynamicMember path: KeyPath<Document, T>) -> T {
return document[keyPath: path]
}
public static func ==(lhs: Document, rhs: ErrorDocument) -> Bool {
return lhs == rhs.document
}
}
/// 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.
@dynamicMemberLookup
public struct SuccessDocument: EncodableJSONAPIDocument {
public var body: Document.Body { return document.body }
private let document: Document
public init(apiDescription: APIDescription,
body: PrimaryResourceBody,
includes: Includes<Include>,
meta: MetaType,
links: LinksType) {
document = .init(apiDescription: apiDescription,
body: body,
includes: includes,
meta: meta,
links: links)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(document)
}
public subscript<T>(dynamicMember path: KeyPath<Document, T>) -> T {
return document[keyPath: path]
}
public static func ==(lhs: Document, rhs: SuccessDocument) -> Bool {
return lhs == rhs.document
}
}
}
extension Document.ErrorDocument: Decodable, JSONAPIDocument
where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
document = try container.decode(Document.self)
guard document.body.isError else {
throw JSONAPIDocumentDecodingError.foundSuccessDocumentWhenExpectingError
}
}
}
extension Document.SuccessDocument: Decodable, JSONAPIDocument
where PrimaryResourceBody: ResourceBody, IncludeType: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
document = try container.decode(Document.self)
guard !document.body.isError else {
throw JSONAPIDocumentDecodingError.foundErrorDocumentWhenExpectingSuccess
}
}
}
extension Document.SuccessDocument where IncludeType == NoIncludes {
/// Create a new Document with the given includes.
public func including<I: JSONAPI.Include>(_ includes: Includes<I>) -> Document<PrimaryResourceBody, MetaType, LinksType, I, APIDescription, Error> {
// Note that if IncludeType is NoIncludes, then we allow anything
// to be included, but if IncludeType already specifies a type
// of thing to be expected then we lock that down.
// See: Document.including() where IncludeType: _Poly1
switch document.body {
case .data(let data):
return .init(apiDescription: document.apiDescription,
body: data.primary,
includes: includes,
meta: data.meta,
links: data.links)
case .errors:
fatalError("SuccessDocument cannot end up in an error state")
}
}
}
// extending where _Poly1 means all non-zero _Poly arities are included
extension Document.SuccessDocument where IncludeType: _Poly1 {
/// Create a new Document adding the given includes. This does not
/// remove existing includes; it is additive.
public func including(_ includes: Includes<IncludeType>) -> Document {
// Note that if IncludeType is NoIncludes, then we allow anything
// to be included, but if IncludeType already specifies a type
// of thing to be expected then we lock that down.
// See: Document.including() where IncludeType == NoIncludes
switch document.body {
case .data(let data):
return .init(apiDescription: document.apiDescription,
body: data.primary,
includes: data.includes + includes,
meta: data.meta,
links: data.links)
case .errors:
fatalError("SuccessDocument cannot end up in an error state")
}
}
}
@@ -0,0 +1,11 @@
//
// DocumentDecodingErro.swift
//
//
// Created by Mathew Polzin on 10/20/19.
//
public enum JSONAPIDocumentDecodingError: Swift.Error {
case foundErrorDocumentWhenExpectingSuccess
case foundSuccessDocumentWhenExpectingError
}
@@ -23,6 +23,7 @@ class DocumentTests: XCTestCase {
XCTAssert(Doc.Error.self == UnknownJSONAPIError.self)
}
// Document
test(JSONAPI.Document<
NoResourceBody,
NoMetadata,
@@ -37,6 +38,35 @@ class DocumentTests: XCTestCase {
meta: .none,
links: .none
))
// Document.SuccessDocument
test(JSONAPI.Document<
NoResourceBody,
NoMetadata,
NoLinks,
NoIncludes,
NoAPIDescription,
UnknownJSONAPIError
>.SuccessDocument(
apiDescription: .none,
body: .none,
includes: .none,
meta: .none,
links: .none
))
// Document.ErrorDocument
test(JSONAPI.Document<
NoResourceBody,
NoMetadata,
NoLinks,
NoIncludes,
NoAPIDescription,
UnknownJSONAPIError
>.ErrorDocument(
apiDescription: .none,
errors: []
))
}
func test_singleDocumentNull() {
@@ -51,11 +81,34 @@ class DocumentTests: XCTestCase {
XCTAssertEqual(document.body.includes?.count, 0)
XCTAssertEqual(document.body.links, NoLinks())
XCTAssertEqual(document.apiDescription, .none)
// SuccessDocument
let document2 = decoded(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
data: single_document_null)
XCTAssert(document == document2)
XCTAssertFalse(document2.body.isError)
XCTAssertNil(document2.body.errors)
XCTAssertNotNil(document2.body.primaryResource)
XCTAssertEqual(document2.body.meta, NoMetadata())
XCTAssertNil(document2.body.primaryResource?.value)
XCTAssertEqual(document2.body.includes?.count, 0)
XCTAssertEqual(document2.body.links, NoLinks())
XCTAssertEqual(document2.apiDescription, .none)
// ErrorDocument
XCTAssertThrowsError(try JSONDecoder().decode(Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
from: single_document_null))
}
func test_singleDocumentNull_encode() {
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.self,
data: single_document_null)
// SuccessDocument
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
data: single_document_null)
}
func test_singleDocumentNullWithAPIDescription() {
@@ -94,6 +147,14 @@ extension DocumentTests {
func test_errorDocumentFailsWithNoAPIDescription() {
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self,
from: error_document_no_metadata))
// SuccessDocument
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
from: error_document_no_metadata))
// ErrorDocument
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
from: error_document_no_metadata))
}
func test_unknownErrorDocumentNoMeta() {
@@ -115,6 +176,32 @@ extension DocumentTests {
XCTAssertEqual(errors.0, document.body.errors)
XCTAssertEqual(errors.0[0], .unknown)
XCTAssertEqual(errors.meta, NoMetadata())
// SuccessDocument
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
from: error_document_no_metadata))
// ErrorDocument
let document2 = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.ErrorDocument.self,
data: error_document_no_metadata)
XCTAssert(document == document2)
XCTAssertTrue(document2.body.isError)
XCTAssertEqual(document2.body.meta, NoMetadata())
XCTAssertNil(document2.body.data)
XCTAssertNil(document2.body.primaryResource)
XCTAssertNil(document2.body.includes)
guard case let .errors(errors2) = document2.body else {
XCTFail("Needed body to be in errors case but it was not.")
return
}
XCTAssertEqual(errors2.0.count, 1)
XCTAssertEqual(errors2.0, document2.body.errors)
XCTAssertEqual(errors2.0[0], .unknown)
XCTAssertEqual(errors2.meta, NoMetadata())
}
func test_unknownErrorDocumentAddIncludingType() {
@@ -620,6 +707,26 @@ extension DocumentTests {
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
}
func test_singleSuccessDocumentNoIncludesAddIncludingType() {
// NOTE distinct from above for being Document.SuccessDocument
let author = Author(id: "1",
attributes: .none,
relationships: .none,
meta: .none,
links: .none)
let document = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
data: single_document_no_includes)
let documentWithIncludes = document.including(Includes<Include1<Author>>(values: [.init(author)]))
XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors)
XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta)
XCTAssertEqual(document.body.links, documentWithIncludes.body.links)
XCTAssertEqual(document.body.includes, Includes<NoIncludes>.none)
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [author])
}
func test_singleDocumentNoIncludesWithAPIDescription() {
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, TestAPIDescription, UnknownJSONAPIError>.self,
data: single_document_no_includes_with_api_description)
@@ -848,6 +955,31 @@ extension DocumentTests {
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor])
}
func test_singleSuccessDocumentSomeIncludesAddIncludes() {
// NOTE distinct from above for being Document.SuccessDocument
let existingAuthor = Author(id: "33",
attributes: .none,
relationships: .none,
meta: .none,
links: .none)
let newAuthor = Author(id: "1",
attributes: .none,
relationships: .none,
meta: .none,
links: .none)
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self,
data: single_document_some_includes)
let documentWithIncludes = document.including(.init(values: [.init(newAuthor)]))
XCTAssertEqual(document.body.errors, documentWithIncludes.body.errors)
XCTAssertEqual(document.body.meta, documentWithIncludes.body.meta)
XCTAssertEqual(document.body.links, documentWithIncludes.body.links)
XCTAssertEqual(documentWithIncludes.body.includes?[Author.self], [existingAuthor, newAuthor])
}
func test_singleDocumentSomeIncludesWithAPIDescription() {
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, Include1<Author>, TestAPIDescription, UnknownJSONAPIError>.self,
data: single_document_some_includes_with_api_description)