Skip to content

Commit 158e26e

Browse files
authored
Merge pull request #83 from mattpolzin/add-relationship-id-metadata
Add id metadata to relationships.
2 parents f64ef95 + f8d02f8 commit 158e26e

30 files changed

Lines changed: 410 additions & 136 deletions

.github/workflows/tests.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ jobs:
1818
- swift:5.2-focal
1919
- swift:5.2-centos8
2020
- swift:5.2-amazonlinux2
21-
- swiftlang/swift:nightly-5.3-xenial
22-
- swiftlang/swift:nightly-5.3-bionic
21+
- swift:5.3-xenial
22+
- swift:5.3-bionic
23+
- swift:5.3-focal
24+
- swift:5.3-centos8
25+
- swift:5.3-amazonlinux2
2326
container: ${{ matrix.image }}
2427
steps:
2528
- name: Checkout code

JSONAPI.playground/Pages/Full Client & Server Example.xcplaygroundpage/Contents.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ typealias UnidentifiedJSONEntity<Description: ResourceObjectDescription> = JSONA
3030
// Create relationship typealiases because we do not expect
3131
// JSON:API Relationships for this particular API to have
3232
// Metadata or Links associated with them.
33-
typealias ToOneRelationship<Entity: JSONAPIIdentifiable> = JSONAPI.ToOneRelationship<Entity, NoMetadata, NoLinks>
34-
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoMetadata, NoLinks>
33+
typealias ToOneRelationship<Entity: JSONAPIIdentifiable> = JSONAPI.ToOneRelationship<Entity, NoIdMetadata, NoMetadata, NoLinks>
34+
typealias ToManyRelationship<Entity: Relatable> = JSONAPI.ToManyRelationship<Entity, NoIdMetadata, NoMetadata, NoLinks>
3535

3636
// Create a typealias for a Document because we do not expect
3737
// JSON:API Documents for this particular API to have Metadata, Links,

JSONAPI.playground/Pages/Full Document Verbose Generation.xcplaygroundpage/Contents.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ enum AuthorDescription: ResourceObjectDescription {
7272
}
7373

7474
struct Relationships: JSONAPI.Relationships {
75-
let articles: ToManyRelationship<Article, ToManyRelationshipMetadata, ToManyRelationshipLinks>
75+
let articles: ToManyRelationship<Article, NoIdMetadata, ToManyRelationshipMetadata, ToManyRelationshipLinks>
7676
}
7777
}
7878

@@ -88,11 +88,11 @@ enum ArticleDescription: ResourceObjectDescription {
8888

8989
struct Relationships: JSONAPI.Relationships {
9090
/// The primary attributed author of the article.
91-
let primaryAuthor: ToOneRelationship<Author, NoMetadata, NoLinks>
91+
let primaryAuthor: ToOneRelationship<Author, NoIdMetadata, NoMetadata, NoLinks>
9292
/// All authors excluding the primary author.
9393
/// It is customary to print the primary author's
9494
/// name first, followed by the other authors.
95-
let otherAuthors: ToManyRelationship<Author, ToManyRelationshipMetadata, ToManyRelationshipLinks>
95+
let otherAuthors: ToManyRelationship<Author, NoIdMetadata, ToManyRelationshipMetadata, ToManyRelationshipLinks>
9696
}
9797
}
9898

