Confidently cover all static members. Like CaseIterable, this macro creates an array of all the static members of a type. This is useful when a type has a few examples as static members.
Here, we have a Chili type so that we can discuss the heat level and names of various chilis. So far, we have two: jalapeño and habenero. For testing or for displaying in the UI, we want to confidently list both the jalapeño & the habenero. With @StaticMemberIterable, we can finally do so.
@StaticMemberIterable
struct Chili {
let name: String
let heatLevel: Int
static let jalapeño = Chili(name: "jalapeño", heatLevel: 2)
static let habenero = Chili(name: "habenero", heatLevel: 5)
}
expands to
struct Chili {
let name: String
let heatLevel: Int
static let jalapeño = Chili(name: "jalapeño", heatLevel: 2)
static let habenero = Chili(name: "habenero", heatLevel: 5)
static let allStaticMembers = [jalapeño, habenero]
}
Installation
In Package.swift, add the package to your dependencies.
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.
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.