Skip to content

Commit 41a2a01

Browse files
committed
Added support for relationship operator ~> to optional relationships
1 parent 53f7f55 commit 41a2a01

4 files changed

Lines changed: 178 additions & 11 deletions

File tree

Sources/JSONAPI/Resource/Entity.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,12 +420,37 @@ public extension EntityProxy {
420420
return entity.relationships[keyPath: path].id
421421
}
422422

423+
/// Access to an Id of an optional `ToOneRelationship`.
424+
/// This allows you to write `entity ~> \.other` instead
425+
/// of `entity.relationships.other?.id`.
426+
public static func ~><OtherEntity: OptionalRelatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.WrappedIdentifier where OtherEntity.WrappedIdentifier == OtherEntity.Identifier? {
427+
// Implementation Note: This signature applies to `ToOneRelationship<E?, _, _>?`
428+
// whereas the one below applies to `ToOneRelationship<E, _, _>?`
429+
return entity.relationships[keyPath: path]?.id
430+
}
431+
432+
/// Access to an Id of an optional `ToOneRelationship`.
433+
/// This allows you to write `entity ~> \.other` instead
434+
/// of `entity.relationships.other?.id`.
435+
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToOneRelationship<OtherEntity, MType, LType>?>) -> OtherEntity.Identifier? where OtherEntity.WrappedIdentifier == OtherEntity.Identifier {
436+
// Implementation Note: This signature applies to `ToOneRelationship<E, _, _>?`
437+
// whereas the one above applies to `ToOneRelationship<E?, _, _>?`
438+
return entity.relationships[keyPath: path]?.id
439+
}
440+
423441
/// Access to all Ids of a `ToManyRelationship`.
424442
/// This allows you to write `entity ~> \.others` instead
425443
/// of `entity.relationships.others.ids`.
426444
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>>) -> [OtherEntity.Identifier] {
427445
return entity.relationships[keyPath: path].ids
428446
}
447+
448+
/// Access to all Ids of an optional `ToManyRelationship`.
449+
/// This allows you to write `entity ~> \.others` instead
450+
/// of `entity.relationships.others?.ids`.
451+
public static func ~><OtherEntity: Relatable, MType: JSONAPI.Meta, LType: JSONAPI.Links>(entity: Self, path: KeyPath<Description.Relationships, ToManyRelationship<OtherEntity, MType, LType>?>) -> [OtherEntity.Identifier]? {
452+
return entity.relationships[keyPath: path]?.ids
453+
}
429454
}
430455

431456
infix operator ~>

Sources/JSONAPITestLib/EntityCheck.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ public extension Entity {
7676
}
7777

