Compare commits

..

6 Commits

Author SHA1 Message Date
Mathew Polzin 1a15ab6f9d Fix return type of success document after adding includes 2019-10-27 16:21:41 -07:00
Mathew Polzin eb85620379 regenerate swift test manifest 2019-10-20 22:30:15 -07:00
Mathew Polzin 57f85476c0 Bump podspec version 2019-10-20 22:29:22 -07:00
Mathew Polzin ea5c0b8601 Add Document.ErrorDocument and Document.SuccessDocument types 2019-10-20 22:23:52 -07:00
Mathew Polzin b46429a0ad Update README.md 2019-10-15 23:37:02 -07:00
Mathew Polzin dbc6ecc6d8 Update README.md 2019-10-12 21:39:29 -07:00
6 changed files with 330 additions and 2 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
#
spec.name = "MP-JSONAPI"
spec.version = "2.4.0"
spec.version = "2.5.0"
spec.summary = "Swift Codable JSON API framework."
# This description is used to generate tags and improve search results.
+2 -1
View File
@@ -16,12 +16,13 @@ See the JSON API Spec here: https://jsonapi.org/format/
- [Compound Example](https://colab.research.google.com/drive/1BdF0Kc7l2ixDfBZEL16FY6palweDszQU)
- [Metadata Example](https://colab.research.google.com/drive/10dEESwiE9I3YoyfzVeOVwOKUTEgLT3qr)
- [Custom Errors Example](https://colab.research.google.com/drive/1TIv6STzlHrkTf_-9Eu8sv8NoaxhZcFZH)
- [PATCH Example](https://colab.research.google.com/drive/16KY-0BoLQKiSUh9G7nYmHzB8b2vhXA2U)
### Serverside
- [GET Example](https://colab.research.google.com/drive/1krbhzSfz8mwkBTQQnKUZJLEtYsJKSfYX)
- [POST Example](https://colab.research.google.com/drive/1z3n70LwRY7vLIgbsMghvnfHA67QiuqpQ)
### Combined
### Client+Server
This library works well when used by both the server responsible for serialization and the client responsible for deserialization. Check out the [example](#example) further down in this README.
## Table of Contents
+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>.SuccessDocument {
// 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.SuccessDocument {
// 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,27 @@ 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)]))
XCTAssert(type(of: documentWithIncludes) == Document<NoResourceBody, NoMetadata, NoLinks, Include1<Author>, NoAPIDescription, UnknownJSONAPIError>.SuccessDocument.self)
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 +956,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)
+49
View File
@@ -42,6 +42,17 @@ extension Attribute_FunctorTests {
]
}
extension BasicJSONAPIErrorTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__BasicJSONAPIErrorTests = [
("test_decodeAFewExamples", test_decodeAFewExamples),
("test_definedFields", test_definedFields),
("test_initAndEquality", test_initAndEquality),
]
}
extension ComputedPropertiesTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
@@ -145,6 +156,8 @@ extension DocumentTests {
("test_singleDocumentSomeIncludesWithMetadata_encode", test_singleDocumentSomeIncludesWithMetadata_encode),
("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription),
("test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode", test_singleDocumentSomeIncludesWithMetadataWithAPIDescription_encode),
("test_singleSuccessDocumentNoIncludesAddIncludingType", test_singleSuccessDocumentNoIncludesAddIncludingType),
("test_singleSuccessDocumentSomeIncludesAddIncludes", test_singleSuccessDocumentSomeIncludesAddIncludes),
("test_sparseIncludeFullPrimaryResource", test_sparseIncludeFullPrimaryResource),
("test_sparseIncludeSparsePrimaryResource", test_sparseIncludeSparsePrimaryResource),
("test_sparsePrimaryResource", test_sparsePrimaryResource),
@@ -196,6 +209,21 @@ extension EmptyObjectDecoderTests {
]
}
extension GenericJSONAPIErrorTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__GenericJSONAPIErrorTests = [
("test_decodeKnown", test_decodeKnown),
("test_decodeUnknown", test_decodeUnknown),
("test_definedFields", test_definedFields),
("test_encode", test_encode),
("test_encodeUnknown", test_encodeUnknown),
("test_initAndEquality", test_initAndEquality),
("test_payloadAccess", test_payloadAccess),
]
}
extension IncludedTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
@@ -205,6 +233,8 @@ extension IncludedTests {
("test_ComboSparseAndFullIncludeTypes", test_ComboSparseAndFullIncludeTypes),
("test_EightDifferentIncludes", test_EightDifferentIncludes),
("test_EightDifferentIncludes_encode", test_EightDifferentIncludes_encode),
("test_ElevenDifferentIncludes", test_ElevenDifferentIncludes),
("test_ElevenDifferentIncludes_encode", test_ElevenDifferentIncludes_encode),
("test_FiveDifferentIncludes", test_FiveDifferentIncludes),
("test_FiveDifferentIncludes_encode", test_FiveDifferentIncludes_encode),
("test_FourDifferentIncludes", test_FourDifferentIncludes),
@@ -323,6 +353,22 @@ extension ResourceBodyTests {
]
}
extension ResourceObjectReplacingTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
// to regenerate.
static let __allTests__ResourceObjectReplacingTests = [
("test_replaceImmutableAttributes", test_replaceImmutableAttributes),
("test_replaceImmutableRelationships", test_replaceImmutableRelationships),
("test_replaceMutableAttributes", test_replaceMutableAttributes),
("test_replaceMutableRelationships", test_replaceMutableRelationships),
("test_tapImmutableAttributes", test_tapImmutableAttributes),
("test_tapImmutableRelationships", test_tapImmutableRelationships),
("test_tapMutableAttributes", test_tapMutableAttributes),
("test_tapMutableRelationships", test_tapMutableRelationships),
]
}
extension ResourceObjectTests {
// DO NOT MODIFY: This is autogenerated, use:
// `swift test --generate-linuxmain`
@@ -442,16 +488,19 @@ public func __allTests() -> [XCTestCaseEntry] {
testCase(APIDescriptionTests.__allTests__APIDescriptionTests),
testCase(AttributeTests.__allTests__AttributeTests),
testCase(Attribute_FunctorTests.__allTests__Attribute_FunctorTests),
testCase(BasicJSONAPIErrorTests.__allTests__BasicJSONAPIErrorTests),
testCase(ComputedPropertiesTests.__allTests__ComputedPropertiesTests),
testCase(CustomAttributesTests.__allTests__CustomAttributesTests),
testCase(DocumentTests.__allTests__DocumentTests),
testCase(EmptyObjectDecoderTests.__allTests__EmptyObjectDecoderTests),
testCase(GenericJSONAPIErrorTests.__allTests__GenericJSONAPIErrorTests),
testCase(IncludedTests.__allTests__IncludedTests),
testCase(LinksTests.__allTests__LinksTests),
testCase(NonJSONAPIRelatableTests.__allTests__NonJSONAPIRelatableTests),
testCase(PolyProxyTests.__allTests__PolyProxyTests),
testCase(RelationshipTests.__allTests__RelationshipTests),
testCase(ResourceBodyTests.__allTests__ResourceBodyTests),
testCase(ResourceObjectReplacingTests.__allTests__ResourceObjectReplacingTests),
testCase(ResourceObjectTests.__allTests__ResourceObjectTests),
testCase(SparseFieldEncoderTests.__allTests__SparseFieldEncoderTests),
testCase(SparseFieldsetTests.__allTests__SparseFieldsetTests),