Now that Swift 5.9 is out, version 1.0.0 has been tagged!
.package(url: "https://github.com/FullQueueDeveloper/StaticMemberIterable.git",
from: "1.0.0"),
A real-life example of StaticMemberIterable
And here's another example from some code I'm working on today. With StaticMemberIterable, I'm not limited to one "rawValue" for the enum. I can add a "short name" to also match on, when conforming to ExpressibleByArgument
I have three infrastructure-as-code directories, and I'm running Terraform through SPX to take advantage of op run to render secrets to my scripts.
If Swift Argument Parser supported short names for commands, this wouldn't be necessary. Regardless, here we are, with a real-life use of StaticMemberIterable.
First, with an enum
I first implemented this with an enum, since I prefer using first-party tools when possible.
enum TerraformDir_EnumStyle: String, ExpressibleByArgument, CaseIterable {
case cloudflare
case digitalocean
case kubernetes
var defaultValueDescription: String {
"\(self.rawValue.yellow) or \(self.short.yellow) for short"
}
var short: String {
switch self {
case .cloudflare: "cf"
case .digitalocean: "do"
case .kubernetes: "k8s"
}
}
init?(rawValue: String) {
guard let found = Self.allCases.first(where: {
$0.rawValue == rawValue || $0.short == rawValue
}) else {
return nil
}
self = found
}
}
Second, with StaticMemberIterable
The implementation of var short: String really bugged me. The short name should be close to the raw-value name of the enum. I couldn't override the raw value though, since the long name is the actual name of the directory (and also a valid value). Then I remembered I wrote something to help with this, StaticMemberIterable. I think this reads much nicer than the enum, since the long name and short name are right next to each other.
@StaticMemberIterable
struct TerraformDir: ExpressibleByArgument {
static let cloudflare = Self(name: "cloudflare", shortName: "cf")
static let digitalocean = Self(name: "digitalocean", shortName: "do")
static let kubernetes = Self(name: "kubernetes", shortName: "k8s")
let name: String
let shortName: String
init(name: String, shortName: String) {
self.name = name
self.shortName = shortName
}
var defaultValueDescription: String {
"\(self.name.yellow) or \(self.shortName.yellow) for short"
}
init?(argument: String) {
let maybe = Self.allStaticMembers.first { $0.name == argument || $0.shortName == argument }
guard let found = maybe else {
return nil
}
self = found
}
}
Finally, the full example
Here's the full example for context. It's less than 100 lines.
import Sh
import Foundation
import ArgumentParser
import Rainbow
import StaticMemberIterable
@main
struct Terraform: ParsableCommand {
@Argument(help: "Which terraform dir to run. Options are \(TerraformDir.allStaticMembers.map({ $0.defaultValueDescription }).joined(separator: ", "))")
var dir: TerraformDir
@Argument(parsing: .allUnrecognized, help: "Arguments passed along to terraform")
var terraformArguments: [String] = []
mutating func run() throws {
try sh(.terminal,
"op run --env-file op.env -- terraform \(terraformArguments.joined(separator: " "))",
workingDirectory: dir.name
)
}
}
@StaticMemberIterable
struct TerraformDir: ExpressibleByArgument {
static let cloudflare = Self(name: "cloudflare", shortName: "cf")
static let digitalocean = Self(name: "digitalocean", shortName: "do")
static let kubernetes = Self(name: "kubernetes", shortName: "k8s")
let name: String
let shortName: String
init(name: String, shortName: String) {
self.name = name
self.shortName = shortName
}
var defaultValueDescription: String {
"\(self.name.yellow) or \(self.shortName.yellow) for short"
}
init?(argument: String) {
let maybe = Self.allStaticMembers.first { $0.name == argument || $0.shortName == argument }
guard let found = maybe else {
return nil
}
self = found
}
}
enum TerraformDir_EnumStyle: String, ExpressibleByArgument, CaseIterable {
case cloudflare
case digitalocean
case kubernetes
var defaultValueDescription: String {
"\(self.rawValue.yellow) or \(self.short.yellow) for short"
}
var short: String {
switch self {
case .cloudflare: "cf"
case .digitalocean: "do"
case .kubernetes: "k8s"
}
}
init?(rawValue: String) {
guard let found = Self.allCases.first(where: {
$0.rawValue == rawValue || $0.short == rawValue
}) else {
return nil
}
self = found
}
}
And here is the Package.swift if you're interested.
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "Terraform-SPX-Scripts",
platforms: [
.macOS(.v13),
],
dependencies: [
.package(url: "https://github.com/FullQueueDeveloper/Sh.git", from: "1.3.0"),
.package(url: "https://github.com/FullQueueDeveloper/StaticMemberIterable.git", from: "1.0.0"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.2.0"),
],
targets: [
.executableTarget(
name: "tf",
dependencies: [
"Sh",
"StaticMemberIterable",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
]),
]
)
Hashtag soup: #1password #SPX #terraform swiftpm #sh ArgumentParser