A real-life example of StaticMemberIterable

Now that Swift 5.9 is out, version 1.0.0 of StaticMemberIterable has been tagged! And here’s an example from some code I’m working on today.

A little background. 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 modeled my three terraform directories with an enum, since I prefer using first-party tools when possible. It implements Swift Argument Parser’s ExpressibleByArgument so that it can be parsed from text on the command line.

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 ArgumentParser
import Foundation
import Rainbow
import Sh
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
  }
}

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/DanielSincere/Sh.git", from: "1.3.0"),
    .package(url: "https://github.com/DanielSincere/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"),
      ]),
  ]
)

Thanks!

Thanks for reading! StaticMemberIterable is available for free on GitHub and requires Swift 5.9. If you like the macro, please sponsor me on GitHub.