Skip to content

Commit 5ad33c6

Browse files
authored
Merge pull request #66 from mattpolzin/feature/compound-resources
Add CompoundResource.
2 parents 0d43093 + 0095828 commit 5ad33c6

8 files changed

Lines changed: 630 additions & 17 deletions

File tree

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//
2+
// CompoundResource.swift
3+
//
4+
//
5+
// Created by Mathew Polzin on 5/25/20.
6+
//
7+
8+
/// A Resource Object and any relevant related resources. This object
9+
/// is helpful in the context of constructing a Document.
10+
///
11+
/// You can resolve a primary resource and all of the intended includes
12+
/// for that resource and pass them around as a `CompoundResource`
13+
/// prior to constructing a Document.
14+
///
15+
/// Among other things, using this abstraction means you do not need to
16+
/// specialized for a single or batch document at the same time as you are
17+
/// resolving (i.e. materializing or decoding) one or more resources and its
18+
/// relatives.
19+
public struct CompoundResource<JSONAPIModel: JSONAPI.ResourceObjectType, JSONAPIIncludeType: JSONAPI.Include>: Equatable {
20+
public let primary: JSONAPIModel
21+
public let relatives: [JSONAPIIncludeType]
22+
23+
public init(primary: JSONAPIModel, relatives: [JSONAPIIncludeType]) {
24+
self.primary = primary
25+
self.relatives = relatives
26+
}
27+
}
28+
29+
extension EncodableJSONAPIDocument where PrimaryResourceBody: EncodableResourceBody, PrimaryResourceBody.PrimaryResource: ResourceObjectType {
30+
public typealias CompoundResource = JSONAPI.CompoundResource<PrimaryResourceBody.PrimaryResource, IncludeType>
31+
}
32+
33+
extension SucceedableJSONAPIDocument where PrimaryResourceBody: SingleResourceBodyProtocol, PrimaryResourceBody.PrimaryResource: ResourceObjectType {
34+
35+
public init(
36+
apiDescription: APIDescription,
37+
resource: CompoundResource,
38+
meta: MetaType,
39+
links: LinksType
40+
) {
41+
self.init(
42+
apiDescription: apiDescription,
43+
body: .init(resourceObject: resource.primary),
44+
includes: .init(values: resource.relatives),
45+
meta: meta,
46+
links: links
47+
)
48+
}
49+
}
50+
51+
extension SucceedableJSONAPIDocument where PrimaryResourceBody: ManyResourceBodyProtocol, PrimaryResourceBody.PrimaryResource: ResourceObjectType, IncludeType: Hashable {
52+
53+
public init(
54+
apiDescription: APIDescription,
55+
resources: [CompoundResource],
56+
meta: MetaType,
57+
links: LinksType
58+
) {
59+
var included = Set<Int>()
60+
let includes = resources.reduce(into: [IncludeType]()) { (result, next) in
61+
for include in next.relatives {
62+
if !included.contains(include.hashValue) {
63+
included.insert(include.hashValue)
64+
result.append(include)
65+
}
66+
}
67+
}
68+
self.init(
69+
apiDescription: apiDescription,
70+
body: .init(resourceObjects: resources.map(\.primary)),
71+
includes: .init(values: Array(includes)),
72+
meta: meta,
73+
links: links
74+
)
75+
}
76+
}

Sources/JSONAPI/Document/Document.swift

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public protocol DocumentBody: DocumentBodyContext {
7878
}
7979

8080
/// An `EncodableJSONAPIDocument` supports encoding but not decoding.
81-
/// It is actually more restrictive than `JSONAPIDocument` which supports both
81+
/// It is more restrictive than `CodableJSONAPIDocument` which supports both
8282
/// encoding and decoding.
8383
public protocol EncodableJSONAPIDocument: Equatable, Encodable, DocumentBodyContext {
8484
associatedtype APIDescription: APIDescriptionType
@@ -103,6 +103,38 @@ public protocol EncodableJSONAPIDocument: Equatable, Encodable, DocumentBodyCont
103103
var apiDescription: APIDescription { get }
104104
}
105105

