main
swift 74 lines 2.35 KB
Raw
1 import Foundation
2
3 final class ContentRepository {
4 let entries: [GuideEntry]
5 let collections: [GuideCollection]
6
7 private let entriesByID: [String: GuideEntry]
8
9 convenience init(bundle: Bundle = .main) {
10 self.init(
11 entries: ContentRepository.loadEntries(from: bundle),
12 collections: ContentRepository.loadCollections(from: bundle)
13 )
14 }
15
16 /// Creates a repository from in-memory content. Useful for tests and previews
17 /// that need deterministic fixtures without loading bundled JSON.
18 init(entries: [GuideEntry], collections: [GuideCollection]) {
19 self.entries = entries
20 self.collections = collections
21 self.entriesByID = Dictionary(uniqueKeysWithValues: entries.map { ($0.id, $0) })
22 }
23
24 var featuredEntries: [GuideEntry] {
25 entries.filter(\.isFeatured)
26 }
27
28 func entry(id: String) -> GuideEntry? {
29 entriesByID[id]
30 }
31
32 func entries(ids: [String]) -> [GuideEntry] {
33 ids.compactMap { entriesByID[$0] }
34 }
35
36 func relatedEntries(for entry: GuideEntry) -> [GuideEntry] {
37 entries(ids: entry.relatedIDs)
38 }
39
40 func entries(in collection: GuideCollection) -> [GuideEntry] {
41 entries(ids: collection.entryIDs)
42 }
43
44 private static func loadEntries(from bundle: Bundle) -> [GuideEntry] {
45 loadJSON(named: "guide_entries", from: bundle) ?? []
46 }
47
48 private static func loadCollections(from bundle: Bundle) -> [GuideCollection] {
49 loadJSON(named: "collections", from: bundle) ?? []
50 }
51
52 private static func loadJSON<T: Decodable>(named fileName: String, from bundle: Bundle) -> T? {
53 let decoder = JSONDecoder()
54 let candidateURLs = [
55 bundle.url(
56 forResource: fileName, withExtension: "json", subdirectory: "Resources/Content"),
57 bundle.url(forResource: fileName, withExtension: "json"),
58 ]
59
60 for candidate in candidateURLs {
61 guard let url = candidate else { continue }
62
63 do {
64 let data = try Data(contentsOf: url)
65 return try decoder.decode(T.self, from: data)
66 } catch {
67 assertionFailure("Failed to decode \(fileName).json: \(error.localizedDescription)")
68 }
69 }
70
71 assertionFailure("Missing bundled content file: \(fileName).json")
72 return nil
73 }
74 }