Add support for encoding JSONAPIDocument and add tests. Fix support for decoding null primary data for single resource document.

This commit is contained in:
Mathew Polzin
2018-11-16 23:59:49 -08:00
parent 04bd0421cd
commit 032cf42c08
6 changed files with 121 additions and 69 deletions
+2 -3
View File
@@ -39,9 +39,9 @@ The primary goals of this framework are:
### Encoding
#### Document
- [ ] `data`
- [x] `data`
- [x] `included`
- [ ] `errors`
- [x] `errors` (untested)
- [ ] `meta`
- [ ] `jsonapi`
- [ ] `links`
@@ -78,7 +78,6 @@ The primary goals of this framework are:
- [ ] Property-based testing (using `SwiftCheck`)
- [ ] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
- [ ] Add errors that check consistency from one part of a document to another (i.e. includes must be referenced by a relationship in the primary resource object).
## Usage
### Prerequisites
+29 -16
View File
@@ -12,13 +12,13 @@
/// API uses snake case, you will want to use
/// a conversion such as the one offerred by the
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError & Decodable> {
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError>: Equatable {
public let body: Body
// public let meta: Meta?
// public let jsonApi: APIDescription?
// public let links: Links?
public enum Body {
public enum Body: Equatable {
case errors([Error])
case data(primary: ResourceBody, included: Includes<Include>)
@@ -34,7 +34,7 @@ public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: Inclu
}
}
extension JSONAPIDocument: Decodable {
extension JSONAPIDocument: Codable {
private enum RootCodingKeys: String, CodingKey {
case data
case errors
@@ -46,26 +46,39 @@ extension JSONAPIDocument: Decodable {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RootCodingKeys.self)
let maybeData = try container.decodeIfPresent(ResourceBody.self, forKey: .data)
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
assert(!(maybeData != nil && errors != nil), "JSON API Spec dictates data and errors will not both be present.")
assert((maybeIncludes == nil || maybeData != nil), "JSON API Spec dictates that includes will not be present if data is not present.")
// TODO come back to this and make robust
if let errors = errors {
body = .errors(errors)
return
}
let data = try container.decode(ResourceBody.self, forKey: .data)
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
guard let data = maybeData else {
body = .errors([.unknown]) // TODO: this should be more descriptive
return
}
// TODO come back to this and make robust
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: RootCodingKeys.self)
switch body {
case .errors(let errors):
var errContainer = container.nestedUnkeyedContainer(forKey: .errors)
for error in errors {
try errContainer.encode(error)
}
case .data(primary: let resourceBody, included: let includes):
try container.encode(resourceBody, forKey: .data)
if Include.self != NoIncludes.self {
try container.encode(includes, forKey: .included)
}
}
}
}
+7 -2
View File
@@ -5,16 +5,21 @@
// Created by Mathew Polzin on 11/10/18.
//
public protocol JSONAPIError: Swift.Error {
public protocol JSONAPIError: Swift.Error, Equatable, Codable {
static var unknown: Self { get }
}
public enum BasicJSONAPIError: JSONAPIError & Decodable {
public enum BasicJSONAPIError: JSONAPIError {
case unknownError
public init(from decoder: Decoder) throws {
self = .unknown
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode("unknown")
}
public static var unknown: BasicJSONAPIError {
return .unknownError
+71 -48
View File
@@ -10,68 +10,91 @@ import JSONAPI
class DocumentTests: XCTestCase {
func test_singleDocumentNoIncludes() {
let document = try? JSONDecoder().decode(JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self, from: single_document_no_includes)
XCTAssertNotNil(document)
func test_singleDocumentNull() {
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: single_document_null)
guard let d = document else { return }
XCTAssertFalse(document.body.isError)
XCTAssertNotNil(document.body.data)
XCTAssertNil(document.body.data?.primary.value)
XCTAssertEqual(document.body.data?.included.count, 0)
}
func test_singleDocumentNull_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: single_document_null)
}
func test_singleDocumentNoIncludes() {
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: single_document_no_includes)
XCTAssertFalse(d.body.isError)
XCTAssertNotNil(d.body.data)
XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1")
XCTAssertEqual(d.body.data?.included.count, 0)
XCTAssertFalse(document.body.isError)
XCTAssertNotNil(document.body.data)
XCTAssertEqual(document.body.data?.0.value?.id.rawValue, "1")
XCTAssertEqual(document.body.data?.included.count, 0)
}
func test_singleDocumentNoIncludes_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: single_document_no_includes)
}
func test_singleDocumentSomeIncludes() {
let document = try? JSONDecoder().decode(JSONAPIDocument<SingleResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self, from: single_document_some_includes)
XCTAssertNotNil(document)
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
data: single_document_some_includes)
guard let d = document else { return }
XCTAssertFalse(document.body.isError)
XCTAssertNotNil(document.body.data)
XCTAssertEqual(document.body.data?.0.value?.id.rawValue, "1")
XCTAssertEqual(document.body.data?.included.count, 1)
XCTAssertEqual(document.body.data?.included[Author.self].count, 1)
XCTAssertEqual(document.body.data?.included[Author.self][0].id.rawValue, "33")
}
XCTAssertFalse(d.body.isError)
XCTAssertNotNil(d.body.data)
XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1")
XCTAssertEqual(d.body.data?.included.count, 1)
XCTAssertEqual(d.body.data?.included[Author.self].count, 1)
XCTAssertEqual(d.body.data?.included[Author.self][0].id.rawValue, "33")
func test_singleDocumentSomeIncludes_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
data: single_document_some_includes)
}
func test_manyDocumentNoIncludes() {
let document = try? JSONDecoder().decode(JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self, from: many_document_no_includes)
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: many_document_no_includes)
XCTAssertNotNil(document)
guard let d = document else { return }
XCTAssertFalse(d.body.isError)
XCTAssertNotNil(d.body.data)
XCTAssertEqual(d.body.data?.0.values.count, 3)
XCTAssertEqual(d.body.data?.0.values[0].id.rawValue, "1")
XCTAssertEqual(d.body.data?.0.values[1].id.rawValue, "2")
XCTAssertEqual(d.body.data?.0.values[2].id.rawValue, "3")
XCTAssertEqual(d.body.data?.included.count, 0)
XCTAssertFalse(document.body.isError)
XCTAssertNotNil(document.body.data)
XCTAssertEqual(document.body.data?.0.values.count, 3)
XCTAssertEqual(document.body.data?.0.values[0].id.rawValue, "1")
XCTAssertEqual(document.body.data?.0.values[1].id.rawValue, "2")
XCTAssertEqual(document.body.data?.0.values[2].id.rawValue, "3")
XCTAssertEqual(document.body.data?.included.count, 0)
}
func test_manyDocumentNoIncludes_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: many_document_no_includes)
}
func test_manyDocumentSomeIncludes() {
let document = try? JSONDecoder().decode(JSONAPIDocument<ManyResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self, from: many_document_some_includes)
XCTAssertNotNil(document)
guard let d = document else { return }
XCTAssertFalse(d.body.isError)
XCTAssertNotNil(d.body.data)
XCTAssertEqual(d.body.data?.0.values.count, 3)
XCTAssertEqual(d.body.data?.0.values[0].id.rawValue, "1")
XCTAssertEqual(d.body.data?.0.values[1].id.rawValue, "2")
XCTAssertEqual(d.body.data?.0.values[2].id.rawValue, "3")
XCTAssertEqual(d.body.data?.included.count, 3)
XCTAssertEqual(d.body.data?.included[Author.self].count, 3)
XCTAssertEqual(d.body.data?.included[Author.self][0].id.rawValue, "33")
XCTAssertEqual(d.body.data?.included[Author.self][1].id.rawValue, "22")
XCTAssertEqual(d.body.data?.included[Author.self][2].id.rawValue, "11")
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
data: many_document_some_includes)
XCTAssertFalse(document.body.isError)
XCTAssertNotNil(document.body.data)
XCTAssertEqual(document.body.data?.0.values.count, 3)
XCTAssertEqual(document.body.data?.0.values[0].id.rawValue, "1")
XCTAssertEqual(document.body.data?.0.values[1].id.rawValue, "2")
XCTAssertEqual(document.body.data?.0.values[2].id.rawValue, "3")
XCTAssertEqual(document.body.data?.included.count, 3)
XCTAssertEqual(document.body.data?.included[Author.self].count, 3)
XCTAssertEqual(document.body.data?.included[Author.self][0].id.rawValue, "33")
XCTAssertEqual(document.body.data?.included[Author.self][1].id.rawValue, "22")
XCTAssertEqual(document.body.data?.included[Author.self][2].id.rawValue, "11")
}
func test_manyDocumentSomeIncludes_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<ManyResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
data: many_document_some_includes)
}
enum AuthorType: EntityDescription {
@@ -5,6 +5,12 @@
// Created by Mathew Polzin on 11/12/18.
//
let single_document_null = """
{
"data": null
}
""".data(using: .utf8)!
let single_document_no_includes = """
{
"data": {
+6
View File
@@ -3,9 +3,15 @@ import XCTest
extension DocumentTests {
static let __allTests = [
("test_manyDocumentNoIncludes", test_manyDocumentNoIncludes),
("test_manyDocumentNoIncludes_encode", test_manyDocumentNoIncludes_encode),
("test_manyDocumentSomeIncludes", test_manyDocumentSomeIncludes),
("test_manyDocumentSomeIncludes_encode", test_manyDocumentSomeIncludes_encode),
("test_singleDocumentNoIncludes", test_singleDocumentNoIncludes),
("test_singleDocumentNoIncludes_encode", test_singleDocumentNoIncludes_encode),
("test_singleDocumentNull", test_singleDocumentNull),
("test_singleDocumentNull_encode", test_singleDocumentNull_encode),
("test_singleDocumentSomeIncludes", test_singleDocumentSomeIncludes),
("test_singleDocumentSomeIncludes_encode", test_singleDocumentSomeIncludes_encode),
]
}