Skip to content

Commit 1e2a87a

Browse files
authored
Merge pull request #69 from mattpolzin/filter-relatives
Add filtering of relatives to CompoundResource
2 parents 2940be3 + a57c6b7 commit 1e2a87a

2 files changed

Lines changed: 194 additions & 1 deletion

File tree

Sources/JSONAPI/Document/CompoundResource.swift

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
// Created by Mathew Polzin on 5/25/20.
66
//
77

8+
public protocol CompoundResourceProtocol {
9+
associatedtype JSONAPIModel: JSONAPI.ResourceObjectType
10+
associatedtype JSONAPIIncludeType: JSONAPI.Include
11+
12+
var primary: JSONAPIModel { get }
13+
var relatives: [JSONAPIIncludeType] { get }
14+
15+
func filteringRelatives(by filter: (JSONAPIIncludeType) -> Bool) -> Self
16+
}
17+
818
/// A Resource Object and any relevant related resources. This object
919
/// is helpful in the context of constructing a Document.
1020
///
@@ -16,14 +26,49 @@
1626
/// specialized for a single or batch document at the same time as you are
1727
/// resolving (i.e. materializing or decoding) one or more resources and its
1828
/// relatives.
19-
public struct CompoundResource<JSONAPIModel: JSONAPI.ResourceObjectType, JSONAPIIncludeType: JSONAPI.Include>: Equatable {
29+
///
30+
/// - Important: This type is not intended to guarantee
31+
/// that all `relationships` of the primary resource are available
32+
/// in the `relatives` array.
33+
public struct CompoundResource<JSONAPIModel: JSONAPI.ResourceObjectType, JSONAPIIncludeType: JSONAPI.Include>: Equatable, CompoundResourceProtocol {
2034
public let primary: JSONAPIModel
2135
public let relatives: [JSONAPIIncludeType]
2236

2337
public init(primary: JSONAPIModel, relatives: [JSONAPIIncludeType]) {
2438
self.primary = primary
2539
self.relatives = relatives
2640
}
41+
42+
/// Create a new Compound Resource having
43+
/// filtered the relatives by the given closure (which
44+
/// must return `true` for any relative that should
45+
/// remain part of the `CompoundObject`).
46+
///
47+
/// This does not remove relatives from the primary
48+
/// resource's `relationships`, it just filters out
49+
/// which relatives have complete resource objects
50+
/// in the newly created `CompoundResource`.
51+
public func filteringRelatives(by filter: (JSONAPIIncludeType) -> Bool) -> CompoundResource {
52+
return .init(
53+
primary: primary,
54+
relatives: relatives.filter(filter)
55+
)
56+
}
57+
}
58+
59+
extension Sequence where Element: CompoundResourceProtocol {
60+
/// Create new Compound Resources having
61+
/// filtered the relatives by the given closure (which
62+
/// must return `true` for any relative that should
63+
/// remain part of the `CompoundObject`).
64+
///
65+
/// This does not remove relatives from the primary
66+
/// resource's `relationships`, it just filters out
67+
/// which relatives have complete resource objects
68+
/// in the newly created `CompoundResource`.
69+
public func filteringRelatives(by filter: (Element.JSONAPIIncludeType) -> Bool) -> [Element] {
70+
return map { $0.filteringRelatives(by: filter) }
71+
}
2772
}
2873

2974
extension EncodableJSONAPIDocument where PrimaryResourceBody: EncodableResourceBody, PrimaryResourceBody.PrimaryResource: ResourceObjectType {

Tests/JSONAPITests/Document/DocumentCompoundResourceTests.swift

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,94 @@ final class DocumentCompoundResourceTests: XCTestCase {
105105
XCTAssertEqual(document.body.includes?.values, [.init(author)])
106106
}
107107

108+
func test_singleDocumentWithFilteredIncludes() {
109+
let author = DocumentTests.Author(
110+
attributes: .none,
111+
relationships: .none,
112+
meta: .none,
113+
links: .none
114+
)
115+
116+
let ids = [
117+
DocumentTests.Book.Id(),
118+
DocumentTests.Book.Id(),
119+
DocumentTests.Book.Id()
120+
]
121+
122+
let book = DocumentTests.Book(
123+
id: ids[0],
124+
attributes: .init(pageCount: 10),
125+
relationships: .init(
126+
author: .init(resourceObject: author),
127+
series: .init(ids: [ids[1], ids[2]]),
128+
collection: nil
129+
),
130+
meta: .none,
131+
links: .none
132+
)
133+
134+
let book2 = DocumentTests.Book(
135+
id: ids[1],
136+
attributes: .init(pageCount: 10),
137+
relationships: .init(
138+
author: .init(resourceObject: author),
139+
series: .init(ids: [ids[0], ids[2]]),
140+
collection: nil
141+
),
142+
meta: .none,
143+
links: .none
144+
)
145+
146+
let book3 = DocumentTests.Book(
147+
id: ids[2],
148+
attributes: .init(pageCount: 10),
149+
relationships: .init(
150+
author: .init(resourceObject: author),
151+
series: .init(ids: [ids[0], ids[1]]),
152+
collection: nil
153+
),
154+
meta: .none,
155+
links: .none
156+
)
157+
158+
typealias Document = JSONAPI.Document<SingleResourceBody<DocumentTests.Book>, NoMetadata, NoLinks, Include2<DocumentTests.Author, DocumentTests.Book>, NoAPIDescription, UnknownJSONAPIError>
159+
160+
let compoundBook = Document.CompoundResource(
161+
primary: book,
162+
relatives: [.init(author), .init(book2), .init(book3)]
163+
)
164+
165+
let document = Document(
166+
apiDescription: .none,
167+
resource: compoundBook.filteringRelatives { $0.value is DocumentTests.Author },
168+
meta: .none,
169+
links: .none
170+
)
171+
172+
XCTAssertEqual(document.body.primaryResource?.value, book)
173+
XCTAssertEqual(document.body.includes?.values, [.init(author)])
174+
175+
let document2 = Document(
176+
apiDescription: .none,
177+
resource: compoundBook.filteringRelatives { $0.value is DocumentTests.Book },
178+
meta: .none,
179+
links: .none
180+
)
181+
182+
XCTAssertEqual(document2.body.primaryResource?.value, book)
183+
XCTAssertEqual(document2.body.includes?.values, [.init(book2), .init(book3)])
184+
185+
let document3 = Document(
186+
apiDescription: .none,
187+
resource: compoundBook,
188+
meta: .none,
189+
links: .none
190+
)
191+
192+
XCTAssertEqual(document3.body.primaryResource?.value, book)
193+
XCTAssertEqual(document3.body.includes?.values, [.init(author), .init(book2), .init(book3)])
194+
}
195+
108196
func test_batchDocumentNoIncludes() {
109197
let author = DocumentTests.Author(
110198
attributes: .none,
@@ -351,4 +439,64 @@ final class DocumentCompoundResourceTests: XCTestCase {
351439
XCTAssertEqual(document.body.primaryResource?.values, [book, book2])
352440
XCTAssertEqual(document.body.includes?.values, [.init(author), .init(author2)])
353441
}
442+
443+
func test_batchDocumentWithFilteredIncludes() {
444+
let author = DocumentTests.Author(
445+
attributes: .none,
446+
relationships: .none,
447+
meta: .none,
448+
links: .none
449+
)
450+
451+
let author2 = DocumentTests.Author(
452+
attributes: .none,
453+
relationships: .none,
454+
meta: .none,
455+
links: .none
456+
)
457+
458+
let book = DocumentTests.Book(
459+
attributes: .init(pageCount: 10),
460+
relationships: .init(
461+
author: .init(resourceObject: author),
462+
series: .init(ids: []),
463+
collection: nil
464+
),
465+
meta: .none,
466+
links: .none
467+
)
468+
469+
let book2 = DocumentTests.Book(
470+
attributes: .init(pageCount: 10),
471+
relationships: .init(
472+
author: .init(resourceObject: author2),
473+
series: .init(ids: []),
474+
collection: nil
475+
),
476+
meta: .none,
477+
links: .none
478+
)
479+
480+
typealias Document = JSONAPI.Document<ManyResourceBody<DocumentTests.Book>, NoMetadata, NoLinks, Include1<DocumentTests.Author>, NoAPIDescription, UnknownJSONAPIError>
481+
482+
let compoundBook = Document.CompoundResource(
483+
primary: book,
484+
relatives: [.init(author)]
485+
)
486+
487+
let compoundBook2 = Document.CompoundResource(
488+
primary: book2,
489+
relatives: [.init(author2)]
490+
)
491+
492+
let document = Document(
493+
apiDescription: .none,
494+
resources: [compoundBook, compoundBook2].filteringRelatives { $0.a?.id == author.id },
495+
meta: .none,
496+
links: .none
497+
)
498+
499+
XCTAssertEqual(document.body.primaryResource?.values, [book, book2])
500+
XCTAssertEqual(document.body.includes?.values, [.init(author)])
501+
}
354502
}

0 commit comments

Comments
 (0)