Commit 6957cdf5cc
6957cdf5cc508d89941c26e159b1b8b3ba1a9ce8
parent: 48eb4d9dbf
Verified · cmc
cmc <hello@cleberg.net> · 2026-09-17 15:06 UTC
Add EntryID
Layout: unified · split
Sources/KeycaskCore/EntryID.swift
added
+45
| @@ -0,0 +1,45 @@ |
| |
1 | public struct EntryID: Hashable, Sendable, CustomStringConvertible { |
| |
2 | public static let alphabet: [Character] = Array("abcdefghijkmnpqrstuvwxyz23456789") |
| |
3 | public static let length = 8 |
| |
4 | |
| |
5 | public let rawValue: String |
| |
6 | |
| |
7 | public init?(_ raw: String) { |
| |
8 | guard raw.count == Self.length else { return nil } |
| |
9 | let allowed = Set(Self.alphabet) |
| |
10 | guard raw.allSatisfy({ allowed.contains($0) }) else { return nil } |
| |
11 | rawValue = raw |
| |
12 | } |
| |
13 | |
| |
14 | public static func random() -> EntryID { |
| |
15 | var rng = SystemRandomNumberGenerator() |
| |
16 | return random(using: &rng) |
| |
17 | } |
| |
18 | |
| |
19 | public static func random(using rng: inout some RandomNumberGenerator) -> EntryID { |
| |
20 | var chars: [Character] = [] |
| |
21 | chars.reserveCapacity(length) |
| |
22 | for _ in 0..<length { |
| |
23 | chars.append(alphabet[Int(rng.next(upperBound: UInt32(alphabet.count)))]) |
| |
24 | } |
| |
25 | return EntryID(String(chars))! |
| |
26 | } |
| |
27 | |
| |
28 | public var description: String { rawValue } |
| |
29 | } |
| |
30 | |
| |
31 | extension EntryID: Codable { |
| |
32 | public init(from decoder: any Decoder) throws { |
| |
33 | let raw = try decoder.singleValueContainer().decode(String.self) |
| |
34 | guard let id = EntryID(raw) else { |
| |
35 | throw DecodingError.dataCorrupted( |
| |
36 | .init(codingPath: decoder.codingPath, debugDescription: "invalid entry id \(raw)")) |
| |
37 | } |
| |
38 | self = id |
| |
39 | } |
| |
40 | |
| |
41 | public func encode(to encoder: any Encoder) throws { |
| |
42 | var container = encoder.singleValueContainer() |
| |
43 | try container.encode(rawValue) |
| |
44 | } |
| |
45 | } |
Sources/KeycaskCore/KeycaskError.swift
+2 −2
| @@ -5,7 +5,7 @@ public enum KeycaskError: Error, Equatable, Sendable { |
| 5 | case corrupt(String) |
5 | case corrupt(String) |
| 6 | case vaultExists(String) |
6 | case vaultExists(String) |
| 7 | case noVault(String) |
7 | case noVault(String) |
| 8 | case duplicateID(String) |
8 | case duplicateID(EntryID) |
| 9 | case io(String) |
9 | case io(String) |
| 10 | case usage(String) |
10 | case usage(String) |
| 11 | case failure(String) |
11 | case failure(String) |
| @@ -29,7 +29,7 @@ public enum KeycaskError: Error, Equatable, Sendable { |
| 29 | case .corrupt(let why): "vault is corrupt: \(why)" |
29 | case .corrupt(let why): "vault is corrupt: \(why)" |
| 30 | case .vaultExists(let path): "vault \(path) already exists" |
30 | case .vaultExists(let path): "vault \(path) already exists" |
| 31 | case .noVault(let path): "vault \(path) not found (run `keycask init`)" |
31 | case .noVault(let path): "vault \(path) not found (run `keycask init`)" |
| 32 | case .duplicateID(let id): "duplicate id \(id)" |
32 | case .duplicateID(let id): "duplicate id \(id.rawValue)" |
| 33 | case .io(let why): why |
33 | case .io(let why): why |
| 34 | case .usage(let why): why |
34 | case .usage(let why): why |
| 35 | case .failure(let why): why |
35 | case .failure(let why): why |
Tests/KeycaskCoreTests/EntryIDTests.swift
added
+55
| @@ -0,0 +1,55 @@ |
| |
1 | import Foundation |
| |
2 | import Testing |
| |
3 | |
| |
4 | @testable import KeycaskCore |
| |
5 | |
| |
6 | @Suite struct EntryIDTests { |
| |
7 | @Test func randomIDsHaveLengthEightFromTheAlphabet() { |
| |
8 | let allowed = Set(EntryID.alphabet) |
| |
9 | for _ in 0..<200 { |
| |
10 | let id = EntryID.random() |
| |
11 | #expect(id.rawValue.count == 8) |
| |
12 | #expect(id.rawValue.allSatisfy { allowed.contains($0) }) |
| |
13 | } |
| |
14 | } |
| |
15 | |
| |
16 | @Test func alphabetExcludesAmbiguousCharacters() { |
| |
17 | let alphabet = Set(EntryID.alphabet) |
| |
18 | #expect(alphabet.count == 32) |
| |
19 | for bad in ["l", "o", "0", "1"] { |
| |
20 | #expect(!alphabet.contains(Character(bad))) |
| |
21 | } |
| |
22 | } |
| |
23 | |
| |
24 | @Test func parsingValidatesLengthAndAlphabet() { |
| |
25 | #expect(EntryID("abcd2345") != nil) |
| |
26 | #expect(EntryID("abcd234") == nil) |
| |
27 | #expect(EntryID("abcd23456") == nil) |
| |
28 | #expect(EntryID("abcd234l") == nil) |
| |
29 | #expect(EntryID("ABCD2345") == nil) |
| |
30 | } |
| |
31 | |
| |
32 | @Test func codableIsABareString() throws { |
| |
33 | let id = EntryID("abcd2345")! |
| |
34 | let data = try JSONEncoder().encode([id]) |
| |
35 | #expect(String(decoding: data, as: UTF8.self) == "[\"abcd2345\"]") |
| |
36 | let back = try JSONDecoder().decode([EntryID].self, from: data) |
| |
37 | #expect(back == [id]) |
| |
38 | #expect(throws: DecodingError.self) { |
| |
39 | try JSONDecoder().decode([EntryID].self, from: Data("[\"bad\"]".utf8)) |
| |
40 | } |
| |
41 | } |
| |
42 | |
| |
43 | @Test func seededGeneratorIsDeterministic() { |
| |
44 | struct Counter: RandomNumberGenerator { |
| |
45 | var n: UInt64 = 0 |
| |
46 | mutating func next() -> UInt64 { |
| |
47 | n += 1 |
| |
48 | return n |
| |
49 | } |
| |
50 | } |
| |
51 | var a = Counter() |
| |
52 | var b = Counter() |
| |
53 | #expect(EntryID.random(using: &a) == EntryID.random(using: &b)) |
| |
54 | } |
| |
55 | } |
Tests/KeycaskCoreTests/KeycaskErrorTests.swift
+1 −1
| @@ -8,7 +8,7 @@ import Testing |
| 8 | #expect(KeycaskError.io("x").exitCode == 1) |
8 | #expect(KeycaskError.io("x").exitCode == 1) |
| 9 | #expect(KeycaskError.corrupt("x").exitCode == 1) |
9 | #expect(KeycaskError.corrupt("x").exitCode == 1) |
| 10 | #expect(KeycaskError.vaultExists("x").exitCode == 1) |
10 | #expect(KeycaskError.vaultExists("x").exitCode == 1) |
| 11 | #expect(KeycaskError.duplicateID("abcd2345").exitCode == 1) |
11 | #expect(KeycaskError.duplicateID(EntryID("abcd2345")!).exitCode == 1) |
| 12 | #expect(KeycaskError.usage("x").exitCode == 2) |
12 | #expect(KeycaskError.usage("x").exitCode == 2) |
| 13 | #expect(KeycaskError.notFound("x").exitCode == 3) |
13 | #expect(KeycaskError.notFound("x").exitCode == 3) |
| 14 | #expect(KeycaskError.noVault("/p").exitCode == 3) |
14 | #expect(KeycaskError.noVault("/p").exitCode == 3) |