Files
solidcraft/xtask/src/features.rs
T

123 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `cargo xtask features`: SolidCraft's own feature checklist, `docs/features.md`.
//!
//! The page lists every registered command by area (the first part of its dotted id), with its
//! label, id and toolbar place. It comes only from SolidCraft's command registry
//! (`solidcraft-cli commands`), so it can't drift from what the program does; it compares with
//! nothing else. Planned commands (registered, not built yet) are listed as planned, with their
//! issue. Counts are omitted so unrelated command additions do not edit shared headers.
use std::path::Path;
use serde_json::Value;
/// Areas in the order the page lists them (id prefix, heading). Other prefixes follow,
/// alphabetically, under their prefix.
const AREAS: &[(&str, &str)] = &[
("sketch", "Sketch"),
("solid", "Solid modelling"),
("surface", "Surfaces"),
("construct", "Construction geometry"),
("sheet", "Sheet metal"),
("plastic", "Plastic parts"),
("component", "Components"),
("joint", "Assembly joints"),
("config", "Configurations"),
("timeline", "Timeline"),
("inspect", "Inspect"),
("file", "Files"),
("doc", "Documents"),
("edit", "Edit"),
];
pub fn run(root: &Path) -> Result<(), String> {
let out = crate::cargo()
.args(["run", "-q", "--release", "-p", "solidcraft-cli", "--", "commands"])
.output()
.map_err(|e| format!("solidcraft-cli: {e}"))?;
if !out.status.success() {
return Err(format!("solidcraft-cli commands failed:\n{}", String::from_utf8_lossy(&out.stderr)));
}
let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?;
let list = cmds.as_array().map(Vec::as_slice).unwrap_or_default();
let md = page(list);
std::fs::write(root.join("docs/features.md"), md).map_err(|e| format!("docs/features.md: {e}"))?;
println!("features: {} commands → docs/features.md", list.len());
Ok(())
}
fn area(id: &str) -> &str {
id.split('.').next().unwrap_or_default()
}
/// The checklist page for the registry's commands.
pub fn page(cmds: &[Value]) -> String {
let s = |c: &Value, k: &str| c[k].as_str().unwrap_or_default().to_string();
let mut prefixes: Vec<String> = cmds.iter().map(|c| area(&s(c, "id")).to_string()).collect();
prefixes.sort();
prefixes.dedup();
let mut order: Vec<(String, String)> =
AREAS.iter().filter(|(p, _)| prefixes.iter().any(|x| x == p)).map(|(p, h)| (p.to_string(), h.to_string())).collect();
for p in prefixes.iter().filter(|p| !AREAS.iter().any(|(a, _)| a == p)) {
order.push((p.clone(), format!("`{p}.*`")));
}
let planned = |c: &Value| c["planned"].as_str().map(str::to_string);
// Counts made every command addition edit the same header, even across unrelated areas.
// Keep generated text local to each command so parallel branches merge cleanly more often.
let mut md = String::from(
"# Features\n\nSolidCraft's commands by area, generated by `cargo xtask features` from the command registry \
(`solidcraft-cli commands`). Planned commands are marked in their rows. Every command runs the same way from the toolbar, a script, \
`solidcraft-cli exec <id> '<json>'`, the control channel and MCP; parameters are in the \
[command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).\n",
);
for (prefix, heading) in &order {
let mut in_area: Vec<&Value> = cmds.iter().filter(|c| area(&s(c, "id")) == prefix).collect();
in_area.sort_by_key(|c| s(c, "id"));
md += &format!("\n## {heading}\n\n| Command | Id | Toolbar |\n|---|---|---|\n");
for c in in_area {
let place = match (s(c, "tab"), s(c, "panel")) {
(t, _) if t.is_empty() => "–".to_string(),
(t, p) if p.is_empty() => t,
(t, p) => format!("{t} › {p}"),
};
let key = c["shortcut"].as_str().map(|k| format!(" ({k})")).unwrap_or_default();
let note = planned(c).map(|i| format!(" *(planned: [#{i}](https://github.com/bherbruck/solidcraft/issues/{i}))*")).unwrap_or_default();
md += &format!("| {}{key}{note} | `{}` | {place} |\n", s(c, "label").replace('|', "/"), s(c, "id"));
}
}
md
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn page_groups_by_area() {
let cmds: Value = serde_json::from_str(
r#"[{"id":"solid.extrude","label":"Extrude","tab":"SOLID","panel":"CREATE","shortcut":"E"},
{"id":"sketch.line","label":"Line","tab":"SKETCH","panel":"CREATE"},
{"id":"view.fit","label":"Fit","tab":"","panel":""},
{"id":"solid.knit","label":"Knit","tab":"SOLID","panel":"CREATE","planned":"9"}]"#,
)
.unwrap();
let md = page(cmds.as_array().unwrap());
assert!(md.contains("Planned commands are marked in their rows."));
let (sk, so, other) = (md.find("## Sketch").unwrap(), md.find("## Solid modelling").unwrap(), md.find("## `view.*`").unwrap());
assert!(sk < so && so < other);
assert!(md.contains("| Extrude (E) | `solid.extrude` | SOLID › CREATE |"));
assert!(md.contains("| Fit | `view.fit` | – |"));
assert!(md.contains("| Knit *(planned: [#9](https://github.com/bherbruck/solidcraft/issues/9))* | `solid.knit` | SOLID › CREATE |"));
}
#[test]
fn unrelated_additions_do_not_rewrite_existing_lines_and_order_is_stable() {
let commands = vec![serde_json::json!({"id":"view.zoom","label":"Zoom"}), serde_json::json!({"id":"view.fit","label":"Fit"})];
let before = page(&commands);
let mut reordered = commands.clone();
reordered.reverse();
assert_eq!(before, page(&reordered));
reordered.push(serde_json::json!({"id":"view.pan","label":"Pan"}));
let after = page(&reordered);
assert_eq!(before, after.lines().filter(|l| !l.contains("`view.pan`")).collect::<Vec<_>>().join("\n") + "\n");
}
}