From 5ca42a4e2a9715bb9b5a660fa57d2ca4c2687029 Mon Sep 17 00:00:00 2001 From: Christophe Vila Date: Sat, 19 Sep 2026 16:55:41 +0200 Subject: [PATCH] feat: add flattenEntries/flattenGroups tree-flattening helpers Co-Authored-By: Claude Haiku 4.5 Claude-Session: https://claude.ai/code/session_01WYqycDFsynHH9VnnK7LNSf --- .../MyPassCore/Session/GroupTree.swift | 46 ++++++++++++++++ .../MyPassCoreTests/GroupTreeTests.swift | 52 +++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 MyPassCore/Sources/MyPassCore/Session/GroupTree.swift create mode 100644 MyPassCore/Tests/MyPassCoreTests/GroupTreeTests.swift diff --git a/MyPassCore/Sources/MyPassCore/Session/GroupTree.swift b/MyPassCore/Sources/MyPassCore/Session/GroupTree.swift new file mode 100644 index 0000000..8268e24 --- /dev/null +++ b/MyPassCore/Sources/MyPassCore/Session/GroupTree.swift @@ -0,0 +1,46 @@ +import Foundation + +/// An entry paired with the full ancestor chain of groups leading to it, excluding the +/// vault's root group. Empty `breadcrumb` means the entry lives directly in the root group. +public struct FlatEntry: Identifiable, Equatable { + public let entry: Entry + public let parentGroupId: UUID + public let breadcrumb: [Group] + public var id: UUID { entry.id } +} + +/// A group paired with the ancestor chain leading to its *parent*, excluding the vault's +/// root group. The root group itself appears with an empty breadcrumb. +public struct FlatGroup: Identifiable, Equatable { + public let group: Group + public let breadcrumb: [Group] + public var id: UUID { group.id } +} + +/// Walks the vault tree once, pairing every entry with its full ancestor path. +public func flattenEntries(in root: Group) -> [FlatEntry] { + var result: [FlatEntry] = [] + func walk(_ group: Group, ancestorBreadcrumb: [Group]) { + for entry in group.entries { + result.append(FlatEntry(entry: entry, parentGroupId: group.id, breadcrumb: ancestorBreadcrumb)) + } + for sub in group.subgroups { + walk(sub, ancestorBreadcrumb: ancestorBreadcrumb + [sub]) + } + } + walk(root, ancestorBreadcrumb: []) + return result +} + +/// Walks the vault tree once, producing every group (including the root) with its ancestor path. +public func flattenGroups(in root: Group) -> [FlatGroup] { + var result: [FlatGroup] = [FlatGroup(group: root, breadcrumb: [])] + func walk(_ group: Group, ancestorBreadcrumb: [Group]) { + for sub in group.subgroups { + result.append(FlatGroup(group: sub, breadcrumb: ancestorBreadcrumb)) + walk(sub, ancestorBreadcrumb: ancestorBreadcrumb + [sub]) + } + } + walk(root, ancestorBreadcrumb: []) + return result +} diff --git a/MyPassCore/Tests/MyPassCoreTests/GroupTreeTests.swift b/MyPassCore/Tests/MyPassCoreTests/GroupTreeTests.swift new file mode 100644 index 0000000..bd27e9a --- /dev/null +++ b/MyPassCore/Tests/MyPassCoreTests/GroupTreeTests.swift @@ -0,0 +1,52 @@ +import XCTest +@testable import MyPassCore + +final class GroupTreeTests: XCTestCase { + private func makeSampleTree() -> (root: Group, work: Group, email: Group) { + let supportEntry = Entry(title: "Support Login", username: "alice") + let email = Group(name: "Email", entries: [supportEntry]) + let bankEntry = Entry(title: "Bank", username: "bob") + let work = Group(name: "Work", subgroups: [email], entries: [bankEntry]) + let rootEntry = Entry(title: "Root Entry", username: "root-user") + let root = Group(name: "Root", subgroups: [work], entries: [rootEntry]) + return (root, work, email) + } + + func test_flattenEntries_rootEntry_hasEmptyBreadcrumb() { + let (root, _, _) = makeSampleTree() + let flat = flattenEntries(in: root) + let rootEntry = flat.first { $0.entry.title == "Root Entry" } + XCTAssertEqual(rootEntry?.breadcrumb, []) + XCTAssertEqual(rootEntry?.parentGroupId, root.id) + } + + func test_flattenEntries_directChildEntry_hasSingleGroupBreadcrumb() { + let (root, work, _) = makeSampleTree() + let flat = flattenEntries(in: root) + let bankEntry = flat.first { $0.entry.title == "Bank" } + XCTAssertEqual(bankEntry?.breadcrumb.map(\.name), ["Work"]) + XCTAssertEqual(bankEntry?.parentGroupId, work.id) + } + + func test_flattenEntries_nestedEntry_hasFullBreadcrumb() { + let (root, _, email) = makeSampleTree() + let flat = flattenEntries(in: root) + let supportEntry = flat.first { $0.entry.title == "Support Login" } + XCTAssertEqual(supportEntry?.breadcrumb.map(\.name), ["Work", "Email"]) + XCTAssertEqual(supportEntry?.parentGroupId, email.id) + } + + func test_flattenGroups_includesRootFirstWithEmptyBreadcrumb() { + let (root, _, _) = makeSampleTree() + let flat = flattenGroups(in: root) + XCTAssertEqual(flat.first?.group.id, root.id) + XCTAssertEqual(flat.first?.breadcrumb, []) + } + + func test_flattenGroups_nestedGroup_hasBreadcrumb() { + let (root, _, _) = makeSampleTree() + let flat = flattenGroups(in: root) + let email = flat.first { $0.group.name == "Email" } + XCTAssertEqual(email?.breadcrumb.map(\.name), ["Work"]) + } +}