Skip to content

Commit 032cf42

Browse files
committed
Add support for encoding JSONAPIDocument and add tests. Fix support for decoding null primary data for single resource document.
1 parent 04bd042 commit 032cf42

6 files changed

Lines changed: 120 additions & 68 deletions

File tree

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ The primary goals of this framework are:
3939

4040
### Encoding
4141
#### Document
42-
- [ ] `data`
42+
- [x] `data`
4343
- [x] `included`
44-
- [ ] `errors`
44+
- [x] `errors` (untested)
4545
- [ ] `meta`
4646
- [ ] `jsonapi`
4747
- [ ] `links`
@@ -78,7 +78,6 @@ The primary goals of this framework are:
7878
- [ ] Property-based testing (using `SwiftCheck`)
7979
- [ ] Roll my own `Result` or find an alternative that doesn't use `Foundation`.
8080
- [ ] Create more descriptive errors that are easier to use for troubleshooting.
81-
- [ ] 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).
8281

8382
## Usage
8483
### Prerequisites

Sources/JSONAPI/Document/Document.swift

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@
1212
/// API uses snake case, you will want to use
1313
/// a conversion such as the one offerred by the
1414
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
15-
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError & Decodable> {
15+
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: IncludeDecoder, Error: JSONAPIError>: Equatable {
1616
public let body: Body
1717
// public let meta: Meta?
1818
// public let jsonApi: APIDescription?
1919
// public let links: Links?
2020

21-
public enum Body {
21+
public enum Body: Equatable {
2222
case errors([Error])
2323
case data(primary: ResourceBody, included: Includes<Include>)
2424

@@ -34,7 +34,7 @@ public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, Include: Inclu
3434
}
3535
}
3636

37-
extension JSONAPIDocument: Decodable {
37+
extension JSONAPIDocument: Codable {
3838
private enum RootCodingKeys: String, CodingKey {
3939
case data
4040
case errors
@@ -46,26 +46,39 @@ extension JSONAPIDocument: Decodable {
4646

4747
public init(from decoder: Decoder) throws {
4848
let container = try decoder.container(keyedBy: RootCodingKeys.self)
49-
50-
let maybeData = try container.decodeIfPresent(ResourceBody.self, forKey: .data)
51-
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
49+
5250
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
53-
54-
assert(!(maybeData != nil && errors != nil), "JSON API Spec dictates data and errors will not both be present.")
55-
assert((maybeIncludes == nil || maybeData != nil), "JSON API Spec dictates that includes will not be present if data is not present.")
56-
57-
// TODO come back to this and make robust
58-
51+
5952
if let errors = errors {
6053
body = .errors(errors)
6154
return
6255
}
56+
57+
let data = try container.decode(ResourceBody.self, forKey: .data)
58+
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
6359

64-
guard let data = maybeData else {
65-
body = .errors([.unknown]) // TODO: this should be more descriptive
66-
return
67-
}
60+
// TODO come back to this and make robust
6861

6962
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none)
7063
}
64+
65+
public func encode(to encoder: Encoder) throws {
66+
var container = encoder.container(keyedBy: RootCodingKeys.self)
67+
68+
switch body {
69+
case .errors(let errors):
70+
var errContainer = container.nestedUnkeyedContainer(forKey: .errors)
71+
72+
for error in errors {
73+
try errContainer.encode(error)
74+
}
75+
76+
case .data(primary: let resourceBody, included: let includes):
77+
try container.encode(resourceBody, forKey: .data)
78+
79+
if Include.self != NoIncludes.self {
80+
try container.encode(includes, forKey: .included)
81+
}
82+
}
83+
}
7184
}

Sources/JSONAPI/Document/Error.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,21 @@
55
// Created by Mathew Polzin on 11/10/18.
66
//
77

8-
public protocol JSONAPIError: Swift.Error {
8+
public protocol JSONAPIError: Swift.Error, Equatable, Codable {
99
static var unknown: Self { get }
1010
}
1111

12-
public enum BasicJSONAPIError: JSONAPIError & Decodable {
12+
public enum BasicJSONAPIError: JSONAPIError {
1313
case unknownError
1414

1515
public init(from decoder: Decoder) throws {
1616
self = .unknown
1717
}
18+
19+
public func encode(to encoder: Encoder) throws {
20+
var container = encoder.singleValueContainer()
21+
try container.encode("unknown")
22+
}
1823

1924
public static var unknown: BasicJSONAPIError {
2025
return .unknownError

Tests/JSONAPITests/Document/DocumentTests.swift

Lines changed: 70 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,68 +10,91 @@ import JSONAPI
1010

1111
class DocumentTests: XCTestCase {
1212

13+
func test_singleDocumentNull() {
14+
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
15+
data: single_document_null)
16+
17+
XCTAssertFalse(document.body.isError)
18+
XCTAssertNotNil(document.body.data)
19+
XCTAssertNil(document.body.data?.primary.value)
20+
XCTAssertEqual(document.body.data?.included.count, 0)
21+
}
22+
23+
func test_singleDocumentNull_encode() {
24+
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
25+
data: single_document_null)
26+
}
27+
1328
func test_singleDocumentNoIncludes() {
14-
let document = try? JSONDecoder().decode(JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self, from: single_document_no_includes)
29+
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
30+
data: single_document_no_includes)
1531

16-
XCTAssertNotNil(document)
32+
XCTAssertFalse(document.body.isError)
33+
XCTAssertNotNil(document.body.data)
34+
XCTAssertEqual(document.body.data?.0.value?.id.rawValue, "1")
35+
XCTAssertEqual(document.body.data?.included.count, 0)
36+
}
1737

18-
guard let d = document else { return }
19-
20-
XCTAssertFalse(d.body.isError)
21-
XCTAssertNotNil(d.body.data)
22-
XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1")
23-
XCTAssertEqual(d.body.data?.included.count, 0)
38+
func test_singleDocumentNoIncludes_encode() {
39+
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
40+
data: single_document_no_includes)
2441
}
2542

2643
func test_singleDocumentSomeIncludes() {
27-
let document = try? JSONDecoder().decode(JSONAPIDocument<SingleResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self, from: single_document_some_includes)
28-
29-
XCTAssertNotNil(document)
44+
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
45+
data: single_document_some_includes)
3046

31-
guard let d = document else { return }
47+
XCTAssertFalse(document.body.isError)
48+
XCTAssertNotNil(document.body.data)
49+
XCTAssertEqual(document.body.data?.0.value?.id.rawValue, "1")
50+
XCTAssertEqual(document.body.data?.included.count, 1)
51+
XCTAssertEqual(document.body.data?.included[Author.self].count, 1)
52+
XCTAssertEqual(document.body.data?.included[Author.self][0].id.rawValue, "33")
53+
}
3254

33-
XCTAssertFalse(d.body.isError)
34-
XCTAssertNotNil(d.body.data)
35-
XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1")
36-
XCTAssertEqual(d.body.data?.included.count, 1)
37-
XCTAssertEqual(d.body.data?.included[Author.self].count, 1)
38-
XCTAssertEqual(d.body.data?.included[Author.self][0].id.rawValue, "33")
55+
func test_singleDocumentSomeIncludes_encode() {
56+
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
57+
data: single_document_some_includes)
3958
}
4059

4160
func test_manyDocumentNoIncludes() {
42-
let document = try? JSONDecoder().decode(JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self, from: many_document_no_includes)
43-
44-
XCTAssertNotNil(document)
61+
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self,
62+
data: many_document_no_includes)
4563

46-
guard let d = document else { return }
47-
48-
XCTAssertFalse(d.body.isError)
49-
XCTAssertNotNil(d.body.data)
50-
XCTAssertEqual(d.body.data?.0.values.count, 3)
51-
XCTAssertEqual(d.body.data?.0.values[0].id.rawValue, "1")
52-
XCTAssertEqual(d.body.data?.0.values[1].id.rawValue, "2")
53-
XCTAssertEqual(d.body.data?.0.values[2].id.rawValue, "3")
54-
XCTAssertEqual(d.body.data?.included.count, 0)
64+
XCTAssertFalse(document.body.isError)
65+
XCTAssertNotNil(document.body.data)
66+
XCTAssertEqual(document.body.data?.0.values.count, 3)
67+
XCTAssertEqual(document.body.data?.0.values[0].id.rawValue, "1")
68+
XCTAssertEqual(document.body.data?.0.values[1].id.rawValue, "2")
69+
XCTAssertEqual(document.body.data?.0.values[2].id.rawValue, "3")
70+
XCTAssertEqual(document.body.data?.included.count, 0)
71+
}
72+
73+
func test_manyDocumentNoIncludes_encode() {
74+
test_DecodeEncodeEquality(type: JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self,
75+
data: many_document_no_includes)
5576
}
5677

5778
func test_manyDocumentSomeIncludes() {
58-
let document = try? JSONDecoder().decode(JSONAPIDocument<ManyResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self, from: many_document_some_includes)
59-
60-
XCTAssertNotNil(document)
61-
62-
guard let d = document else { return }
63-
64-
XCTAssertFalse(d.body.isError)
65-
XCTAssertNotNil(d.body.data)
66-
XCTAssertEqual(d.body.data?.0.values.count, 3)
67-
XCTAssertEqual(d.body.data?.0.values[0].id.rawValue, "1")
68-
XCTAssertEqual(d.body.data?.0.values[1].id.rawValue, "2")
69-
XCTAssertEqual(d.body.data?.0.values[2].id.rawValue, "3")
70-
XCTAssertEqual(d.body.data?.included.count, 3)
71-
XCTAssertEqual(d.body.data?.included[Author.self].count, 3)
72-
XCTAssertEqual(d.body.data?.included[Author.self][0].id.rawValue, "33")
73-
XCTAssertEqual(d.body.data?.included[Author.self][1].id.rawValue, "22")
74-
XCTAssertEqual(d.body.data?.included[Author.self][2].id.rawValue, "11")
79+
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
80+
data: many_document_some_includes)
81+
82+
XCTAssertFalse(document.body.isError)
83+
XCTAssertNotNil(document.body.data)
84+
XCTAssertEqual(document.body.data?.0.values.count, 3)
85+
XCTAssertEqual(document.body.data?.0.values[0].id.rawValue, "1")
86+
XCTAssertEqual(document.body.data?.0.values[1].id.rawValue, "2")
87+
XCTAssertEqual(document.body.data?.0.values[2].id.rawValue, "3")
88+
XCTAssertEqual(document.body.data?.included.count, 3)
89+
XCTAssertEqual(document.body.data?.included[Author.self].count, 3)
90+
XCTAssertEqual(document.body.data?.included[Author.self][0].id.rawValue, "33")
91+
XCTAssertEqual(document.body.data?.included[Author.self][1].id.rawValue, "22")
92+
XCTAssertEqual(document.body.data?.included[Author.self][2].id.rawValue, "11")
93+
}
94+
95+
func test_manyDocumentSomeIncludes_encode() {
96+
test_DecodeEncodeEquality(type: JSONAPIDocument<ManyResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
97+
data: many_document_some_includes)
7598
}
7699

77100
enum AuthorType: EntityDescription {

Tests/JSONAPITests/Document/stubs/DocumentStubs.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
// Created by Mathew Polzin on 11/12/18.
66
//
77

8+
let single_document_null = """
9+
{
10+
"data": null
11+
}
12+
""".data(using: .utf8)!
13+
814
let single_document_no_includes = """
915
{
1016
"data": {

Tests/JSONAPITests/XCTestManifests.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,15 @@ import XCTest
33
extension DocumentTests {
44
static let __allTests = [
55
("test_manyDocumentNoIncludes", test_manyDocumentNoIncludes),
6+
("test_manyDocumentNoIncludes_encode", test_manyDocumentNoIncludes_encode),
67
("test_manyDocumentSomeIncludes", test_manyDocumentSomeIncludes),
8+
("test_manyDocumentSomeIncludes_encode", test_manyDocumentSomeIncludes_encode),
79
("test_singleDocumentNoIncludes", test_singleDocumentNoIncludes),
10+
("test_singleDocumentNoIncludes_encode", test_singleDocumentNoIncludes_encode),
11+
("test_singleDocumentNull", test_singleDocumentNull),
12+
("test_singleDocumentNull_encode", test_singleDocumentNull_encode),
813
("test_singleDocumentSomeIncludes", test_singleDocumentSomeIncludes),
14+
("test_singleDocumentSomeIncludes_encode", test_singleDocumentSomeIncludes_encode),
915
]
1016
}
1117

0 commit comments

Comments
 (0)