krz/keycask

Password manager: Swift core library, CLI for macOS/Linux/Windows, iOS/macOS app. cli password-manager swift

Sources/KeycaskCore/Vault.swift

65 lines · 2081 bytes

 1import Foundation
 2
 3public struct Vault: Codable, Equatable, Sendable {
 4    public var entries: [Entry]
 5
 6    public init(entries: [Entry] = []) {
 7        self.entries = entries
 8    }
 9
10    public func entry(id: EntryID) -> Entry? {
11        entries.first { $0.id == id }
12    }
13
14    public mutating func add(_ entry: Entry) throws {
15        guard self.entry(id: entry.id) == nil else { throw KeycaskError.duplicateID(entry.id) }
16        var normalized = entry
17        normalized.tags = Entry.normalize(tags: entry.tags)
18        entries.append(normalized)
19    }
20
21    public mutating func remove(id: EntryID) throws {
22        guard let index = entries.firstIndex(where: { $0.id == id }) else {
23            throw KeycaskError.notFound(id.rawValue)
24        }
25        entries.remove(at: index)
26    }
27
28    public mutating func update(
29        id: EntryID, now: Date = .now, _ change: (inout Entry) -> Void
30    ) throws {
31        guard let index = entries.firstIndex(where: { $0.id == id }) else {
32            throw KeycaskError.notFound(id.rawValue)
33        }
34        change(&entries[index])
35        entries[index].tags = Entry.normalize(tags: entries[index].tags)
36        entries[index].updated = Entry.truncateToSeconds(now)
37    }
38
39    public func resolve(_ ref: String) throws -> Entry {
40        if let id = EntryID(ref), let hit = entry(id: id) {
41            return hit
42        }
43        let byName = entries.filter { $0.name == ref }
44        switch byName.count {
45        case 0: throw KeycaskError.notFound(ref)
46        case 1: return byName[0]
47        default: throw KeycaskError.ambiguous(name: ref, candidates: byName)
48        }
49    }
50
51    public func filter(tag: String) -> [Entry] {
52        sortedEntries.filter { $0.hasTag(tag) }
53    }
54
55    public func search(_ query: String) -> [Entry] {
56        sortedEntries.filter { $0.matches(query) }
57    }
58
59    public var sortedEntries: [Entry] {
60        entries.sorted { a, b in
61            let (la, lb) = (a.name.lowercased(), b.name.lowercased())
62            return la == lb ? a.id.rawValue < b.id.rawValue : la < lb
63        }
64    }
65}