Skip to content

Commit 0740225

Browse files
committed
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.
1 parent 8274ecb commit 0740225

4 files changed

Lines changed: 85 additions & 21 deletions

File tree

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ If you find that something in the JSON API v1.0 Spec is not explicitly missing f
2626
To create an Xcode project for JSONAPI, run
2727
`swift package generate-xcodeproj`
2828

29+
### Running the Playground
30+
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.
31+
32+
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.
33+
2934
## Project Status
3035

3136
### Decoding
@@ -278,14 +283,20 @@ let responseStructure = JSONAPIDocument<SingleResourceBody<Person>, NoMetadata,
278283
let document = try decoder.decode(responseStructure, from: data)
279284
```
280285

281-
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."
286+
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."
282287

283288
#### `ResourceBody`
284289

285-
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.
290+
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.
286291

287292
If you expect a response to not have a "data" top-level key at all, then use `NoResourceBody` instead.
288293

294+
##### nullable `PrimaryResource`
295+
296+
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.
297+
298+
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`.
299+
289300
#### `MetaType`
290301

291302
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:

Sources/JSONAPI/Document/ResourceBody.swift

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

8-
public protocol PrimaryResource: Equatable, Codable {}
8+
public protocol MaybePrimaryResource: Equatable, Codable {}
99

10+
/// A PrimaryResource is a type that can be used in the body of a JSON API
11+
/// document as the primary resource.
12+
public protocol PrimaryResource: MaybePrimaryResource {}
13+
14+
extension Optional: MaybePrimaryResource where Wrapped: PrimaryResource {}
15+
16+
/// A ResourceBody is a representation of the body of the JSON API Document.
17+
/// It can either be one resource (which can be specified as optional or not)
18+
/// or it can contain many resources (and array with zero or more entries).
1019
public protocol ResourceBody: Codable, Equatable {
1120
}
1221

