mirror of
https://github.com/encounter/JSONAPI.git
synced 2026-03-30 11:18:38 -07:00
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.
This commit is contained in:
@@ -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<SingleResourceBody<Person>, 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<PrimaryResource>` or a `ManyResourceBody<PrimaryResource>`. 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:
|
||||
|
||||
@@ -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<Entity: JSONAPI.PrimaryResource>: ResourceBody {
|
||||
public let value: Entity?
|
||||
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: 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
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import JSONAPI
|
||||
class DocumentTests: XCTestCase {
|
||||
|
||||
func test_singleDocumentNull() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
let document = decoded(type: Document<SingleResourceBody<Article?>, 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<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
data: single_document_null)
|
||||
}
|
||||
|
||||
func test_singleDocumentNonOptionalFailsOnNull() {
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
from: single_document_null))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Error Document Tests
|
||||
extension DocumentTests {
|
||||
func test_unknownErrorDocumentNoMeta() {
|
||||
let document = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
data: error_document_no_metadata)
|
||||
@@ -200,7 +208,10 @@ class DocumentTests: XCTestCase {
|
||||
test_DecodeEncodeEquality(type: Document<NoResourceBody, TestPageMetadata, TestLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
data: error_document_no_metadata)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Meta Document Tests
|
||||
extension DocumentTests {
|
||||
func test_metaDataDocument() {
|
||||
let document = decoded(type: Document<NoResourceBody, TestPageMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
data: metadata_document)
|
||||
@@ -242,7 +253,11 @@ class DocumentTests: XCTestCase {
|
||||
|
||||
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, TestPageMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, from: metadata_document_missing_metadata2))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Single Document Tests
|
||||
extension DocumentTests {
|
||||
func test_singleDocumentNoIncludes() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, 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<SingleResourceBody<Article?>, 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<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
data: single_document_no_includes)
|
||||
}
|
||||
|
||||
func test_singleDocumentNoIncludesWithMetadata() {
|
||||
let document = decoded(type: Document<SingleResourceBody<Article>, 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<SingleResourceBody<Article>, TestPageMetadata, TestLinks, Include1<Author>, 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<SingleResourceBody<Poly2<Article, Author>>, 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<SingleResourceBody<Poly2<Article, Author>>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_no_includes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ManyResourceBody Tests
|
||||
extension DocumentTests {
|
||||
func test_manyDocumentNoIncludes() {
|
||||
let document = decoded(type: Document<ManyResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
|
||||
data: many_document_no_includes)
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user