Skip to content

Commit b0b9896

Browse files
leogdionclaude
andcommitted
Refine ConfigKey/OptionalConfigKey API for improved ergonomics
- Change envPrefix default from "BUSHEL" to nil for cleaner defaults - Remove 'base' argument label for more concise syntax - Add bushelPrefixed: convenience initializers for BUSHEL-prefixed keys - Remove redundant .screamingSnakeCaseNoPrefix case - Deprecate .boolDefault property (use .defaultValue instead) - Add CustomDebugStringConvertible conformance for better debugging - Add clarifying documentation for boolean empty string handling Breaking Changes: - All ConfigKey/OptionalConfigKey call sites must remove 'base:' label - Keys requiring "BUSHEL" prefix should use bushelPrefixed: initializer - .screamingSnakeCaseNoPrefix removed (use .screamingSnakeCase(prefix: nil)) Migration: - ConfigKey<Bool>(base: "sync.verbose") → ConfigKey<Bool>(bushelPrefixed: "sync.verbose") - ConfigKey<String>(base: "cloudkit.key_id", envPrefix: nil, ...) → ConfigKey<String>("cloudkit.key_id", envPrefix: nil, ...) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 3cdfd76 commit b0b9896

File tree

4 files changed

+105
-53
lines changed

4 files changed

+105
-53
lines changed