7878
for relationship in relationshipsMirror.children {
79-
if relationship.value as? _RelationshipType == nil {
79+
if relationship.value as? _RelationshipType == nil,
80+
relationship.value as? OptionalRelationshipType == nil {
8081
problems.append(.nonRelationship(named: relationship.label ?? "unnamed"))
8182
}
8283
}

Tests/JSONAPITests/Entity/EntityTests.swift

Lines changed: 71 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ class EntityTests: XCTestCase {
2424

2525
XCTAssertEqual(entity2 ~> \.other, entity1.id)
2626
}
27+
28+
func test_optional_relationship_operator_access() {
29+
30+
}
2731

2832
func test_toMany_relationship_operator_access() {
2933
let entity1 = TestEntity1()
@@ -33,6 +37,10 @@ class EntityTests: XCTestCase {
3337

3438
XCTAssertEqual(entity3 ~> \.others, [entity1.id, entity2.id, entity4.id])
3539
}
40+
41+
func test_optionalToMany_relationship_opeartor_access() {
42+
43+
}
3644

3745
func test_relationshipIds() {
3846
let entity1 = TestEntity1()
@@ -63,9 +71,12 @@ class EntityTests: XCTestCase {
6371
let _ = TestEntity6(id: .init(rawValue: "6"), attributes: .init(here: .init(value: "here"), maybeHere: nil, maybeNull: .init(value: nil)))
6472
let _ = TestEntity7(id: .init(rawValue: "7"), attributes: .init(here: .init(value: "hello"), maybeHereMaybeNull: .init(value: "world")))
6573
XCTAssertNoThrow(try TestEntity8(id: .init(rawValue: "8"), attributes: .init(string: .init(value: "hello"), int: .init(value: 10), stringFromInt: .init(rawValue: 20), plus: .init(rawValue: 30), doubleFromInt: .init(rawValue: 32), omitted: nil, nullToString: .init(rawValue: nil))))
66-
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil))
67-
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: .init(entity: nil)))
68-
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: .init(entity: entity1, meta: .none, links: .none)))
74+
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: nil, optionalMany: nil))
75+
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: .init(entity: nil), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil))
76+
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: .init(entity: entity1, meta: .none, links: .none), optionalOne: nil, optionalNullableOne: nil, optionalMany: nil))
77+
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: entity1.pointer, optionalNullableOne: nil, optionalMany: nil))
78+
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(entity: entity1, meta: .none, links: .none), optionalMany: nil))
79+
let _ = TestEntity9(id: .init(rawValue: "9"), relationships: .init(one: entity1.pointer, nullableOne: nil, optionalOne: nil, optionalNullableOne: .init(entity: entity1, meta: .none, links: .none), optionalMany: .init(entities: [], meta: .none, links: .none)))
6980
let e10id1 = TestEntity10.Identifier(rawValue: "hello")
7081
let e10id2 = TestEntity10.Id(rawValue: "world")
7182
let e10id3: TestEntity10.Id = "!"
@@ -275,18 +286,50 @@ extension EntityTests {
275286

276287
// MARK: Relationship omission and nullification
277288
extension EntityTests {
289+
func test_nullableRelationshipNotNullOrOmitted() {
290+
let entity = decoded(type: TestEntity9.self,
291+
data: entity_optional_not_omitted_relationship)
292+
293+
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
294+
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
295+
XCTAssertNil(entity ~> \.optionalOne)
296+
XCTAssertEqual((entity ~> \.optionalNullableOne)?.rawValue, "1229")
297+
XCTAssertNoThrow(try TestEntity9.check(entity))
298+
}
299+
300+
func test_nullableRelationshipNotNullOrOmitted_encode() {
301+
test_DecodeEncodeEquality(type: TestEntity9.self,
302+
data: entity_optional_not_omitted_relationship)
303+
}
304+
278305
func test_nullableRelationshipNotNull() {
279306
let entity = decoded(type: TestEntity9.self,
280-
data: entity_omitted_relationship)
307+
data: entity_omitted_relationship)
281308

282309
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
283310
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
311+
XCTAssertNil(entity ~> \.optionalNullableOne)
284312
XCTAssertNoThrow(try TestEntity9.check(entity))
285313
}
286314

287315
func test_nullableRelationshipNotNull_encode() {
288316
test_DecodeEncodeEquality(type: TestEntity9.self,
289-
data: entity_omitted_relationship)
317+
data: entity_omitted_relationship)
318+
}
319+
320+
func test_optionalNullableRelationshipNulled() {
321+
let entity = decoded(type: TestEntity9.self,
322+
data: entity_optional_nullable_nulled_relationship)
323+
324+
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
325+
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
326+
XCTAssertNil(entity ~> \.optionalNullableOne)
327+
XCTAssertNoThrow(try TestEntity9.check(entity))
328+
}
329+
330+
func test_optionalNullableRelationshipNulled_encode() {
331+
test_DecodeEncodeEquality(type: TestEntity9.self,
332+
data: entity_optional_nullable_nulled_relationship)
290333
}
291334

