use std::collections::BTreeMap;
use clap::Command;
use serde_json::{json, Value};
use crate::tree::Tree;
pub const COMMAND: &str = "porcelain-manifest";
pub fn command() -> Command {
Command::new(COMMAND)
.about("Print the hand-written commands as JSON (regenerates generated/porcelain.json)")
.hide(true)
}
pub fn manifest(cmd: &Command, tree: &Tree) -> Value {
let visible = |c: &Command| !c.is_hide_set();
let names = |c: &Command| -> Vec<String> {
let mut v: Vec<String> = c
.get_subcommands()
.filter(|s| visible(s))
.map(|s| s.get_name().to_string())
.collect();
v.sort();
v
};
fn nested(cmd: &Command, path: &str, out: &mut BTreeMap<String, Vec<String>>) {
let mut children: Vec<_> = cmd.get_subcommands().filter(|s| !s.is_hide_set()).collect();
children.sort_by_key(|s| s.get_name());
if !children.is_empty() {
out.insert(
path.to_string(),
children.iter().map(|s| s.get_name().to_string()).collect(),
);
for child in children {
nested(child, &format!("{path} {}", child.get_name()), out);
}
}
}
let mut out: BTreeMap<String, Vec<String>> = BTreeMap::new();
for sub in cmd.get_subcommands().filter(|s| visible(s)) {
let name = sub.get_name();
match tree.services.iter().find(|s| s.noun == name) {
None => {
out.insert(name.to_string(), names(sub));
nested(sub, name, &mut out);
}
Some(svc) => {
let extra: Vec<String> = names(sub)
.into_iter()
.filter(|n| {
!svc.collections.iter().any(|c| &c.noun == n)
&& !svc.commands.iter().any(|m| &m.verb == n)
&& (crate::HAND_WRITTEN.contains(&(name, n.as_str()))
|| !svc
.porcelain
.iter()
.any(|p| p.words().first() == Some(&n.as_str())))
})
.collect();
if !extra.is_empty() {
for child in sub
.get_subcommands()
.filter(|c| extra.iter().any(|n| n == c.get_name()))
{
nested(child, &format!("{name} {}", child.get_name()), &mut out);
}
out.insert(name.to_string(), extra);
}
}
}
}
out.entry("help".to_string()).or_default();
json!({
"generated": "@generated by `sylphx porcelain-manifest` (scripts/ci/regenerate.sh). Do not edit.",
"commands": out,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn tree() -> Tree {
crate::tree::load()
}
#[test]
fn lists_porcelain_and_service_porcelain_only() {
let t = tree();
let m = manifest(&crate::cli(&t), &t);
let c = &m["commands"];
assert!(c["login"].is_array());
assert_eq!(c["org"], json!(["use"]));
assert!(c["help"].is_array());
assert!(c["devices"].as_array().is_some_and(|d| !d.is_empty()));
assert_eq!(c["ai"], json!(["top"]));
assert_eq!(c["auth"], json!(["enable", "status"]));
assert!(c["access"].is_null());
assert!(c[COMMAND].is_null());
}
#[test]
fn a_new_hand_written_command_shows_up_without_any_list() {
let t = tree();
let svc = t
.services
.first()
.expect("a generated service")
.noun
.clone();
let cmd = crate::cli(&t)
.subcommand(Command::new("frobnicate"))
.mut_subcommand(svc.clone(), |s| {
s.subcommand(
Command::new("zap")
.subcommand(Command::new("nested").subcommand(Command::new("run"))),
)
});
let m = manifest(&cmd, &t);
assert_eq!(m["commands"]["frobnicate"], json!([]));
assert_eq!(m["commands"][format!("{svc} zap")], json!(["nested"]));
assert_eq!(m["commands"][format!("{svc} zap nested")], json!(["run"]));
assert!(m["commands"][&svc]
.as_array()
.is_some_and(|v| v.contains(&json!("zap"))));
}
#[test]
fn the_checked_in_manifest_is_fresh() {
let t = tree();
let want = serde_json::to_string_pretty(&manifest(&crate::cli(&t), &t)).unwrap() + "\n";
let have = include_str!("../generated/porcelain.json");
assert_eq!(
have, want,
"generated/porcelain.json is stale: regenerate it"
);
}
}