JSONAPI.playground/Sources/Entities.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ extension String: CreatableRawIdType {
2525

2626
// MARK: - typealiases for convenience
2727
public typealias ExampleEntity<Description: ResourceObjectDescription> = ResourceObject<Description, NoMetadata, NoLinks, String>
28-
public typealias ToOne<E: JSONAPIIdentifiable> = ToOneRelationship<E, NoMetadata, NoLinks>
29-
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoMetadata, NoLinks>
28+
public typealias ToOne<E: JSONAPIIdentifiable> = ToOneRelationship<E, NoIdMetadata, NoMetadata, NoLinks>
29+
public typealias ToMany<E: Relatable> = ToManyRelationship<E, NoIdMetadata, NoMetadata, NoLinks>
3030

3131
// MARK: - A few resource objects (entities)
3232
public enum PersonDescription: ResourceObjectDescription {

JSONAPI.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Pod::Spec.new do |spec|
1616
#
1717

1818
spec.name = "MP-JSONAPI"
19-
spec.version = "4.0.0"
19+
spec.version = "5.0.0"
2020
spec.summary = "Swift Codable JSON API framework."
2121

2222
# This description is used to generate tags and improve search results.

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/JSONAPI/Meta/Meta.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,10 @@ public struct NoMetadata: Meta, CustomStringConvertible {
2828

2929
public var description: String { return "No Metadata" }
3030
}
31+
32+
/// The type of metadata found in a Resource Identifier Object.
33+
///
34+
/// It is sometimes more legible to differentiate between types of metadata
35+
/// even when the underlying type is the same. This typealias is only here
36+
/// to make code more easily understandable.
37+
public typealias NoIdMetadata = NoMetadata

Sources/JSONAPI/Resource/Relationship.swift

Lines changed: 135 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -33,107 +33,173 @@ public struct MetaRelationship<MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>
3333

3434
/// A `ResourceObject` relationship that can be encoded to or decoded from
3535
/// a JSON API "Resource Linkage."
36+
///
3637
/// See https://jsonapi.org/format/#document-resource-object-linkage
38+
///
3739
/// A convenient typealias might make your code much more legible: `One<ResourceObjectDescription>`
38-
public struct ToOneRelationship<Identifiable: JSONAPI.JSONAPIIdentifiable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
40+
///
41+
/// The `IdMetaType` (if not `NoIdMetadata`) will be parsed out of the Resource Identifier Object.
42+
/// (see https://jsonapi.org/format/#document-resource-identifier-objects)
43+
///
44+
/// The `MetaType` (if not `NoMetadata`) will be parsed out of the Relationship Object.
45+
/// (see https://jsonapi.org/format/#document-resource-object-relationships)
46+
public struct ToOneRelationship<Identifiable: JSONAPI.JSONAPIIdentifiable, IdMetaType: JSONAPI.Meta, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
3947

4048
public let id: Identifiable.ID
4149

50+
public let idMeta: IdMetaType
51+
4252
public let meta: MetaType
4353
public let links: LinksType
4454

55+
public init(id: (Identifiable.ID, IdMetaType), meta: MetaType, links: LinksType) {
56+
self.id = id.0
57+
self.idMeta = id.1
58+
self.meta = meta
59+
self.links = links
60+
}
61+
}
62+
63+
extension ToOneRelationship where IdMetaType == NoIdMetadata {
4564
public init(id: Identifiable.ID, meta: MetaType, links: LinksType) {
4665
self.id = id
66+
self.idMeta = .none
4767
self.meta = meta
4868
self.links = links
4969
}
5070
}
5171

5272
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
73+
public init(id: (Identifiable.ID, IdMetaType)) {
74+
self.init(id: id, meta: .none, links: .none)
75+
}
76+
}
77+
78+
extension ToOneRelationship where IdMetaType == NoIdMetadata, MetaType == NoMetadata, LinksType == NoLinks {
5379
public init(id: Identifiable.ID) {
5480
self.init(id: id, meta: .none, links: .none)
5581
}
5682
}
5783

