mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Add Document.ErrorDocument and Document.SuccessDocument types
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user