From e9f935b523f81ec27beaf2f4d3e21c89845da4fe Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 19 May 2026 11:05:31 +0100 Subject: [PATCH 1/2] New Rebase --- .swiftlint.yml | 1 + .../.github/actions/cloudkit-sync/action.yml | 18 +- .../.github/workflows/BushelCloud.yml | 18 +- .../.github/workflows/bushel-cloud-build.yml | 7 +- .../BushelCloud/.github/workflows/codeql.yml | 9 +- Examples/BushelCloud/.gitrepo | 4 +- Examples/BushelCloud/Package.resolved | 2 +- Examples/BushelCloud/Package.swift | 16 +- .../Configuration/ConfigKey+BUSHEL.swift | 60 ++++ .../Sources/ConfigKeyKit/ConfigKey.swift | 181 ----------- .../ConfigKeyKit/ConfigurationKey.swift | 84 ----- .../ConfigKeyKit/OptionalConfigKey.swift | 117 ------- .../ConfigKeySourceTests.swift | 21 -- .../ConfigKeyKitTests/ConfigKeyTests.swift | 58 ---- .../ConfigKeyKitTests/NamingStyleTests.swift | 37 --- .../OptionalConfigKeyTests.swift | 62 ---- .../.github/workflows/CelestraCloud.yml | 2 +- .../.github/workflows/codeql.yml | 4 +- .../.github/workflows/update-feeds.yml | 2 +- Examples/CelestraCloud/.gitrepo | 4 +- Examples/MistDemo/Package.resolved | 2 +- Examples/MistDemo/Package.swift | 12 +- .../actions/setup-configkeykit/action.yml | 26 ++ .../.github/actions/setup-tools/action.yml | 29 ++ .../.github/workflows/ConfigKeyKit.yml | 288 ++++++++++++++++++ .../.github/workflows/check-unsafe-flags.yml | 39 +++ .../.github/workflows/claude-code-review.yml | 54 ++++ .../ConfigKeyKit/.github/workflows/claude.yml | 50 +++ .../.github/workflows/cleanup-caches.yml | 29 ++ .../ConfigKeyKit/.github/workflows/codeql.yml | 82 +++++ .../.github/workflows/swift-source-compat.yml | 29 ++ Packages/ConfigKeyKit/.gitignore | 84 +++++ Packages/ConfigKeyKit/.gitrepo | 12 + Packages/ConfigKeyKit/.periphery.yml | 3 + Packages/ConfigKeyKit/.spi.yml | 5 + Packages/ConfigKeyKit/.swift-format | 70 +++++ Packages/ConfigKeyKit/.swift-version | 1 + Packages/ConfigKeyKit/.swiftlint.yml | 141 +++++++++ Packages/ConfigKeyKit/LICENSE | 21 ++ Packages/ConfigKeyKit/Makefile | 22 ++ Packages/ConfigKeyKit/Package.swift | 47 +++ Packages/ConfigKeyKit/README.md | 106 +++++++ Packages/ConfigKeyKit/Scripts/header.sh | 113 +++++++ Packages/ConfigKeyKit/Scripts/lint.sh | 67 ++++ .../Sources/ConfigKeyKit/Command.swift | 4 +- .../ConfigKeyKit/CommandConfiguration.swift | 4 +- .../ConfigKeyKit/CommandLineParser.swift | 4 +- .../ConfigKeyKit/CommandRegistry.swift | 4 +- .../ConfigKeyKit/CommandRegistryError.swift | 2 +- .../Sources/ConfigKeyKit/ConfigKey+Bool.swift | 14 +- .../ConfigKeyKit/ConfigKey+Debug.swift | 2 +- .../Sources/ConfigKeyKit/ConfigKey.swift | 10 +- .../ConfigKeyKit/ConfigKeySource.swift | 2 +- .../ConfigKeyKit/ConfigurationKey.swift | 6 +- .../ConfigKeyKit/ConfigurationParseable.swift | 5 +- .../Sources/ConfigKeyKit/NamingStyle.swift | 2 +- .../OptionalConfigKey+Debug.swift | 2 +- .../ConfigKeyKit/OptionalConfigKey.swift | 8 +- .../ConfigKeyKit/StandardNamingStyle.swift | 4 +- .../CommandLineParserTests.swift | 12 +- .../CommandRegistry+AvailableCommands.swift | 23 +- .../CommandRegistry+CommandCreation.swift | 17 +- ...CommandRegistry+CommandTypeRetrieval.swift | 15 +- .../CommandRegistry+ConcurrentAccess.swift | 23 +- .../CommandRegistry+Errors.swift | 6 +- .../CommandRegistry+Metadata.swift | 15 +- .../CommandRegistry+Overwrite.swift | 15 +- .../CommandRegistry+Registration.swift | 21 +- .../CommandRegistry+TestCommandTypes.swift | 10 +- .../CommandRegistry/CommandRegistry.swift | 14 +- .../ConfigKeySourceTests.swift | 43 +++ .../ConfigKeyKitTests/ConfigKeyTests.swift | 83 +++++ .../ConfigKeyKitTests/NamingStyleTests.swift | 59 ++++ .../OptionalConfigKeyTests.swift | 84 +++++ .../ConfigKeyKitTests/TestEnvironment.swift | 16 + Packages/ConfigKeyKit/codecov.yml | 9 + Packages/ConfigKeyKit/mise.toml | 7 + .../APITokenAuthenticator.swift | 4 +- .../ServerToServerAuthenticator.swift | 4 +- .../WebAuthTokenAuthenticator.swift | 4 +- .../CloudKitError+OpenAPI.swift | 30 +- .../CloudKitService+AssetUpload.swift | 28 +- .../CloudKitService+WriteOperations.swift | 45 ++- .../CloudKitService/CloudKitService.swift | 1 + .../Extensions/JSONDecoder+Shared.swift | 37 +++ .../Extensions/JSONEncoder+Shared.swift | 37 +++ .../Models/RecordOperation+EncodedSize.swift | 6 +- ...oudKitServiceTests.SizeLimits+Assets.swift | 4 +- 88 files changed, 1991 insertions(+), 793 deletions(-) create mode 100644 Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigKey+BUSHEL.swift delete mode 100644 Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift delete mode 100644 Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift delete mode 100644 Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift delete mode 100644 Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift delete mode 100644 Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift delete mode 100644 Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift delete mode 100644 Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift create mode 100644 Packages/ConfigKeyKit/.github/actions/setup-configkeykit/action.yml create mode 100644 Packages/ConfigKeyKit/.github/actions/setup-tools/action.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/ConfigKeyKit.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/check-unsafe-flags.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/claude-code-review.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/claude.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/cleanup-caches.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/codeql.yml create mode 100644 Packages/ConfigKeyKit/.github/workflows/swift-source-compat.yml create mode 100644 Packages/ConfigKeyKit/.gitignore create mode 100644 Packages/ConfigKeyKit/.gitrepo create mode 100644 Packages/ConfigKeyKit/.periphery.yml create mode 100644 Packages/ConfigKeyKit/.spi.yml create mode 100644 Packages/ConfigKeyKit/.swift-format create mode 100644 Packages/ConfigKeyKit/.swift-version create mode 100644 Packages/ConfigKeyKit/.swiftlint.yml create mode 100644 Packages/ConfigKeyKit/LICENSE create mode 100644 Packages/ConfigKeyKit/Makefile create mode 100644 Packages/ConfigKeyKit/Package.swift create mode 100644 Packages/ConfigKeyKit/README.md create mode 100755 Packages/ConfigKeyKit/Scripts/header.sh create mode 100755 Packages/ConfigKeyKit/Scripts/lint.sh rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/Command.swift (97%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/CommandConfiguration.swift (96%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/CommandLineParser.swift (97%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/CommandRegistry.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/CommandRegistryError.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigKey+Bool.swift (86%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigKey+Debug.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigKey.swift (95%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigKeySource.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigurationKey.swift (95%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/ConfigurationParseable.swift (95%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/NamingStyle.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift (98%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/OptionalConfigKey.swift (96%) rename {Examples/MistDemo => Packages/ConfigKeyKit}/Sources/ConfigKeyKit/StandardNamingStyle.swift (94%) rename {Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests}/CommandLineParserTests.swift (85%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+AvailableCommands.swift (78%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandCreation.swift (82%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandTypeRetrieval.swift (85%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+ConcurrentAccess.swift (81%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Errors.swift (95%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Metadata.swift (86%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Overwrite.swift (82%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Registration.swift (80%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+TestCommandTypes.swift (94%) rename Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift => Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry.swift (82%) create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeyTests.swift create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/NamingStyleTests.swift create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift create mode 100644 Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/TestEnvironment.swift create mode 100644 Packages/ConfigKeyKit/codecov.yml create mode 100644 Packages/ConfigKeyKit/mise.toml create mode 100644 Sources/MistKit/Extensions/JSONDecoder+Shared.swift create mode 100644 Sources/MistKit/Extensions/JSONEncoder+Shared.swift diff --git a/.swiftlint.yml b/.swiftlint.yml index 7f5af019..e70f8bb9 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -122,6 +122,7 @@ excluded: - .build - Mint - Examples + - Packages - Sources/MistKitOpenAPI - Package.swift indentation_width: diff --git a/Examples/BushelCloud/.github/actions/cloudkit-sync/action.yml b/Examples/BushelCloud/.github/actions/cloudkit-sync/action.yml index 3d41f1f8..bcfcd26b 100644 --- a/Examples/BushelCloud/.github/actions/cloudkit-sync/action.yml +++ b/Examples/BushelCloud/.github/actions/cloudkit-sync/action.yml @@ -130,6 +130,14 @@ inputs: description: 'Run export after sync and generate reports' required: false default: 'true' + mistkit-branch: + description: 'MistKit ref to check out when falling back to a fresh build' + required: false + default: 'v1.0.0-beta.2' + configkeykit-branch: + description: 'ConfigKeyKit ref to check out when falling back to a fresh build' + required: false + default: 'main' runs: using: "composite" @@ -147,7 +155,15 @@ runs: - name: Setup MistKit if: steps.download-binary.outcome != 'success' - uses: ./.github/actions/setup-mistkit + uses: brightdigit/MistKit/.github/actions/setup-mistkit@main + with: + branch: ${{ inputs.mistkit-branch }} + + - name: Setup ConfigKeyKit + if: steps.download-binary.outcome != 'success' + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: ${{ inputs.configkeykit-branch }} - name: Build binary (fallback if artifact unavailable) if: steps.download-binary.outcome != 'success' diff --git a/Examples/BushelCloud/.github/workflows/BushelCloud.yml b/Examples/BushelCloud/.github/workflows/BushelCloud.yml index f83f5a53..2d5732aa 100644 --- a/Examples/BushelCloud/.github/workflows/BushelCloud.yml +++ b/Examples/BushelCloud/.github/workflows/BushelCloud.yml @@ -21,7 +21,8 @@ concurrency: env: PACKAGE_NAME: BushelCloud - MISTKIT_BRANCH: v1.0.0-beta.1 + MISTKIT_BRANCH: v1.0.0-beta.2 + CONFIGKEYKIT_BRANCH: main jobs: configure: @@ -89,6 +90,11 @@ jobs: with: branch: ${{ env.MISTKIT_BRANCH }} + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: ${{ env.CONFIGKEYKIT_BRANCH }} + - uses: brightdigit/swift-build@v1 id: build with: @@ -178,6 +184,11 @@ jobs: with: branch: ${{ env.MISTKIT_BRANCH }} + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: ${{ env.CONFIGKEYKIT_BRANCH }} + - name: Build and Test id: build uses: brightdigit/swift-build@v1 @@ -243,6 +254,11 @@ jobs: with: branch: ${{ env.MISTKIT_BRANCH }} + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: ${{ env.CONFIGKEYKIT_BRANCH }} + - name: Build and Test id: build uses: brightdigit/swift-build@v1 diff --git a/Examples/BushelCloud/.github/workflows/bushel-cloud-build.yml b/Examples/BushelCloud/.github/workflows/bushel-cloud-build.yml index 8a0a13d5..aed2ca72 100644 --- a/Examples/BushelCloud/.github/workflows/bushel-cloud-build.yml +++ b/Examples/BushelCloud/.github/workflows/bushel-cloud-build.yml @@ -40,7 +40,12 @@ jobs: - name: Setup MistKit uses: brightdigit/MistKit/.github/actions/setup-mistkit@main with: - branch: v1.0.0-beta.1 + branch: v1.0.0-beta.2 + + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: main - name: Verify Swift version run: | diff --git a/Examples/BushelCloud/.github/workflows/codeql.yml b/Examples/BushelCloud/.github/workflows/codeql.yml index a653cb18..624ed00b 100644 --- a/Examples/BushelCloud/.github/workflows/codeql.yml +++ b/Examples/BushelCloud/.github/workflows/codeql.yml @@ -71,7 +71,14 @@ jobs: - name: Setup MistKit - uses: ./.github/actions/setup-mistkit + uses: brightdigit/MistKit/.github/actions/setup-mistkit@main + with: + branch: v1.0.0-beta.2 + + - name: Setup ConfigKeyKit + uses: brightdigit/ConfigKeyKit/.github/actions/setup-configkeykit@main + with: + branch: main # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # If this step fails, then you should remove it and run the build manually (see below) diff --git a/Examples/BushelCloud/.gitrepo b/Examples/BushelCloud/.gitrepo index 9c3dc537..26600d51 100644 --- a/Examples/BushelCloud/.gitrepo +++ b/Examples/BushelCloud/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:brightdigit/BushelCloud.git branch = mistkit - commit = 5bb449083cf63d4752dea48fe5579efc16ba7374 - parent = 38f0d77f93f1df4384271be2ff865cae2e2f813d + commit = 66b595eb2e9d3a12a385edaae4a0e549f9d48da5 + parent = c31250a988eede3e8523ac6b97096ec2c91e99b2 method = merge cmdver = 0.4.9 diff --git a/Examples/BushelCloud/Package.resolved b/Examples/BushelCloud/Package.resolved index e0b877de..740e5324 100644 --- a/Examples/BushelCloud/Package.resolved +++ b/Examples/BushelCloud/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "c3ac1cf77d89f143a19ef295fe93dc532ed8453816f62104a1d89923205611da", + "originHash" : "19206e85a58e39bd539ec38237e3cc167902ae05697e150f556c029593646dbe", "pins" : [ { "identity" : "bushelkit", diff --git a/Examples/BushelCloud/Package.swift b/Examples/BushelCloud/Package.swift index 00cfd538..f9dc5fbe 100644 --- a/Examples/BushelCloud/Package.swift +++ b/Examples/BushelCloud/Package.swift @@ -87,12 +87,12 @@ let package = Package( .visionOS(.v2) ], products: [ - .library(name: "ConfigKeyKit", targets: ["ConfigKeyKit"]), .library(name: "BushelCloudKit", targets: ["BushelCloudKit"]), .executable(name: "bushel-cloud", targets: ["BushelCloudCLI"]) ], dependencies: [ .package(name: "MistKit", path: "../.."), + .package(name: "ConfigKeyKit", path: "../../Packages/ConfigKeyKit"), .package(url: "https://github.com/brightdigit/BushelKit.git", from: "3.0.0-alpha.2"), .package(url: "https://github.com/brightdigit/IPSWDownloads.git", from: "1.0.0"), .package(url: "https://github.com/scinfu/SwiftSoup.git", from: "2.6.0"), @@ -103,15 +103,10 @@ let package = Package( ) ], targets: [ - .target( - name: "ConfigKeyKit", - dependencies: [], - swiftSettings: swiftSettings - ), .target( name: "BushelCloudKit", dependencies: [ - .target(name: "ConfigKeyKit"), + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), .product(name: "BushelLogging", package: "BushelKit"), .product(name: "BushelFoundation", package: "BushelKit"), @@ -130,13 +125,6 @@ let package = Package( ], swiftSettings: swiftSettings ), - .testTarget( - name: "ConfigKeyKitTests", - dependencies: [ - .target(name: "ConfigKeyKit") - ], - swiftSettings: swiftSettings - ), .testTarget( name: "BushelCloudKitTests", dependencies: [ diff --git a/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigKey+BUSHEL.swift b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigKey+BUSHEL.swift new file mode 100644 index 00000000..9b71aa17 --- /dev/null +++ b/Examples/BushelCloud/Sources/BushelCloudKit/Configuration/ConfigKey+BUSHEL.swift @@ -0,0 +1,60 @@ +// +// ConfigKey+BUSHEL.swift +// BushelCloud +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +public import ConfigKeyKit + +// MARK: - BushelCloud-Specific Config Key Helpers + +extension ConfigKey { + /// Convenience initializer for keys with `BUSHEL_` environment-variable prefix. + /// - Parameters: + /// - base: Base key string (e.g., "sync.dry_run") + /// - defaultVal: Required default value + public init(bushelPrefixed base: String, default defaultVal: Value) { + self.init(base, envPrefix: "BUSHEL", default: defaultVal) + } +} + +extension ConfigKey where Value == Bool { + /// Convenience initializer for boolean keys with `BUSHEL_` environment-variable prefix. + /// - Parameters: + /// - base: Base key string (e.g., "sync.verbose") + /// - defaultVal: Default value (defaults to false) + public init(bushelPrefixed base: String, default defaultVal: Bool = false) { + self.init(base, envPrefix: "BUSHEL", default: defaultVal) + } +} + +extension OptionalConfigKey { + /// Convenience initializer for optional keys with `BUSHEL_` environment-variable prefix. + /// - Parameter base: Base key string (e.g., "sync.min_interval") + public init(bushelPrefixed base: String) { + self.init(base, envPrefix: "BUSHEL") + } +} diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift deleted file mode 100644 index b497316d..00000000 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigKey.swift +++ /dev/null @@ -1,181 +0,0 @@ -// -// ConfigKey.swift -// BushelCloud -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -internal import Foundation - -// MARK: - Generic Configuration Key - -/// Configuration key for values with default fallbacks -/// -/// Use `ConfigKey` when a configuration value has a sensible default -/// that should be used when not provided by the user. The `read()` method -/// will always return a non-optional value. -/// -/// Example: -/// ```swift -/// let containerID = ConfigKey( -/// base: "cloudkit.container_id", -/// default: "iCloud.com.brightdigit.Bushel" -/// ) -/// // read(containerID) returns String (non-optional) -/// ``` -public struct ConfigKey: ConfigurationKey, Sendable { - private let baseKey: String? - private let styles: [ConfigKeySource: any NamingStyle] - private let explicitKeys: [ConfigKeySource: String] - public let defaultValue: Value // Non-optional! - - /// Initialize with explicit CLI and ENV keys and required default - public init(cli: String? = nil, env: String? = nil, default defaultVal: Value) { - self.baseKey = nil - self.styles = [:] - var keys: [ConfigKeySource: String] = [:] - if let cli = cli { keys[.commandLine] = cli } - if let env = env { keys[.environment] = env } - self.explicitKeys = keys - self.defaultValue = defaultVal - } - - /// Initialize from a base key string with naming styles and required default - /// - Parameters: - /// - base: Base key string (e.g., "cloudkit.container_id") - /// - styles: Dictionary mapping sources to naming styles - /// - defaultVal: Required default value - public init( - base: String, - styles: [ConfigKeySource: any NamingStyle], - default defaultVal: Value - ) { - self.baseKey = base - self.styles = styles - self.explicitKeys = [:] - self.defaultValue = defaultVal - } - - /// Convenience initializer with standard naming conventions and required default - /// - Parameters: - /// - base: Base key string (e.g., "cloudkit.container_id") - /// - envPrefix: Prefix for environment variable (defaults to nil) - /// - defaultVal: Required default value - public init(_ base: String, envPrefix: String? = nil, default defaultVal: Value) { - self.baseKey = base - self.styles = [ - .commandLine: StandardNamingStyle.dotSeparated, - .environment: StandardNamingStyle.screamingSnakeCase(prefix: envPrefix), - ] - self.explicitKeys = [:] - self.defaultValue = defaultVal - } - - public func key(for source: ConfigKeySource) -> String? { - // Check for explicit key first - if let explicit = explicitKeys[source] { - return explicit - } - - // Generate from base key and style - guard let base = baseKey, let style = styles[source] else { - return nil - } - - return style.transform(base) - } -} - -extension ConfigKey: CustomDebugStringConvertible { - public var debugDescription: String { - let cliKey = key(for: .commandLine) ?? "nil" - let envKey = key(for: .environment) ?? "nil" - return "ConfigKey(cli: \(cliKey), env: \(envKey), default: \(defaultValue))" - } -} - -// MARK: - Convenience Initializers for BUSHEL Prefix - -extension ConfigKey { - /// Convenience initializer for keys with BUSHEL prefix - /// - Parameters: - /// - base: Base key string (e.g., "sync.dry_run") - /// - defaultVal: Required default value - public init(bushelPrefixed base: String, default defaultVal: Value) { - self.init(base, envPrefix: "BUSHEL", default: defaultVal) - } -} - -// MARK: - Specialized Initializers for Booleans - -extension ConfigKey where Value == Bool { - /// Non-optional default value accessor for booleans - @available(*, deprecated, message: "Use defaultValue directly instead") - public var boolDefault: Bool { - defaultValue // Already non-optional! - } - - /// Initialize a boolean configuration key with non-optional default - /// - Parameters: - /// - cli: Command-line argument name - /// - env: Environment variable name - /// - defaultVal: Default value (defaults to false) - public init(cli: String, env: String, default defaultVal: Bool = false) { - self.baseKey = nil - self.styles = [:] - var keys: [ConfigKeySource: String] = [:] - keys[.commandLine] = cli - keys[.environment] = env - self.explicitKeys = keys - self.defaultValue = defaultVal - } - - /// Initialize a boolean configuration key from base string - /// - Parameters: - /// - base: Base key string (e.g., "sync.verbose") - /// - envPrefix: Prefix for environment variable (defaults to nil) - /// - defaultVal: Default value (defaults to false) - public init(_ base: String, envPrefix: String? = nil, default defaultVal: Bool = false) { - self.baseKey = base - self.styles = [ - .commandLine: StandardNamingStyle.dotSeparated, - .environment: StandardNamingStyle.screamingSnakeCase(prefix: envPrefix), - ] - self.explicitKeys = [:] - self.defaultValue = defaultVal - } -} - -// MARK: - BUSHEL Prefix Convenience - -extension ConfigKey where Value == Bool { - /// Convenience initializer for boolean keys with BUSHEL prefix - /// - Parameters: - /// - base: Base key string (e.g., "sync.verbose") - /// - defaultVal: Default value (defaults to false) - public init(bushelPrefixed base: String, default defaultVal: Bool = false) { - self.init(base, envPrefix: "BUSHEL", default: defaultVal) - } -} diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift deleted file mode 100644 index 28a3e51e..00000000 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/ConfigurationKey.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// ConfigurationKey.swift -// BushelCloud -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -internal import Foundation - -// MARK: - Configuration Key Source - -/// Source for configuration keys (CLI arguments or environment variables) -public enum ConfigKeySource: CaseIterable, Sendable { - /// Command-line arguments (e.g., --cloudkit-container-id) - case commandLine - - /// Environment variables (e.g., CLOUDKIT_CONTAINER_ID) - case environment -} - -// MARK: - Naming Style - -/// Protocol for transforming base key strings into different naming conventions -public protocol NamingStyle: Sendable { - /// Transform a base key string according to this naming style - /// - Parameter base: Base key string (e.g., "cloudkit.container_id") - /// - Returns: Transformed key string - func transform(_ base: String) -> String -} - -/// Common naming styles for configuration keys -public enum StandardNamingStyle: NamingStyle, Sendable { - /// Dot-separated lowercase (e.g., "cloudkit.container_id") - case dotSeparated - - /// Screaming snake case with prefix (e.g., "BUSHEL_CLOUDKIT_CONTAINER_ID") - case screamingSnakeCase(prefix: String?) - - public func transform(_ base: String) -> String { - switch self { - case .dotSeparated: - return base - - case .screamingSnakeCase(let prefix): - let snakeCase = base.uppercased().replacingOccurrences(of: ".", with: "_") - if let prefix = prefix { - return "\(prefix)_\(snakeCase)" - } - return snakeCase - } - } -} - -// MARK: - Configuration Key Protocol - -/// Protocol for configuration keys that support multiple sources -public protocol ConfigurationKey: Sendable { - /// Get the key string for a specific source - /// - Parameter source: The configuration source (CLI or ENV) - /// - Returns: The key string for that source, or nil if the key doesn't support that source - func key(for source: ConfigKeySource) -> String? -} diff --git a/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift b/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift deleted file mode 100644 index 5b818e50..00000000 --- a/Examples/BushelCloud/Sources/ConfigKeyKit/OptionalConfigKey.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// OptionalConfigKey.swift -// BushelCloud -// -// Created by Leo Dion. -// Copyright © 2026 BrightDigit. -// -// Permission is hereby granted, free of charge, to any person -// obtaining a copy of this software and associated documentation -// files (the "Software"), to deal in the Software without -// restriction, including without limitation the rights to use, -// copy, modify, merge, publish, distribute, sublicense, and/or -// sell copies of the Software, and to permit persons to whom the -// Software is furnished to do so, subject to the following -// conditions: -// -// The above copyright notice and this permission notice shall be -// included in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -// OTHER DEALINGS IN THE SOFTWARE. -// - -internal import Foundation - -// MARK: - Optional Configuration Key - -/// Configuration key for optional values without defaults -/// -/// Use `OptionalConfigKey` when a configuration value has no sensible default -/// and should be `nil` when not provided by the user. The `read()` method -/// will return an optional value. -/// -/// Example: -/// ```swift -/// let apiKey = OptionalConfigKey(base: "api.key") -/// // read(apiKey) returns String? -/// ``` -public struct OptionalConfigKey: ConfigurationKey, Sendable { - private let baseKey: String? - private let styles: [ConfigKeySource: any NamingStyle] - private let explicitKeys: [ConfigKeySource: String] - - /// Initialize with explicit CLI and ENV keys (no default) - public init(cli: String? = nil, env: String? = nil) { - self.baseKey = nil - self.styles = [:] - var keys: [ConfigKeySource: String] = [:] - if let cli = cli { keys[.commandLine] = cli } - if let env = env { keys[.environment] = env } - self.explicitKeys = keys - } - - /// Initialize from a base key string with naming styles (no default) - /// - Parameters: - /// - base: Base key string (e.g., "cloudkit.key_id") - /// - styles: Dictionary mapping sources to naming styles - public init( - base: String, - styles: [ConfigKeySource: any NamingStyle] - ) { - self.baseKey = base - self.styles = styles - self.explicitKeys = [:] - } - - /// Convenience initializer with standard naming conventions (no default) - /// - Parameters: - /// - base: Base key string (e.g., "cloudkit.key_id") - /// - envPrefix: Prefix for environment variable (defaults to nil) - public init(_ base: String, envPrefix: String? = nil) { - self.baseKey = base - self.styles = [ - .commandLine: StandardNamingStyle.dotSeparated, - .environment: StandardNamingStyle.screamingSnakeCase(prefix: envPrefix), - ] - self.explicitKeys = [:] - } - - public func key(for source: ConfigKeySource) -> String? { - // Check for explicit key first - if let explicit = explicitKeys[source] { - return explicit - } - - // Generate from base key and style - guard let base = baseKey, let style = styles[source] else { - return nil - } - - return style.transform(base) - } -} - -extension OptionalConfigKey: CustomDebugStringConvertible { - public var debugDescription: String { - let cliKey = key(for: .commandLine) ?? "nil" - let envKey = key(for: .environment) ?? "nil" - return "OptionalConfigKey(cli: \(cliKey), env: \(envKey))" - } -} - -// MARK: - Convenience Initializers for BUSHEL Prefix - -extension OptionalConfigKey { - /// Convenience initializer for keys with BUSHEL prefix - /// - Parameter base: Base key string (e.g., "sync.min_interval") - public init(bushelPrefixed base: String) { - self.init(base, envPrefix: "BUSHEL") - } -} diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift deleted file mode 100644 index ce45cdea..00000000 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ConfigKeySourceTests.swift -// ConfigKeyKit -// -// Tests for ConfigKeySource enum -// - -internal import Testing - -@testable import ConfigKeyKit - -@Suite("ConfigKeySource Tests") -internal struct ConfigKeySourceTests { - @Test("All cases") - internal func allCases() { - let sources = ConfigKeySource.allCases - #expect(sources.count == 2) - #expect(sources.contains(.commandLine)) - #expect(sources.contains(.environment)) - } -} diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift deleted file mode 100644 index 3c13267d..00000000 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/ConfigKeyTests.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// ConfigKeyTests.swift -// ConfigKeyKit -// -// Tests for ConfigKey configuration -// - -internal import Testing - -@testable import ConfigKeyKit - -@Suite("ConfigKey Tests") -internal struct ConfigKeyTests { - @Test("ConfigKey with explicit keys and default") - internal func explicitKeys() { - let key = ConfigKey(cli: "test.key", env: "TEST_KEY", default: "default-value") - - #expect(key.key(for: .commandLine) == "test.key") - #expect(key.key(for: .environment) == "TEST_KEY") - #expect(key.defaultValue == "default-value") - } - - @Test("ConfigKey with base string and default prefix") - internal func baseStringWithDefaultPrefix() { - let key = ConfigKey( - bushelPrefixed: "cloudkit.container_id", default: "iCloud.com.example.App" - ) - - #expect(key.key(for: .commandLine) == "cloudkit.container_id") - #expect(key.key(for: .environment) == "BUSHEL_CLOUDKIT_CONTAINER_ID") - #expect(key.defaultValue == "iCloud.com.example.App") - } - - @Test("ConfigKey with base string and no prefix") - internal func baseStringNoPrefix() { - let key = ConfigKey( - "cloudkit.container_id", envPrefix: nil, default: "iCloud.com.example.App" - ) - - #expect(key.key(for: .commandLine) == "cloudkit.container_id") - #expect(key.key(for: .environment) == "CLOUDKIT_CONTAINER_ID") - #expect(key.defaultValue == "iCloud.com.example.App") - } - - @Test("ConfigKey with default value") - internal func defaultValue() { - let key = ConfigKey(cli: "test.key", env: "TEST_KEY", default: "default-value") - - #expect(key.defaultValue == "default-value") - } - - @Test("Boolean ConfigKey with default") - internal func booleanDefaultValue() { - let key = ConfigKey(bushelPrefixed: "sync.verbose", default: false) - - #expect(key.defaultValue == false) - } -} diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift deleted file mode 100644 index 001a65d7..00000000 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/NamingStyleTests.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// NamingStyleTests.swift -// ConfigKeyKit -// -// Tests for naming style transformations -// - -internal import Testing - -@testable import ConfigKeyKit - -@Suite("NamingStyle Tests") -internal struct NamingStyleTests { - @Test("Dot-separated style") - internal func dotSeparatedStyle() { - let style = StandardNamingStyle.dotSeparated - #expect(style.transform("cloudkit.container_id") == "cloudkit.container_id") - } - - @Test("Screaming snake case with prefix") - internal func screamingSnakeCaseWithPrefix() { - let style = StandardNamingStyle.screamingSnakeCase(prefix: "BUSHEL") - #expect(style.transform("cloudkit.container_id") == "BUSHEL_CLOUDKIT_CONTAINER_ID") - } - - @Test("Screaming snake case without prefix") - internal func screamingSnakeCaseNoPrefix() { - let style = StandardNamingStyle.screamingSnakeCase(prefix: nil) - #expect(style.transform("cloudkit.container_id") == "CLOUDKIT_CONTAINER_ID") - } - - @Test("Screaming snake case with nil prefix") - internal func screamingSnakeCaseNilPrefix() { - let style = StandardNamingStyle.screamingSnakeCase(prefix: nil) - #expect(style.transform("sync.verbose") == "SYNC_VERBOSE") - } -} diff --git a/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift b/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift deleted file mode 100644 index 425157b1..00000000 --- a/Examples/BushelCloud/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// OptionalConfigKeyTests.swift -// ConfigKeyKit -// -// Tests for OptionalConfigKey configuration -// - -internal import Testing - -@testable import ConfigKeyKit - -@Suite("OptionalConfigKey Tests") -internal struct OptionalConfigKeyTests { - @Test("OptionalConfigKey with explicit keys") - internal func explicitKeys() { - let key = OptionalConfigKey(cli: "test.key", env: "TEST_KEY") - - #expect(key.key(for: .commandLine) == "test.key") - #expect(key.key(for: .environment) == "TEST_KEY") - } - - @Test("OptionalConfigKey with base string and default prefix") - internal func baseStringWithDefaultPrefix() { - let key = OptionalConfigKey(bushelPrefixed: "cloudkit.key_id") - - #expect(key.key(for: .commandLine) == "cloudkit.key_id") - #expect(key.key(for: .environment) == "BUSHEL_CLOUDKIT_KEY_ID") - } - - @Test("OptionalConfigKey with base string and no prefix") - internal func baseStringNoPrefix() { - let key = OptionalConfigKey("cloudkit.key_id", envPrefix: nil) - - #expect(key.key(for: .commandLine) == "cloudkit.key_id") - #expect(key.key(for: .environment) == "CLOUDKIT_KEY_ID") - } - - @Test("OptionalConfigKey and ConfigKey generate identical keys") - internal func keyGenerationParity() { - let optional = OptionalConfigKey(bushelPrefixed: "test.key") - let withDefault = ConfigKey(bushelPrefixed: "test.key", default: "default") - - #expect(optional.key(for: .commandLine) == withDefault.key(for: .commandLine)) - #expect(optional.key(for: .environment) == withDefault.key(for: .environment)) - } - - @Test("OptionalConfigKey for Int type") - internal func intOptionalKey() { - let key = OptionalConfigKey(bushelPrefixed: "sync.min_interval") - - #expect(key.key(for: .commandLine) == "sync.min_interval") - #expect(key.key(for: .environment) == "BUSHEL_SYNC_MIN_INTERVAL") - } - - @Test("OptionalConfigKey for Double type") - internal func doubleOptionalKey() { - let key = OptionalConfigKey(bushelPrefixed: "fetch.interval_global") - - #expect(key.key(for: .commandLine) == "fetch.interval_global") - #expect(key.key(for: .environment) == "BUSHEL_FETCH_INTERVAL_GLOBAL") - } -} diff --git a/Examples/CelestraCloud/.github/workflows/CelestraCloud.yml b/Examples/CelestraCloud/.github/workflows/CelestraCloud.yml index 023e20de..a878600d 100644 --- a/Examples/CelestraCloud/.github/workflows/CelestraCloud.yml +++ b/Examples/CelestraCloud/.github/workflows/CelestraCloud.yml @@ -21,7 +21,7 @@ concurrency: env: PACKAGE_NAME: CelestraCloud - MISTKIT_BRANCH: v1.0.0-beta.1 + MISTKIT_BRANCH: v1.0.0-beta.2 jobs: configure: diff --git a/Examples/CelestraCloud/.github/workflows/codeql.yml b/Examples/CelestraCloud/.github/workflows/codeql.yml index 341134d6..df1ac023 100644 --- a/Examples/CelestraCloud/.github/workflows/codeql.yml +++ b/Examples/CelestraCloud/.github/workflows/codeql.yml @@ -58,7 +58,9 @@ jobs: swift package --version - name: Setup MistKit - uses: ./.github/actions/setup-mistkit + uses: brightdigit/MistKit/.github/actions/setup-mistkit@main + with: + branch: v1.0.0-beta.2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/Examples/CelestraCloud/.github/workflows/update-feeds.yml b/Examples/CelestraCloud/.github/workflows/update-feeds.yml index 100a7179..8a44920b 100644 --- a/Examples/CelestraCloud/.github/workflows/update-feeds.yml +++ b/Examples/CelestraCloud/.github/workflows/update-feeds.yml @@ -49,7 +49,7 @@ env: CLOUDKIT_KEY_ID: ${{ secrets.CLOUDKIT_KEY_ID }} CLOUDKIT_ENVIRONMENT: ${{ (github.event_name == 'pull_request' || github.event_name == 'push') && 'development' || github.event.inputs.environment || 'production' }} CLOUDKIT_PRIVATE_KEY_PATH: /tmp/cloudkit_key.pem - MISTKIT_BRANCH: v1.0.0-beta.1 + MISTKIT_BRANCH: v1.0.0-beta.2 jobs: # Determine which tier to run based on schedule or manual input diff --git a/Examples/CelestraCloud/.gitrepo b/Examples/CelestraCloud/.gitrepo index 76b381ba..e16d2783 100644 --- a/Examples/CelestraCloud/.gitrepo +++ b/Examples/CelestraCloud/.gitrepo @@ -6,7 +6,7 @@ [subrepo] remote = git@github.com:brightdigit/CelestraCloud.git branch = mistkit - commit = ea897c34cc0cc63c0a4c35bb99bf819535a47c6e - parent = 38f0d77f93f1df4384271be2ff865cae2e2f813d + commit = d91df88dcfe6b8c7cccd2d8257edb0472059ac2f + parent = 3e7a61518aaffa14c259c38087bf8ca75bf080cf method = merge cmdver = 0.4.9 diff --git a/Examples/MistDemo/Package.resolved b/Examples/MistDemo/Package.resolved index 2fa36330..0a714070 100644 --- a/Examples/MistDemo/Package.resolved +++ b/Examples/MistDemo/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "7284c3deec21f39c02edfa30e7214ff910bbb668d02643c0e02f07ab3341122d", + "originHash" : "74809d363120c26bf126107d8453c0c07f761ce0be02bd2e5df3cc4c3b3ced84", "pins" : [ { "identity" : "async-http-client", diff --git a/Examples/MistDemo/Package.swift b/Examples/MistDemo/Package.swift index ab997e79..f2e35720 100644 --- a/Examples/MistDemo/Package.swift +++ b/Examples/MistDemo/Package.swift @@ -106,6 +106,7 @@ let package = Package( ], dependencies: [ .package(name: "MistKit", path: "../.."), + .package(name: "ConfigKeyKit", path: "../../Packages/ConfigKeyKit"), .package( url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.0.0" @@ -125,11 +126,6 @@ let package = Package( ), ], targets: [ - .target( - name: "ConfigKeyKit", - dependencies: [], - swiftSettings: swiftSettings - ), .target( name: "MistDemoApp", dependencies: ["MistDemoKit"], @@ -138,7 +134,7 @@ let package = Package( .target( name: "MistDemoKit", dependencies: [ - "ConfigKeyKit", + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), .product( name: "Hummingbird", @@ -167,7 +163,7 @@ let package = Package( name: "MistDemo", dependencies: [ "MistDemoKit", - "ConfigKeyKit", + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), ], swiftSettings: swiftSettings @@ -176,7 +172,7 @@ let package = Package( name: "MistDemoTests", dependencies: [ "MistDemoKit", - "ConfigKeyKit", + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), .product(name: "MistKit", package: "MistKit"), .product(name: "MistKitOpenAPI", package: "MistKit"), .product( diff --git a/Packages/ConfigKeyKit/.github/actions/setup-configkeykit/action.yml b/Packages/ConfigKeyKit/.github/actions/setup-configkeykit/action.yml new file mode 100644 index 00000000..a4985e0b --- /dev/null +++ b/Packages/ConfigKeyKit/.github/actions/setup-configkeykit/action.yml @@ -0,0 +1,26 @@ +name: Setup ConfigKeyKit +description: Replaces a local ConfigKeyKit path dependency with a remote branch reference + +inputs: + branch: + description: ConfigKeyKit branch to use (leave empty to keep the local path dependency) + +runs: + using: composite + steps: + - name: Update Package.swift (Unix) + if: inputs.branch != '' && runner.os != 'Windows' + shell: bash + run: | + if [ "$RUNNER_OS" = "macOS" ]; then + sed -i '' 's|\.package(name: "ConfigKeyKit", path: "[^"]*")|.package(url: "https://github.com/brightdigit/ConfigKeyKit.git", branch: "'"${{ inputs.branch }}"'")|g' Package.swift + else + sed -i 's|\.package(name: "ConfigKeyKit", path: "[^"]*")|.package(url: "https://github.com/brightdigit/ConfigKeyKit.git", branch: "'"${{ inputs.branch }}"'")|g' Package.swift + fi + rm -f Package.resolved + - name: Update Package.swift (Windows) + if: inputs.branch != '' && runner.os == 'Windows' + shell: pwsh + run: | + (Get-Content Package.swift) -replace '\.package\(name: "ConfigKeyKit", path: "[^"]*"\)', ".package(url: `"https://github.com/brightdigit/ConfigKeyKit.git`", branch: `"${{ inputs.branch }}`")" | Set-Content Package.swift + Remove-Item -Path Package.resolved -Force -ErrorAction SilentlyContinue diff --git a/Packages/ConfigKeyKit/.github/actions/setup-tools/action.yml b/Packages/ConfigKeyKit/.github/actions/setup-tools/action.yml new file mode 100644 index 00000000..069f32e9 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/actions/setup-tools/action.yml @@ -0,0 +1,29 @@ +name: Setup mise tools +description: >- + Restore (or build + save) the mise tool cache and put the binaries on PATH. + Implemented as a composite action so the cache scope is the caller job's + scope — reusable workflows scope caches separately, which silently breaks + hand-off between a setup job and a consumer lint job. + +runs: + using: composite + steps: + - name: Cache mise tools + id: mise-cache + uses: actions/cache@v4 + with: + path: ~/.local/share/mise/installs + key: mise-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('mise.toml') }} + restore-keys: | + mise-v2-${{ runner.os }}-${{ runner.arch }}- + - name: Install mise tools (cache miss) + if: steps.mise-cache.outputs.cache-hit != 'true' + uses: jdx/mise-action@v4 + with: + cache: false + - name: Configure PATH for cached mise tools + if: steps.mise-cache.outputs.cache-hit == 'true' + uses: jdx/mise-action@v4 + with: + install: false + cache: false diff --git a/Packages/ConfigKeyKit/.github/workflows/ConfigKeyKit.yml b/Packages/ConfigKeyKit/.github/workflows/ConfigKeyKit.yml new file mode 100644 index 00000000..2bcb8a19 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/ConfigKeyKit.yml @@ -0,0 +1,288 @@ +name: ConfigKeyKit +on: + push: + branches: + - main + paths-ignore: + - '**.md' + - 'docs/**' + - 'LICENSE' + - '.github/ISSUE_TEMPLATE/**' + pull_request: + paths-ignore: + - '**.md' + - 'docs/**' + - 'LICENSE' + - '.github/ISSUE_TEMPLATE/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +env: + PACKAGE_NAME: ConfigKeyKit + +jobs: + configure: + name: Configure Matrix + runs-on: ubuntu-latest + outputs: + full-matrix: ${{ steps.check.outputs.full }} + ubuntu-os: ${{ steps.matrix.outputs.ubuntu-os }} + ubuntu-swift: ${{ steps.matrix.outputs.ubuntu-swift }} + ubuntu-type: ${{ steps.matrix.outputs.ubuntu-type }} + steps: + - id: check + name: Determine matrix scope + run: | + FULL=false + REF="${{ github.ref }}" + EVENT="${{ github.event_name }}" + BASE_REF="${{ github.base_ref }}" + + if [[ "$REF" == "refs/heads/main" ]]; then + FULL=true + elif [[ "$REF" =~ ^refs/heads/v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then + FULL=true + elif [[ "$EVENT" == "pull_request" ]]; then + if [[ "$BASE_REF" == "main" || "$BASE_REF" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+ ]]; then + FULL=true + fi + fi + + echo "full=$FULL" >> "$GITHUB_OUTPUT" + echo "Full matrix: $FULL (ref=$REF, event=$EVENT, base_ref=$BASE_REF)" + + - id: matrix + name: Build matrix values + run: | + if [[ "${{ steps.check.outputs.full }}" == "true" ]]; then + echo 'ubuntu-os=["noble","jammy"]' >> "$GITHUB_OUTPUT" + echo 'ubuntu-swift=[{"version":"6.2"},{"version":"6.3"}]' >> "$GITHUB_OUTPUT" + echo 'ubuntu-type=["","wasm","wasm-embedded"]' >> "$GITHUB_OUTPUT" + else + echo 'ubuntu-os=["noble"]' >> "$GITHUB_OUTPUT" + echo 'ubuntu-swift=[{"version":"6.3"}]' >> "$GITHUB_OUTPUT" + echo 'ubuntu-type=[""]' >> "$GITHUB_OUTPUT" + fi + + build-ubuntu: + name: Build on Ubuntu + needs: configure + runs-on: ubuntu-latest + container: swift:${{ matrix.swift.version }}-${{ matrix.os }} + if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + os: ${{ fromJSON(needs.configure.outputs.ubuntu-os) }} + swift: ${{ fromJSON(needs.configure.outputs.ubuntu-swift) }} + type: ${{ fromJSON(needs.configure.outputs.ubuntu-type) }} + steps: + - uses: actions/checkout@v6 + - uses: brightdigit/swift-build@v1 + id: build + with: + type: ${{ matrix.type }} + wasmtime-version: "40.0.2" + wasm-swift-flags: >- + -Xcc -D_WASI_EMULATED_SIGNAL + -Xcc -D_WASI_EMULATED_MMAN + -Xlinker -lwasi-emulated-signal + -Xlinker -lwasi-emulated-mman + - name: Install curl (required by Codecov uploader) + if: steps.build.outputs.contains-code-coverage == 'true' + run: | + if command -v apt-get >/dev/null 2>&1; then + apt-get update && apt-get install -y --no-install-recommends curl ca-certificates + fi + - name: Install coverage.py (silences codecov-cli probe warning) + if: steps.build.outputs.contains-code-coverage == 'true' + run: pip3 install --quiet --user coverage 2>/dev/null || true + - uses: sersoft-gmbh/swift-coverage-action@v5 + if: steps.build.outputs.contains-code-coverage == 'true' + id: coverage-files + with: + fail-on-empty-output: true + - name: Upload coverage to Codecov + if: steps.build.outputs.contains-code-coverage == 'true' + uses: codecov/codecov-action@v6 + with: + fail_ci_if_error: true + flags: swift-${{ matrix.swift.version }}-${{ matrix.os }} + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + files: ${{ join(fromJSON(steps.coverage-files.outputs.files), ',') }} + + build-windows: + name: Build on Windows + needs: configure + runs-on: ${{ matrix.runs-on }} + if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + runs-on: [windows-2022, windows-2025] + swift: + - version: swift-6.2-release + build: 6.2-RELEASE + - version: swift-6.3-release + build: 6.3-RELEASE + steps: + - uses: actions/checkout@v6 + - uses: brightdigit/swift-build@v1 + id: build + with: + windows-swift-version: ${{ matrix.swift.version }} + windows-swift-build: ${{ matrix.swift.build }} + - name: Upload coverage to Codecov + if: steps.build.outputs.contains-code-coverage == 'true' + uses: codecov/codecov-action@v6 + with: + fail_ci_if_error: true + flags: swift-${{ matrix.swift.version }},windows + verbose: true + token: ${{ secrets.CODECOV_TOKEN }} + os: windows + swift_project: ConfigKeyKit + + build-android: + name: Build on Android + needs: configure + runs-on: ubuntu-latest + if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + swift: + - version: "6.2" + - version: "6.3" + android-api-level: [33, 34] + steps: + - uses: actions/checkout@v6 + - name: Free disk space + uses: jlumbroso/free-disk-space@v1.3.1 + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: true + docker-images: true + swap-storage: true + - uses: brightdigit/swift-build@v1 + with: + type: android + android-swift-version: ${{ matrix.swift.version }} + android-api-level: ${{ matrix.android-api-level }} + android-run-tests: true + + build-macos: + name: Build on macOS + runs-on: macos-26 + if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + include: + - xcode: "/Applications/Xcode_26.4.app" + - type: ios + xcode: "/Applications/Xcode_26.4.app" + deviceName: "iPhone 17 Pro" + osVersion: "26.4.1" + download-platform: true + steps: + - uses: actions/checkout@v6 + - name: Build and Test + id: build + uses: brightdigit/swift-build@v1 + with: + scheme: ${{ env.PACKAGE_NAME }} + type: ${{ matrix.type }} + xcode: ${{ matrix.xcode }} + deviceName: ${{ matrix.deviceName }} + osVersion: ${{ matrix.osVersion }} + download-platform: ${{ matrix.download-platform }} + - name: Install coverage.py (silences codecov-cli probe warning) + if: steps.build.outputs.contains-code-coverage == 'true' + run: pip3 install --quiet --user coverage 2>/dev/null || true + - name: Process Coverage + if: steps.build.outputs.contains-code-coverage == 'true' + uses: sersoft-gmbh/swift-coverage-action@v5 + - name: Upload Coverage + if: steps.build.outputs.contains-code-coverage == 'true' + uses: codecov/codecov-action@v6 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ${{ matrix.type && format('{0}{1}', matrix.type, matrix.osVersion) || 'spm' }} + + build-macos-platforms: + name: Build on macOS (Platforms) + needs: configure + runs-on: ${{ matrix.runs-on }} + if: ${{ needs.configure.outputs.full-matrix == 'true' && !contains(github.event.head_commit.message, 'ci skip') }} + strategy: + fail-fast: false + matrix: + include: + - type: macos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.4.app" + - type: watchos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.4.app" + deviceName: "Apple Watch Ultra 3 (49mm)" + osVersion: "26.4" + download-platform: true + - type: tvos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.4.app" + deviceName: "Apple TV" + osVersion: "26.4" + download-platform: true + - type: visionos + runs-on: macos-26 + xcode: "/Applications/Xcode_26.4.app" + deviceName: "Apple Vision Pro" + osVersion: "26.4.1" + download-platform: true + steps: + - uses: actions/checkout@v6 + - name: Build and Test + id: build + uses: brightdigit/swift-build@v1 + with: + scheme: ${{ env.PACKAGE_NAME }} + type: ${{ matrix.type }} + xcode: ${{ matrix.xcode }} + deviceName: ${{ matrix.deviceName }} + osVersion: ${{ matrix.osVersion }} + download-platform: ${{ matrix.download-platform }} + - name: Install coverage.py (silences codecov-cli probe warning) + if: steps.build.outputs.contains-code-coverage == 'true' + run: pip3 install --quiet --user coverage 2>/dev/null || true + - name: Process Coverage + if: steps.build.outputs.contains-code-coverage == 'true' + uses: sersoft-gmbh/swift-coverage-action@v5 + - name: Upload Coverage + if: steps.build.outputs.contains-code-coverage == 'true' + uses: codecov/codecov-action@v6 + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: ${{ matrix.type && format('{0}{1}', matrix.type, matrix.osVersion) || 'spm' }} + + lint: + name: Linting + runs-on: ubuntu-latest + if: ${{ !cancelled() && !failure() && !contains(github.event.head_commit.message, 'ci skip') }} + needs: [build-ubuntu, build-macos, build-macos-platforms, build-windows, build-android] + concurrency: + group: lint-tools-${{ github.head_ref || github.ref }} + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + - uses: ./.github/actions/setup-tools + - name: Lint + run: | + set -e + ./Scripts/lint.sh diff --git a/Packages/ConfigKeyKit/.github/workflows/check-unsafe-flags.yml b/Packages/ConfigKeyKit/.github/workflows/check-unsafe-flags.yml new file mode 100644 index 00000000..348f4430 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/check-unsafe-flags.yml @@ -0,0 +1,39 @@ +name: Check for unsafeFlags + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + dump-package-check: + name: Dump Swift package (authoritative) and scan JSON + runs-on: ubuntu-latest + container: + image: swift:latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install jq + run: | + apt-get update && apt-get install -y jq + + - name: Dump package JSON and check for unsafeFlags + shell: bash + run: | + set -euo pipefail + # Compute unsafeFlags array directly from the dump (don't store the full dump variable) + unsafe_flags=$(swift package dump-package | jq -c '[.. | objects | .unsafeFlags? // empty]') + # Check array length to decide failure + if [ "$(echo "$unsafe_flags" | jq 'length')" -gt 0 ]; then + echo "ERROR: unsafeFlags found in resolved package JSON:" + echo "$unsafe_flags" | jq '.' || true + echo "--- resolved package dump (first 200 lines) ---" + # Print a sample of the authoritative dump (re-run dump-package for the sample) + swift package dump-package | sed -n '1,200p' || true + exit 1 + else + echo "No unsafeFlags in resolved package JSON." + fi diff --git a/Packages/ConfigKeyKit/.github/workflows/claude-code-review.yml b/Packages/ConfigKeyKit/.github/workflows/claude-code-review.yml new file mode 100644 index 00000000..9adfd522 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/claude-code-review.yml @@ -0,0 +1,54 @@ +name: Claude Code Review + +on: + pull_request: + types: [opened, synchronize, ready_for_review, reopened] + # Optional: Only run on specific file changes + # paths: + # - "src/**/*.ts" + # - "src/**/*.tsx" + # - "src/**/*.js" + # - "src/**/*.jsx" + +jobs: + claude-review: + # Optional: Filter by PR author + # if: | + # github.event.pull_request.user.login == 'external-contributor' || + # github.event.pull_request.user.login == 'new-developer' || + # github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' + + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code Review + id: claude-review + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + allowed_bots: 'codefactor-io[bot]' + prompt: | + Please review this pull request and provide feedback on: + - Code quality and best practices + - Potential bugs or issues + - Performance considerations + - Security concerns + - Test coverage + + Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback. + + Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR. + + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://docs.anthropic.com/en/docs/claude-code/sdk#command-line for available options + claude_args: '--model sonnet --allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"' \ No newline at end of file diff --git a/Packages/ConfigKeyKit/.github/workflows/claude.yml b/Packages/ConfigKeyKit/.github/workflows/claude.yml new file mode 100644 index 00000000..d300267f --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/claude.yml @@ -0,0 +1,50 @@ +name: Claude Code + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + issues: + types: [opened, assigned] + pull_request_review: + types: [submitted] + +jobs: + claude: + if: | + (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || + (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || + (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + issues: read + id-token: write + actions: read # Required for Claude to read CI results on PRs + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Run Claude Code + id: claude + uses: anthropics/claude-code-action@v1 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + + # This is an optional setting that allows Claude to read CI results on PRs + additional_permissions: | + actions: read + + # Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it. + # prompt: 'Update the pull request description to include a summary of changes.' + + # Optional: Add claude_args to customize behavior and configuration + # See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md + # or https://code.claude.com/docs/en/cli-reference for available options + # claude_args: '--allowed-tools Bash(gh pr:*)' + diff --git a/Packages/ConfigKeyKit/.github/workflows/cleanup-caches.yml b/Packages/ConfigKeyKit/.github/workflows/cleanup-caches.yml new file mode 100644 index 00000000..f0124e2c --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/cleanup-caches.yml @@ -0,0 +1,29 @@ +name: Cleanup Branch Caches +on: + delete: + +jobs: + cleanup: + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Cleanup caches for deleted branch + uses: actions/github-script@v9 + with: + script: | + const ref = `refs/heads/${context.payload.ref}`; + const caches = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: ref, + }); + for (const cache of caches.data.actions_caches) { + console.log(`Deleting cache: ${cache.key}`); + await github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id, + }); + } + console.log(`Deleted ${caches.data.actions_caches.length} cache(s) for ${ref}`); diff --git a/Packages/ConfigKeyKit/.github/workflows/codeql.yml b/Packages/ConfigKeyKit/.github/workflows/codeql.yml new file mode 100644 index 00000000..2b3b13fe --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/codeql.yml @@ -0,0 +1,82 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches-ignore: + - '*WIP' + pull_request: + # The branches below must be a subset of the branches above + branches: [ "main" ] + schedule: + - cron: '20 11 * * 3' + +jobs: + analyze: + name: Analyze + # CodeQL Swift analysis requires macOS runners — Linux is not supported + # ("Swift analysis is only supported on macOS runner images"). Other languages + # can run on Linux, hence the conditional. + runs-on: ${{ (matrix.language == 'swift' && 'macos-26') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ 'swift' ] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Xcode + if: matrix.language == 'swift' + run: sudo xcode-select -s /Applications/Xcode_26.4.app/Contents/Developer + + - name: Verify Swift Version + if: matrix.language == 'swift' + run: | + swift --version + swift package --version + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - run: | + echo "Run, Build Application using script" + swift build + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{matrix.language}}" diff --git a/Packages/ConfigKeyKit/.github/workflows/swift-source-compat.yml b/Packages/ConfigKeyKit/.github/workflows/swift-source-compat.yml new file mode 100644 index 00000000..982157d1 --- /dev/null +++ b/Packages/ConfigKeyKit/.github/workflows/swift-source-compat.yml @@ -0,0 +1,29 @@ +name: Swift Source Compatibility + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +jobs: + swift-source-compat-suite: + name: Test Swift ${{ matrix.container }} For Source Compatibility Suite + runs-on: ubuntu-latest + if: ${{ !contains(github.event.head_commit.message, 'ci skip') }} + + strategy: + fail-fast: false + matrix: + container: + - swift:6.2 + - swift:6.3 + + container: ${{ matrix.container }} + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Test Swift 6.x For Source Compatibility + run: swift build --disable-sandbox --verbose --configuration release diff --git a/Packages/ConfigKeyKit/.gitignore b/Packages/ConfigKeyKit/.gitignore new file mode 100644 index 00000000..20909d84 --- /dev/null +++ b/Packages/ConfigKeyKit/.gitignore @@ -0,0 +1,84 @@ +# macOS +.DS_Store + +# Xcode +# +# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore + +## User settings +xcuserdata/ + +## Obj-C/Swift specific +*.hmap + +## App packaging +*.ipa +*.dSYM.zip +*.dSYM + +## Playgrounds +timeline.xctimeline +playground.xcworkspace + +# Swift Package Manager +# +# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. +# Packages/ +# Package.pins +# *.xcodeproj +# +# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata +# hence it is not needed unless you have added a package configuration file to your project +Package.resolved +.swiftpm/ +.build/ +DerivedData/ +.index-build/ + +# Generated Xcode projects/workspaces +*.xcodeproj +*.xcworkspace + +# CocoaPods +# +# We recommend against adding the Pods directory to your .gitignore. However +# you should judge for yourself, the pros and cons are mentioned at: +# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control +# +# Pods/ +# +# Add this line if you want to avoid checking in source code from the Xcode workspace +# *.xcworkspace + +# Carthage +# +# Add this line if you want to avoid checking in source code from Carthage dependencies. +# Carthage/Checkouts + +Carthage/Build/ + +# fastlane +# +# It is recommended to not store the screenshots in the git repo. +# Instead, use fastlane to re-generate the screenshots whenever they are needed. +# For more information about the recommended setup visit: +# https://docs.fastlane.tools/best-practices/source-control/#source-control + +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots/**/*.png +fastlane/test_output + +# IDE +.vscode/ +.idea/ + +# mise / mint local installs +.mint/ + +# Editor scratch +*.sw? + +# Claude +.claude/settings.local.json +.claude/scheduled_tasks.lock diff --git a/Packages/ConfigKeyKit/.gitrepo b/Packages/ConfigKeyKit/.gitrepo new file mode 100644 index 00000000..0d4e3142 --- /dev/null +++ b/Packages/ConfigKeyKit/.gitrepo @@ -0,0 +1,12 @@ +; DO NOT EDIT (unless you know what you are doing) +; +; This subdirectory is a git "subrepo", and this file is maintained by the +; git-subrepo command. See https://github.com/ingydotnet/git-subrepo#readme +; +[subrepo] + remote = git@github.com:brightdigit/ConfigKeyKit.git + branch = main + commit = a9a8bc8be5b33d4aa732a9d0d06a05e8281b4855 + parent = 5d1a87aeaffbdfc883b2d13467503dda592d1ec0 + method = merge + cmdver = 0.4.9 diff --git a/Packages/ConfigKeyKit/.periphery.yml b/Packages/ConfigKeyKit/.periphery.yml new file mode 100644 index 00000000..963c035a --- /dev/null +++ b/Packages/ConfigKeyKit/.periphery.yml @@ -0,0 +1,3 @@ +retain_public: true +retain_unused_protocol_func_params: true +retain_assign_only_properties: true diff --git a/Packages/ConfigKeyKit/.spi.yml b/Packages/ConfigKeyKit/.spi.yml new file mode 100644 index 00000000..bd205001 --- /dev/null +++ b/Packages/ConfigKeyKit/.spi.yml @@ -0,0 +1,5 @@ +version: 1 +builder: + configs: + - documentation_targets: [ConfigKeyKit] + swift_version: 6.2 diff --git a/Packages/ConfigKeyKit/.swift-format b/Packages/ConfigKeyKit/.swift-format new file mode 100644 index 00000000..257f5557 --- /dev/null +++ b/Packages/ConfigKeyKit/.swift-format @@ -0,0 +1,70 @@ +{ + "fileScopedDeclarationPrivacy" : { + "accessLevel" : "fileprivate" + }, + "indentation" : { + "spaces" : 2 + }, + "indentConditionalCompilationBlocks" : true, + "indentSwitchCaseLabels" : false, + "lineBreakAroundMultilineExpressionChainComponents" : false, + "lineBreakBeforeControlFlowKeywords" : false, + "lineBreakBeforeEachArgument" : false, + "lineBreakBeforeEachGenericRequirement" : false, + "lineLength" : 100, + "maximumBlankLines" : 1, + "multiElementCollectionTrailingCommas" : true, + "noAssignmentInExpressions" : { + "allowedFunctions" : [ + "XCTAssertNoThrow" + ] + }, + "prioritizeKeepingFunctionOutputTogether" : false, + "respectsExistingLineBreaks" : true, + "rules" : { + "AllPublicDeclarationsHaveDocumentation" : true, + "AlwaysUseLiteralForEmptyCollectionInit" : false, + "AlwaysUseLowerCamelCase" : true, + "AmbiguousTrailingClosureOverload" : true, + "BeginDocumentationCommentWithOneLineSummary" : false, + "DoNotUseSemicolons" : true, + "DontRepeatTypeInStaticProperties" : true, + "FileScopedDeclarationPrivacy" : false, + "FullyIndirectEnum" : true, + "GroupNumericLiterals" : true, + "IdentifiersMustBeASCII" : true, + "NeverForceUnwrap" : true, + "NeverUseForceTry" : true, + "NeverUseImplicitlyUnwrappedOptionals" : true, + "NoAccessLevelOnExtensionDeclaration" : true, + "NoAssignmentInExpressions" : true, + "NoBlockComments" : true, + "NoCasesWithOnlyFallthrough" : true, + "NoEmptyTrailingClosureParentheses" : true, + "NoLabelsInCasePatterns" : true, + "NoLeadingUnderscores" : true, + "NoParensAroundConditions" : true, + "NoPlaygroundLiterals" : true, + "NoVoidReturnOnFunctionSignature" : true, + "OmitExplicitReturns" : false, + "OneCasePerLine" : true, + "OneVariableDeclarationPerLine" : true, + "OnlyOneTrailingClosureArgument" : true, + "OrderedImports" : true, + "ReplaceForEachWithForLoop" : true, + "ReturnVoidInsteadOfEmptyTuple" : true, + "TypeNamesShouldBeCapitalized" : true, + "UseEarlyExits" : false, + "UseExplicitNilCheckInConditions" : true, + "UseLetInEveryBoundCaseVariable" : true, + "UseShorthandTypeNames" : true, + "UseSingleLinePropertyGetter" : true, + "UseSynthesizedInitializer" : true, + "UseTripleSlashForDocumentationComments" : true, + "UseWhereClausesInForLoops" : true, + "ValidateDocumentationComments" : true + }, + "spacesAroundRangeFormationOperators" : false, + "tabWidth" : 2, + "version" : 1 +} diff --git a/Packages/ConfigKeyKit/.swift-version b/Packages/ConfigKeyKit/.swift-version new file mode 100644 index 00000000..f9da12e1 --- /dev/null +++ b/Packages/ConfigKeyKit/.swift-version @@ -0,0 +1 @@ +6.3.2 \ No newline at end of file diff --git a/Packages/ConfigKeyKit/.swiftlint.yml b/Packages/ConfigKeyKit/.swiftlint.yml new file mode 100644 index 00000000..08f1c1fc --- /dev/null +++ b/Packages/ConfigKeyKit/.swiftlint.yml @@ -0,0 +1,141 @@ +opt_in_rules: + - array_init + - closure_body_length + - closure_end_indentation + - closure_spacing + - collection_alignment + - conditional_returns_on_newline + - contains_over_filter_count + - contains_over_filter_is_empty + - contains_over_first_not_nil + - contains_over_range_nil_comparison + - convenience_type + - discouraged_object_literal + - discouraged_optional_boolean + - empty_collection_literal + - empty_count + - empty_string + - empty_xctest_method + - enum_case_associated_values_count + - expiring_todo + - explicit_acl + - explicit_init + - explicit_top_level_acl + - fatal_error_message + - file_name + - file_name_no_space + - file_types_order + - first_where + - flatmap_over_map_reduce + - force_unwrapping + - ibinspectable_in_extension + - identical_operands + - implicit_return + - implicitly_unwrapped_optional + - indentation_width + - joined_default_parameter + - last_where + - legacy_multiple + - legacy_random + - literal_expression_end_indentation + - lower_acl_than_parent + - missing_docs + - modifier_order + - multiline_arguments + - multiline_arguments_brackets + - multiline_function_chains + - multiline_literal_brackets + - multiline_parameters + - nimble_operator + - nslocalizedstring_key + - nslocalizedstring_require_bundle + - number_separator + - object_literal + - one_declaration_per_file + - operator_usage_whitespace + - optional_enum_case_matching + - overridden_super_call + - override_in_extension + - pattern_matching_keywords + - prefer_self_type_over_type_of_self + - prefer_zero_over_explicit_init + - private_action + - private_outlet + - prohibited_interface_builder + - prohibited_super_call + - quick_discouraged_call + - quick_discouraged_focused_test + - quick_discouraged_pending_test + - reduce_into + - redundant_nil_coalescing + - redundant_type_annotation + - required_enum_case + - single_test_class + - sorted_first_last + - sorted_imports + - static_operator + - strong_iboutlet + - toggle_bool + - type_contents_order + - unavailable_function + - unneeded_parentheses_in_closure_argument + - unowned_variable_capture + - untyped_error_in_catch + - vertical_parameter_alignment_on_call + - vertical_whitespace_closing_braces + - vertical_whitespace_opening_braces + - xct_specific_matcher + - yoda_condition +analyzer_rules: + - unused_import + - unused_declaration +cyclomatic_complexity: + - 6 + - 12 +file_length: + warning: 225 + error: 300 +function_body_length: + - 50 + - 76 +function_parameter_count: 8 +line_length: + - 108 + - 200 +closure_body_length: + - 50 + - 60 +type_name: + min_length: 3 + max_length: + warning: 50 + error: 60 +identifier_name: + excluded: + - id + - no +excluded: + - DerivedData + - .build + - Package.swift +indentation_width: + indentation_width: 2 +file_name: + severity: error +fatal_error_message: + severity: error +disabled_rules: + - nesting + - implicit_getter + - switch_case_alignment + - closure_parameter_position + - trailing_comma + - opening_brace + - optional_data_string_conversion + - pattern_matching_keywords +custom_rules: + no_unchecked_sendable: + name: "No Unchecked Sendable" + regex: '@unchecked\s+Sendable' + message: "Use proper Sendable conformance instead of @unchecked Sendable to maintain strict concurrency safety" + severity: error diff --git a/Packages/ConfigKeyKit/LICENSE b/Packages/ConfigKeyKit/LICENSE new file mode 100644 index 00000000..5bf4bad4 --- /dev/null +++ b/Packages/ConfigKeyKit/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 BrightDigit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Packages/ConfigKeyKit/Makefile b/Packages/ConfigKeyKit/Makefile new file mode 100644 index 00000000..96f245d6 --- /dev/null +++ b/Packages/ConfigKeyKit/Makefile @@ -0,0 +1,22 @@ +.PHONY: help build test lint clean + +help: + @echo "Available targets:" + @echo " build - Build the package" + @echo " test - Run the test suite" + @echo " lint - Run swift-format + swiftlint + periphery via Scripts/lint.sh" + @echo " clean - Clean build artifacts" + @echo " help - Show this help message" + +build: + swift build + +test: + swift test + +lint: + @./Scripts/lint.sh + +clean: + swift package clean + rm -rf .build diff --git a/Packages/ConfigKeyKit/Package.swift b/Packages/ConfigKeyKit/Package.swift new file mode 100644 index 00000000..315080a3 --- /dev/null +++ b/Packages/ConfigKeyKit/Package.swift @@ -0,0 +1,47 @@ +// swift-tools-version: 6.2 + +// swiftlint:disable explicit_acl explicit_top_level_acl + +import PackageDescription + +// MARK: - Swift Settings Configuration + +let swiftSettings: [SwiftSetting] = [ + // Swift 6.2 Upcoming Features (not yet enabled by default) + // SE-0335: Introduce existential `any` + .enableUpcomingFeature("ExistentialAny"), + // SE-0409: Access-level modifiers on import declarations + .enableUpcomingFeature("InternalImportsByDefault"), + // SE-0444: Member import visibility (Swift 6.1+) + .enableUpcomingFeature("MemberImportVisibility"), + // SE-0413: Typed throws + .enableUpcomingFeature("FullTypedThrows"), +] + +let package = Package( + name: "ConfigKeyKit", + platforms: [ + .macOS(.v15), + .iOS(.v18), + .tvOS(.v18), + .watchOS(.v11), + .visionOS(.v2), + ], + products: [ + .library(name: "ConfigKeyKit", targets: ["ConfigKeyKit"]), + ], + targets: [ + .target( + name: "ConfigKeyKit", + dependencies: [], + swiftSettings: swiftSettings + ), + .testTarget( + name: "ConfigKeyKitTests", + dependencies: ["ConfigKeyKit"], + swiftSettings: swiftSettings + ), + ] +) + +// swiftlint:enable explicit_acl explicit_top_level_acl diff --git a/Packages/ConfigKeyKit/README.md b/Packages/ConfigKeyKit/README.md new file mode 100644 index 00000000..a7a413e6 --- /dev/null +++ b/Packages/ConfigKeyKit/README.md @@ -0,0 +1,106 @@ +# ConfigKeyKit + +A tiny, dependency-free Swift package for defining configuration keys that +resolve consistently across multiple sources (command-line arguments, +environment variables, …). + +ConfigKeyKit pairs naturally with [`apple/swift-configuration`][swift-config] +but does **not** depend on it — the package is intentionally Foundation-only +so it can be adopted by any Swift app or library without pulling in a +configuration framework. + +## What's inside + +A single `ConfigKeyKit` product bundles: + +- **Key types** — `ConfigKey`, `OptionalConfigKey`, `ConfigurationKey`, `NamingStyle`, `StandardNamingStyle`, `ConfigKeySource`. +- **CLI scaffolding** — `Command`, `CommandRegistry`, `CommandLineParser`, `ConfigurationParseable`. A lightweight command-dispatch pattern you can ignore if you only need keys. + +## Usage + +```swift +import ConfigKeyKit + +let containerID = ConfigKey( + "cloudkit.container_id", + envPrefix: "MYAPP", + default: "iCloud.com.example.MyApp" +) + +containerID.key(for: .commandLine) // "cloudkit.container_id" +containerID.key(for: .environment) // "MYAPP_CLOUDKIT_CONTAINER_ID" +containerID.defaultValue // "iCloud.com.example.MyApp" +``` + +You then feed those resolved key strings into whatever provider stack you +prefer (Swift Configuration, environment lookup, manual argument parsing, …). + +## Pairing with `swift-configuration` + +`ConfigKey` resolves a single base key into per-source strings, which slots +neatly into [`swift-configuration`][swift-config]'s provider stack. Each +provider sees the key name it expects (dot-separated for CLI, screaming snake +case for ENV), while your call site stays declarative: + +```swift +import Configuration +import ConfigKeyKit + +let containerID = ConfigKey( + "cloudkit.container_id", + envPrefix: "MYAPP", + default: "iCloud.com.example.MyApp" +) + +let config = ConfigReader(providers: [ + CommandLineArgumentsProvider(), + EnvironmentVariablesProvider(), +]) + +extension ConfigReader { + func string(for key: ConfigKey) -> String where Value == String { + if let cli = key.key(for: .commandLine), + let value = self.string(forKey: cli) { + return value + } + if let env = key.key(for: .environment), + let value = self.string(forKey: env) { + return value + } + return key.defaultValue + } +} + +let resolved = config.string(for: containerID) +// Looks up "cloudkit.container_id" on the CLI, falls back to +// "MYAPP_CLOUDKIT_CONTAINER_ID" in the environment, then the default. +``` + +## Used by + +- [MistDemo][mistdemo] — the demo app inside [MistKit][mistkit]. +- [BushelCloud][bushelcloud]. + +## Adding to your `Package.swift` + +```swift +.package(url: "https://github.com/brightdigit/ConfigKeyKit.git", from: "1.0.0-beta.1"), +``` + +```swift +.target( + name: "MyApp", + dependencies: [ + .product(name: "ConfigKeyKit", package: "ConfigKeyKit"), + ] +), +``` + +## License + +MIT — see [LICENSE](LICENSE). + +[swift-config]: https://github.com/apple/swift-configuration +[mistkit]: https://github.com/brightdigit/MistKit +[mistdemo]: https://github.com/brightdigit/MistKit/tree/main/Examples/MistDemo +[bushelcloud]: https://github.com/brightdigit/BushelCloud diff --git a/Packages/ConfigKeyKit/Scripts/header.sh b/Packages/ConfigKeyKit/Scripts/header.sh new file mode 100755 index 00000000..809f88ac --- /dev/null +++ b/Packages/ConfigKeyKit/Scripts/header.sh @@ -0,0 +1,113 @@ +#!/bin/bash + +# Function to print usage +usage() { + echo "Usage: $0 -d directory -c creator -o company -p package [-y year]" + echo " -d directory Directory to read from (including subdirectories)" + echo " -c creator Name of the creator" + echo " -o company Name of the company with the copyright" + echo " -p package Package or library name" + echo " -y year Copyright year (optional, defaults to current year)" + exit 1 +} + +# Get the current year if not provided +current_year=$(date +"%Y") + +# Default values +year="$current_year" + +# Parse arguments +while getopts ":d:c:o:p:y:" opt; do + case $opt in + d) directory="$OPTARG" ;; + c) creator="$OPTARG" ;; + o) company="$OPTARG" ;; + p) package="$OPTARG" ;; + y) year="$OPTARG" ;; + *) usage ;; + esac +done + +# Check for mandatory arguments +if [ -z "$directory" ] || [ -z "$creator" ] || [ -z "$company" ] || [ -z "$package" ]; then + usage +fi + +# Define the header template using a heredoc +read -r -d '' header_template <<'EOF' +// +// %s +// %s +// +// Created by %s. +// Copyright © %s %s. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +EOF + +# Loop through each Swift file in the specified directory and subdirectories +find "$directory" -type f -name "*.swift" | while read -r file; do + # Skip files carrying `// swift-format-ignore-file` anywhere in the leading + # comment block. This is the opt-out used by generated files (e.g. + # swift-openapi-generator emits it via `additionalFileComments`) and lets + # them sit anywhere in the tree without needing a path-based exclusion. + if awk ' + /^\/\/[[:space:]]*swift-format-ignore-file[[:space:]]*$/ { found = 1; exit } + /^[[:space:]]*$/ || /^\/\// { next } + { exit } + END { exit !found } + ' "$file"; then + echo "Skipping $file due to swift-format-ignore directive." + continue + fi + + # Create the header with the current filename + # Escape % characters in user-provided values to prevent format specifier injection + filename=$(basename "$file" | sed 's/%/%%/g') + package_safe=$(printf '%s' "$package" | sed 's/%/%%/g') + creator_safe=$(printf '%s' "$creator" | sed 's/%/%%/g') + year_safe=$(printf '%s' "$year" | sed 's/%/%%/g') + company_safe=$(printf '%s' "$company" | sed 's/%/%%/g') + + header=$(printf "$header_template" "$filename" "$package_safe" "$creator_safe" "$year_safe" "$company_safe") + + # Remove all consecutive lines at the beginning which start with "// ", contain only whitespace, or only "//" + awk ' + BEGIN { skip = 1 } + { + if (skip && ($0 ~ /^\/\/ / || $0 ~ /^\/\/$/ || $0 ~ /^$/)) { + next + } + skip = 0 + print + }' "$file" > temp_file + + # Add the header to the cleaned file + (echo "$header"; echo; cat temp_file) > "$file" + + # Remove the temporary file + rm temp_file +done + +echo "Headers added or files skipped appropriately across all Swift files in the directory and subdirectories." diff --git a/Packages/ConfigKeyKit/Scripts/lint.sh b/Packages/ConfigKeyKit/Scripts/lint.sh new file mode 100755 index 00000000..e2e602b6 --- /dev/null +++ b/Packages/ConfigKeyKit/Scripts/lint.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# Remove set -e to allow script to continue running + +ERRORS=0 + +run_command() { + "$@" || ERRORS=$((ERRORS + 1)) +} + +if [ "$LINT_MODE" = "INSTALL" ]; then + exit +fi + +echo "LintMode: $LINT_MODE" + +# More portable way to get script directory +if [ -z "$SRCROOT" ]; then + SCRIPT_DIR=$(dirname "$(readlink -f "$0")") + PACKAGE_DIR="${SCRIPT_DIR}/.." +else + PACKAGE_DIR="${SRCROOT}" +fi + +# Ensure mise-managed tools are on PATH outside CI (CI uses jdx/mise-action) +if command -v mise >/dev/null 2>&1 && [ -z "$CI" ]; then + eval "$(mise -C "$PACKAGE_DIR" env -s bash)" +fi + +if [ "$LINT_MODE" = "NONE" ]; then + exit +elif [ "$LINT_MODE" = "STRICT" ]; then + SWIFTFORMAT_OPTIONS="--configuration .swift-format" + SWIFTLINT_OPTIONS="--strict" +else + SWIFTFORMAT_OPTIONS="--configuration .swift-format" + SWIFTLINT_OPTIONS="" +fi + +pushd $PACKAGE_DIR + +if [ -z "$CI" ]; then + run_command swift-format format $SWIFTFORMAT_OPTIONS --recursive --parallel --in-place Sources Tests + run_command swiftlint --fix +fi + +if [ -z "$FORMAT_ONLY" ]; then + run_command swift-format lint --configuration .swift-format --recursive --parallel $SWIFTFORMAT_OPTIONS Sources Tests + run_command swiftlint lint $SWIFTLINT_OPTIONS + run_command swift build --build-tests +fi + +$PACKAGE_DIR/Scripts/header.sh -d $PACKAGE_DIR/Sources -c "Leo Dion" -o "BrightDigit" -p "ConfigKeyKit" + +if [ -z "$CI" ]; then + run_command periphery scan $PERIPHERY_OPTIONS --disable-update-check +fi + +popd + +if [ $ERRORS -gt 0 ]; then + echo "Linting completed with $ERRORS error(s)" + exit 1 +else + echo "Linting completed successfully" + exit 0 +fi diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/Command.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/Command.swift similarity index 97% rename from Examples/MistDemo/Sources/ConfigKeyKit/Command.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/Command.swift index f9baa1cc..f3743061 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/Command.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/Command.swift @@ -1,6 +1,6 @@ // // Command.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,8 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - /// Generic protocol for CLI commands using Swift Configuration public protocol Command: Sendable { /// Associated configuration type for this command diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandConfiguration.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandConfiguration.swift similarity index 96% rename from Examples/MistDemo/Sources/ConfigKeyKit/CommandConfiguration.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandConfiguration.swift index c3c21924..251449e4 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandConfiguration.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandConfiguration.swift @@ -1,6 +1,6 @@ // // CommandConfiguration.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -28,7 +28,7 @@ // /// Command configuration for identifying and routing commands -public struct CommandConfiguration { +public struct CommandConfiguration: Sendable { /// The name used to invoke this command on the CLI. public let commandName: String /// A short description of what the command does. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandLineParser.swift similarity index 97% rename from Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandLineParser.swift index 1e03d791..22aa09f5 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandLineParser.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandLineParser.swift @@ -1,6 +1,6 @@ // // CommandLineParser.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -30,7 +30,7 @@ internal import Foundation /// Command line argument parser for Swift Configuration integration -public struct CommandLineParser { +public struct CommandLineParser: Sendable { private let arguments: [String] /// Initialize with command line arguments. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistry.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistry.swift index 27e836e2..06f44e38 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistry.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistry.swift @@ -1,6 +1,6 @@ // // CommandRegistry.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,8 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - /// Actor-based registry for managing available commands public actor CommandRegistry { /// Metadata about a command diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistryError.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistryError.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistryError.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistryError.swift index 51b221cb..033ae83a 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/CommandRegistryError.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/CommandRegistryError.swift @@ -1,6 +1,6 @@ // // CommandRegistryError.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Bool.swift similarity index 86% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Bool.swift index eb9420d3..b30909a0 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Bool.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Bool.swift @@ -1,6 +1,6 @@ // // ConfigKey+Bool.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,17 +27,7 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - -// MARK: - Specialized Initializers for Booleans - extension ConfigKey where Value == Bool { - /// Non-optional default value accessor for booleans - @available(*, deprecated, message: "Use defaultValue directly instead") - public var boolDefault: Bool { - defaultValue // Already non-optional! - } - /// Initialize a boolean configuration key with non-optional default /// - Parameters: /// - cli: Command-line argument name @@ -68,5 +58,3 @@ extension ConfigKey where Value == Bool { self.defaultValue = defaultVal } } - -// Application-specific boolean key helpers should be added in application code diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Debug.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Debug.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Debug.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Debug.swift index f0ab9191..2f23a42b 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey+Debug.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey+Debug.swift @@ -1,6 +1,6 @@ // // ConfigKey+Debug.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey.swift similarity index 95% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey.swift index a6a278f6..4115c1a8 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKey.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKey.swift @@ -1,6 +1,6 @@ // // ConfigKey.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,10 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - -// MARK: - Generic Configuration Key - /// Configuration key for values with default fallbacks /// /// Use `ConfigKey` when a configuration value has a sensible default @@ -40,7 +36,7 @@ internal import Foundation /// Example: /// ```swift /// let containerID = ConfigKey( -/// base: "cloudkit.container_id", +/// "cloudkit.container_id", /// default: "iCloud.com.example.MyApp" /// ) /// // read(containerID) returns String (non-optional) @@ -50,7 +46,7 @@ public struct ConfigKey: ConfigurationKey, Sendable { internal let styles: [ConfigKeySource: any NamingStyle] internal let explicitKeys: [ConfigKeySource: String] /// The default value returned when no source provides a value. - public let defaultValue: Value // Non-optional! + public let defaultValue: Value /// The base key string used for this configuration key public var base: String? { baseKey } diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKeySource.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKeySource.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigKeySource.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKeySource.swift index 1adb946f..242f5108 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigKeySource.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigKeySource.swift @@ -1,6 +1,6 @@ // // ConfigKeySource.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationKey.swift similarity index 95% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationKey.swift index 70458be3..a15da10a 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationKey.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationKey.swift @@ -1,6 +1,6 @@ // // ConfigurationKey.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,10 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - -// MARK: - Configuration Key Protocol - /// Protocol for configuration keys that support multiple sources public protocol ConfigurationKey: Sendable { /// Get the key string for a specific source diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationParseable.swift similarity index 95% rename from Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationParseable.swift index 417b1b24..393bcba1 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/ConfigurationParseable.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/ConfigurationParseable.swift @@ -1,6 +1,6 @@ // // ConfigurationParseable.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,8 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - /// Protocol for configuration types that can parse themselves /// from command line arguments and environment variables. public protocol ConfigurationParseable: Sendable { @@ -47,7 +45,6 @@ public protocol ConfigurationParseable: Sendable { init(configuration: ConfigReader, base: BaseConfig?) async throws } -/// Extension for root configurations (where BaseConfig == Never) extension ConfigurationParseable where BaseConfig == Never { /// Convenience initializer for root configs that don't need a parent public init(configuration: ConfigReader) async throws { diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/NamingStyle.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/NamingStyle.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/NamingStyle.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/NamingStyle.swift index bb72ddd8..6df8614f 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/NamingStyle.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/NamingStyle.swift @@ -1,6 +1,6 @@ // // NamingStyle.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift similarity index 98% rename from Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift index a15a0079..b1e01471 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey+Debug.swift @@ -1,6 +1,6 @@ // // OptionalConfigKey+Debug.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey.swift similarity index 96% rename from Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey.swift index 0a80c71d..23b63d46 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/OptionalConfigKey.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/OptionalConfigKey.swift @@ -1,6 +1,6 @@ // // OptionalConfigKey.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,10 +27,6 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - -// MARK: - Optional Configuration Key - /// Configuration key for optional values without defaults /// /// Use `OptionalConfigKey` when a configuration value has no sensible default @@ -39,7 +35,7 @@ internal import Foundation /// /// Example: /// ```swift -/// let apiKey = OptionalConfigKey(base: "api.key") +/// let apiKey = OptionalConfigKey("api.key") /// // read(apiKey) returns String? /// ``` public struct OptionalConfigKey: ConfigurationKey, Sendable { diff --git a/Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/StandardNamingStyle.swift similarity index 94% rename from Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift rename to Packages/ConfigKeyKit/Sources/ConfigKeyKit/StandardNamingStyle.swift index 5b626df8..faee3b8d 100644 --- a/Examples/MistDemo/Sources/ConfigKeyKit/StandardNamingStyle.swift +++ b/Packages/ConfigKeyKit/Sources/ConfigKeyKit/StandardNamingStyle.swift @@ -1,6 +1,6 @@ // // StandardNamingStyle.swift -// MistDemo +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -34,7 +34,7 @@ public enum StandardNamingStyle: NamingStyle, Sendable { /// Dot-separated lowercase (e.g., "cloudkit.container_id") case dotSeparated - /// Screaming snake case with prefix (e.g., "APP_CLOUDKIT_CONTAINER_ID") + /// Screaming snake case with optional prefix (e.g., "APP_CLOUDKIT_CONTAINER_ID") case screamingSnakeCase(prefix: String?) /// Transform the base key string into the appropriate naming style. diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandLineParserTests.swift similarity index 85% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandLineParserTests.swift index d80cd19c..d01e4a4c 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandLineParserTests.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandLineParserTests.swift @@ -1,6 +1,6 @@ // // CommandLineParserTests.swift -// MistDemoTests +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -35,7 +35,7 @@ internal import Testing internal struct CommandLineParserTests { @Test("parseCommandName returns nil when only executable name is present") internal func noArgsReturnsNil() { - let parser = CommandLineParser(arguments: ["mistdemo"]) + let parser = CommandLineParser(arguments: ["mytool"]) #expect(parser.parseCommandName() == nil) #expect(parser.commandArguments().isEmpty) @@ -44,7 +44,7 @@ internal struct CommandLineParserTests { @Test("parseCommandName returns the first non-option argument") internal func parsesCommand() { - let parser = CommandLineParser(arguments: ["mistdemo", "query", "--limit", "10"]) + let parser = CommandLineParser(arguments: ["mytool", "query", "--limit", "10"]) #expect(parser.parseCommandName() == "query") #expect(parser.commandArguments() == ["--limit", "10"]) @@ -52,7 +52,7 @@ internal struct CommandLineParserTests { @Test("parseCommandName returns nil when first argument is a global option") internal func globalOptionReturnsNilCommand() { - let parser = CommandLineParser(arguments: ["mistdemo", "--config-file", "/tmp/x.json"]) + let parser = CommandLineParser(arguments: ["mytool", "--config-file", "/tmp/x.json"]) #expect(parser.parseCommandName() == nil) #expect(parser.commandArguments() == ["--config-file", "/tmp/x.json"]) @@ -60,7 +60,7 @@ internal struct CommandLineParserTests { @Test("commandArguments strips the executable + command but keeps the rest verbatim") internal func commandArgumentsPreserveRest() { - let parser = CommandLineParser(arguments: ["mistdemo", "lookup", "rec-1", "rec-2"]) + let parser = CommandLineParser(arguments: ["mytool", "lookup", "rec-1", "rec-2"]) #expect(parser.commandArguments() == ["rec-1", "rec-2"]) } @@ -70,7 +70,7 @@ internal struct CommandLineParserTests { arguments: ["--help", "-h", "help"] ) internal func helpTokens(token: String) { - let parser = CommandLineParser(arguments: ["mistdemo", "query", token]) + let parser = CommandLineParser(arguments: ["mytool", "query", token]) #expect(parser.isHelpRequested() == true) } diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+AvailableCommands.swift similarity index 78% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+AvailableCommands.swift index 3a8e0582..5d081211 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+AvailableCommands.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+AvailableCommands.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+AvailableCommands.swift -// MistDemo +// CommandRegistry+AvailableCommands.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,20 +27,19 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Available Commands") internal struct AvailableCommands { @Test("Available commands lists registered commands") internal func availableCommands() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) - await registry.register(CommandRegistryTests.AnotherCommand.self) + await registry.register(CommandRegistry.TestCommand.self) + await registry.register(CommandRegistry.AnotherCommand.self) let commands = await registry.availableCommands @@ -51,7 +50,7 @@ extension CommandRegistryTests { @Test("Available commands returns empty for new registry") internal func availableCommandsEmpty() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() let commands = await registry.availableCommands @@ -60,10 +59,10 @@ extension CommandRegistryTests { @Test("Available commands are sorted") internal func availableCommandsSorted() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.AnotherCommand.self) - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.AnotherCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let commands = await registry.availableCommands diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandCreation.swift similarity index 82% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandCreation.swift index 7bdc4bc7..f732ceb2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandCreation.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandCreation.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+CommandCreation.swift -// MistDemo +// CommandRegistry+CommandCreation.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,28 +27,27 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Command Creation") internal struct CommandCreation { @Test("Create command instance") internal func createCommandInstance() async throws { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let command = try await registry.createCommand(named: "test") - #expect(command is CommandRegistryTests.TestCommand) + #expect(command is CommandRegistry.TestCommand) } @Test("Create command instance throws for unknown command") internal func createCommandInstanceThrows() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() await #expect(throws: CommandRegistryError.self) { try await registry.createCommand(named: "unknown") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandTypeRetrieval.swift similarity index 85% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandTypeRetrieval.swift index b485e82c..cff425d8 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+CommandTypeRetrieval.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+CommandTypeRetrieval.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+CommandTypeRetrieval.swift -// MistDemo +// CommandRegistry+CommandTypeRetrieval.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,19 +27,18 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Command Type Retrieval") internal struct CommandTypeRetrieval { @Test("Get command type by name") internal func getCommandType() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let commandType = await registry.commandType(named: "test") @@ -48,7 +47,7 @@ extension CommandRegistryTests { @Test("Get command type for unregistered command") internal func getCommandTypeUnregistered() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() let commandType = await registry.commandType(named: "nonexistent") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+ConcurrentAccess.swift similarity index 81% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+ConcurrentAccess.swift index 47d7d990..7f722614 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+ConcurrentAccess.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+ConcurrentAccess.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+ConcurrentAccess.swift -// MistDemo +// CommandRegistry+ConcurrentAccess.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,24 +27,23 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Concurrent Access") internal struct ConcurrentAccess { @Test("Concurrent registration") internal func concurrentRegistration() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() await withTaskGroup(of: Void.self) { group in group.addTask { - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) } group.addTask { - await registry.register(CommandRegistryTests.AnotherCommand.self) + await registry.register(CommandRegistry.AnotherCommand.self) } } @@ -54,9 +53,9 @@ extension CommandRegistryTests { @Test("Concurrent reads") internal func concurrentReads() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) await withTaskGroup(of: Bool.self) { group in for _ in 0..<10 { @@ -76,11 +75,11 @@ extension CommandRegistryTests { @Test("Mixed concurrent operations") internal func mixedConcurrentOperations() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() await withTaskGroup(of: Void.self) { group in group.addTask { - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) } group.addTask { _ = await registry.isRegistered("test") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Errors.swift similarity index 95% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Errors.swift index bc6b10c8..cce54203 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Errors.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Errors.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+Errors.swift -// MistDemo +// CommandRegistry+Errors.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -32,7 +32,7 @@ internal import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Errors") internal struct Errors { @Test("Unknown command error has description") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Metadata.swift similarity index 86% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Metadata.swift index 19806ad1..f12242ed 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Metadata.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Metadata.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+Metadata.swift -// MistDemo +// CommandRegistry+Metadata.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,19 +27,18 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Metadata") internal struct Metadata { @Test("Get command metadata") internal func getCommandMetadata() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let metadata = await registry.metadata(for: "test") @@ -51,7 +50,7 @@ extension CommandRegistryTests { @Test("Get metadata for unregistered command") internal func getMetadataForUnregistered() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() let metadata = await registry.metadata(for: "nonexistent") diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Overwrite.swift similarity index 82% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Overwrite.swift index 487e7931..c77bde00 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Overwrite.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Overwrite.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+Overwrite.swift -// MistDemo +// CommandRegistry+Overwrite.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,20 +27,19 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Overwrite") internal struct Overwrite { @Test("Registering same command twice overwrites") internal func registerCommandTwiceOverwrites() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let commands = await registry.availableCommands #expect(commands.count == 1) diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Registration.swift similarity index 80% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Registration.swift index ccf99b02..8c86972b 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+Registration.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+Registration.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+Registration.swift -// MistDemo +// CommandRegistry+Registration.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,19 +27,18 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation -internal import Testing +import Testing @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { @Suite("Registration") internal struct Registration { @Test("Register a command") internal func registerCommand() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) + await registry.register(CommandRegistry.TestCommand.self) let isRegistered = await registry.isRegistered("test") #expect(isRegistered == true) @@ -47,10 +46,10 @@ extension CommandRegistryTests { @Test("Register multiple commands") internal func registerMultipleCommands() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() - await registry.register(CommandRegistryTests.TestCommand.self) - await registry.register(CommandRegistryTests.AnotherCommand.self) + await registry.register(CommandRegistry.TestCommand.self) + await registry.register(CommandRegistry.AnotherCommand.self) let testRegistered = await registry.isRegistered("test") let anotherRegistered = await registry.isRegistered("another") @@ -61,7 +60,7 @@ extension CommandRegistryTests { @Test("Unregistered command returns false") internal func unregisteredCommand() async { - let registry = CommandRegistry() + let registry = ConfigKeyKit.CommandRegistry() let isRegistered = await registry.isRegistered("nonexistent") #expect(isRegistered == false) diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+TestCommandTypes.swift similarity index 94% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+TestCommandTypes.swift index 1d86a994..e62d3bc5 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests+TestCommandTypes.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry+TestCommandTypes.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests+TestCommandTypes.swift -// MistDemo +// CommandRegistry+TestCommandTypes.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -27,11 +27,9 @@ // OTHER DEALINGS IN THE SOFTWARE. // -internal import Foundation - @testable import ConfigKeyKit -extension CommandRegistryTests { +extension CommandRegistry { internal struct TestCommand: Command { internal typealias Config = TestConfig @@ -81,7 +79,7 @@ extension CommandRegistryTests { } } - internal struct TestConfigReader { + internal struct TestConfigReader: Sendable { // Minimal config reader for testing } } diff --git a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry.swift similarity index 82% rename from Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift rename to Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry.swift index 68712d1c..e55378f2 100644 --- a/Examples/MistDemo/Tests/MistDemoTests/ConfigKeyKit/CommandRegistry/CommandRegistryTests.swift +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/CommandRegistry/CommandRegistry.swift @@ -1,6 +1,6 @@ // -// CommandRegistryTests.swift -// MistDemo +// CommandRegistry.swift +// ConfigKeyKit // // Created by Leo Dion. // Copyright © 2026 BrightDigit. @@ -29,5 +29,11 @@ internal import Testing -@Suite("CommandRegistry") -internal enum CommandRegistryTests {} +@Suite( + "CommandRegistry", + .disabled( + if: TestEnvironment.hangsOnTestRunning, + "Windows + Swift 6.2: async/actor tests hang the Swift Testing runner mid-flight" + ) +) +internal enum CommandRegistry {} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift new file mode 100644 index 00000000..48eab164 --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeySourceTests.swift @@ -0,0 +1,43 @@ +// +// ConfigKeySourceTests.swift +// ConfigKeyKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Testing + +@testable import ConfigKeyKit + +@Suite("ConfigKeySource Tests") +internal struct ConfigKeySourceTests { + @Test("All cases") + internal func allCases() { + let sources = ConfigKeySource.allCases + #expect(sources.count == 2) + #expect(sources.contains(.commandLine)) + #expect(sources.contains(.environment)) + } +} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeyTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeyTests.swift new file mode 100644 index 00000000..79daeca1 --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/ConfigKeyTests.swift @@ -0,0 +1,83 @@ +// +// ConfigKeyTests.swift +// ConfigKeyKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Testing + +@testable import ConfigKeyKit + +@Suite("ConfigKey Tests") +internal struct ConfigKeyTests { + @Test("ConfigKey with explicit keys and default") + internal func explicitKeys() { + let key = ConfigKey(cli: "test.key", env: "TEST_KEY", default: "default-value") + + #expect(key.key(for: .commandLine) == "test.key") + #expect(key.key(for: .environment) == "TEST_KEY") + #expect(key.defaultValue == "default-value") + } + + @Test("ConfigKey with base string and custom env prefix") + internal func baseStringWithCustomPrefix() { + let key = ConfigKey( + "cloudkit.container_id", envPrefix: "MYAPP", default: "iCloud.com.example.App" + ) + + #expect(key.key(for: .commandLine) == "cloudkit.container_id") + #expect(key.key(for: .environment) == "MYAPP_CLOUDKIT_CONTAINER_ID") + #expect(key.defaultValue == "iCloud.com.example.App") + } + + @Test("ConfigKey with base string and no prefix") + internal func baseStringNoPrefix() { + let key = ConfigKey( + "cloudkit.container_id", envPrefix: nil, default: "iCloud.com.example.App" + ) + + #expect(key.key(for: .commandLine) == "cloudkit.container_id") + #expect(key.key(for: .environment) == "CLOUDKIT_CONTAINER_ID") + #expect(key.defaultValue == "iCloud.com.example.App") + } + + @Test("ConfigKey exposes its base string") + internal func baseAccessor() { + let withBase = ConfigKey("cloudkit.container_id", default: "default") + let withoutBase = ConfigKey(cli: "x", env: "X", default: "default") + + #expect(withBase.base == "cloudkit.container_id") + #expect(withoutBase.base == nil) + } + + @Test("Boolean ConfigKey with default") + internal func booleanDefaultValue() { + let key = ConfigKey("sync.verbose", envPrefix: "MYAPP", default: false) + + #expect(key.defaultValue == false) + #expect(key.key(for: .environment) == "MYAPP_SYNC_VERBOSE") + } +} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/NamingStyleTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/NamingStyleTests.swift new file mode 100644 index 00000000..9969a08e --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/NamingStyleTests.swift @@ -0,0 +1,59 @@ +// +// NamingStyleTests.swift +// ConfigKeyKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Testing + +@testable import ConfigKeyKit + +@Suite("NamingStyle Tests") +internal struct NamingStyleTests { + @Test("Dot-separated style") + internal func dotSeparatedStyle() { + let style = StandardNamingStyle.dotSeparated + #expect(style.transform("cloudkit.container_id") == "cloudkit.container_id") + } + + @Test("Screaming snake case with prefix") + internal func screamingSnakeCaseWithPrefix() { + let style = StandardNamingStyle.screamingSnakeCase(prefix: "MYAPP") + #expect(style.transform("cloudkit.container_id") == "MYAPP_CLOUDKIT_CONTAINER_ID") + } + + @Test("Screaming snake case without prefix") + internal func screamingSnakeCaseNoPrefix() { + let style = StandardNamingStyle.screamingSnakeCase(prefix: nil) + #expect(style.transform("cloudkit.container_id") == "CLOUDKIT_CONTAINER_ID") + } + + @Test("Screaming snake case with nil prefix on shorter key") + internal func screamingSnakeCaseNilPrefixShort() { + let style = StandardNamingStyle.screamingSnakeCase(prefix: nil) + #expect(style.transform("sync.verbose") == "SYNC_VERBOSE") + } +} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift new file mode 100644 index 00000000..945f622b --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/OptionalConfigKeyTests.swift @@ -0,0 +1,84 @@ +// +// OptionalConfigKeyTests.swift +// ConfigKeyKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Testing + +@testable import ConfigKeyKit + +@Suite("OptionalConfigKey Tests") +internal struct OptionalConfigKeyTests { + @Test("OptionalConfigKey with explicit keys") + internal func explicitKeys() { + let key = OptionalConfigKey(cli: "test.key", env: "TEST_KEY") + + #expect(key.key(for: .commandLine) == "test.key") + #expect(key.key(for: .environment) == "TEST_KEY") + } + + @Test("OptionalConfigKey with base string and custom env prefix") + internal func baseStringWithCustomPrefix() { + let key = OptionalConfigKey("cloudkit.key_id", envPrefix: "MYAPP") + + #expect(key.key(for: .commandLine) == "cloudkit.key_id") + #expect(key.key(for: .environment) == "MYAPP_CLOUDKIT_KEY_ID") + } + + @Test("OptionalConfigKey with base string and no prefix") + internal func baseStringNoPrefix() { + let key = OptionalConfigKey("cloudkit.key_id", envPrefix: nil) + + #expect(key.key(for: .commandLine) == "cloudkit.key_id") + #expect(key.key(for: .environment) == "CLOUDKIT_KEY_ID") + } + + @Test("OptionalConfigKey and ConfigKey generate identical keys") + internal func keyGenerationParity() { + let optional = OptionalConfigKey("test.key", envPrefix: "MYAPP") + let withDefault = ConfigKey("test.key", envPrefix: "MYAPP", default: "default") + + #expect(optional.key(for: .commandLine) == withDefault.key(for: .commandLine)) + #expect(optional.key(for: .environment) == withDefault.key(for: .environment)) + } + + @Test("OptionalConfigKey for Int type") + internal func intOptionalKey() { + let key = OptionalConfigKey("sync.min_interval", envPrefix: "MYAPP") + + #expect(key.key(for: .commandLine) == "sync.min_interval") + #expect(key.key(for: .environment) == "MYAPP_SYNC_MIN_INTERVAL") + } + + @Test("OptionalConfigKey for Double type") + internal func doubleOptionalKey() { + let key = OptionalConfigKey("fetch.interval_global", envPrefix: "MYAPP") + + #expect(key.key(for: .commandLine) == "fetch.interval_global") + #expect(key.key(for: .environment) == "MYAPP_FETCH_INTERVAL_GLOBAL") + } +} diff --git a/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/TestEnvironment.swift b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/TestEnvironment.swift new file mode 100644 index 00000000..c44b2684 --- /dev/null +++ b/Packages/ConfigKeyKit/Tests/ConfigKeyKitTests/TestEnvironment.swift @@ -0,0 +1,16 @@ +// +// TestEnvironment.swift +// ConfigKeyKit +// +// Created by Leo Dion on 5/19/26. +// + +internal enum TestEnvironment { + internal static let hangsOnTestRunning: Bool = { + #if os(Windows) && swift(<6.3) + return true + #else + return false + #endif + }() +} diff --git a/Packages/ConfigKeyKit/codecov.yml b/Packages/ConfigKeyKit/codecov.yml new file mode 100644 index 00000000..d07d53ee --- /dev/null +++ b/Packages/ConfigKeyKit/codecov.yml @@ -0,0 +1,9 @@ +coverage: + status: + patch: + default: + target: auto + threshold: 2% + +ignore: + - "Tests" diff --git a/Packages/ConfigKeyKit/mise.toml b/Packages/ConfigKeyKit/mise.toml new file mode 100644 index 00000000..6df20abb --- /dev/null +++ b/Packages/ConfigKeyKit/mise.toml @@ -0,0 +1,7 @@ +[settings] +experimental = true + +[tools] +"spm:swiftlang/swift-format" = "602.0.0" +"aqua:realm/SwiftLint" = "0.62.2" +"spm:peripheryapp/periphery" = "3.7.4" diff --git a/Sources/MistKit/Authentication/APITokenAuthenticator.swift b/Sources/MistKit/Authentication/APITokenAuthenticator.swift index 69cb0cea..9b7cff93 100644 --- a/Sources/MistKit/Authentication/APITokenAuthenticator.swift +++ b/Sources/MistKit/Authentication/APITokenAuthenticator.swift @@ -72,7 +72,7 @@ public struct APITokenAuthenticator: Authenticator { /// by `encoded()`. Re-runs format validation, so a corrupted or stale /// payload throws `TokenManagerError.invalidCredentials`. public init(decoding data: Data) throws { - let wire = try JSONDecoder().decode(WireFormat.self, from: data) + let wire = try JSONDecoder.shared.decode(WireFormat.self, from: data) try self.init(token: wire.token) } @@ -86,6 +86,6 @@ public struct APITokenAuthenticator: Authenticator { /// JSON-encodes the API token for persistence by `TokenStorage`. public func encoded() throws -> Data { - try JSONEncoder().encode(WireFormat(token: token)) + try JSONEncoder.shared.encode(WireFormat(token: token)) } } diff --git a/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift b/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift index a263fc5d..7bfa29ca 100644 --- a/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift +++ b/Sources/MistKit/Authentication/ServerToServerAuthenticator.swift @@ -141,7 +141,7 @@ public struct ServerToServerAuthenticator: Authenticator { /// produced by `encoded()`. Re-runs key parse + key-ID validation, so a /// corrupted payload throws `TokenManagerError.invalidCredentials`. public init(decoding data: Data) throws { - let wire = try JSONDecoder().decode(WireFormat.self, from: data) + let wire = try JSONDecoder.shared.decode(WireFormat.self, from: data) guard let keyData = Data(base64Encoded: wire.privateKey) else { throw TokenManagerError.invalidCredentials(.encodedPayloadInvalidBase64) } @@ -191,6 +191,6 @@ public struct ServerToServerAuthenticator: Authenticator { privateKey: privateKey.rawRepresentation.base64EncodedString(), bodyBufferLimit: bodyBufferLimit ) - return try JSONEncoder().encode(wire) + return try JSONEncoder.shared.encode(wire) } } diff --git a/Sources/MistKit/Authentication/WebAuthTokenAuthenticator.swift b/Sources/MistKit/Authentication/WebAuthTokenAuthenticator.swift index 6f4135b7..552b1ccf 100644 --- a/Sources/MistKit/Authentication/WebAuthTokenAuthenticator.swift +++ b/Sources/MistKit/Authentication/WebAuthTokenAuthenticator.swift @@ -95,7 +95,7 @@ public struct WebAuthTokenAuthenticator: Authenticator { /// produced by `encoded()`. Re-runs format validation, so a corrupted /// or stale payload throws `TokenManagerError.invalidCredentials`. public init(decoding data: Data) throws { - let wire = try JSONDecoder().decode(WireFormat.self, from: data) + let wire = try JSONDecoder.shared.decode(WireFormat.self, from: data) try self.init(apiToken: wire.apiToken, webAuthToken: wire.webAuthToken) } @@ -114,6 +114,6 @@ public struct WebAuthTokenAuthenticator: Authenticator { /// JSON-encodes both tokens for persistence by `TokenStorage`. public func encoded() throws -> Data { - try JSONEncoder().encode(WireFormat(apiToken: apiToken, webAuthToken: webAuthToken)) + try JSONEncoder.shared.encode(WireFormat(apiToken: apiToken, webAuthToken: webAuthToken)) } } diff --git a/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift b/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift index b663404a..dcfbfb57 100644 --- a/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift +++ b/Sources/MistKit/CloudKitService/CloudKitError+OpenAPI.swift @@ -73,6 +73,19 @@ extension CloudKitError { } } + /// Build an `.httpError` for an undocumented response and log the occurrence. + /// The full response value is logged at `.debug` because it may echo server-side + /// request data (e.g. emails passed to `lookupUsersByEmail`); the `.warning` line + /// stays sanitized so it can ship to ops/log aggregators without leaking PII. + internal static func undocumented(statusCode: Int, response: some Any) -> CloudKitError { + let logger = Logger(subsystem: .api) + logger.debug("Unhandled response (HTTP \(statusCode)): \(response)") + logger.warning( + "Unhandled \(type(of: response)) (HTTP \(statusCode)) - treating as generic HTTP error" + ) + return .httpError(statusCode: statusCode) + } + /// Returns a copy of this error with the given hint attached. /// /// If `self` is already `.quotaExceeded`, the existing reason is preserved @@ -86,7 +99,9 @@ extension CloudKitError { /// with information that can only be computed from the local request state /// (e.g., the actual encoded record size, the asset byte count). internal func addingQuotaHint(_ hint: QuotaHint?) -> CloudKitError { - guard let hint else { return self } + guard let hint else { + return self + } switch self { case .quotaExceeded(let reason, _): return .quotaExceeded(reason: reason, hint: hint) @@ -98,17 +113,4 @@ extension CloudKitError { return self } } - - /// Build an `.httpError` for an undocumented response and log the occurrence. - /// The full response value is logged at `.debug` because it may echo server-side - /// request data (e.g. emails passed to `lookupUsersByEmail`); the `.warning` line - /// stays sanitized so it can ship to ops/log aggregators without leaking PII. - internal static func undocumented(statusCode: Int, response: some Any) -> CloudKitError { - let logger = Logger(subsystem: .api) - logger.debug("Unhandled response (HTTP \(statusCode)): \(response)") - logger.warning( - "Unhandled \(type(of: response)) (HTTP \(statusCode)) - treating as generic HTTP error" - ) - return .httpError(statusCode: statusCode) - } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift b/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift index 6de368bf..d432fbbd 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+AssetUpload.swift @@ -40,6 +40,20 @@ internal import Logging @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) extension CloudKitService { + /// Returns a `.assetExceedsSizeLimit` hint when local `data` is over + /// CloudKit's per-asset upload limit. Returns `nil` otherwise (the + /// `QUOTA_EXCEEDED` is presumably caused by the user's iCloud storage + /// being full, not the asset size). + private static func assetSizeQuotaHint(for data: Data) -> QuotaHint? { + guard data.count > maxAssetUploadBytes else { + return nil + } + return .assetExceedsSizeLimit( + dataBytes: data.count, + maxBytes: maxAssetUploadBytes + ) + } + /// Upload binary data to a CloudKit asset upload URL /// /// This is step 2 of the two-step asset upload process. @@ -84,7 +98,7 @@ extension CloudKitService { Logger(subsystem: .api).debug("Asset upload response: \(responseString)") } - let uploadResponse = try JSONDecoder().decode( + let uploadResponse = try JSONDecoder.shared.decode( AssetUploadResponse.self, from: responseData ) @@ -101,16 +115,4 @@ extension CloudKitService { .addingQuotaHint(Self.assetSizeQuotaHint(for: data)) } } - - /// Returns a `.assetExceedsSizeLimit` hint when local `data` is over - /// CloudKit's per-asset upload limit. Returns `nil` otherwise (the - /// `QUOTA_EXCEEDED` is presumably caused by the user's iCloud storage - /// being full, not the asset size). - private static func assetSizeQuotaHint(for data: Data) -> QuotaHint? { - guard data.count > maxAssetUploadBytes else { return nil } - return .assetExceedsSizeLimit( - dataBytes: data.count, - maxBytes: maxAssetUploadBytes - ) - } } diff --git a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift index 0a82e207..c6b9322b 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService+WriteOperations.swift @@ -40,6 +40,28 @@ internal import OpenAPIRuntime #endif extension CloudKitService { + /// Inspect a batch of API record operations and return a `QuotaHint` for + /// the first record whose JSON-encoded size exceeds CloudKit's per-record + /// limit. Returns `nil` if every record is within bounds — which is the + /// usual case when the server's `QUOTA_EXCEEDED` is caused by storage-quota + /// exhaustion rather than per-record size. + private static func recordSizeQuotaHint( + for apiOperations: [Components.Schemas.RecordOperation] + ) -> QuotaHint? { + for (index, operation) in apiOperations.enumerated() { + guard let record = operation.record, + let encoded = try? JSONEncoder.shared.encode(record), + encoded.count > maxRecordDataBytes + else { continue } + return .recordExceedsSizeLimit( + operationIndex: index, + encodedBytes: encoded.count, + maxBytes: maxRecordDataBytes + ) + } + return nil + } + /// Modify (create, update, or delete) CloudKit records /// - Parameters: /// - operations: Array of record operations to perform @@ -96,29 +118,6 @@ extension CloudKitService { } } - /// Inspect a batch of API record operations and return a `QuotaHint` for - /// the first record whose JSON-encoded size exceeds CloudKit's per-record - /// limit. Returns `nil` if every record is within bounds — which is the - /// usual case when the server's `QUOTA_EXCEEDED` is caused by storage-quota - /// exhaustion rather than per-record size. - private static func recordSizeQuotaHint( - for apiOperations: [Components.Schemas.RecordOperation] - ) -> QuotaHint? { - let encoder = JSONEncoder() - for (index, operation) in apiOperations.enumerated() { - guard let record = operation.record, - let encoded = try? encoder.encode(record), - encoded.count > maxRecordDataBytes - else { continue } - return .recordExceedsSizeLimit( - operationIndex: index, - encodedBytes: encoded.count, - maxBytes: maxRecordDataBytes - ) - } - return nil - } - /// Create a single record in CloudKit /// - Parameters: /// - recordType: The type of record to create diff --git a/Sources/MistKit/CloudKitService/CloudKitService.swift b/Sources/MistKit/CloudKitService/CloudKitService.swift index 02b06e01..cdfcbddc 100644 --- a/Sources/MistKit/CloudKitService/CloudKitService.swift +++ b/Sources/MistKit/CloudKitService/CloudKitService.swift @@ -54,6 +54,7 @@ internal import OpenAPIRuntime /// `fetchCaller` via web-auth from one fully-populated `Credentials`. public struct CloudKitService: Sendable { // swiftlint:disable force_unwrapping + // swift-format-ignore: NeverForceUnwrap /// The base URL for CloudKit Web Services. public static let baseURL = URL(string: "https://api.apple-cloudkit.com")! // swiftlint:enable force_unwrapping diff --git a/Sources/MistKit/Extensions/JSONDecoder+Shared.swift b/Sources/MistKit/Extensions/JSONDecoder+Shared.swift new file mode 100644 index 00000000..0f707985 --- /dev/null +++ b/Sources/MistKit/Extensions/JSONDecoder+Shared.swift @@ -0,0 +1,37 @@ +// +// JSONDecoder+Shared.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +extension JSONDecoder { + /// Shared default-configured decoder. `JSONDecoder.decode(_:from:)` is + /// documented thread-safe once configuration is finalized, so a single + /// instance is safe across concurrent decoders. + internal static let shared = JSONDecoder() +} diff --git a/Sources/MistKit/Extensions/JSONEncoder+Shared.swift b/Sources/MistKit/Extensions/JSONEncoder+Shared.swift new file mode 100644 index 00000000..a5855a34 --- /dev/null +++ b/Sources/MistKit/Extensions/JSONEncoder+Shared.swift @@ -0,0 +1,37 @@ +// +// JSONEncoder+Shared.swift +// MistKit +// +// Created by Leo Dion. +// Copyright © 2026 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +internal import Foundation + +extension JSONEncoder { + /// Shared default-configured encoder. `JSONEncoder.encode(_:)` is documented + /// thread-safe once configuration is finalized, so a single instance is + /// safe across concurrent encoders. + internal static let shared = JSONEncoder() +} diff --git a/Sources/MistKit/Models/RecordOperation+EncodedSize.swift b/Sources/MistKit/Models/RecordOperation+EncodedSize.swift index ddd0b80d..a30a6414 100644 --- a/Sources/MistKit/Models/RecordOperation+EncodedSize.swift +++ b/Sources/MistKit/Models/RecordOperation+EncodedSize.swift @@ -45,7 +45,9 @@ extension RecordOperation { /// ``CloudKitService/maxAssetUploadBytes``. public func encodedRecordSize() throws -> Int { let apiOperation = try Components.Schemas.RecordOperation(from: self) - guard let record = apiOperation.record else { return 0 } - return try JSONEncoder().encode(record).count + guard let record = apiOperation.record else { + return 0 + } + return try JSONEncoder.shared.encode(record).count } } diff --git a/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift index a08de99e..2d320cea 100644 --- a/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift +++ b/Tests/MistKitTests/CloudKitService/SizeLimits/CloudKitServiceTests.SizeLimits+Assets.swift @@ -99,7 +99,9 @@ extension CloudKitServiceTests.SizeLimits { using: Self.failingUploader(status: 413) ) } throws: { error in - guard let ckError = error as? CloudKitError else { return false } + guard let ckError = error as? CloudKitError else { + return false + } // No quota hint should be attached — the bare 413 propagates as-is. if case .quotaExceeded(_, let hint) = ckError, hint != nil { return false From 6f0b911b5fdd508ef15427bca44007a4e5275d4b Mon Sep 17 00:00:00 2001 From: Leo Dion Date: Tue, 19 May 2026 11:06:24 +0100 Subject: [PATCH 2/2] Fix subrepo parents after local rebase Co-Authored-By: Claude Opus 4.7 (1M context) --- Examples/BushelCloud/.gitrepo | 2 +- Examples/CelestraCloud/.gitrepo | 2 +- Packages/ConfigKeyKit/.gitrepo | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Examples/BushelCloud/.gitrepo b/Examples/BushelCloud/.gitrepo index 26600d51..cd66c7f0 100644 --- a/Examples/BushelCloud/.gitrepo +++ b/Examples/BushelCloud/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/BushelCloud.git branch = mistkit commit = 66b595eb2e9d3a12a385edaae4a0e549f9d48da5 - parent = c31250a988eede3e8523ac6b97096ec2c91e99b2 + parent = abff797b5cea271cf680f60e3e6d34ea359925d9 method = merge cmdver = 0.4.9 diff --git a/Examples/CelestraCloud/.gitrepo b/Examples/CelestraCloud/.gitrepo index e16d2783..43366061 100644 --- a/Examples/CelestraCloud/.gitrepo +++ b/Examples/CelestraCloud/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/CelestraCloud.git branch = mistkit commit = d91df88dcfe6b8c7cccd2d8257edb0472059ac2f - parent = 3e7a61518aaffa14c259c38087bf8ca75bf080cf + parent = abff797b5cea271cf680f60e3e6d34ea359925d9 method = merge cmdver = 0.4.9 diff --git a/Packages/ConfigKeyKit/.gitrepo b/Packages/ConfigKeyKit/.gitrepo index 0d4e3142..843af89e 100644 --- a/Packages/ConfigKeyKit/.gitrepo +++ b/Packages/ConfigKeyKit/.gitrepo @@ -7,6 +7,6 @@ remote = git@github.com:brightdigit/ConfigKeyKit.git branch = main commit = a9a8bc8be5b33d4aa732a9d0d06a05e8281b4855 - parent = 5d1a87aeaffbdfc883b2d13467503dda592d1ec0 + parent = abff797b5cea271cf680f60e3e6d34ea359925d9 method = merge cmdver = 0.4.9