13-
public struct SingleResourceBody<Entity: JSONAPI.PrimaryResource>: ResourceBody {
14-
public let value: Entity?
22+
public struct SingleResourceBody<Entity: JSONAPI.MaybePrimaryResource>: ResourceBody {
23+
public let value: Entity
1524

16-
public init(entity: Entity?) {
25+
public init(entity: Entity) {
1726
self.value = entity
1827
}
1928
}
@@ -37,8 +46,10 @@ extension SingleResourceBody {
3746
public init(from decoder: Decoder) throws {
3847
let container = try decoder.singleValueContainer()
3948

40-
if container.decodeNil() {
41-
value = nil
49+
let anyNil: Any? = nil
50+
if container.decodeNil(),
51+
let val = anyNil as? Entity {
52+
value = val
4253
return
4354
}
4455

@@ -48,7 +59,7 @@ extension SingleResourceBody {
4859
public func encode(to encoder: Encoder) throws {
4960
var container = encoder.singleValueContainer()
5061

51-
if value == nil {
62+
if (value as Any?) == nil {
5263
try container.encodeNil()
5364
return
5465
}

Tests/JSONAPITests/Document/DocumentTests.swift

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import JSONAPI
1111
class DocumentTests: XCTestCase {
1212

1313
func test_singleDocumentNull() {
14-
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
14+
let document = decoded(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
1515
data: single_document_null)
1616

1717
XCTAssertFalse(document.body.isError)
@@ -23,10 +23,18 @@ class DocumentTests: XCTestCase {
2323
}
2424

2525
func test_singleDocumentNull_encode() {
26-
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
26+
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
2727
data: single_document_null)
2828
}
2929

30+
func test_singleDocumentNonOptionalFailsOnNull() {
31+
XCTAssertThrowsError(try JSONDecoder().decode(Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
32+
from: single_document_null))
33+
}
34+
}
35+
36+
// MARK: - Error Document Tests
37+
extension DocumentTests {
3038
func test_unknownErrorDocumentNoMeta() {
3139
let document = decoded(type: Document<NoResourceBody, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
3240
data: error_document_no_metadata)
@@ -200,7 +208,10 @@ class DocumentTests: XCTestCase {
200208
test_DecodeEncodeEquality(type: Document<NoResourceBody, TestPageMetadata, TestLinks, NoIncludes, UnknownJSONAPIError>.self,
201209
data: error_document_no_metadata)
202210
}
211+
}
203212

213+
// MARK: - Meta Document Tests
214+
extension DocumentTests {
204215
func test_metaDataDocument() {
205216
let document = decoded(type: Document<NoResourceBody, TestPageMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
206217
data: metadata_document)
@@ -242,15 +253,19 @@ class DocumentTests: XCTestCase {
242253

243254
XCTAssertThrowsError(try JSONDecoder().decode(Document<NoResourceBody, TestPageMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, from: metadata_document_missing_metadata2))
244255
}
256+
}
245257

258+
259+
// MARK: Single Document Tests
260+
extension DocumentTests {
246261
func test_singleDocumentNoIncludes() {
247262
let document = decoded(type: Document<SingleResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
248263
data: single_document_no_includes)
249264

250265
XCTAssertFalse(document.body.isError)
251266
XCTAssertNil(document.body.errors)
252267
XCTAssertNotNil(document.body.primaryData)
253-
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
268+
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
254269
XCTAssertEqual(document.body.includes?.count, 0)
255270
XCTAssertEqual(document.body.meta, NoMetadata())
256271
}
@@ -260,14 +275,31 @@ class DocumentTests: XCTestCase {
260275
data: single_document_no_includes)
261276
}
262277

278+
func test_singleDocumentNoIncludesOptionalNotNull() {
279+
let document = decoded(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
280+
data: single_document_no_includes)
281+
282+
XCTAssertFalse(document.body.isError)
283+
XCTAssertNil(document.body.errors)
284+
XCTAssertNotNil(document.body.primaryData)
285+
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
286+
XCTAssertEqual(document.body.includes?.count, 0)
287+
XCTAssertEqual(document.body.meta, NoMetadata())
288+
}
289+
290+
func test_singleDocumentNoIncludesOptionalNotNull_encode() {
291+
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article?>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
292+
data: single_document_no_includes)
293+
}
294+
263295
func test_singleDocumentNoIncludesWithMetadata() {
264296
let document = decoded(type: Document<SingleResourceBody<Article>, TestPageMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
265297
data: single_document_no_includes_with_metadata)
266298

267299
XCTAssertFalse(document.body.isError)
268300
XCTAssertNil(document.body.errors)
269301
XCTAssertNotNil(document.body.primaryData)
270-
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
302+
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
271303
XCTAssertEqual(document.body.includes?.count, 0)
272304
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
273305
}
@@ -284,7 +316,7 @@ class DocumentTests: XCTestCase {
284316
XCTAssertFalse(document.body.isError)
285317
XCTAssertNil(document.body.errors)
286318
XCTAssertNotNil(document.body.primaryData)
287-
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
319+
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
288320
XCTAssertEqual(document.body.includes?.count, 0)
289321
XCTAssertEqual(document.body.meta, NoMetadata())
290322
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
@@ -306,7 +338,7 @@ class DocumentTests: XCTestCase {
306338
XCTAssertFalse(document.body.isError)
307339
XCTAssertNil(document.body.errors)
308340
XCTAssertNotNil(document.body.primaryData)
309-
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
341+
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
310342
XCTAssertEqual(document.body.includes?.count, 0)
311343
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
312344
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
@@ -336,7 +368,7 @@ class DocumentTests: XCTestCase {
336368
XCTAssertFalse(document.body.isError)
337369
XCTAssertNil(document.body.errors)
338370
XCTAssertNotNil(document.body.primaryData)
339-
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
371+
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
340372
XCTAssertEqual(document.body.includes?.count, 1)
341373
XCTAssertEqual(document.body.includes?[Author.self].count, 1)
342374
XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33")
@@ -354,7 +386,7 @@ class DocumentTests: XCTestCase {
354386
XCTAssertFalse(document.body.isError)
355387
XCTAssertNil(document.body.errors)
356388
XCTAssertNotNil(document.body.primaryData)
357-
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
389+
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
358390
XCTAssertEqual(document.body.includes?.count, 1)
359391
XCTAssertEqual(document.body.includes?[Author.self].count, 1)
360392
XCTAssertEqual(document.body.includes?[Author.self][0].id.rawValue, "33")
@@ -373,7 +405,7 @@ class DocumentTests: XCTestCase {
373405
XCTAssertFalse(document.body.isError)
374406
XCTAssertNil(document.body.errors)
375407
XCTAssertNotNil(document.body.primaryData)
376-
XCTAssertEqual(document.body.primaryData?.value?.id.rawValue, "1")
408+
XCTAssertEqual(document.body.primaryData?.value.id.rawValue, "1")
377409
XCTAssertEqual(document.body.meta, TestPageMetadata(total: 70, limit: 40, offset: 10))
378410
XCTAssertEqual(document.body.links?.link.url, "https://website.com")
379411
XCTAssertEqual(document.body.links?.link.meta, NoMetadata())
@@ -388,19 +420,25 @@ class DocumentTests: XCTestCase {
388420
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Article>, TestPageMetadata, TestLinks, Include1<Author>, UnknownJSONAPIError>.self,
389421
data: single_document_some_includes_with_metadata_with_links)
390422
}
423+
}
391424

425+
// MARK: Poly PrimaryResource Tests
426+
extension DocumentTests {
392427
func test_singleDocument_PolyPrimaryResource() {
393428
let article = Article(id: Id(rawValue: "1"), relationships: .init(author: ToOneRelationship(id: Id(rawValue: "33"))))
394429
let document = decoded(type: Document<SingleResourceBody<Poly2<Article, Author>>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_no_includes)
395430

396-
XCTAssertEqual(document.body.primaryData?.value?[Article.self], article)
397-
XCTAssertNil(document.body.primaryData?.value?[Author.self])
431+
XCTAssertEqual(document.body.primaryData?.value[Article.self], article)
432+
XCTAssertNil(document.body.primaryData?.value[Author.self])
398433
}
399434

400435
func test_singleDocument_PolyPrimaryResource_encode() {
401436
test_DecodeEncodeEquality(type: Document<SingleResourceBody<Poly2<Article, Author>>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self, data: single_document_no_includes)
402437
}
403-
438+
}
439+
440+
// MARK: - ManyResourceBody Tests
441+
extension DocumentTests {
404442
func test_manyDocumentNoIncludes() {
405443
let document = decoded(type: Document<ManyResourceBody<Article>, NoMetadata, NoLinks, NoIncludes, UnknownJSONAPIError>.self,
406444
data: many_document_no_includes)

Tests/JSONAPITests/XCTestManifests.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ extension DocumentTests {
5757
("test_singleDocumentNoIncludes", test_singleDocumentNoIncludes),
5858
("test_singleDocumentNoIncludes_encode", test_singleDocumentNoIncludes_encode),
5959
("test_singleDocumentNoIncludesMissingMetadata", test_singleDocumentNoIncludesMissingMetadata),
60+
("test_singleDocumentNoIncludesOptionalNotNull", test_singleDocumentNoIncludesOptionalNotNull),
61+
("test_singleDocumentNoIncludesOptionalNotNull_encode", test_singleDocumentNoIncludesOptionalNotNull_encode),
6062
("test_singleDocumentNoIncludesWithLinks", test_singleDocumentNoIncludesWithLinks),
6163
("test_singleDocumentNoIncludesWithLinks_encode", test_singleDocumentNoIncludesWithLinks_encode),
6264
("test_singleDocumentNoIncludesWithMetadata", test_singleDocumentNoIncludesWithMetadata),
@@ -66,6 +68,7 @@ extension DocumentTests {
6668
("test_singleDocumentNoIncludesWithMetadataWithLinks_encode", test_singleDocumentNoIncludesWithMetadataWithLinks_encode),
6769
("test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinks_encode", test_singleDocumentNoIncludesWithSomeIncludesMetadataWithLinks_encode),
6870
("test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinks", test_singleDocumentNoIncludesWithSomeIncludesWithMetadataWithLinks),
71+
("test_singleDocumentNonOptionalFailsOnNull", test_singleDocumentNonOptionalFailsOnNull),
6972
("test_singleDocumentNull", test_singleDocumentNull),
7073
("test_singleDocumentNull_encode", test_singleDocumentNull_encode),
7174
("test_singleDocumentSomeIncludes", test_singleDocumentSomeIncludes),
@@ -116,6 +119,7 @@ extension EntityTests {
116119
("test_EntitySomeRelationshipsNoAttributes_encode", test_EntitySomeRelationshipsNoAttributes_encode),
117120
("test_EntitySomeRelationshipsSomeAttributes", test_EntitySomeRelationshipsSomeAttributes),
118121
("test_EntitySomeRelationshipsSomeAttributes_encode", test_EntitySomeRelationshipsSomeAttributes_encode),
122+
("test_initialization", test_initialization),
119123
("test_IntOver10_encode", test_IntOver10_encode),
120124
("test_IntOver10_failure", test_IntOver10_failure),
121125
("test_IntOver10_success", test_IntOver10_success),

0 commit comments

Comments
 (0)