Sources/BushelCloudKit/Configuration/ConfigurationKeys.swift

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@ internal enum ConfigurationKeys {
1616
internal enum CloudKit {
1717
// Using base key with auto-generation (no prefix for CloudKit ENV vars)
1818
internal static let containerID = ConfigKey<String>(
19-
base: "cloudkit.container_id",
19+
"cloudkit.container_id",
2020
envPrefix: nil, // Generates: CLI="cloudkit.container_id", ENV="CLOUDKIT_CONTAINER_ID"
2121
default: "iCloud.com.brightdigit.Bushel"
2222
)
2323

2424
internal static let keyID = OptionalConfigKey<String>(
25-
base: "cloudkit.key_id",
25+
"cloudkit.key_id",
2626
envPrefix: nil
2727
)
2828

2929
internal static let privateKeyPath = OptionalConfigKey<String>(
30-
base: "cloudkit.private_key_path",
30+
"cloudkit.private_key_path",
3131
envPrefix: nil
3232
)
3333
}
@@ -37,7 +37,7 @@ internal enum ConfigurationKeys {
3737
/// VirtualBuddy TSS API configuration keys
3838
internal enum VirtualBuddy {
3939
internal static let apiKey = OptionalConfigKey<String>(
40-
base: "virtualbuddy.api_key",
40+
"virtualbuddy.api_key",
4141
envPrefix: nil // Generates: ENV="VIRTUALBUDDY_API_KEY"
4242
)
4343
}
@@ -47,8 +47,7 @@ internal enum ConfigurationKeys {
4747
/// Fetch throttling configuration keys
4848
internal enum Fetch {
4949
internal static let intervalGlobal = OptionalConfigKey<Double>(
50-
base: "fetch.interval_global",
51-
envPrefix: "BUSHEL" // Generates: ENV="BUSHEL_FETCH_INTERVAL_GLOBAL"
50+
bushelPrefixed: "fetch.interval_global" // Generates: ENV="BUSHEL_FETCH_INTERVAL_GLOBAL"
5251
)
5352

5453
/// Generate per-source interval key dynamically
@@ -57,7 +56,7 @@ internal enum ConfigurationKeys {
5756
internal static func intervalKey(for source: String) -> OptionalConfigKey<Double> {
5857
let normalized = source.replacingOccurrences(of: ".", with: "_")
5958
return OptionalConfigKey<Double>(
60-
base: "fetch.interval.\(normalized)",
59+
"fetch.interval.\(normalized)",
6160
envPrefix: nil // CLI: "fetch.interval.appledb_dev", ENV: "FETCH_INTERVAL_APPLEDB_DEV"
6261
)
6362
}
@@ -67,51 +66,51 @@ internal enum ConfigurationKeys {
6766

6867
/// Sync command configuration keys (using base key with BUSHEL prefix)
6968
internal enum Sync {
70-
internal static let dryRun = ConfigKey<Bool>(base: "sync.dry_run")
71-
internal static let restoreImagesOnly = ConfigKey<Bool>(base: "sync.restore_images_only")
72-
internal static let xcodeOnly = ConfigKey<Bool>(base: "sync.xcode_only")
73-
internal static let swiftOnly = ConfigKey<Bool>(base: "sync.swift_only")
74-
internal static let noBetas = ConfigKey<Bool>(base: "sync.no_betas")
75-
internal static let noAppleWiki = ConfigKey<Bool>(base: "sync.no_apple_wiki")
76-
internal static let verbose = ConfigKey<Bool>(base: "sync.verbose")
77-
internal static let force = ConfigKey<Bool>(base: "sync.force")
78-
internal static let minInterval = OptionalConfigKey<Int>(base: "sync.min_interval")
79-
internal static let source = OptionalConfigKey<String>(base: "sync.source")
69+
internal static let dryRun = ConfigKey<Bool>(bushelPrefixed: "sync.dry_run")
70+
internal static let restoreImagesOnly = ConfigKey<Bool>(bushelPrefixed: "sync.restore_images_only")
71+
internal static let xcodeOnly = ConfigKey<Bool>(bushelPrefixed: "sync.xcode_only")
72+
internal static let swiftOnly = ConfigKey<Bool>(bushelPrefixed: "sync.swift_only")
73+
internal static let noBetas = ConfigKey<Bool>(bushelPrefixed: "sync.no_betas")
74+
internal static let noAppleWiki = ConfigKey<Bool>(bushelPrefixed: "sync.no_apple_wiki")
75+
internal static let verbose = ConfigKey<Bool>(bushelPrefixed: "sync.verbose")
76+
internal static let force = ConfigKey<Bool>(bushelPrefixed: "sync.force")
77+
internal static let minInterval = OptionalConfigKey<Int>(bushelPrefixed: "sync.min_interval")
78+
internal static let source = OptionalConfigKey<String>(bushelPrefixed: "sync.source")
8079
}
8180

8281
// MARK: - Export Command Configuration
8382

8483
/// Export command configuration keys
8584
internal enum Export {
86-
internal static let output = OptionalConfigKey<String>(base: "export.output")
87-
internal static let pretty = ConfigKey<Bool>(base: "export.pretty")
88-
internal static let signedOnly = ConfigKey<Bool>(base: "export.signed_only")
89-
internal static let noBetas = ConfigKey<Bool>(base: "export.no_betas")
90-
internal static let verbose = ConfigKey<Bool>(base: "export.verbose")
85+
internal static let output = OptionalConfigKey<String>(bushelPrefixed: "export.output")
86+
internal static let pretty = ConfigKey<Bool>(bushelPrefixed: "export.pretty")
87+
internal static let signedOnly = ConfigKey<Bool>(bushelPrefixed: "export.signed_only")
88+
internal static let noBetas = ConfigKey<Bool>(bushelPrefixed: "export.no_betas")
89+
internal static let verbose = ConfigKey<Bool>(bushelPrefixed: "export.verbose")
9190
}
9291

9392
// MARK: - Status Command Configuration
9493

9594
/// Status command configuration keys
9695
internal enum Status {
97-
internal static let errorsOnly = ConfigKey<Bool>(base: "status.errors_only")
98-
internal static let detailed = ConfigKey<Bool>(base: "status.detailed")
96+
internal static let errorsOnly = ConfigKey<Bool>(bushelPrefixed: "status.errors_only")
97+
internal static let detailed = ConfigKey<Bool>(bushelPrefixed: "status.detailed")
9998
}
10099

101100
// MARK: - List Command Configuration
102101

103102
/// List command configuration keys
104103
internal enum List {
105-
internal static let restoreImages = ConfigKey<Bool>(base: "list.restore_images")
106-
internal static let xcodeVersions = ConfigKey<Bool>(base: "list.xcode_versions")
107-
internal static let swiftVersions = ConfigKey<Bool>(base: "list.swift_versions")
104+
internal static let restoreImages = ConfigKey<Bool>(bushelPrefixed: "list.restore_images")
105+
internal static let xcodeVersions = ConfigKey<Bool>(bushelPrefixed: "list.xcode_versions")
106+
internal static let swiftVersions = ConfigKey<Bool>(bushelPrefixed: "list.swift_versions")
108107
}
109108

110109
// MARK: - Clear Command Configuration
111110

112111
/// Clear command configuration keys
113112
internal enum Clear {
114-
internal static let yes = ConfigKey<Bool>(base: "clear.yes")
115-
internal static let verbose = ConfigKey<Bool>(base: "clear.verbose")
113+
internal static let yes = ConfigKey<Bool>(bushelPrefixed: "clear.yes")
114+
internal static let verbose = ConfigKey<Bool>(bushelPrefixed: "clear.verbose")
116115
}
117116
}

Sources/BushelCloudKit/Configuration/ConfigurationLoader.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,16 @@ public actor ConfigurationLoader {
103103
}
104104

105105
/// Read a boolean value with enhanced ENV variable parsing
106-
/// Returns non-optional since ConfigKey has a required default
106+
///
107+
/// Returns non-optional since ConfigKey has a required default.
108+
///
109+
/// Boolean parsing rules:
110+
/// - CLI: Flag presence indicates true (e.g., --verbose)
111+
/// - ENV: Accepts "true", "1", "yes" (case-insensitive)
112+
/// - Empty string in ENV is treated as absent (falls back to default)
113+
///
114+
/// - Parameter key: Configuration key with boolean type
115+
/// - Returns: Boolean value from CLI/ENV or the key's default
107116
private func read(_ key: ConfigKeyKit.ConfigKey<Bool>) -> Bool {
108117
// Try CLI first (presence-based for flags)
109118
if let cliKey = key.key(for: .commandLine),

Sources/ConfigKeyKit/ConfigurationKey.swift

Lines changed: 57 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ public enum StandardNamingStyle: NamingStyle, Sendable {
3636
/// Screaming snake case with prefix (e.g., "BUSHEL_CLOUDKIT_CONTAINER_ID")
3737
case screamingSnakeCase(prefix: String?)
3838

39-
/// Screaming snake case without prefix (e.g., "CLOUDKIT_CONTAINER_ID")
40-
case screamingSnakeCaseNoPrefix
41-
4239
public func transform(_ base: String) -> String {
4340
switch self {
4441
case .dotSeparated:
@@ -50,9 +47,6 @@ public enum StandardNamingStyle: NamingStyle, Sendable {
5047
return "\(prefix)_\(snakeCase)"
5148
}
5249
return snakeCase
53-
54-
case .screamingSnakeCaseNoPrefix:
55-
return base.uppercased().replacingOccurrences(of: ".", with: "_")
5650
}
5751
}
5852
}
@@ -119,9 +113,9 @@ public struct ConfigKey<Value: Sendable>: ConfigurationKey, Sendable {
119113
/// Convenience initializer with standard naming conventions and required default
120114
/// - Parameters:
121115
/// - base: Base key string (e.g., "cloudkit.container_id")
122-
/// - envPrefix: Prefix for environment variable (defaults to "BUSHEL")
116+
/// - envPrefix: Prefix for environment variable (defaults to nil)
123117
/// - defaultVal: Required default value
124-
public init(base: String, envPrefix: String? = "BUSHEL", default defaultVal: Value) {
118+
public init(_ base: String, envPrefix: String? = nil, default defaultVal: Value) {
125119
self.baseKey = base
126120
self.styles = [
127121
.commandLine: StandardNamingStyle.dotSeparated,
@@ -146,6 +140,26 @@ public struct ConfigKey<Value: Sendable>: ConfigurationKey, Sendable {
146140
}
147141
}
148142

143+
extension ConfigKey: CustomDebugStringConvertible {
144+
public var debugDescription: String {
145+
let cliKey = key(for: .commandLine) ?? "nil"
146+
let envKey = key(for: .environment) ?? "nil"
147+
return "ConfigKey(cli: \(cliKey), env: \(envKey), default: \(defaultValue))"
148+
}
149+
}
150+
151+
// MARK: - Convenience Initializers for BUSHEL Prefix
152+
153+
extension ConfigKey {
154+
/// Convenience initializer for keys with BUSHEL prefix
155+
/// - Parameters:
156+
/// - base: Base key string (e.g., "sync.dry_run")
157+
/// - defaultVal: Required default value
158+
public init(bushelPrefixed base: String, default defaultVal: Value) {
159+
self.init(base, envPrefix: "BUSHEL", default: defaultVal)
160+
}
161+
}
162+
149163
// MARK: - Optional Configuration Key
150164

151165
/// Configuration key for optional values without defaults
@@ -190,8 +204,8 @@ public struct OptionalConfigKey<Value: Sendable>: ConfigurationKey, Sendable {
190204
/// Convenience initializer with standard naming conventions (no default)
191205
/// - Parameters:
192206
/// - base: Base key string (e.g., "cloudkit.key_id")
193-
/// - envPrefix: Prefix for environment variable (defaults to "BUSHEL")
194-
public init(base: String, envPrefix: String? = "BUSHEL") {
207+
/// - envPrefix: Prefix for environment variable (defaults to nil)
208+
public init(_ base: String, envPrefix: String? = nil) {
195209
self.baseKey = base
196210
self.styles = [
197211
.commandLine: StandardNamingStyle.dotSeparated,
@@ -215,6 +229,24 @@ public struct OptionalConfigKey<Value: Sendable>: ConfigurationKey, Sendable {
215229
}
216230
}
217231

232+
extension OptionalConfigKey: CustomDebugStringConvertible {
233+
public var debugDescription: String {
234+
let cliKey = key(for: .commandLine) ?? "nil"
235+
let envKey = key(for: .environment) ?? "nil"
236+
return "OptionalConfigKey(cli: \(cliKey), env: \(envKey))"
237+
}
238+
}
239+
240+
// MARK: - Convenience Initializers for BUSHEL Prefix
241+
242+
extension OptionalConfigKey {
243+
/// Convenience initializer for keys with BUSHEL prefix
244+
/// - Parameter base: Base key string (e.g., "sync.min_interval")
245+
public init(bushelPrefixed base: String) {
246+
self.init(base, envPrefix: "BUSHEL")
247+
}
248+
}
249+
218250
// MARK: - Specialized Initializers for Booleans
219251

220252
extension ConfigKey where Value == Bool {
@@ -236,9 +268,9 @@ extension ConfigKey where Value == Bool {
236268
/// Initialize a boolean configuration key from base string
237269
/// - Parameters:
238270
/// - base: Base key string (e.g., "sync.verbose")
239-
/// - envPrefix: Prefix for environment variable (defaults to "BUSHEL")
271+
/// - envPrefix: Prefix for environment variable (defaults to nil)
240272
/// - defaultVal: Default value (defaults to false)
241-
public init(base: String, envPrefix: String? = "BUSHEL", default defaultVal: Bool = false) {
273+
public init(_ base: String, envPrefix: String? = nil, default defaultVal: Bool = false) {
242274
self.baseKey = base
243275
self.styles = [
244276
.commandLine: StandardNamingStyle.dotSeparated,
@@ -249,7 +281,20 @@ extension ConfigKey where Value == Bool {
249281
}
250282

251283
/// Non-optional default value accessor for booleans
284+
@available(*, deprecated, message: "Use defaultValue directly instead")
252285
public var boolDefault: Bool {
253286
defaultValue // Already non-optional!
254287
}
255288
}
289+
290+
// MARK: - BUSHEL Prefix Convenience
291+
292+
extension ConfigKey where Value == Bool {
293+
/// Convenience initializer for boolean keys with BUSHEL prefix
294+
/// - Parameters:
295+
/// - base: Base key string (e.g., "sync.verbose")
296+
/// - defaultVal: Default value (defaults to false)
297+
public init(bushelPrefixed base: String, default defaultVal: Bool = false) {
298+
self.init(base, envPrefix: "BUSHEL", default: defaultVal)
299+
}
300+
}

Tests/ConfigKeyKitTests/ConfigKeyKitTests.swift

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ struct ConfigKeyTests {
2121

2222
@Test("ConfigKey with base string and default prefix")
2323
func baseStringWithDefaultPrefix() {
24-
let key = ConfigKey<String>(base: "cloudkit.container_id", default: "iCloud.com.example.App")
24+
let key = ConfigKey<String>(bushelPrefixed: "cloudkit.container_id", default: "iCloud.com.example.App")
2525

2626
#expect(key.key(for: .commandLine) == "cloudkit.container_id")
2727
#expect(key.key(for: .environment) == "BUSHEL_CLOUDKIT_CONTAINER_ID")
@@ -30,7 +30,7 @@ struct ConfigKeyTests {
3030

3131
@Test("ConfigKey with base string and no prefix")
3232
func baseStringNoPrefix() {
33-
let key = ConfigKey<String>(base: "cloudkit.container_id", envPrefix: nil, default: "iCloud.com.example.App")
33+
let key = ConfigKey<String>("cloudkit.container_id", envPrefix: nil, default: "iCloud.com.example.App")
3434

3535
#expect(key.key(for: .commandLine) == "cloudkit.container_id")
3636
#expect(key.key(for: .environment) == "CLOUDKIT_CONTAINER_ID")
@@ -46,9 +46,8 @@ struct ConfigKeyTests {
4646

4747
@Test("Boolean ConfigKey with default")
4848
func booleanDefaultValue() {
49-
let key = ConfigKey<Bool>(base: "sync.verbose", default: false)
49+
let key = ConfigKey<Bool>(bushelPrefixed: "sync.verbose", default: false)
5050

51-
#expect(key.boolDefault == false)
5251
#expect(key.defaultValue == false)
5352
}
5453
}
@@ -69,7 +68,7 @@ struct NamingStyleTests {
6968

7069
@Test("Screaming snake case without prefix")
7170
func screamingSnakeCaseNoPrefix() {
72-
let style = StandardNamingStyle.screamingSnakeCaseNoPrefix
71+
let style = StandardNamingStyle.screamingSnakeCase(prefix: nil)
7372
#expect(style.transform("cloudkit.container_id") == "CLOUDKIT_CONTAINER_ID")
7473
}
7574

@@ -103,40 +102,40 @@ struct OptionalConfigKeyTests {
103102

104103
@Test("OptionalConfigKey with base string and default prefix")
105104
func baseStringWithDefaultPrefix() {
106-
let key = OptionalConfigKey<String>(base: "cloudkit.key_id")
105+
let key = OptionalConfigKey<String>(bushelPrefixed: "cloudkit.key_id")
107106

108107
#expect(key.key(for: .commandLine) == "cloudkit.key_id")
109108
#expect(key.key(for: .environment) == "BUSHEL_CLOUDKIT_KEY_ID")
110109
}
111110

112111
@Test("OptionalConfigKey with base string and no prefix")
113112
func baseStringNoPrefix() {
114-
let key = OptionalConfigKey<String>(base: "cloudkit.key_id", envPrefix: nil)
113+
let key = OptionalConfigKey<String>("cloudkit.key_id", envPrefix: nil)
115114

116115
#expect(key.key(for: .commandLine) == "cloudkit.key_id")
117116
#expect(key.key(for: .environment) == "CLOUDKIT_KEY_ID")
118117
}
119118

120119
@Test("OptionalConfigKey and ConfigKey generate identical keys")
121120
func keyGenerationParity() {
122-
let optional = OptionalConfigKey<String>(base: "test.key")
123-
let withDefault = ConfigKey<String>(base: "test.key", default: "default")
121+
let optional = OptionalConfigKey<String>(bushelPrefixed: "test.key")
122+
let withDefault = ConfigKey<String>(bushelPrefixed: "test.key", default: "default")
124123

125124
#expect(optional.key(for: .commandLine) == withDefault.key(for: .commandLine))
126125
#expect(optional.key(for: .environment) == withDefault.key(for: .environment))
127126
}
128127

129128
@Test("OptionalConfigKey for Int type")
130129
func intOptionalKey() {
131-
let key = OptionalConfigKey<Int>(base: "sync.min_interval")
130+
let key = OptionalConfigKey<Int>(bushelPrefixed: "sync.min_interval")
132131

133132
#expect(key.key(for: .commandLine) == "sync.min_interval")
134133
#expect(key.key(for: .environment) == "BUSHEL_SYNC_MIN_INTERVAL")
135134
}
136135

137136
@Test("OptionalConfigKey for Double type")
138137
func doubleOptionalKey() {
139-
let key = OptionalConfigKey<Double>(base: "fetch.interval_global")
138+
let key = OptionalConfigKey<Double>(bushelPrefixed: "fetch.interval_global")
140139

141140
#expect(key.key(for: .commandLine) == "fetch.interval_global")
142141
#expect(key.key(for: .environment) == "BUSHEL_FETCH_INTERVAL_GLOBAL")

0 commit comments

Comments
 (0)