Skip to content

Commit

Permalink
Add support for encoding JSONAPIDocument and add tests. Fix support f…
Browse files Browse the repository at this point in the history
…or decoding null primary data for single resource document.
  • Loading branch information
mattpolzin committed Nov 17, 2018
1 parent 04bd042 commit 032cf42
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 68 deletions.
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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
Expand Down
45 changes: 29 additions & 16 deletions Sources/JSONAPI/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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>)

Expand All @@ -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
Expand All @@ -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)
}
}
}
}
9 changes: 7 additions & 2 deletions Sources/JSONAPI/Document/Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
117 changes: 70 additions & 47 deletions Tests/JSONAPITests/Document/DocumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,68 +10,91 @@ import JSONAPI

class DocumentTests: XCTestCase {

func test_singleDocumentNull() {
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: single_document_null)

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 = try? JSONDecoder().decode(JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self, from: single_document_no_includes)
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: single_document_no_includes)

XCTAssertNotNil(document)
XCTAssertFalse(document.body.isError)
XCTAssertNotNil(document.body.data)
XCTAssertEqual(document.body.data?.0.value?.id.rawValue, "1")
XCTAssertEqual(document.body.data?.included.count, 0)
}

guard let d = document else { return }

XCTAssertFalse(d.body.isError)
XCTAssertNotNil(d.body.data)
XCTAssertEqual(d.body.data?.0.value?.id.rawValue, "1")
XCTAssertEqual(d.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)

XCTAssertNotNil(document)
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self,
data: many_document_no_includes)

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 {
Expand Down
6 changes: 6 additions & 0 deletions Tests/JSONAPITests/Document/stubs/DocumentStubs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
6 changes: 6 additions & 0 deletions Tests/JSONAPITests/XCTestManifests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
}

Expand Down

0 comments on commit 032cf42

Please sign in to comment.