Skip to content

Commit c9d3885

Browse files
committed
Made it much more convenient to work with Non-EntityType relationships. Discovered and fixed a bug where nullable relationships were encoded incorrectly.
1 parent 1061283 commit c9d3885

8 files changed

Lines changed: 176 additions & 55 deletions

File tree

Sources/JSONAPI/Resource/Attribute.swift

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,12 +83,13 @@ extension TransformedAttribute {
8383

8484
// See note in decode above about the weirdness
8585
// going on here.
86-
let anyNil: Any? = nil
87-
if let _ = anyNil as? Transformer.From,
88-
(rawValue as Any?) == nil {
89-
try container.encodeNil()
90-
}
91-
86+
// let anyNil: Any? = nil
87+
// let nilRawValue = anyNil as? Transformer.From
88+
// guard rawValue != nilRawValue else {
89+
// try container.encodeNil()
90+
// return
91+
// }
92+
9293
try container.encode(rawValue)
9394
}
9495
}

Sources/JSONAPI/Resource/Id.swift

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,31 @@ public struct Unidentified: MaybeRawId, CustomStringConvertible {
3434
public var description: String { return "Unidentified" }
3535
}
3636

37-
public protocol MaybeId: Codable {
37+
public protocol OptionalId: Codable {
3838
associatedtype IdentifiableType: JSONAPI.JSONTyped
3939
associatedtype RawType: MaybeRawId
40-
}
4140

42-
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
4341
var rawValue: RawType { get }
42+
init(rawValue: RawType)
43+
}
44+
45+
public protocol IdType: OptionalId, CustomStringConvertible, Hashable where RawType: RawIdType {}
46+
47+
extension Optional: MaybeRawId where Wrapped: Codable & Equatable {}
48+
extension Optional: OptionalId where Wrapped: IdType {
49+
public typealias IdentifiableType = Wrapped.IdentifiableType
50+
public typealias RawType = Wrapped.RawType?
51+
52+
public var rawValue: Wrapped.RawType? {
53+
guard case .some(let value) = self else {
54+
return nil
55+
}
56+
return value.rawValue
57+
}
58+
59+
public init(rawValue: Wrapped.RawType?) {
60+
self = rawValue.map { Wrapped(rawValue: $0) }
61+
}
4462
}
4563

