From 07402259c485d2b7516caefede1fda9923073aa7 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Sat, 1 Dec 2018 00:36:39 -0800 Subject: [PATCH] Add ability to specify that a SingleResourceBody should be optional or not (or specifically that its PrimaryResource is nullable or not). add tests. update documentation. --- README.md | 15 ++++- Sources/JSONAPI/Document/ResourceBody.swift | 25 +++++--- .../JSONAPITests/Document/DocumentTests.swift | 62 +++++++++++++++---- Tests/JSONAPITests/XCTestManifests.swift | 4 ++ 4 files changed, 85 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 3f3ce3a..9cfe94e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ If you find that something in the JSON API v1.0 Spec is not explicitly missing f To create an Xcode project for JSONAPI, run `swift package generate-xcodeproj` +### Running the Playground +To run the included Playground files, create an Xcode project using Swift Package Manager, then create an Xcode Workspace in the root of the repository and add both the generated Xcode project and the playground to the Workspace. + +Note that Playground support for importing non-system Frameworks is still a bit touchy as of Swift 4.2. Sometimes building, cleaning and building, or commenting out and then uncommenting import statements (especially in the Entities.swift Playground Source file) can get things working for me when I am getting an error about JSONAPI not being found. + ## Project Status ### Decoding @@ -278,14 +283,20 @@ let responseStructure = JSONAPIDocument, NoMetadata, let document = try decoder.decode(responseStructure, from: data) ``` -This document is guaranteed by the JSON API spec to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata." +A JSON API Document is guaranteed by the JSON API spec to be "data", "metadata", or "errors." If it is "data", it may also contain "metadata" and/or other "included" resources. If it is "errors," it may also contain "metadata." #### `ResourceBody` -The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `Entity` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `Entity` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `Entity`. These `Poly` types work in the same way as the `Include` types described below. +The first generic type of a `JSONAPIDocument` is a `ResourceBody`. This can either be a `SingleResourceBody` or a `ManyResourceBody`. You will find zero or one `PrimaryResource` values in a JSON API document that has a `SingleResourceBody` and you will find zero or more `PrimaryResource` values in a JSON API document that has a `ManyResourceBody`. You can use the `Poly` types (`Poly1` through `Poly6`) to specify that a `ResourceBody` will be one of a few different types of `Entity`. These `Poly` types work in the same way as the `Include` types described below. If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead. +##### nullable `PrimaryResource` + +If you expect a `SingleResourceBody` to sometimes come back `null`, you should make your `PrimaryResource` optional. If you do not make your `PrimaryResource` optional then a `null` primary resource will be considered an error when parsing the JSON. + +You cannot, however, use an optional `PrimaryResource` with a `ManyResourceBody` because JSON API requires that an empty document in that case be represented by an empty array rather than `null`. + #### `MetaType` The second generic type of a `JSONAPIDocument` is a `Meta`. This structure is entirely open-ended. As an example, the JSON API document may contain the following pagination info in its meta entry: diff --git a/Sources/JSONAPI/Document/ResourceBody.swift b/Sources/JSONAPI/Document/ResourceBody.swift index c87870a..3f7522a 100644 --- a/Sources/JSONAPI/Document/ResourceBody.swift +++ b/Sources/JSONAPI/Document/ResourceBody.swift @@ -5,15 +5,24 @@ // Created by Mathew Polzin on 11/10/18. // -public protocol PrimaryResource: Equatable, Codable {} +public protocol MaybePrimaryResource: Equatable, Codable {} +/// A PrimaryResource is a type that can be used in the body of a JSON API +/// document as the primary resource. +public protocol PrimaryResource: MaybePrimaryResource {} + +extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {} + +/// A ResourceBody is a representation of the body of the JSON API Document. +/// It can either be one resource (which can be specified as optional or not) +/// or it can contain many resources (and array with zero or more entries). public protocol ResourceBody: Codable, Equatable { } -public struct SingleResourceBody: ResourceBody { - public let value: Entity? +public struct SingleResourceBody: ResourceBody { + public let value: Entity - public init(entity: Entity?) { + public init(entity: Entity) { self.value = entity } } @@ -37,8 +46,10 @@ extension SingleResourceBody { public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() - if container.decodeNil() { - value = nil + let anyNil: Any? = nil + if container.decodeNil(), + let val = anyNil as? Entity { + value = val return } @@ -48,7 +59,7 @@ extension SingleResourceBody { public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() - if value == nil { + if (value as Any?) == nil { try container.encodeNil() return } diff --git a/Tests/JSONAPITests/Document/DocumentTests.swift b/Tests/JSONAPITests/Document/DocumentTests.swift index bde7d6a..0d1dbc4 100644 --- a/Tests/JSONAPITests/Document/DocumentTests.swift +++ b/Tests/JSONAPITests/Document/DocumentTests.swift @@ -11,7 +11,7 @@ import JSONAPI class DocumentTests: XCTestCase { func test_singleDocumentNull() { - let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, + let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_null) XCTAssertFalse(document.body.isError) @@ -23,10 +23,18 @@ class DocumentTests: XCTestCase { } func test_singleDocumentNull_encode() { - test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, + test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_null) } + func test_singleDocumentNonOptionalFailsOnNull() { + XCTAssertThrowsError(try JSONDecoder().decode(Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, + from: single_document_null)) + } +} + +// MARK: - Error Document Tests +extension DocumentTests { func test_unknownErrorDocumentNoMeta() { let document = decoded(type: Document.self, data: error_document_no_metadata) @@ -200,7 +208,10 @@ class DocumentTests: XCTestCase { test_DecodeEncodeEquality(type: Document.self, data: error_document_no_metadata) } +} +// MARK: - Meta Document Tests +extension DocumentTests { func test_metaDataDocument() { let document = decoded(type: Document.self, data: metadata_document) @@ -242,7 +253,11 @@ class DocumentTests: XCTestCase { XCTAssertThrowsError(try JSONDecoder().decode(Document.self, from: metadata_document_missing_metadata2)) } +} + +// MARK: Single Document Tests +extension DocumentTests { func test_singleDocumentNoIncludes() { let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_no_includes) @@ -250,7 +265,7 @@ class DocumentTests: XCTestCase { XCTAssertFalse(document.body.isError) XCTAssertNil(document.body.errors) XCTAssertNotNil(document.body.primaryData) - XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") XCTAssertEqual(document.body.includes?.count, 0) XCTAssertEqual(document.body.meta, NoMetadata()) } @@ -260,6 +275,23 @@ class DocumentTests: XCTestCase { data: single_document_no_includes) } + func test_singleDocumentNoIncludesOptionalNotNull() { + let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, + data: single_document_no_includes) + + XCTAssertFalse(document.body.isError) + XCTAssertNil(document.body.errors) + XCTAssertNotNil(document.body.primaryData) + XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.includes?.count, 0) + XCTAssertEqual(document.body.meta, NoMetadata()) + } + + func test_singleDocumentNoIncludesOptionalNotNull_encode() { + test_DecodeEncodeEquality(type: Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, + data: single_document_no_includes) + } + func test_singleDocumentNoIncludesWithMetadata() { let document = decoded(type: Document, TestPageMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_no_includes_with_metadata) @@ -267,7 +299,7 @@ class DocumentTests: XCTestCase { XCTAssertFalse(document.body.isError) XCTAssertNil(document.body.errors) XCTAssertNotNil(document.body.primaryData) - XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") XCTAssertEqual(document.body.includes?.count, 0) XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) } @@ -284,7 +316,7 @@ class DocumentTests: XCTestCase { XCTAssertFalse(document.body.isError) XCTAssertNil(document.body.errors) XCTAssertNotNil(document.body.primaryData) - XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") XCTAssertEqual(document.body.includes?.count, 0) XCTAssertEqual(document.body.meta, NoMetadata()) XCTAssertEqual(document.body.links?.link.url, "https://website.com") @@ -306,7 +338,7 @@ class DocumentTests: XCTestCase { XCTAssertFalse(document.body.isError) XCTAssertNil(document.body.errors) XCTAssertNotNil(document.body.primaryData) - XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") XCTAssertEqual(document.body.includes?.count, 0) XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) XCTAssertEqual(document.body.links?.link.url, "https://website.com") @@ -336,7 +368,7 @@ class DocumentTests: XCTestCase { XCTAssertFalse(document.body.isError) XCTAssertNil(document.body.errors) XCTAssertNotNil(document.body.primaryData) - XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") XCTAssertEqual(document.body.includes?.count, 1) XCTAssertEqual(document.body.includes?[Author.self].count, 1) XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33") @@ -354,7 +386,7 @@ class DocumentTests: XCTestCase { XCTAssertFalse(document.body.isError) XCTAssertNil(document.body.errors) XCTAssertNotNil(document.body.primaryData) - XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") XCTAssertEqual(document.body.includes?.count, 1) XCTAssertEqual(document.body.includes?[Author.self].count, 1) XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33") @@ -373,7 +405,7 @@ class DocumentTests: XCTestCase { XCTAssertFalse(document.body.isError) XCTAssertNil(document.body.errors) XCTAssertNotNil(document.body.primaryData) - XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1") + XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1") XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10)) XCTAssertEqual(document.body.links?.link.url, "https://website.com") XCTAssertEqual(document.body.links?.link.meta, NoMetadata()) @@ -388,19 +420,25 @@ class DocumentTests: XCTestCase { test_DecodeEncodeEquality(type: Document, TestPageMetadata, TestLinks, Include1, UnknownJSONAPIError>.self, data: single_document_some_includes_with_metadata_with_links) } +} +// MARK: Poly PrimaryResource Tests +extension DocumentTests { func test_singleDocument_PolyPrimaryResource() { let article = Article(id: Id(rawValue: "1"), relationships: .init(author: ToOneRelationship(id: Id(rawValue: "33")))) let document = decoded(type: Document>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_no_includes) - XCTAssertEqual(document.body.primaryData?.value?[Article.self], article) - XCTAssertNil(document.body.primaryData?.value?[Author.self]) + XCTAssertEqual(document.body.primaryData?.value[Article.self], article) + XCTAssertNil(document.body.primaryData?.value[Author.self]) } func test_singleDocument_PolyPrimaryResource_encode() { test_DecodeEncodeEquality(type: Document>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_no_includes) } - +} + +// MARK: - ManyResourceBody Tests +extension DocumentTests { func test_manyDocumentNoIncludes() { let document = decoded(type: Document, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: many_document_no_includes) diff --git a/Tests/JSONAPITests/XCTestManifests.swift b/Tests/JSONAPITests/XCTestManifests.swift index 571ebb0..b2a0c9d 100644 --- a/Tests/JSONAPITests/XCTestManifests.swift +++ b/Tests/JSONAPITests/XCTestManifests.swift @@ -57,6 +57,8 @@ extension DocumentTests { ("test_singleDocumentNoIncludes", test_singleDocumentNoIncludes), ("test_singleDocumentNoIncludes_encode", test_singleDocumentNoIncludes_encode), ("test_singleDocumentNoIncludesMissingMetadata", test_singleDocumentNoIncludesMissingMetadata), + ("test_singleDocumentNoIncludesOptionalNotNull", test_singleDocumentNoIncludesOptionalNotNull), + ("test_singleDocumentNoIncludesOptionalNotNull_encode", test_singleDocumentNoIncludesOptionalNotNull_encode), ("test_singleDocumentNoIncludesWithLinks", test_singleDocumentNoIncludesWithLinks), ("test_singleDocumentNoIncludesWithLinks_encode", test_singleDocumentNoIncludesWithLinks_encode), ("test_singleDocumentNoIncludesWithMetadata", test_singleDocumentNoIncludesWithMetadata), @@ -66,6 +68,7 @@ extension DocumentTests { ("test_singleDocumentNoIncludesWithMetadataWithLinks_encode", test_singleDocumentNoIncludesWithMetadataWithLinks_encode), ("test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinks_encode", test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinks_encode), ("test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinks", test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinks), + ("test_singleDocumentNonOptionalFailsOnNull", test_singleDocumentNonOptionalFailsOnNull), ("test_singleDocumentNull", test_singleDocumentNull), ("test_singleDocumentNull_encode", test_singleDocumentNull_encode), ("test_singleDocumentSomeIncludes", test_singleDocumentSomeIncludes), @@ -116,6 +119,7 @@ extension EntityTests { ("test_EntitySomeRelationshipsNoAttributes_encode", test_EntitySomeRelationshipsNoAttributes_encode), ("test_EntitySomeRelationshipsSomeAttributes", test_EntitySomeRelationshipsSomeAttributes), ("test_EntitySomeRelationshipsSomeAttributes_encode", test_EntitySomeRelationshipsSomeAttributes_encode), + ("test_initialization", test_initialization), ("test_IntOver10_encode", test_IntOver10_encode), ("test_IntOver10_failure", test_IntOver10_failure), ("test_IntOver10_success", test_IntOver10_success),