diff --git a/Cargo.lock b/Cargo.lock index 70987333..1a6a1669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4662,6 +4662,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "subtle" version = "2.6.1" @@ -4775,6 +4796,7 @@ dependencies = [ "rustify_derive", "serde", "serde_json", + "strum", "thiserror 2.0.18", "vaultrs", ] diff --git a/Cargo.toml b/Cargo.toml index 82bff01f..a4d1206f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,6 +113,10 @@ schemars = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1" similar = "2" +# `derive`-only: `Kind`'s segment strings via `#[strum(serialize = "...")]` +# instead of a hand-written match, so the derive macro is the single source +# of truth (see `swarm-secret-client::path::Kind`). +strum = { version = "0.28.0", features = ["derive"] } tokio = { version = "1", features = [ "fs", "io-util", diff --git a/swarm-secret-client/Cargo.toml b/swarm-secret-client/Cargo.toml index bf65953d..eed8fe9e 100644 --- a/swarm-secret-client/Cargo.toml +++ b/swarm-secret-client/Cargo.toml @@ -13,6 +13,7 @@ reqwest.workspace = true rustify.workspace = true rustify_derive.workspace = true serde.workspace = true +strum.workspace = true thiserror.workspace = true vaultrs.workspace = true diff --git a/swarm-secret-client/src/path.rs b/swarm-secret-client/src/path.rs index 5efa7f8c..528e099a 100644 --- a/swarm-secret-client/src/path.rs +++ b/swarm-secret-client/src/path.rs @@ -25,19 +25,28 @@ pub const ROOT: &str = "swarm"; /// misspelled kind is a 403 at provision time rather than anything a compiler /// sees. [`Kind::ALL`] exists so a test can enumerate the set instead of /// restating it. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// +/// The segment string lives once, on the variant itself +/// (`#[strum(serialize = "...")]`), rather than a second time in a +/// hand-written `as_str()` match — [`strum::IntoStaticStr`] derives that +/// conversion, so the attribute is the only place a segment is spelled. +#[derive(Debug, Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)] pub enum Kind { /// One agent container's own secrets. + #[strum(serialize = "agents")] Agent, /// One hive's secrets, held on behalf of whatever runs there. An identity /// minted per hive rather than per agent lands here even though an agent /// is what uses it. + #[strum(serialize = "hives")] Hive, /// One swarm service — the things a swarm runs beside the controller. + #[strum(serialize = "services")] Service, /// The controller itself. There is one per swarm, so the name segment /// below it does not vary; the shape stays uniform anyway, because one /// grant pattern over `/` is cheaper than a special case. + #[strum(serialize = "controller")] Controller, } @@ -47,15 +56,11 @@ impl Kind { pub const ALL: [Kind; 4] = [Kind::Agent, Kind::Hive, Kind::Service, Kind::Controller]; /// The path segment, which is also what the store's grant is written - /// against. + /// against. Thin wrapper over the derived `Into<&'static str>` so call + /// sites keep the same method-call shape as before. #[must_use] - pub const fn as_str(self) -> &'static str { - match self { - Kind::Agent => "agents", - Kind::Hive => "hives", - Kind::Service => "services", - Kind::Controller => "controller", - } + pub fn as_str(self) -> &'static str { + self.into() } /// What to call the name in an error — singular, because the message reads