4664
public extension IdType {
@@ -53,7 +71,7 @@ public protocol CreatableIdType: IdType {
5371

5472
/// An Entity ID. These IDs can be encoded to or decoded from
5573
/// JSON API IDs.
56-
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Codable, Equatable, MaybeId {
74+
public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equatable, OptionalId {
5775

5876
public let rawValue: RawType
5977

@@ -63,7 +81,8 @@ public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Coda
6381

6482
public init(from decoder: Decoder) throws {
6583
let container = try decoder.singleValueContainer()
66-
rawValue = try container.decode(RawType.self)
84+
let rawValue = try container.decode(RawType.self)
85+
self.init(rawValue: rawValue)
6786
}
6887

6988
public func encode(to encoder: Encoder) throws {
@@ -72,7 +91,11 @@ public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Coda
7291
}
7392
}
7493

75-
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
94+
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {
95+
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
96+
return Id(rawValue: rawValue)
97+
}
98+
}
7699

77100
extension Id: CreatableIdType where RawType: CreatableRawIdType {
78101
public init() {

Sources/JSONAPI/Resource/Relationship.swift

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

8-
public protocol RelationshipType: Codable {
8+
public protocol RelationshipType {
99
associatedtype LinksType
1010
associatedtype MetaType
1111

@@ -117,7 +117,7 @@ extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks
117117
}
118118

119119
public protocol Identifiable: JSONTyped {
120-
associatedtype Identifier: Equatable, Codable
120+
associatedtype Identifier: Equatable
121121
}
122122

123123
/// The Relatable protocol describes anything that
@@ -148,7 +148,7 @@ private enum ResourceIdentifierCodingKeys: String, CodingKey {
148148
case entityType = "type"
149149
}
150150

151-
extension ToOneRelationship {
151+
extension ToOneRelationship: Codable where Identifiable.Identifier: OptionalId {
152152
public init(from decoder: Decoder) throws {
153153
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
154154

@@ -184,7 +184,7 @@ extension ToOneRelationship {
184184
throw JSONAPIEncodingError.typeMismatch(expected: Identifiable.type, found: type)
185185
}
186186

187-
id = try identifier.decode(Identifiable.Identifier.self, forKey: .id)
187+
id = Identifiable.Identifier(rawValue: try identifier.decode(Identifiable.Identifier.RawType.self, forKey: .id))
188188
}
189189

190190
public func encode(to encoder: Encoder) throws {
@@ -202,14 +202,23 @@ extension ToOneRelationship {
202202
try container.encode(links, forKey: .links)
203203
}
204204

205+
// If id is nil, instead of {id: , type: } we will just
206+
// encode `null`
207+
let anyNil: Any? = nil
208+
let nilId = anyNil as? Identifiable.Identifier
209+
guard id != nilId else {
210+
try container.encodeNil(forKey: .data)
211+
return
212+
}
213+
205214
var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
206215

207-
try identifier.encode(id, forKey: .id)
216+
try identifier.encode(id.rawValue, forKey: .id)
208217
try identifier.encode(Identifiable.type, forKey: .entityType)
209218
}
210219
}
211220

212-
extension ToManyRelationship {
221+
extension ToManyRelationship: Codable {
213222
public init(from decoder: Decoder) throws {
214223
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
215224

@@ -237,7 +246,7 @@ extension ToManyRelationship {
237246
throw JSONAPIEncodingError.typeMismatch(expected: Relatable.type, found: type)
238247
}
239248

240-
newIds.append(try identifier.decode(Relatable.Identifier.self, forKey: .id))
249+
newIds.append(Relatable.Identifier(rawValue: try identifier.decode(Relatable.Identifier.RawType.self, forKey: .id)))
241250
}
242251
ids = newIds
243252
}
@@ -258,7 +267,7 @@ extension ToManyRelationship {
258267
for id in ids {
259268
var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
260269

261-
try identifier.encode(id, forKey: .id)
270+
try identifier.encode(id.rawValue, forKey: .id)
262271
try identifier.encode(Relatable.type, forKey: .entityType)
263272
}
264273
}

Tests/JSONAPITests/Entity/EntityTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ extension EntityTests {
226226
XCTAssertEqual(entity[\.here], "Hello")
227227
XCTAssertNil(entity[\.maybeHereMaybeNull])
228228
XCTAssertNoThrow(try TestEntity7.check(entity))
229+
230+
print(encodable: entity)
229231
}
230232

231233
func test_NullOptionalNullableAttribute_encode() {

Tests/JSONAPITests/NonJSONAPIRelatable/NonJSONAPIRelatableTests.swift

Lines changed: 83 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,93 @@ import XCTest
99
import JSONAPI
1010

1111
class NonJSONAPIRelatableTests: XCTestCase {
12+
func test_initialization1() {
13+
let e1 = NonJSONAPIEntity(id: .init(rawValue: "hello"))
14+
let e2 = NonJSONAPIEntity(id: .init(rawValue: "world"))
1215

16+
let entity = TestEntity(relationships: .init(one: .init(id: e1.id), many: .init(ids: [e1.id, e2.id])))
17+
18+
XCTAssertEqual(entity ~> \.one, e1.id)
19+
XCTAssertEqual(entity ~> \.many, [e1.id, e2.id])
20+
21+
XCTAssertNoThrow(try TestEntity.check(entity))
22+
}
23+
24+
func test_initialization2_all_relationships_there() {
25+
let e1 = NonJSONAPIEntity(id: .init(rawValue: "hello"))
26+
let e2 = NonJSONAPIEntity(id: .init(rawValue: "world"))
27+
28+
let entity = TestEntity2(relationships: .init(nullableOne: .init(id: e1.id), nullableMaybeOne: .init(id: e2.id), maybeOne: .init(id: e2.id), maybeMany: .init(ids: [e2.id, e1.id])))
29+
30+
XCTAssertEqual((entity ~> \.nullableOne)?.rawValue, "hello")
31+
XCTAssertEqual((entity ~> \.nullableMaybeOne)?.rawValue, "world")
32+
XCTAssertEqual((entity ~> \.maybeOne)?.rawValue, "world")
33+
XCTAssertEqual((entity ~> \.maybeMany)?.map { $0.rawValue }, ["world", "hello"])
34+
}
35+
36+
func test_initialization2_all_relationships_missing() {
37+
38+
let entity = TestEntity2(relationships: .init(nullableOne: .init(id: nil), nullableMaybeOne: .init(id: nil), maybeOne: nil, maybeMany: nil))
39+
let entity2 = TestEntity2(relationships: .init(nullableOne: .init(id: nil), nullableMaybeOne: nil, maybeOne: nil, maybeMany: nil))
40+
41+
XCTAssertNil((entity ~> \.nullableOne))
42+
XCTAssertNil((entity ~> \.nullableMaybeOne))
43+
XCTAssertNil((entity ~> \.maybeOne))
44+
XCTAssertNil((entity ~> \.maybeMany))
45+
46+
XCTAssertNil((entity2 ~> \.nullableOne))
47+
XCTAssertNil((entity2 ~> \.nullableMaybeOne))
48+
XCTAssertNil((entity2 ~> \.maybeOne))
49+
XCTAssertNil((entity2 ~> \.maybeMany))
50+
}
1351
}
1452

1553
// MARK: - Test Types
1654
extension NonJSONAPIRelatableTests {
17-
// enum TestEntityDescription: EntityDescription {
18-
// static var type: String { return "test" }
19-
//
20-
// typealias Attributes = NoAttributes
21-
//
22-
// struct Relationships: JSONAPI.Relationships {
23-
// let one: ToOneRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>
24-
// let many: ToManyRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>
25-
// }
26-
// }
27-
28-
// enum NonJSONAPIEntityDescription: EntityDescription {
29-
// static var type: String { return "other" }
30-
//
31-
// typealias Attributes = NoAttributes
32-
// typealias Relationships = NoRelationships
33-
// }
55+
enum TestEntityDescription: EntityDescription {
56+
static var type: String { return "test" }
3457

35-
// struct NonJSONAPIEntity: Relatable, OptionalRelatable, JSONTyped {
36-
// static var type: String { return "other" }
37-
//
38-
// typealias Identifier = NonJSONAPIEntity.Id
39-
// typealias WrappedIdentifier = NonJSONAPIEntity.Id
40-
//
41-
// let id: Id
42-
//
43-
// let attributes: NoAttributes
44-
// let relationships: NoRelationships
45-
//
46-
// struct Id: IdType {
47-
// var rawValue: String
48-
//
49-
// typealias IdentifiableType = NonJSONAPIEntity
50-
// typealias RawType = String
51-
// }
52-
// }
58+
typealias Attributes = NoAttributes
59+
60+
struct Relationships: JSONAPI.Relationships {
61+
let one: ToOneRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>
62+
let many: ToManyRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>
63+
}
64+
}
65+
66+
typealias TestEntity = JSONAPI.Entity<TestEntityDescription, NoMetadata, NoLinks, String>
67+
68+
enum TestEntity2Description: EntityDescription {
69+
static var type: String { return "test" }
70+
71+
typealias Attributes = NoAttributes
72+
73+
struct Relationships: JSONAPI.Relationships {
74+
let nullableOne: ToOneRelationship<NonJSONAPIEntity?, NoMetadata, NoLinks>
75+
let nullableMaybeOne: ToOneRelationship<NonJSONAPIEntity?, NoMetadata, NoLinks>?
76+
let maybeOne: ToOneRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>?
77+
let maybeMany: ToManyRelationship<NonJSONAPIEntity, NoMetadata, NoLinks>?
78+
}
79+
}
80+
81+
typealias TestEntity2 = JSONAPI.Entity<TestEntity2Description, NoMetadata, NoLinks, String>
82+
83+
struct NonJSONAPIEntity: Relatable, JSONTyped {
84+
static var type: String { return "other" }
85+
86+
typealias Identifier = NonJSONAPIEntity.Id
87+
88+
let id: Id
89+
90+
struct Id: IdType {
91+
var rawValue: String
92+
93+
typealias IdentifiableType = NonJSONAPIEntity
94+
typealias RawType = String
95+
96+
static func id(from rawValue: String) -> Id {
97+
return Id(rawValue: rawValue)
98+
}
99+
}
100+
}
53101
}

Tests/JSONAPITests/Relationships/RelationshipTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ extension RelationshipTests {
140140
}
141141
}
142142

143+
// MARK: Nullable
144+
extension RelationshipTests {
145+
func test_ToOneNullableIsNullIfNil() {
146+
let relationship = ToOneNullable(entity: nil)
147+
let relationshipData = try! JSONEncoder().encode(relationship)
148+
let relationshipString = String(data: relationshipData, encoding: .utf8)!
149+
150+
XCTAssertEqual(relationshipString, "{\"data\":null}")
151+
}
152+
153+
func test_ToOneNullableIsEqualToNonNullableIfNotNil() {
154+
let entity = TestEntity1()
155+
let relationship1 = ToOneNonNullable(entity: entity)
156+
let relationship2 = ToOneNullable(entity: entity)
157+
158+
XCTAssertEqual(encoded(value: relationship1), encoded(value: relationship2))
159+
}
160+
}
161+
143162
// MARK: Failure tests
144163
extension RelationshipTests {
145164
func test_ToManyTypeMismatch() {
@@ -172,6 +191,9 @@ extension RelationshipTests {
172191
typealias ToManyWithLinks = ToManyRelationship<TestEntity1, NoMetadata, TestLinks>
173192
typealias ToManyWithMetaAndLinks = ToManyRelationship<TestEntity1, TestMeta, TestLinks>
174193

194+
typealias ToOneNullable = ToOneRelationship<TestEntity1?, NoMetadata, NoLinks>
195+
typealias ToOneNonNullable = ToOneRelationship<TestEntity1, NoMetadata, NoLinks>
196+
175197
struct TestMeta: JSONAPI.Meta {
176198
let a: String
177199
}

Tests/JSONAPITests/Test Helpers/EncodeDecode.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ func decoded<T: Decodable>(type: T.Type, data: Data) -> T {
1212
return try! JSONDecoder().decode(T.self, from: data)
1313
}
1414

15+
func encoded<T: Encodable>(value: T) -> Data {
16+
return try! JSONEncoder().encode(value)
17+
}
18+
1519
/// A helper function that tests that decode() == decode().encode().decode().
1620
/// If decoding is well tested and the above is true then encoding is well
1721
/// tested.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// PrintEncoded.swift
3+
// JSONAPITests
4+
//
5+
// Created by Mathew Polzin on 12/8/18.
6+
//
7+
8+
import Foundation
9+
10+
func print<T: Encodable>(encodable: T) {
11+
print(String(data: try! JSONEncoder().encode(encodable), encoding: .utf8)!)
12+
}

0 commit comments

Comments
 (0)