Added meta data to JSONAPIDocument. Added tests around JSONAPIDocument error finally. Needs a few more tests around metadata.

This commit is contained in:
Mathew Polzin
2018-11-22 21:04:58 -08:00
parent 7a3a1b8b65
commit c4032eb351
6 changed files with 284 additions and 29 deletions
+79 -17
View File
@@ -12,41 +12,58 @@
/// 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, IncludeType: JSONAPI.Include, Error: JSONAPIError>: Equatable {
public struct JSONAPIDocument<ResourceBody: JSONAPI.ResourceBody, MetaType: JSONAPI.Meta, IncludeType: JSONAPI.Include, Error: JSONAPIError>: Equatable {
public typealias Include = IncludeType
public let body: Body
// public let meta: Meta?
// public let jsonApi: APIDescription?
// public let links: Links?
public enum Body: Equatable {
case errors([Error])
case data(primary: ResourceBody, included: Includes<Include>)
case errors([Error], meta: MetaType?)
case data(primary: ResourceBody, included: Includes<Include>, meta: MetaType)
case meta(MetaType)
public var isError: Bool {
guard case .errors = self else { return false }
return true
}
public var data: (primary: ResourceBody, included: Includes<Include>)? {
guard case let .data(primary: body, included: includes) = self else { return nil }
return (primary: body, included: includes)
public var data: (primary: ResourceBody, included: Includes<Include>, meta: MetaType)? {
guard case let .data(primary: body, included: includes, meta: meta) = self else { return nil }
return (primary: body, included: includes, meta: meta)
}
public var meta: MetaType? {
guard case let .meta(metadata) = self else { return nil }
return metadata
}
}
public init(errors: [Error]) {
body = .errors(errors)
public init(errors: [Error], meta: MetaType? = nil) {
body = .errors(errors, meta: meta)
}
public init(body: ResourceBody, includes: Includes<Include>) {
self.body = .data(primary: body, included: includes)
public init(body: ResourceBody, includes: Includes<Include>, meta: MetaType) {
self.body = .data(primary: body, included: includes, meta: meta)
}
}
extension JSONAPIDocument where IncludeType == NoIncludes {
public init(body: ResourceBody, meta: MetaType) {
self.body = .data(primary: body, included: .none, meta: meta)
}
}
extension JSONAPIDocument where MetaType == NoMetadata {
public init(body: ResourceBody, includes: Includes<Include>) {
self.body = .data(primary: body, included: includes, meta: .none)
}
}
extension JSONAPIDocument where IncludeType == NoIncludes, MetaType == NoMetadata {
public init(body: ResourceBody) {
self.body = .data(primary: body, included: .none)
self.body = .data(primary: body, included: .none, meta: .none)
}
}
@@ -65,36 +82,81 @@ extension JSONAPIDocument: Codable {
let errors = try container.decodeIfPresent([Error].self, forKey: .errors)
let meta: MetaType?
if let noMeta = NoMetadata() as? MetaType {
meta = noMeta
} else {
do {
meta = try container.decode(MetaType.self, forKey: .meta)
} catch {
meta = nil
}
}
// If there are errors, there cannot be a body. Return errors and any metadata found.
if let errors = errors {
body = .errors(errors)
body = .errors(errors, meta: meta)
return
}
let maybeData: ResourceBody?
if ResourceBody.self == NoResourceBody.self {
maybeData = nil
} else {
maybeData = try container.decode(ResourceBody.self, forKey: .data)
}
// If there were not errors but there is also no data, try to find metadata.
// No metadata is against JSON API Spec, but otherwise we can form a
// metadat-only document.
guard let data = maybeData else {
guard let metaVal = meta else {
throw JSONAPIEncodingError.missingOrMalformedMetadata
}
body = .meta(metaVal)
return
}
let data = try container.decode(ResourceBody.self, forKey: .data)
let maybeIncludes = try container.decodeIfPresent(Includes<Include>.self, forKey: .included)
// TODO come back to this and make robust
guard let metaVal = meta else {
throw JSONAPIEncodingError.missingOrMalformedMetadata
}
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none)
body = .data(primary: data, included: maybeIncludes ?? Includes<Include>.none, meta: metaVal)
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: RootCodingKeys.self)
switch body {
case .errors(let errors):
case .errors(let errors, let meta):
var errContainer = container.nestedUnkeyedContainer(forKey: .errors)
for error in errors {
try errContainer.encode(error)
}
case .data(primary: let resourceBody, included: let includes):
if MetaType.self != NoMetadata.self,
let metaVal = meta {
try container.encode(metaVal, forKey: .meta)
}
case .data(primary: let resourceBody, included: let includes, let meta):
try container.encode(resourceBody, forKey: .data)
if Include.self != NoIncludes.self {
try container.encode(includes, forKey: .included)
}
if MetaType.self != NoMetadata.self {
try container.encode(meta, forKey: .meta)
}
case .meta(let metadata):
try container .encode(metadata, forKey: .meta)
}
}
}
@@ -24,6 +24,12 @@ public struct ManyResourceBody<Entity: JSONAPI.EntityType>: ResourceBody {
}
}
/// Use NoResourceBody to indicate you expect a JSON API document to not
/// contain a "data" top-level key.
public struct NoResourceBody: ResourceBody {
public static var none: NoResourceBody { return NoResourceBody() }
}
// MARK: Decodable
extension SingleResourceBody {
public init(from decoder: Decoder) throws {
+24
View File
@@ -0,0 +1,24 @@
//
// Meta.swift
// JSONAPI
//
// Created by Mathew Polzin on 11/21/18.
//
import Foundation
/// Conform a type to this protocol to indicate it can be encoded to or decoded from
/// the meta data attached to a component of a JSON API document. Different meta data
/// can be stored all over the place: On the root document, on a resource object, on
/// link objects, etc.
///
/// JSON API Metadata is totally open ended. It can take whatever JSON-compliant structure
/// the server and client agree upon.
public protocol Meta: Codable, Equatable {
}
public struct NoMetadata: Meta {
public static var none: NoMetadata { return NoMetadata() }
public init() { }
}
@@ -96,6 +96,7 @@ private enum ResourceIdentifierCodingKeys: String, CodingKey {
public enum JSONAPIEncodingError: Swift.Error {
case typeMismatch(expected: String, found: String)
case illegalEncoding(String)
case missingOrMalformedMetadata
}
extension ToOneRelationship {
+137 -12
View File
@@ -11,22 +11,106 @@ import JSONAPI
class DocumentTests: XCTestCase {
func test_singleDocumentNull() {
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, NoMetadata, NoIncludes, BasicJSONAPIError>.self,
data: single_document_null)
XCTAssertFalse(document.body.isError)
XCTAssertNotNil(document.body.data)
XCTAssertNil(document.body.meta)
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)
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, NoMetadata, NoIncludes, BasicJSONAPIError>.self,
data: single_document_null)
}
func test_unknownErrorDocumentNoMeta() {
let document = decoded(type: JSONAPIDocument<NoResourceBody, NoMetadata, NoIncludes, BasicJSONAPIError>.self,
data: error_document_no_metadata)
XCTAssertTrue(document.body.isError)
XCTAssertNil(document.body.meta)
XCTAssertNil(document.body.data)
guard case let .errors(errors) = document.body else {
XCTFail("Needed body to be in errors case but it was not.")
return
}
XCTAssertEqual(errors.0.count, 1)
XCTAssertEqual(errors.0[0], .unknown)
XCTAssertEqual(errors.meta, NoMetadata())
}
func test_unknownErrorDocumentNoMeta_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<NoResourceBody, NoMetadata, NoIncludes, BasicJSONAPIError>.self,
data: error_document_no_metadata)
}
func test_errorDocumentNoMeta() {
let document = decoded(type: JSONAPIDocument<NoResourceBody, NoMetadata, NoIncludes, TestError>.self,
data: error_document_no_metadata)
XCTAssertTrue(document.body.isError)
XCTAssertNil(document.body.meta)
XCTAssertNil(document.body.data)
guard case let .errors(errors) = document.body else {
XCTFail("Needed body to be in errors case but it was not.")
return
}
XCTAssertEqual(errors.0.count, 1)
XCTAssertEqual(errors.0[0], TestError.basic(.init(code: 1, description: "Boooo!")))
XCTAssertEqual(errors.meta, NoMetadata())
}
func test_errorDocumentNoMeta_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<NoResourceBody, NoMetadata, NoIncludes, TestError>.self,
data: error_document_no_metadata)
}
func test_errorDocumentWithMeta() {
let document = decoded(type: JSONAPIDocument<NoResourceBody, TestPageMetadata, NoIncludes, BasicJSONAPIError>.self,
data: error_document_with_metadata)
XCTAssertTrue(document.body.isError)
XCTAssertNil(document.body.meta)
XCTAssertNil(document.body.data)
guard case let .errors(errors) = document.body else {
XCTFail("Needed body to be in errors case but it was not.")
return
}
XCTAssertEqual(errors.0.count, 1)
XCTAssertEqual(errors.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
}
func test_errorDocumentWithMeta_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<NoResourceBody, TestPageMetadata, NoIncludes, BasicJSONAPIError>.self,
data: error_document_with_metadata)
}
func test_metaDataDocument() {
let document = decoded(type: JSONAPIDocument<NoResourceBody, TestPageMetadata, NoIncludes, BasicJSONAPIError>.self,
data: metadata_document)
XCTAssertFalse(document.body.isError)
XCTAssertEqual(document.body.meta?.total, 100)
XCTAssertEqual(document.body.meta?.limit, 50)
XCTAssertEqual(document.body.meta?.offset, 0)
}
func test_metaDataDocument_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<NoResourceBody, TestPageMetadata, NoIncludes, BasicJSONAPIError>.self,
data: metadata_document)
}
func test_singleDocumentNoIncludes() {
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, NoMetadata, NoIncludes, BasicJSONAPIError>.self,
data: single_document_no_includes)
XCTAssertFalse(document.body.isError)
@@ -36,12 +120,12 @@ class DocumentTests: XCTestCase {
}
func test_singleDocumentNoIncludes_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, Include0, BasicJSONAPIError>.self,
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, NoMetadata, NoIncludes, BasicJSONAPIError>.self,
data: single_document_no_includes)
}
func test_singleDocumentSomeIncludes() {
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
let document = decoded(type: JSONAPIDocument<SingleResourceBody<Article>, NoMetadata, Include1<Author>, BasicJSONAPIError>.self,
data: single_document_some_includes)
XCTAssertFalse(document.body.isError)
@@ -53,12 +137,12 @@ class DocumentTests: XCTestCase {
}
func test_singleDocumentSomeIncludes_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
test_DecodeEncodeEquality(type: JSONAPIDocument<SingleResourceBody<Article>, NoMetadata, Include1<Author>, BasicJSONAPIError>.self,
data: single_document_some_includes)
}
func test_manyDocumentNoIncludes() {
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self,
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, NoMetadata, NoIncludes, BasicJSONAPIError>.self,
data: many_document_no_includes)
XCTAssertFalse(document.body.isError)
@@ -71,12 +155,12 @@ class DocumentTests: XCTestCase {
}
func test_manyDocumentNoIncludes_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<ManyResourceBody<Article>, Include0, BasicJSONAPIError>.self,
test_DecodeEncodeEquality(type: JSONAPIDocument<ManyResourceBody<Article>, NoMetadata, NoIncludes, BasicJSONAPIError>.self,
data: many_document_no_includes)
}
func test_manyDocumentSomeIncludes() {
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
let document = decoded(type: JSONAPIDocument<ManyResourceBody<Article>, NoMetadata, Include1<Author>, BasicJSONAPIError>.self,
data: many_document_some_includes)
XCTAssertFalse(document.body.isError)
@@ -93,10 +177,14 @@ class DocumentTests: XCTestCase {
}
func test_manyDocumentSomeIncludes_encode() {
test_DecodeEncodeEquality(type: JSONAPIDocument<ManyResourceBody<Article>, Include1<Author>, BasicJSONAPIError>.self,
test_DecodeEncodeEquality(type: JSONAPIDocument<ManyResourceBody<Article>, NoMetadata, Include1<Author>, BasicJSONAPIError>.self,
data: many_document_some_includes)
}
}
// MARK: - Test Types
extension DocumentTests {
enum AuthorType: EntityDescription {
static var type: String { return "authors" }
@@ -117,4 +205,41 @@ class DocumentTests: XCTestCase {
}
typealias Article = Entity<ArticleType>
struct TestPageMetadata: JSONAPI.Meta {
let total: Int
let limit: Int
let offset: Int
}
enum TestError: JSONAPIError {
case unknownError
case basic(BasicError)
struct BasicError: Codable, Equatable {
let code: Int
let description: String
}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self = (try? .basic(container.decode(BasicError.self))) ?? .unknown
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .unknownError:
try container.encode("unknown")
case .basic(let error):
try container.encode(error)
}
}
public static var unknown: DocumentTests.TestError {
return .unknownError
}
}
}
@@ -150,3 +150,40 @@ let many_document_some_includes = """
]
}
""".data(using: .utf8)!
let error_document_no_metadata = """
{
"errors": [
{
"description": "Boooo!",
"code": 1
}
]
}
""".data(using: .utf8)!
let error_document_with_metadata = """
{
"errors": [
{
"description": "Boooo!",
"code": 1
}
],
"meta": {
"total": 70,
"limit": 40,
"offset": 10
}
}
""".data(using: .utf8)!
let metadata_document = """
{
"meta": {
"total": 100,
"limit": 50,
"offset": 0
}
}
""".data(using: .utf8)!