Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

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"
        );
    }
}