106+
/// A Document that can be constructed as successful (i.e. not an error document).
107+
public protocol SucceedableJSONAPIDocument: EncodableJSONAPIDocument {
108+
/// Create a successful JSONAPI:Document.
109+
///
110+
/// - Parameters:
111+
/// - apiDescription: The description of the API (a.k.a. the "JSON:API Object").
112+
/// - body: The primary resource body of the JSON:API Document. Generally a single resource or a batch of resources.
113+
/// - includes: All related resources that are included in this Document.
114+
/// - meta: Any metadata associated with the Document.
115+
/// - links: Any links associated with the Document.
116+
///
117+
init(
118+
apiDescription: APIDescription,
119+
body: PrimaryResourceBody,
120+
includes: Includes<IncludeType>,
121+
meta: MetaType,
122+
links: LinksType
123+
)
124+
}
125+
126+
/// A Document that can be constructed as failed (i.e. an error document with no primary
127+
/// resource).
128+
public protocol FailableJSONAPIDocument: EncodableJSONAPIDocument {
129+
/// Create an error JSONAPI:Document.
130+
init(
131+
apiDescription: APIDescription,
132+
errors: [Error],
133+
meta: MetaType?,
134+
links: LinksType?
135+
)
136+
}
137+
106138
/// A `CodableJSONAPIDocument` supports encoding and decoding of a JSON:API
107139
/// compliant Document.
108140
public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable where PrimaryResourceBody: JSONAPI.CodableResourceBody, IncludeType: Decodable {}
@@ -115,7 +147,7 @@ public protocol CodableJSONAPIDocument: EncodableJSONAPIDocument, Decodable wher
115147
/// API uses snake case, you will want to use
116148
/// a conversion such as the one offerred by the
117149
/// Foundation JSONEncoder/Decoder: `KeyDecodingStrategy`
118-
public struct Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: EncodableJSONAPIDocument {
150+
public struct Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, MetaType: JSONAPI.Meta, LinksType: JSONAPI.Links, IncludeType: JSONAPI.Include, APIDescription: APIDescriptionType, Error: JSONAPIError>: EncodableJSONAPIDocument, SucceedableJSONAPIDocument, FailableJSONAPIDocument {
119151
public typealias Include = IncludeType
120152
public typealias BodyData = Body.Data
121153

@@ -125,19 +157,23 @@ public struct Document<PrimaryResourceBody: JSONAPI.EncodableResourceBody, MetaT
125157
// See `EncodableJSONAPIDocument` for documentation.
126158
public let body: Body
127159

128-
public init(apiDescription: APIDescription,
129-
errors: [Error],
130-
meta: MetaType? = nil,
131-
links: LinksType? = nil) {
160+
public init(
161+
apiDescription: APIDescription,
162+
errors: [Error],
163+
meta: MetaType? = nil,
164+
links: LinksType? = nil
165+
) {
132166
body = .errors(errors, meta: meta, links: links)
133167
self.apiDescription = apiDescription
134168
}
135169

136-
public init(apiDescription: APIDescription,
137-
body: PrimaryResourceBody,
138-
includes: Includes<Include>,
139-
meta: MetaType,
140-
links: LinksType) {
170+
public init(
171+
apiDescription: APIDescription,
172+
body: PrimaryResourceBody,
173+
includes: Includes<Include>,
174+
meta: MetaType,
175+
links: LinksType
176+
) {
141177
self.body = .data(
142178
.init(
143179
primary: body,
@@ -449,14 +485,19 @@ extension Document.Body.Data: CustomStringConvertible {
449485
extension Document {
450486
/// A Document that only supports error bodies. This is useful if you wish to pass around a
451487
/// Document type but you wish to constrain it to error values.
452-
public struct ErrorDocument: EncodableJSONAPIDocument {
488+
public struct ErrorDocument: EncodableJSONAPIDocument, FailableJSONAPIDocument {
453489
public typealias BodyData = Document.BodyData
454490

455491
public var body: Document.Body { return document.body }
456492

457493
private let document: Document
458494

459-
public init(apiDescription: APIDescription, errors: [Error], meta: MetaType? = nil, links: LinksType? = nil) {
495+
public init(
496+
apiDescription: APIDescription,
497+
errors: [Error],
498+
meta: MetaType? = nil,
499+
links: LinksType? = nil
500+
) {
460501
document = .init(apiDescription: apiDescription, errors: errors, meta: meta, links: links)
461502
}
462503

@@ -500,7 +541,7 @@ extension Document {
500541

501542
/// A Document that only supports success bodies. This is useful if you wish to pass around a
502543
/// Document type but you wish to constrain it to success values.
503-
public struct SuccessDocument: EncodableJSONAPIDocument {
544+
public struct SuccessDocument: EncodableJSONAPIDocument, SucceedableJSONAPIDocument {
504545
public typealias BodyData = Document.BodyData
505546
public typealias APIDescription = Document.APIDescription
506547
public typealias Body = Document.Body

Sources/JSONAPI/Document/ResourceBody.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public func +<R: ResourceBodyAppendable>(_ left: R, right: R) -> R {
5050

5151
public protocol SingleResourceBodyProtocol: EncodableResourceBody {
5252
var value: PrimaryResource { get }
53+
54+
init(resourceObject: PrimaryResource)
5355
}
5456

5557
/// A type allowing for a document body containing 1 primary resource.
@@ -65,6 +67,8 @@ public struct SingleResourceBody<PrimaryResource: JSONAPI.OptionalEncodablePrima
6567

6668
public protocol ManyResourceBodyProtocol: EncodableResourceBody {
6769
var values: [PrimaryResource] { get }
70+
71+
init(resourceObjects: [PrimaryResource])
6872
}
6973

7074
/// A type allowing for a document body containing 0 or more primary resources.

Sources/JSONAPI/Resource/Id.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,14 @@ public struct Id<RawType: MaybeRawId, IdentifiableType: JSONAPI.JSONTyped>: Equa
9797
}
9898
}
9999

100-
extension Id: Hashable, CustomStringConvertible, AbstractId, IdType where RawType: RawIdType {
100+
extension Id: Hashable where RawType: RawIdType {
101+
public func hash(into hasher: inout Hasher) {
102+
hasher.combine(ObjectIdentifier(Self.self))
103+
hasher.combine(rawValue)
104+
}
105+
}
106+
107+
extension Id: CustomStringConvertible, AbstractId, IdType where RawType: RawIdType {
101108
public static func id(from rawValue: RawType) -> Id<RawType, IdentifiableType> {
102109
return Id(rawValue: rawValue)
103110
}

Sources/JSONAPI/Resource/Resource Object/ResourceObject.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,18 @@ public struct ResourceObject<Description: JSONAPI.ResourceObjectDescription, Met
151151
}
152152
}
153153

154+
// `ResourceObject` is hashable as an identifiable resource which semantically
155+
// means that two different resources with the same ID should yield the same
156+
// hash value.
157+
//
158+
// "equatability" in this context will determine if two resources have _all_ the same
159+
// properties, whereas hash value will determine if two resources have the same Id.
160+
extension ResourceObject: Hashable where EntityRawIdType: RawIdType {
161+
public func hash(into hasher: inout Hasher) {
162+
hasher.combine(id)
163+
}
164+
}
165+
154166
extension ResourceObject: Identifiable, IdentifiableResourceObjectType, Relatable where EntityRawIdType: JSONAPI.RawIdType {
155167
public typealias Identifier = ResourceObject.Id
156168
}

0 commit comments

Comments
 (0)