holger-cli/src/table.rs
//! **Every call the console makes, and the command that makes it here** (lane B3).
//!
//! The console (holger-ui) is a thin layer over holger's gRPC services; so is this client,
//! over the SAME services. Each row is one RPC (`Service.Method`, holger's proto names)
//! and the words that run it (`holger-cli <words…>`). `tests/every_ui_call_is_a_command.rs`
//! reads the console's source and fails when it calls an RPC that has no row, and the unit
//! test below fails when a row names a command the parser does not have.
/// `(rpc, command words)`.
pub const COMMANDS: &[(&str, &str)] = &[
// ── AdminService ────────────────────────────────────────────────────────
("AdminService.Health", "health"),
("AdminService.WhoAmI", "whoami"),
("AdminService.ListRepositories", "repos"),
("AdminService.ServerProfile", "profile"),
("AdminService.AdminSnapshot", "snapshot"),
("AdminService.Topology", "board"),
("AdminService.FlowSvg", "flow"),
("AdminService.WatchedApp", "app"),
("AdminService.SecuritySummary", "security"),
("AdminService.ImportBundle", "bundle import"),
("AdminService.ImportIntelBundle", "intel import"),
// ── RepositoryService ───────────────────────────────────────────────────
("RepositoryService.ListArtifacts", "artifacts list"),
("RepositoryService.FetchArtifact", "artifacts fetch"),
("RepositoryService.PutArtifactStream", "artifacts put"),
// ── ArchiveService ──────────────────────────────────────────────────────
("ArchiveService.ListArchiveFiles", "archive files"),
("ArchiveService.ArchiveInfo", "archive info"),
// ── SearchService ───────────────────────────────────────────────────────
("SearchService.Search", "search"),
// ── SbomService ─────────────────────────────────────────────────────────
("SbomService.AttachSbom", "sbom attach"),
("SbomService.FetchSbom", "sbom fetch"),
// ── PropertyService ─────────────────────────────────────────────────────
("PropertyService.GetProperties", "property get"),
("PropertyService.SetProperty", "property set"),
// ── PromotionService ────────────────────────────────────────────────────
("PromotionService.PromoteArtifact", "promote"),
("PromotionService.ListIntakeArtifacts", "intake list"),
("PromotionService.GetArtifactState", "intake state"),
("PromotionService.GetArtifactEvidence", "intake evidence"),
("PromotionService.AdvanceArtifact", "intake advance"),
("PromotionService.ListDependencySets", "intake sets"),
("PromotionService.GetDependencySet", "intake set"),
(
"PromotionService.ApproveDependencySet",
"intake set approve",
),
("PromotionService.RefuseDependencySet", "intake set refuse"),
("PromotionService.RequestWaiver", "intake waiver request"),
("PromotionService.ListWaivers", "intake waivers"),
("PromotionService.RevokeWaiver", "intake waiver revoke"),
// ── AuditService ────────────────────────────────────────────────────────
("AuditService.GetArtifactTrail", "audit trail"),
("AuditService.QueryDownloads", "audit downloads"),
("AuditService.QueryAudit", "audit query"),
("AuditService.AggregateAudit", "audit aggregate"),
("AuditService.ExportAudit", "audit export"),
("AuditService.VerifyAuditChain", "audit verify"),
// ── MigrationService ────────────────────────────────────────────────────
("MigrationService.Discover", "migration discover"),
("MigrationService.Start", "migration start"),
("MigrationService.Watch", "migration watch"),
("MigrationService.Cancel", "migration cancel"),
// ── ReplicationService ──────────────────────────────────────────────────
("ReplicationService.Replicate", "replicate"),
// ── JobService, AdmissionService ────────────────────────────────────────
("JobService.ListJobs", "jobs"),
("JobService.GetJob", "job"),
("AdmissionService.ListUsage", "admission usage"),
("AdmissionService.AdmitPlan", "admission plan"),
// ── UsageService ────────────────────────────────────────────────────────
("UsageService.ReadUsage", "usage"),
// ── RoomService (holger.room.v1) ────────────────────────────────────────
("RoomService.Room", "room show"),
("RoomService.PrivateEstate", "room private-estate"),
// ── AccountService (holger.account.v1) ──────────────────────────────────
("AccountService.MyRepositories", "account mine"),
("AccountService.CreateRepository", "account create"),
("AccountService.Grant", "account grant"),
// ── TokenService (holger.token.v1) ──────────────────────────────────────
("TokenService.Mint", "token mint"),
("TokenService.List", "token list"),
("TokenService.Revoke", "token revoke"),
// ── holger.doors.v1: the acts that had no door (B3, HOL-PRODUCTS-7) ─────
("BundleService.Export", "bundle export"),
(
"BundleService.AllowFirstImport",
"bundle allow-first-import",
),
("FirstKeyService.Invite", "first-key"),
("DownstreamService.Invite", "downstream invite"),
];
/// The commands that call no RPC: the credential store, on this machine.
pub const LOCAL: &[&str] = &[
"login",
"logout",
// The offline tools (HOL-PRODUCTS): the bundle's local verbs, the verifier, the courier.
"bundle prepare",
"bundle approve",
"bundle finalize",
"bundle inspect",
"bundle extract",
"verify",
"agent",
];
/// **The console's calls this client makes by another RPC of the same act**, `(the
/// console's rpc, this client's rpc)`: the console uploads with the unary `PutArtifact`,
/// `artifacts put` streams the file (`PutArtifactStream`, never whole in memory).
pub const SAME_ACT: &[(&str, &str)] = &[(
"RepositoryService.PutArtifact",
"RepositoryService.PutArtifactStream",
)];
/// Pure: the command words for `rpc`, if there is a row (or a row for the same act).
pub fn command_for(rpc: &str) -> Option<&'static str> {
let rpc = SAME_ACT
.iter()
.find(|(console, _)| *console == rpc)
.map_or(rpc, |(_, ours)| *ours);
COMMANDS.iter().find(|(r, _)| *r == rpc).map(|(_, c)| *c)
}
/// Pure: a console accessor (`grpc::admin()`) and its method (`admin_snapshot`) as the
/// RPC they call (`AdminService.AdminSnapshot`): the accessor names the service, the
/// method is the RPC in snake case.
pub fn rpc_of(accessor: &str, method: &str) -> Option<String> {
let service = match accessor {
"admin" => "AdminService",
"repositories" => "RepositoryService",
"archives" => "ArchiveService",
"search" => "SearchService",
"sboms" => "SbomService",
"properties" => "PropertyService",
"promotion" => "PromotionService",
"audit" => "AuditService",
"migration" => "MigrationService",
"replication" => "ReplicationService",
"jobs" => "JobService",
"admission" => "AdmissionService",
"usage" => "UsageService",
"room" => "RoomService",
"account" => "AccountService",
"tokens" => "TokenService",
_ => return None,
};
let camel: String = method
.split('_')
.filter(|p| !p.is_empty())
.map(|p| {
let mut c = p.chars();
c.next()
.map(|f| f.to_ascii_uppercase().to_string() + c.as_str())
.unwrap_or_default()
})
.collect();
// The two proto names whose case snake_case cannot round-trip.
let camel = match camel.as_str() {
"WhoAmI" | "WhoAmi" => "WhoAmI".to_owned(),
_ => camel,
};
Some(format!("{service}.{camel}"))
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory as _;
fn reaches(words: &str) -> bool {
let mut cmd = crate::cli::Cli::command();
for w in words.split(' ') {
match cmd.find_subcommand(w) {
Some(c) => cmd = c.clone(),
None => return false,
}
}
// A command that takes its own arguments OR a verb (`intake set <id>`, `intake set
// approve <id>`) is a command itself.
cmd.get_subcommands().next().is_none() || cmd.is_args_conflicts_with_subcommands_set()
}
#[test]
fn every_row_is_a_command_the_parser_has() {
for (rpc, words) in COMMANDS {
assert!(
reaches(words),
"{rpc}: `holger-cli {words}` is not a command"
);
}
for words in LOCAL {
assert!(reaches(words), "`holger-cli {words}` is not a command");
}
}
#[test]
fn every_command_the_parser_has_is_a_row() {
fn leaves(cmd: &clap::Command, prefix: &str, out: &mut Vec<String>) {
for sub in cmd.get_subcommands().filter(|s| s.get_name() != "help") {
let words = if prefix.is_empty() {
sub.get_name().to_owned()
} else {
format!("{prefix} {}", sub.get_name())
};
if sub.get_subcommands().next().is_none() {
out.push(words);
} else {
if sub.is_args_conflicts_with_subcommands_set() {
out.push(words.clone());
}
leaves(sub, &words, out);
}
}
}
let mut all = Vec::new();
leaves(&crate::cli::Cli::command(), "", &mut all);
for words in &all {
assert!(
COMMANDS.iter().any(|(_, c)| c == words) || LOCAL.contains(&words.as_str()),
"`holger-cli {words}` calls no listed RPC"
);
}
let mut seen = std::collections::BTreeSet::new();
for (rpc, words) in COMMANDS {
assert!(seen.insert(*words), "two rows for `{words}`");
assert!(rpc.contains('.'), "{rpc}");
}
}
#[test]
fn a_console_accessor_and_method_name_their_rpc() {
assert_eq!(
rpc_of("admin", "admin_snapshot").as_deref(),
Some("AdminService.AdminSnapshot")
);
assert_eq!(
rpc_of("admin", "who_am_i").as_deref(),
Some("AdminService.WhoAmI")
);
assert_eq!(
rpc_of("tokens", "mint").as_deref(),
Some("TokenService.Mint")
);
assert_eq!(
rpc_of("promotion", "list_intake_artifacts").as_deref(),
Some("PromotionService.ListIntakeArtifacts")
);
assert_eq!(rpc_of("nothing", "x"), None);
assert_eq!(command_for("SbomService.FetchSbom"), Some("sbom fetch"));
assert_eq!(
command_for("RepositoryService.PutArtifact"),
Some("artifacts put"),
"the console's unary upload is the streamed put"
);
}
}