5884
extension ToOneRelationship {
59-
public init<T: ResourceObjectType>(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.ID {
85+
public init<T: ResourceObjectType>(resourceObject: T, meta: MetaType, links: LinksType) where T.Id == Identifiable.ID, IdMetaType == NoIdMetadata {
6086
self.init(id: resourceObject.id, meta: meta, links: links)
6187
}
6288
}
6389

64-
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks {
90+
extension ToOneRelationship where MetaType == NoMetadata, LinksType == NoLinks, IdMetaType == NoIdMetadata {
6591
public init<T: ResourceObjectType>(resourceObject: T) where T.Id == Identifiable.ID {
6692
self.init(id: resourceObject.id, meta: .none, links: .none)
6793
}
6894
}
6995

70-
extension ToOneRelationship where Identifiable: OptionalRelatable {
96+
extension ToOneRelationship where Identifiable: OptionalRelatable, IdMetaType == NoIdMetadata {
7197
public init<T: ResourceObjectType>(resourceObject: T?, meta: MetaType, links: LinksType) where T.Id == Identifiable.Wrapped.ID {
7298
self.init(id: resourceObject?.id, meta: meta, links: links)
7399
}
74100
}
75101

76-
extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks {
102+
extension ToOneRelationship where Identifiable: OptionalRelatable, MetaType == NoMetadata, LinksType == NoLinks, IdMetaType == NoIdMetadata {
77103
public init<T: ResourceObjectType>(resourceObject: T?) where T.Id == Identifiable.Wrapped.ID {
78104
self.init(id: resourceObject?.id, meta: .none, links: .none)
79105
}
80106
}
81107

82108
/// An ResourceObject relationship that can be encoded to or decoded from
83109
/// a JSON API "Resource Linkage."
110+
///
84111
/// See https://jsonapi.org/format/#document-resource-object-linkage
112+
///
85113
/// A convenient typealias might make your code much more legible: `Many<ResourceObjectDescription>`
86-
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
114+
///
115+
/// The `IdMetaType` (if not `NoIdMetadata`) will be parsed out of the Resource Identifier Object.
116+
/// (see https://jsonapi.org/format/#document-resource-identifier-objects)
117+
///
118+
/// The `MetaType` (if not `NoMetadata`) will be parsed out of the Relationship Object.
119+
/// (see https://jsonapi.org/format/#document-resource-object-relationships)
120+
public struct ToManyRelationship<Relatable: JSONAPI.Relatable, IdMetaType: JSONAPI.Meta, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links>: RelationshipType, Equatable {
121+
122+
public struct ID: Equatable {
123+
public let id: Relatable.ID
124+
public let meta: IdMetaType
125+
126+
public init(id: Relatable.ID, meta: IdMetaType) {
127+
self.id = id
128+
self.meta = meta
129+
}
87130

88-
public let ids: [Relatable.ID]
131+
internal init(_ idPair: (Relatable.ID, IdMetaType)) {
132+
self.init(id: idPair.0, meta: idPair.1)
133+
}
134+
}
135+
136+
public let idsWithMeta: [ID]
137+
138+
public var ids: [Relatable.ID] {
139+
idsWithMeta.map(\.id)
140+
}
89141

90142
public let meta: MetaType
91143
public let links: LinksType
92144

93-
public init(ids: [Relatable.ID], meta: MetaType, links: LinksType) {
94-
self.ids = ids
145+
public init(idsWithMetadata ids: [(Relatable.ID, IdMetaType)], meta: MetaType, links: LinksType) {
146+
self.idsWithMeta = ids.map { ID.init($0) }
95147
self.meta = meta
96148
self.links = links
97149
}
98150

99-
public init<T: JSONAPIIdentifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.ID == Relatable.ID {
100-
ids = pointers.map(\.id)
151+
public init<T: JSONAPIIdentifiable>(pointers: [ToOneRelationship<T, NoIdMetadata, NoMetadata, NoLinks>], meta: MetaType, links: LinksType) where T.ID == Relatable.ID, IdMetaType == NoIdMetadata {
152+
idsWithMeta = pointers.map { .init(id: $0.id, meta: .none) }
101153
self.meta = meta
102154
self.links = links
103155
}
104156

105-
public init<T: ResourceObjectType>(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.ID {
157+
public init<T: ResourceObjectType>(resourceObjects: [T], meta: MetaType, links: LinksType) where T.Id == Relatable.ID, IdMetaType == NoIdMetadata {
106158
self.init(ids: resourceObjects.map(\.id), meta: meta, links: links)
107159
}
108160

109161
private init(meta: MetaType, links: LinksType) {
110-
self.init(ids: [], meta: meta, links: links)
162+
self.init(idsWithMetadata: [], meta: meta, links: links)
111163
}
112164

113165
public static func none(withMeta meta: MetaType, links: LinksType) -> ToManyRelationship {
114166
return ToManyRelationship(meta: meta, links: links)
115167
}
116168
}
117169

170+
extension ToManyRelationship where IdMetaType == NoIdMetadata {
171+
public init(ids: [Relatable.ID], meta: MetaType, links: LinksType) {
172+
self.idsWithMeta = ids.map { .init(id: $0, meta: .none) }
173+
self.meta = meta
174+
self.links = links
175+
}
176+
}
177+
118178
extension ToManyRelationship where MetaType == NoMetadata, LinksType == NoLinks {
119179

120-
public init(ids: [Relatable.ID]) {
121-
self.init(ids: ids, meta: .none, links: .none)
180+
public init(idsWithMetadata ids: [(Relatable.ID, IdMetaType)]) {
181+
self.init(idsWithMetadata: ids, meta: .none, links: .none)
122182
}
123183

124-
public init<T: JSONAPIIdentifiable>(pointers: [ToOneRelationship<T, NoMetadata, NoLinks>]) where T.ID == Relatable.ID {
184+
public init<T: JSONAPIIdentifiable>(pointers: [ToOneRelationship<T, NoIdMetadata, NoMetadata, NoLinks>]) where T.ID == Relatable.ID, IdMetaType == NoIdMetadata {
125185
self.init(pointers: pointers, meta: .none, links: .none)
126186
}
127187

128188
public static var none: ToManyRelationship {
129189
return .none(withMeta: .none, links: .none)
130190
}
131191

132-
public init<T: ResourceObjectType>(resourceObjects: [T]) where T.Id == Relatable.ID {
192+
public init<T: ResourceObjectType>(resourceObjects: [T]) where T.Id == Relatable.ID, IdMetaType == NoIdMetadata {
133193
self.init(resourceObjects: resourceObjects, meta: .none, links: .none)
134194
}
135195
}
136196

197+
extension ToManyRelationship where IdMetaType == NoIdMetadata, MetaType == NoMetadata, LinksType == NoLinks {
198+
public init(ids: [Relatable.ID]) {
199+
self.init(ids: ids, meta: .none, links: .none)
200+
}
201+
}
202+
137203
public protocol JSONAPIIdentifiable: JSONTyped {
138204
associatedtype ID: Equatable
139205
}
@@ -164,6 +230,7 @@ private enum ResourceLinkageCodingKeys: String, CodingKey {
164230
private enum ResourceIdentifierCodingKeys: String, CodingKey {
165231
case id = "id"
166232
case entityType = "type"
233+
case metadata = "meta"
167234
}
168235

169236
extension MetaRelationship: Codable {
@@ -196,6 +263,9 @@ extension MetaRelationship: Codable {
196263
}
197264
}
198265

266+
fileprivate protocol _Optional {}
267+
extension Optional: _Optional {}
268+
199269
extension ToOneRelationship: Codable where Identifiable.ID: OptionalId {
200270
public init(from decoder: Decoder) throws {
201271
let container = try decoder.container(keyedBy: ResourceLinkageCodingKeys.self)
@@ -228,6 +298,23 @@ extension ToOneRelationship: Codable where Identifiable.ID: OptionalId {
228298
)
229299
)
230300
}
301+
// if we know we aren't getting any Resource Identifer Object at all
302+
// (which we do inside this block) then we better either be expecting
303+
// no Id Metadata or optional Id Metadata or else we will report an
304+
// error.
305+
if let noIdMeta = NoIdMetadata() as? IdMetaType {
306+
idMeta = noIdMeta
307+
} else if let nilMeta = anyNil as? IdMetaType {
308+
idMeta = nilMeta
309+
} else {
310+
throw DecodingError.valueNotFound(
311+
Self.self,
312+
DecodingError.Context(
313+
codingPath: decoder.codingPath,
314+
debugDescription: "Expected non-null relationship data with metadata inside."
315+
)
316+
)
317+
}
231318
id = val
232319
return
233320
}
@@ -256,6 +343,15 @@ extension ToOneRelationship: Codable where Identifiable.ID: OptionalId {
256343
)
257344
}
258345

346+
let idMeta: IdMetaType
347+
let maybeNoIdMeta: IdMetaType? = NoIdMetadata() as? IdMetaType
348+
if let noIdMeta = maybeNoIdMeta {
349+
idMeta = noIdMeta
350+
} else {
351+
idMeta = try identifier.decode(IdMetaType.self, forKey: .metadata)
352+
}
353+
self.idMeta = idMeta
354+
259355
id = Identifiable.ID(rawValue: try identifier.decode(Identifiable.ID.RawType.self, forKey: .id))
260356
}
261357

@@ -282,6 +378,9 @@ extension ToOneRelationship: Codable where Identifiable.ID: OptionalId {
282378
var identifier = container.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self, forKey: .data)
283379

284380
try identifier.encode(id.rawValue, forKey: .id)
381+
if IdMetaType.self != NoMetadata.self {
382+
try identifier.encode(idMeta, forKey: .metadata)
383+
}
285384
try identifier.encode(Identifiable.jsonType, forKey: .entityType)
286385
}
287386
}
@@ -311,10 +410,10 @@ extension ToManyRelationship: Codable {
311410
throw error
312411
}
313412
throw JSONAPICodingError.quantityMismatch(expected: .many,
314-
path: context.codingPath)
413+
path: context.codingPath)
315414
}
316415

317-
var newIds = [Relatable.ID]()
416+
var newIds = [ID]()
318417
while !identifiers.isAtEnd {
319418
let identifier = try identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
320419

@@ -324,9 +423,19 @@ extension ToManyRelationship: Codable {
324423
throw JSONAPICodingError.typeMismatch(expected: Relatable.jsonType, found: type, path: decoder.codingPath)
325424
}
326425

327-
newIds.append(Relatable.ID(rawValue: try identifier.decode(Relatable.ID.RawType.self, forKey: .id)))
426+
let id = try identifier.decode(Relatable.ID.RawType.self, forKey: .id)
427+
428+
let idMeta: IdMetaType
429+
let maybeNoIdMeta: IdMetaType? = NoIdMetadata() as? IdMetaType
430+
if let noIdMeta = maybeNoIdMeta {
431+
idMeta = noIdMeta
432+
} else {
433+
idMeta = try identifier.decode(IdMetaType.self, forKey: .metadata)
434+
}
435+
436+
newIds.append(.init(id: Relatable.ID(rawValue: id), meta: idMeta) )
328437
}
329-
ids = newIds
438+
idsWithMeta = newIds
330439
}
331440

332441
public func encode(to encoder: Encoder) throws {
@@ -342,10 +451,13 @@ extension ToManyRelationship: Codable {
342451

343452
var identifiers = container.nestedUnkeyedContainer(forKey: .data)
344453

345-
for id in ids {
454+
for id in idsWithMeta {
346455
var identifier = identifiers.nestedContainer(keyedBy: ResourceIdentifierCodingKeys.self)
347456

348-
try identifier.encode(id.rawValue, forKey: .id)
457+
try identifier.encode(id.id.rawValue, forKey: .id)
458+
if IdMetaType.self != NoMetadata.self {
459+
try identifier.encode(id.meta, forKey: .metadata)
460+
}
349461
try identifier.encode(Relatable.jsonType, forKey: .entityType)
350462
}
351463
}

0 commit comments

Comments
 (0)