mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
Add support for encoding JSONAPIDocument and add tests. Fix support for decoding null primary data for single resource document.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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),
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user