292335
func test_nullableRelationshipIsNull() {
@@ -295,13 +338,30 @@ extension EntityTests {
295338

296339
XCTAssertNil(entity ~> \.nullableOne)
297340
XCTAssertEqual((entity ~> \.one).rawValue, "4452")
341+
XCTAssertNil(entity ~> \.optionalNullableOne)
298342
XCTAssertNoThrow(try TestEntity9.check(entity))
299343
}
300344

301345
func test_nullableRelationshipIsNull_encode() {
302346
test_DecodeEncodeEquality(type: TestEntity9.self,
303347
data: entity_nulled_relationship)
304348
}
349+
350+
func test_optionalToManyIsNotOmitted() {
351+
let entity = decoded(type: TestEntity9.self,
352+
data: entity_optional_to_many_relationship_not_omitted)
353+
354+
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "3323")
355+
XCTAssertEqual((entity ~> \.one).rawValue, "4459")
356+
XCTAssertEqual((entity ~> \.optionalMany)?[0].rawValue, "332223")
357+
XCTAssertNil(entity ~> \.optionalNullableOne)
358+
XCTAssertNoThrow(try TestEntity9.check(entity))
359+
}
360+
361+
func test_optionalToManyIsNotOmitted_encode() {
362+
test_DecodeEncodeEquality(type: TestEntity9.self,
363+
data: entity_optional_to_many_relationship_not_omitted)
364+
}
305365
}
306366

307367
// MARK: Relationships of same type as root entity
@@ -581,13 +641,14 @@ extension EntityTests {
581641

582642
let nullableOne: ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>
583643

644+
let optionalOne: ToOneRelationship<TestEntity1, NoMetadata, NoLinks>?
645+
646+
let optionalNullableOne: ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>?
647+
648+
let optionalMany: ToManyRelationship<TestEntity1, NoMetadata, NoLinks>?
649+
584650
// a nullable many is not allowed. it should
585651
// just be an empty array.
586-
587-
// omitted relationships are not allowed either,
588-
// so ToOneRelationship<TestEntity1>? (with the
589-
// question on the relationship, not the entity)
590-
// is not a thing.
591652
}
592653
}
593654

Tests/JSONAPITests/Entity/stubs/EntityStubs.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,57 @@ let entity_int_to_string_attribute = """
228228
}
229229
""".data(using: .utf8)!
230230

231+
let entity_optional_not_omitted_relationship = """
232+
{
233+
"id": "1",
234+
"type": "ninth_test_entities",
235+
"relationships": {
236+
"nullableOne": {
237+
"data": {
238+
"id": "3323",
239+
"type": "test_entities"
240+
}
241+
},
242+
"one": {
243+
"data": {
244+
"id": "4459",
245+
"type": "test_entities"
246+
}
247+
},
248+
"optionalNullableOne": {
249+
"data": {
250+
"id": "1229",
251+
"type": "test_entities"
252+
}
253+
}
254+
}
255+
}
256+
""".data(using: .utf8)!
257+
258+
let entity_optional_nullable_nulled_relationship = """
259+
{
260+
"id": "1",
261+
"type": "ninth_test_entities",
262+
"relationships": {
263+
"nullableOne": {
264+
"data": {
265+
"id": "3323",
266+
"type": "test_entities"
267+
}
268+
},
269+
"one": {
270+
"data": {
271+
"id": "4459",
272+
"type": "test_entities"
273+
}
274+
},
275+
"optionalNullableOne": {
276+
"data": null
277+
}
278+
}
279+
}
280+
""".data(using: .utf8)!
281+
231282
let entity_omitted_relationship = """
232283
{
233284
"id": "1",
@@ -249,6 +300,35 @@ let entity_omitted_relationship = """
249300
}
250301
""".data(using: .utf8)!
251302

303+
let entity_optional_to_many_relationship_not_omitted = """
304+
{
305+
"id": "1",
306+
"type": "ninth_test_entities",
307+
"relationships": {
308+
"nullableOne": {
309+
"data": {
310+
"id": "3323",
311+
"type": "test_entities"
312+
}
313+
},
314+
"one": {
315+
"data": {
316+
"id": "4459",
317+
"type": "test_entities"
318+
}
319+
},
320+
"optionalMany": {
321+
"data": [
322+
{
323+
"id": "332223",
324+
"type": "test_entities"
325+
}
326+
]
327+
}
328+
}
329+
}
330+
""".data(using: .utf8)!
331+
252332
let entity_nulled_relationship = """
253333
{
254334
"id": "1",

0 commit comments

Comments
 (0)