diff --git a/.gitignore b/.gitignore index ad8ab2bc..f0722c0b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .DS_Store +/.claude/settings.local.json +/.vscode /.build /Packages /*.xcodeproj diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d22d0a61 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,51 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +SwiftkubeModel is a zero-dependency Swift package providing Codable, Hashable, Sendable model structs for all Kubernetes API objects. Currently tracks Kubernetes v1.33.3. + +## Build & Test Commands + +```bash +swift build # Build the package +swift test # Run all tests +swift test --filter SwiftkubeModelTests.GroupVersionKindTests # Run a single test class +swift format -i Sources/ Tests/ # Format code +``` + +## Architecture + +### Code Generation + +Most files under `Sources/Model/` are **generated** by an external tool called `Swiftkube:ModelGen` (not in this repo). Generated files have this header comment: +``` +// Generated by Swiftkube:ModelGen +// Kubernetes v1.33.3 +``` +Do not manually edit generated files unless you understand they will be overwritten on the next generation run. + +### Source Layout + +- **`Sources/Model///`** — Generated Kubernetes API structs, one file per type. Files follow the naming convention `TypeName+group.version.swift` (e.g., `Pod+core.v1.swift`). Each API group is a Swift namespace enum (e.g., `core.v1`, `apps.v1`, `networking.v1beta1`). + +- **`Sources/Model/`** (top-level files) — Core infrastructure: + - `KubernetesResource.swift` — Protocol hierarchy: `KubernetesResource` → `MetadataHavingResource` → `KubernetesAPIResource`, plus capability marker protocols (`NamespacedResource`, `ClusterScopedResource`, `ReadableResource`, `ListableResource`, `CreatableResource`, `DeletableResource`, etc.) + - `GroupVersionKind.swift` / `GroupVersionResource.swift` — GVK/GVR types and their lookup tables (`+DefaultResources`, `+KubernetesAPIResource`, `+Meta`, `+ResourceName`) + - `UnstructuredResource.swift` — Type-erased resource for unknown/CRD types + - `IntOrString.swift`, `Quantity.swift`, `JSONObject.swift` — Special Kubernetes types + +- **`Sources/Builders/`** — Closure-based builder functions under the `sk` namespace enum. Only covers common types (core/v1, apps/v1, meta/v1). + +- **`Sources/Codable/`** — Custom encoding/decoding support (`Any+Codable.swift`, `NullWrapper.swift`). + +- **`Sources/Extensions/`** — Convenience extensions on model types and `Hashes.swift` for Hashable conformance. + +### Key Patterns + +- All resources are `Codable`, `Hashable`, and `Sendable` structs. +- API resources use `var` properties (not `let`) for mutability. +- `apiVersion` and `kind` are `let` constants with default values on each API resource struct. +- Resources with `JSONObject` fields (dictionary-backed) store values as `Dictionary`. +- The `sk` enum provides builder functions that use `inout` closure patterns via the internal `build(_:with:)` helper. diff --git a/Package.swift b/Package.swift index 97028cc0..94699656 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version: 5.6 +// swift-tools-version: 5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -6,7 +6,7 @@ import PackageDescription let package = Package( name: "SwiftkubeModel", platforms: [ - .macOS(.v10_13), .iOS(.v12), .tvOS(.v12), .watchOS(.v5) + .macOS(.v13), .iOS(.v16), .tvOS(.v16), .watchOS(.v9) ], products: [ .library( diff --git a/Sources/Builders/Builders.swift b/Sources/Builders/Builders.swift index af08dc24..10c94e22 100644 --- a/Sources/Builders/Builders.swift +++ b/Sources/Builders/Builders.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - sk diff --git a/Sources/Builders/apps/v1/appsV1+Deployment.swift b/Sources/Builders/apps/v1/appsV1+Deployment.swift index a48d2f15..dca4516c 100644 --- a/Sources/Builders/apps/v1/appsV1+Deployment.swift +++ b/Sources/Builders/apps/v1/appsV1+Deployment.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+ConfigMap.swift b/Sources/Builders/core/v1/coreV1+ConfigMap.swift index e1c16c3c..b43d6597 100644 --- a/Sources/Builders/core/v1/coreV1+ConfigMap.swift +++ b/Sources/Builders/core/v1/coreV1+ConfigMap.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+Container.swift b/Sources/Builders/core/v1/coreV1+Container.swift index 1363b7a6..80521ded 100644 --- a/Sources/Builders/core/v1/coreV1+Container.swift +++ b/Sources/Builders/core/v1/coreV1+Container.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+Namespace.swift b/Sources/Builders/core/v1/coreV1+Namespace.swift index 96b1ca86..945ebc32 100644 --- a/Sources/Builders/core/v1/coreV1+Namespace.swift +++ b/Sources/Builders/core/v1/coreV1+Namespace.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+Node.swift b/Sources/Builders/core/v1/coreV1+Node.swift index bf5e2e15..15aa6356 100644 --- a/Sources/Builders/core/v1/coreV1+Node.swift +++ b/Sources/Builders/core/v1/coreV1+Node.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+ObjectReference.swift b/Sources/Builders/core/v1/coreV1+ObjectReference.swift index 5aaa5e59..42e12fe9 100644 --- a/Sources/Builders/core/v1/coreV1+ObjectReference.swift +++ b/Sources/Builders/core/v1/coreV1+ObjectReference.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+Pod.swift b/Sources/Builders/core/v1/coreV1+Pod.swift index f236b924..8439f5cf 100644 --- a/Sources/Builders/core/v1/coreV1+Pod.swift +++ b/Sources/Builders/core/v1/coreV1+Pod.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+Secret.swift b/Sources/Builders/core/v1/coreV1+Secret.swift index 7adc8ea0..e422b953 100644 --- a/Sources/Builders/core/v1/coreV1+Secret.swift +++ b/Sources/Builders/core/v1/coreV1+Secret.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+Service.swift b/Sources/Builders/core/v1/coreV1+Service.swift index 69d05d7c..3d9eca01 100644 --- a/Sources/Builders/core/v1/coreV1+Service.swift +++ b/Sources/Builders/core/v1/coreV1+Service.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+ServiceAccount.swift b/Sources/Builders/core/v1/coreV1+ServiceAccount.swift index ed852df2..98196c71 100644 --- a/Sources/Builders/core/v1/coreV1+ServiceAccount.swift +++ b/Sources/Builders/core/v1/coreV1+ServiceAccount.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/core/v1/coreV1+Volume.swift b/Sources/Builders/core/v1/coreV1+Volume.swift index 760ff273..ac3135ea 100644 --- a/Sources/Builders/core/v1/coreV1+Volume.swift +++ b/Sources/Builders/core/v1/coreV1+Volume.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/meta/v1/metaV1+Metadata.swift b/Sources/Builders/meta/v1/metaV1+Metadata.swift index 03c8b8a3..e611289e 100644 --- a/Sources/Builders/meta/v1/metaV1+Metadata.swift +++ b/Sources/Builders/meta/v1/metaV1+Metadata.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/meta/v1/metaV1+Selector.swift b/Sources/Builders/meta/v1/metaV1+Selector.swift index d652fc72..41ebf584 100644 --- a/Sources/Builders/meta/v1/metaV1+Selector.swift +++ b/Sources/Builders/meta/v1/metaV1+Selector.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Builders/meta/v1/metaV1+Status.swift b/Sources/Builders/meta/v1/metaV1+Status.swift index 3225bb13..42e90e6c 100644 --- a/Sources/Builders/meta/v1/metaV1+Status.swift +++ b/Sources/Builders/meta/v1/metaV1+Status.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension sk { diff --git a/Sources/Codable/Any+Codable.swift b/Sources/Codable/Any+Codable.swift index 2788781e..cf657675 100644 --- a/Sources/Codable/Any+Codable.swift +++ b/Sources/Codable/Any+Codable.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - JSONCodingKeys diff --git a/Sources/Extensions/CronJobExtensions+batch.v1.swift b/Sources/Extensions/CronJobExtensions+batch.v1.swift index e51c315e..e6a38255 100644 --- a/Sources/Extensions/CronJobExtensions+batch.v1.swift +++ b/Sources/Extensions/CronJobExtensions+batch.v1.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif public extension batch.v1.CronJob { func generateJob(withName name: String = "manual") throws -> batch.v1.Job { diff --git a/Sources/Extensions/Hashes.swift b/Sources/Extensions/Hashes.swift index bc44a57d..cc6af779 100644 --- a/Sources/Extensions/Hashes.swift +++ b/Sources/Extensions/Hashes.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif /// GenerateRandomHash returns a three character. I func GenerateRandomHash(length: Int) -> String { diff --git a/Sources/Model/GroupVersionKind.swift b/Sources/Model/GroupVersionKind.swift index 74edd016..c11132df 100644 --- a/Sources/Model/GroupVersionKind.swift +++ b/Sources/Model/GroupVersionKind.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - GroupVersionKind diff --git a/Sources/Model/GroupVersionResource.swift b/Sources/Model/GroupVersionResource.swift index 40756dcf..89b67e70 100644 --- a/Sources/Model/GroupVersionResource.swift +++ b/Sources/Model/GroupVersionResource.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - GroupVersionResource diff --git a/Sources/Model/IntOrString.swift b/Sources/Model/IntOrString.swift index 5b85a5fc..32faafc6 100644 --- a/Sources/Model/IntOrString.swift +++ b/Sources/Model/IntOrString.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - IntOrString diff --git a/Sources/Model/JSONObject.swift b/Sources/Model/JSONObject.swift index e4212f1d..6f7b8b16 100644 --- a/Sources/Model/JSONObject.swift +++ b/Sources/Model/JSONObject.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - JSONObject diff --git a/Sources/Model/KubernetesResource.swift b/Sources/Model/KubernetesResource.swift index a35f0396..7ca7c14c 100644 --- a/Sources/Model/KubernetesResource.swift +++ b/Sources/Model/KubernetesResource.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - KubernetesResource diff --git a/Sources/Model/Quantity.swift b/Sources/Model/Quantity.swift index 81e6a5f4..b2ce99e3 100644 --- a/Sources/Model/Quantity.swift +++ b/Sources/Model/Quantity.swift @@ -14,28 +14,11 @@ // limitations under the License. // -import Foundation - -// MARK: - Quantity - -extension String { - - subscript(offset: Int) -> Character { - self[index(startIndex, offsetBy: offset)] - } - - subscript(_ range: CountableRange) -> String { - let start = index(startIndex, offsetBy: max(0, range.lowerBound)) - let end = index(start, offsetBy: min(count - range.lowerBound, - range.upperBound - range.lowerBound)) - return String(self[start ..< end]) - } - - subscript(_ range: CountablePartialRangeFrom) -> String { - let start = index(startIndex, offsetBy: max(0, range.lowerBound)) - return String(self[start...]) - } -} +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - UnitType @@ -45,10 +28,19 @@ public enum UnitType: Sendable { } func pow(_ base: Int, _ exponent: Int) -> Decimal { + let baseDecimal = Decimal(base) if exponent < 0 { - return 1 / pow(Decimal(base), -1 * exponent) + var result = Decimal(1) + for _ in 0 ..< -exponent { + result /= baseDecimal + } + return result } else { - return pow(Decimal(base), exponent) + var result = Decimal(1) + for _ in 0 ..< exponent { + result *= baseDecimal + } + return result } } @@ -161,24 +153,21 @@ public struct Quantity: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, } mutating func parseData(str: String) throws -> Bool { - let range = NSRange(location: 0, length: str.count) - let regex = try! NSRegularExpression(pattern: "^\\d*\\.?\\d*e?\\d*", options: []) - let results = regex.matches(in: str, options: [], range: range) - if results.count != 1 { + let regex = #/^\d*\.?\d*e?\d*/# + guard let match = str.prefixMatch(of: regex) else { throw DecodingError.dataCorrupted(DecodingError.Context( codingPath: [], debugDescription: "Cannot decode value: " + str )) } - let r = results[0].range - guard let num = Decimal(string: str[r.location ..< (r.location + r.length)]) else { + guard let num = Decimal(string: String(match.output)) else { return false } if num < 0 { return false } - let unit = str[(r.location + r.length)...] + let unit = str[match.range.upperBound...] switch unit { case "Ki", "Mi", "Gi", "Ti", "Pi", "Ei": unitType = .binarySI @@ -190,7 +179,7 @@ public struct Quantity: ExpressibleByStringLiteral, ExpressibleByIntegerLiteral, debugDescription: "Cannot decode value: " + str )) } - self.unit = unit + self.unit = String(unit) decimalValue = num * getUnitMultiple() return true } @@ -283,8 +272,12 @@ public extension Quantity { continue } - let numInt = Int64(truncating: NSDecimalNumber(decimal: num)) - let vInt = Int64(truncating: NSDecimalNumber(decimal: dec.pair)) + guard let numInt = Int64(num.description), + let vInt = Int64(dec.pair.description) + else { + i += 1 + continue + } let remain = numInt % vInt if remain == 0 { i += 1 diff --git a/Sources/Model/UnstructuredResource.swift b/Sources/Model/UnstructuredResource.swift index 70855b8e..a4e7e753 100644 --- a/Sources/Model/UnstructuredResource.swift +++ b/Sources/Model/UnstructuredResource.swift @@ -14,7 +14,11 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif // MARK: - UnstructuredResource diff --git a/Sources/Model/UnstructuredResourceList.swift b/Sources/Model/UnstructuredResourceList.swift index 84413f5b..f493b32e 100644 --- a/Sources/Model/UnstructuredResourceList.swift +++ b/Sources/Model/UnstructuredResourceList.swift @@ -14,9 +14,13 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif -// MARK: - UnstructuredResource +// MARK: - UnstructuredResourceList /// /// UnstructuredResourceList is a collection of UnstructuredResources. diff --git a/Sources/SwiftkubeModel.swift b/Sources/SwiftkubeModel.swift index f9be5167..30465516 100644 --- a/Sources/SwiftkubeModel.swift +++ b/Sources/SwiftkubeModel.swift @@ -14,7 +14,13 @@ // limitations under the License. // -import Foundation +#if canImport(FoundationEssentials) + import FoundationEssentials +#else + import Foundation +#endif + +// MARK: - SwiftkubeModelError /// Represents SwiftkubeModel errors. public enum SwiftkubeModelError: Error {