Skip to content

Commit 163ac94

Browse files
committed
I did some more type wrangling to finally get the Id type to specialize on Entity rather than EntityDescription. The compiler gets into trouble depending on which of a few semantically identical routes are taken, but I finally stumbled upon the correct combination of protocols and extensions to get the job done. this was always the ideal outcome, but I was not sure the Swift compiler would allow it.
1 parent e67b9fc commit 163ac94

17 files changed

Lines changed: 110 additions & 75 deletions

File tree

JSONAPI.playground/Pages/Usage.xcplaygroundpage/Contents.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,13 @@ let peopleFromData = peopleResponse.body.primaryData?.values
4040
let dogsFromData = peopleResponse.body.includes?[Dog.self]
4141
let housesFromData = peopleResponse.body.includes?[House.self]
4242

43+
print("-----")
44+
print(peopleResponse)
45+
print("-----")
46+
4347
// MARK: - Pass successfully parsed body to other parts of the code
4448

4549
if case let .data(bodyData) = peopleResponse.body {
46-
print(bodyData)
4750
print("first person's name: \(bodyData.primary.values[0][\.fullName])")
4851
} else {
4952
print("no body data")

JSONAPI.playground/Sources/Entities.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ extension String: CreatableRawIdType {
2424
}
2525

2626
// MARK: - Entity typealias for convenience
27-
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, Id<String, Description>>
27+
public typealias ExampleEntity<Description: EntityDescription> = Entity<Description, String>
2828

2929
// MARK: - A few resource objects (entities)
3030
public enum PersonDescription: EntityDescription {
@@ -60,9 +60,9 @@ public enum PersonDescription: EntityDescription {
6060

6161
public typealias Person = ExampleEntity<PersonDescription>
6262

63-
public extension Entity where Description == PersonDescription, Identifier == Id<String, PersonDescription> {
64-
public init(id: Person.Identifier? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
65-
self = try Person(id: id ?? Person.Identifier(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
63+
public extension Entity where Description == PersonDescription, EntityRawIdType == String {
64+
public init(id: Person.Id? = nil,name: [String], favoriteColor: String, friends: [Person], dogs: [Dog], home: House) throws {
65+
self = try Person(id: id ?? Person.Id(), attributes: .init(name: .init(rawValue: name), favoriteColor: .init(rawValue: favoriteColor)), relationships: .init(friends: .init(entities: friends), dogs: .init(entities: dogs), home: .init(entity: home)))
6666
}
6767
}
6868

@@ -89,12 +89,12 @@ public enum DogDescription: EntityDescription {
8989

9090
public typealias Dog = ExampleEntity<DogDescription>
9191

92-
public extension Entity where Description == DogDescription, Identifier == Id<String, DogDescription> {
92+
public extension Entity where Description == DogDescription, EntityRawIdType == String {
9393
public init(name: String, owner: Person?) throws {
9494
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: DogDescription.Relationships(owner: .init(entity: owner)))
9595
}
9696

97-
public init(name: String, owner: Person.Identifier) throws {
97+
public init(name: String, owner: Person.Id) throws {
9898
self = try Dog(attributes: .init(name: .init(rawValue: name)), relationships: .init(owner: .init(id: owner)))
9999
}
100100
}

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,13 +170,13 @@ An `Entity` needs to be specialized on two generic types. The first is the `Enti
170170

171171
#### `IdType`
172172

173-
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `EntityDescription` of the type of entity the Id identifies. Having the `EntityDescription` type associated with the Id makes it easy to store all of your entities in a local hash broken out by `EntityDescription`; You can pass Ids around and always know where to look for the `Entity` to which the Id refers. `RawIdType`s are documented below.
173+
An `IdType` packages up two pieces of information: A unique identifier of a given `RawIdType` and the `Entity` type that the Id identifies. Having the `Entity` type associated with the Id makes it easy to store all of your entities in a local hash broken out by `Entity` type; You can pass Ids around and always know where to look for the `Entity` to which the Id refers. `RawIdType`s are documented below.
174174

175175
#### Convenient `typealiases`
176176

177177
Often you can use one `RawIdType` for many if not all of your `Entities`. That means you can save yourself some boilerplate by using `typealias`es like the following:
178178
```
179-
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Id<String, Description>>
179+
public typealias Entity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, String>
180180
181181
public typealias NewEntity<Description: JSONAPI.EntityDescription> = JSONAPI.Entity<Description, Unidentified>
182182
```

Sources/JSONAPI/Document/Document.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,23 @@ extension Document {
243243

244244
extension Document: CustomStringConvertible {
245245
public var description: String {
246-
return "Document(body: \(String(describing: body))"
246+
return "Document(\(String(describing: body)))"
247+
}
248+
}
249+
250+
extension Document.Body: CustomStringConvertible {
251+
public var description: String {
252+
switch self {
253+
case .errors(let errors, meta: let meta, links: let links):
254+
return "errors: \(String(describing: errors)), meta: \(String(describing: meta)), links: \(String(describing: links))"
255+
case .data(let data):
256+
return String(describing: data)
257+
}
258+
}
259+
}
260+
261+
extension Document.Body.Data: CustomStringConvertible {
262+
public var description: String {
263+
return "primary: \(String(describing: primary)), includes: \(String(describing: includes)), meta: \(String(describing: meta)), links: \(String(describing: links))"
247264
}
248265
}

Sources/JSONAPI/Meta/Links.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
public protocol Links: Codable, Equatable {}
1010

1111
/// Use NoLinks where no links should belong to a JSON API component
12-
public struct NoLinks: Links {
12+
public struct NoLinks: Links, CustomStringConvertible {
1313
public static var none: NoLinks { return NoLinks() }
1414
public init() {}
15+
16+
public var description: String { return "No Links" }
1517
}
1618

1719
public protocol JSONAPIURL: Codable, Equatable {}

Sources/JSONAPI/Meta/Meta.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@ public protocol Meta: Codable, Equatable {
1919
// nullable.
2020
extension Optional: Meta where Wrapped: Meta {}
2121

22-
public struct NoMetadata: Meta {
22+
public struct NoMetadata: Meta, CustomStringConvertible {
2323
public static var none: NoMetadata { return NoMetadata() }
2424

2525
public init() { }
26+
27+
public var description: String { return "No Metadata" }
2628
}

Sources/JSONAPI/Resource/Entity.swift

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ public protocol EntityDescription {
3939
/// specialization.
4040
public protocol EntityType: PrimaryResource {
4141
associatedtype Description: EntityDescription
42-
associatedtype Identifier: Equatable & Codable
42+
associatedtype EntityRawIdType: JSONAPI.MaybeRawId
43+
44+
typealias Id = JSONAPI.Id<EntityRawIdType, Self>
4345

4446
typealias Attributes = Description.Attributes
4547
typealias Relationships = Description.Relationships
@@ -48,7 +50,7 @@ public protocol EntityType: PrimaryResource {
4850
/// the entity is being created clientside and the
4951
/// server is being asked to create a unique Id. Otherwise,
5052
/// this should be of a type conforming to `IdType`.
51-
var id: Identifier { get }
53+
var id: Id { get }
5254

5355
/// The JSON API compliant attributes of this `Entity`.
5456
var attributes: Attributes { get }
@@ -57,11 +59,13 @@ public protocol EntityType: PrimaryResource {
5759
var relationships: Relationships { get }
5860
}
5961

62+
public protocol IdentifiableEntityType: EntityType, Relatable where EntityRawIdType: JSONAPI.RawIdType {}
63+
6064
/// An `Entity` is a single model type that can be
6165
/// encoded to or decoded from a JSON API
6266
/// "Resource Object."
6367
/// See https://jsonapi.org/format/#document-resource-objects
64-
public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI.Identifier>: EntityType {
68+
public struct Entity<Description: JSONAPI.EntityDescription, EntityRawIdType: JSONAPI.MaybeRawId>: EntityType {
6569

6670
/// The JSON API compliant "type" of this `Entity`.
6771
public static var type: String { return Description.type }
@@ -70,74 +74,79 @@ public struct Entity<Description: JSONAPI.EntityDescription, Identifier: JSONAPI
7074
/// the entity is being created clientside and the
7175
/// server is being asked to create a unique Id. Otherwise,
7276
/// this should be of a type conforming to `IdType`.
73-
public let id: Identifier
77+
public let id: Entity.Id
7478

7579
/// The JSON API compliant attributes of this `Entity`.
7680
public let attributes: Description.Attributes
7781

7882
/// The JSON API compliant relationships of this `Entity`.
7983
public let relationships: Description.Relationships
8084

81-
public init(id: Identifier, attributes: Description.Attributes, relationships: Description.Relationships) {
85+
public init(id: Entity.Id, attributes: Description.Attributes, relationships: Description.Relationships) {
8286
self.id = id
8387
self.attributes = attributes
8488
self.relationships = relationships
8589
}
8690
}
8791

92+
extension Entity: IdentifiableEntityType, Relatable, WrappedRelatable where EntityRawIdType: JSONAPI.RawIdType {
93+
public typealias Identifier = Entity.Id
94+
public typealias WrappedIdentifier = Identifier
95+
}
96+
8897
extension Entity: CustomStringConvertible {
8998
public var description: String {
9099
return "Entity<\(Entity.type)>(id: \(String(describing: id)), attributes: \(String(describing: attributes)), relationships: \(String(describing: relationships)))"
91100
}
92101
}
93102

94103
// MARK: Convenience initializers
95-
extension Entity where Identifier: CreatableIdType {
104+
extension Entity where EntityRawIdType: CreatableRawIdType {
96105
public init(attributes: Description.Attributes, relationships: Description.Relationships) {
97-
self.id = Identifier()
106+
self.id = Entity.Id()
98107
self.attributes = attributes
99108
self.relationships = relationships
100109
}
101110
}
102111

103112
extension Entity where Description.Attributes == NoAttributes {
104-
public init(id: Identifier, relationships: Description.Relationships) {
113+
public init(id: Entity.Id, relationships: Description.Relationships) {
105114
self.init(id: id, attributes: NoAttributes(), relationships: relationships)
106115
}
107116
}
108117

109-
extension Entity where Description.Attributes == NoAttributes, Identifier: CreatableIdType {
118+
extension Entity where Description.Attributes == NoAttributes, EntityRawIdType: CreatableRawIdType {
110119
public init(relationships: Description.Relationships) {
111120
self.init(attributes: NoAttributes(), relationships: relationships)
112121
}
113122
}
114123

115124
extension Entity where Description.Relationships == NoRelationships {
116-
public init(id: Identifier, attributes: Description.Attributes) {
125+
public init(id: Entity.Id, attributes: Description.Attributes) {
117126
self.init(id: id, attributes: attributes, relationships: NoRelationships())
118127
}
119128
}
120129

121-
extension Entity where Description.Relationships == NoRelationships, Identifier: CreatableIdType {
130+
extension Entity where Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
122131
public init(attributes: Description.Attributes) {
123132
self.init(attributes: attributes, relationships: NoRelationships())
124133
}
125134
}
126135

127136
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships {
128-
public init(id: Identifier) {
137+
public init(id: Entity.Id) {
129138
self.init(id: id, attributes: NoAttributes(), relationships: NoRelationships())
130139
}
131140
}
132141

133-
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, Identifier: CreatableIdType {
142+
extension Entity where Description.Attributes == NoAttributes, Description.Relationships == NoRelationships, EntityRawIdType: CreatableRawIdType {
134143
public init() {
135144
self.init(attributes: NoAttributes(), relationships: NoRelationships())
136145
}
137146
}
138147

139148
// MARK: Pointer for Relationships use.
140-
public extension Entity where Identifier: IdType {
149+
public extension Entity where EntityRawIdType: JSONAPI.RawIdType {
141150
/// Get a pointer to this entity that can be used as a
142151
/// relationship to another entity.
143152
public var pointer: ToOneRelationship<Entity> {
@@ -202,7 +211,7 @@ public extension Entity {
202211

203212
try container.encode(Entity.type, forKey: .type)
204213

205-
if Identifier.self != Unidentified<Description>.self {
214+
if EntityRawIdType.self != Unidentified.self {
206215
try container.encode(id, forKey: .id)
207216
}
208217

@@ -224,8 +233,9 @@ public extension Entity {
224233
guard Entity.type == type else {
225234
throw JSONAPIEncodingError.typeMismatch(expected: Description.type, found: type)
226235
}
227-
228-
id = try (Unidentified<Description>() as? Identifier) ?? container.decode(Identifier.self, forKey: .id)
236+
237+
let maybeUnidentified = Unidentified() as? EntityRawIdType
238+
id = try maybeUnidentified.map { Entity.Id(rawValue: $0) } ?? container.decode(Entity.Id.self, forKey: .id)
229239

230240
attributes = try (NoAttributes() as? Description.Attributes) ?? container.decode(Description.Attributes.self, forKey: .attributes)
231241

Sources/JSONAPI/Resource/Id.swift

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

8+
/// All types that are RawIdType and additionally
9+
/// Unidentified conform to this protocol. You
10+
/// should not add conformance to this protocol
11+
/// directly.
12+
public protocol MaybeRawId: Codable, Equatable {}
13+
814
/// Any type that you would like to be encoded to and
915
/// decoded from JSON API ids should conform to this
1016
/// protocol. Conformance for `String` is given.
11-
public protocol RawIdType: Codable, Hashable {}
17+
public protocol RawIdType: MaybeRawId, Hashable {}
1218

1319
/// If you would like to be able to create new
1420
/// Entities with Ids backed by a RawIdType then
@@ -22,19 +28,18 @@ public protocol CreatableRawIdType: RawIdType {
2228

2329
extension String: RawIdType {}
2430

25-
public protocol Identifier: Codable, Equatable {
26-
associatedtype EntityDescription: JSONAPI.EntityDescription
27-
}
28-
29-
public struct Unidentified<EntityDescription: JSONAPI.EntityDescription>: Identifier, CustomStringConvertible {
31+
public struct Unidentified: MaybeRawId, CustomStringConvertible {
3032
public init() {}
3133

32-
public var description: String { return "Id(Unidentified)" }
34+
public var description: String { return "Unidentified" }
3335
}
3436

35-
public protocol IdType: Identifier, Hashable, CustomStringConvertible {
36-
associatedtype RawType: RawIdType
37-
37+
public protocol MaybeId: Codable {
38+
associatedtype EntityType: JSONAPI.EntityType
39+
associatedtype RawType: MaybeRawId
40+
}
41+
42+
public protocol IdType: MaybeId, CustomStringConvertible, Hashable where RawType: RawIdType {
3843
var rawValue: RawType { get }
3944
}
4045

@@ -48,27 +53,33 @@ public protocol CreatableIdType: IdType {
4853

4954
/// An Entity ID. These IDs can be encoded to or decoded from
5055
/// JSON API IDs.
51-
public struct Id<RawType: RawIdType, EntityDescription: JSONAPI.EntityDescription>: IdType {
56+
public struct Id<RawType: MaybeRawId, EntityType: JSONAPI.EntityType>: Codable, Equatable, MaybeId {
5257

5358
public let rawValue: RawType
5459

5560
public init(rawValue: RawType) {
5661
self.rawValue = rawValue
5762
}
58-
63+
5964
public init(from decoder: Decoder) throws {
6065
let container = try decoder.singleValueContainer()
6166
rawValue = try container.decode(RawType.self)
6267
}
63-
68+
6469
public func encode(to encoder: Encoder) throws {
6570
var container = encoder.singleValueContainer()
6671
try container.encode(rawValue)
6772
}
6873
}
6974

75+
extension Id: Hashable, CustomStringConvertible, IdType where RawType: RawIdType {}
76+
7077
extension Id: CreatableIdType where RawType: CreatableRawIdType {
7178
public init() {
7279
rawValue = .unique()
7380
}
7481
}
82+
83+
extension Id where RawType == Unidentified {
84+
public static var unidentified: Id { return .init(rawValue: Unidentified()) }
85+
}

0 commit comments

Comments
 (0)