diff --git a/Cargo.lock b/Cargo.lock index bd601c4412..4d0f9712ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,6 +347,14 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "autodoc" +version = "0.1.0" +dependencies = [ + "env_logger 0.11.5", + "log", +] + [[package]] name = "axum" version = "0.6.20" @@ -2185,6 +2193,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -2208,6 +2226,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime 2.1.0", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index f100358f0d..20f8d0a330 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ members = [ "common/wasm/utils", "common/wireguard", "common/wireguard-types", + "documentation/autodoc", "explorer-api", "explorer-api/explorer-api-requests", "explorer-api/explorer-client", diff --git a/documentation/autodoc/.gitignore b/documentation/autodoc/.gitignore new file mode 100644 index 0000000000..3b29b86939 --- /dev/null +++ b/documentation/autodoc/.gitignore @@ -0,0 +1 @@ +/autodoc-generated-markdown/* diff --git a/documentation/autodoc/Cargo.toml b/documentation/autodoc/Cargo.toml new file mode 100644 index 0000000000..4ff2ee8761 --- /dev/null +++ b/documentation/autodoc/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "autodoc" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +env_logger = "0.11.3" +log.workspace = true diff --git a/documentation/autodoc/README.md b/documentation/autodoc/README.md new file mode 100644 index 0000000000..5e8fd6f837 --- /dev/null +++ b/documentation/autodoc/README.md @@ -0,0 +1,4 @@ +# `autodoc` + +Command output documentation generator WIP + diff --git a/documentation/autodoc/src/main.rs b/documentation/autodoc/src/main.rs new file mode 100644 index 0000000000..58afdb768e --- /dev/null +++ b/documentation/autodoc/src/main.rs @@ -0,0 +1,261 @@ +use log::{debug, info}; +use std::fs::File; +use std::io::{self, Write}; +use std::process::{Command, Output}; +use std::{fs, vec}; + +const WRITE_PATH: &str = "./autodoc-generated-markdown/"; + +fn main() -> io::Result<()> { + env_logger::init(); + + // TODO if this balloons write automated way of grabbing commands from crates. + let commands_with_subcommands = vec![ + ( + "../../target/release/nym-api", + vec!["init", "run", "build-info"], + ), + ( + "../../target/release/nym-client", + vec![ + "init", + "run", + "import-credential", + "list-gateways", + "switch-gateway", + "build-info", + "completions", + "generate-fig-spec", + ], + ), + ( + "../../target/release/nym-socks5-client", + vec![ + "init", + "run", + "import-credential", + "list-gateways", + "add-gateway", + "build-info", + "completions", + "generate-fig-spec", + ], + ), + ( + "../../target/release/nym-node", + vec![ + "build-info", + "bonding-information", + "node-details", + "migrate", + "run", + "sign", + ], + ), + ( + "../../target/release/nymvisor", + vec![ + "init", + "run", + "build-info", + "daemon-build-info", + "add-upgrade", + "config", + ], + ), + ]; + + let commands_with_subsubcommands = vec![( + "../../target/release/nym-cli", + vec![ + ( + "account", + vec!["create", "balance", "pub-key", "send", "send-multiple"], + ), + ("signature", vec!["sign", "verify"]), + ( + "ecash", + vec![ + "issue-ticket-book", + "recover-ticket-book", + "import-ticket-book", + ], + ), + ( + "coconut", + vec![ + "generate-freepass", + "issue-credentials", + "recover-credentials", + "import-credential", + ], + ), + ("block", vec!["get", "time", "current-height"]), + ( + "cosmwasm", + vec![ + "upload", + "init", + "generate-init-message", + "migrate", + "execute", + ], + ), + ("tx", vec!["get", "query"]), + ( + "vesting-schedule", + vec!["create", "query", "vested-balance", "withdraw-vested"], + ), + ("mixnet", vec!["query", "delegators", "operators"]), + ("generate-fig", vec![""]), + ], + )]; + + for (main_command, subcommands) in commands_with_subcommands { + let last_word = get_last_word_from_filepath(main_command); + debug!("now running {last_word:#?}"); + + if !fs::metadata(WRITE_PATH) + .map(|metadata| metadata.is_dir()) + .unwrap_or(false) + { + fs::create_dir_all(WRITE_PATH)?; + } + + let mut file = File::create(format!("{}/{}-commands.md", WRITE_PATH, last_word.unwrap()))?; + writeln!( + file, + "# {} Binary Commands", + format!("`{}`", last_word.unwrap()) + )?; + writeln!( + file, + "\nThese docs are autogenerated by the `autodocs` script. + \n**TODO add link**" + )?; + let output = Command::new(main_command).arg("--help").output()?; + write_output_to_file(&mut file, output)?; + + for subcommand in subcommands { + execute_command(&mut file, main_command, subcommand, None)?; + } + } + + // nym-cli has subsubcommands so needs its own loop + for (main_command, subcommands) in &commands_with_subsubcommands { + let last_word = get_last_word_from_filepath(main_command); + debug!("now running {last_word:#?}"); + let mut file = File::create(format!("{}/{}-commands.md", WRITE_PATH, last_word.unwrap()))?; + writeln!( + file, + "# {} Binary Commands", + format!("`{}`", last_word.unwrap()) + )?; + writeln!( + file, + "\nThese docs are autogenerated by the `autodocs` script. + \n**TODO add link**" + )?; + let output = Command::new(main_command).arg("--help").output()?; + + write_output_to_file(&mut file, output)?; + + for (subcommand, subsubcommands) in subcommands { + writeln!(file, "\n### `{}` ", subcommand)?; + let output = Command::new(main_command) + .arg(subcommand) + .arg("--help") + .output()?; + if !output.stdout.is_empty() { + write_output_to_file(&mut file, output)?; + } else { + debug!("empty stdout - nothing to write"); + } + for subsubcommand in subsubcommands { + execute_command(&mut file, main_command, subcommand, Some(subsubcommand))?; + } + } + } + Ok(()) +} + +fn get_last_word_from_filepath(filepath: &str) -> Option<&str> { + let parts: Vec<&str> = filepath.split('/').collect(); + parts.last().copied() +} + +fn execute_command( + file: &mut File, + main_command: &str, + subcommand: &str, + subsubcommand: Option<&str>, +) -> io::Result<()> { + // checking for the nym-cli subsubcommands + if subsubcommand.is_some() { + writeln!(file, "\n### `{} {}`", subcommand, subsubcommand.unwrap())?; + + info!("executing {} {} --help ", main_command, subcommand); + let output = Command::new(main_command) + .arg(subcommand) + .arg(subsubcommand.unwrap()) + .arg("--help") + .output()?; + if !output.stdout.is_empty() { + write_output_to_file(file, output)?; + } else { + debug!("empty stdout - nothing to write"); + } + // just subcommands + } else { + writeln!(file, "\n### `{}`", subcommand)?; + + // execute help + let output = Command::new(main_command) + .arg(subcommand) + .arg("--help") + .output()?; + if !output.stdout.is_empty() { + write_output_to_file(file, output)?; + } else { + debug!("empty stdout - nothing to write"); + } + + // then execute w/out help: the majority of functions will fail since you're not passing + // required params but thats fine as we can just not render stderr into the final file. + // + // this check is basically checking for the rare commands (rn just one) that start a process with no params + // perhaps if this list grows we could just add a timeout and shunt the running and writing + // into a thread with a timeout or something but for right now its fine / thats overkill + if get_last_word_from_filepath(main_command).unwrap() == "nym-node" + || get_last_word_from_filepath(main_command).unwrap() == "nym-api" + || get_last_word_from_filepath(main_command).unwrap() == "nymvisor" + && subcommand == "run" + { + info!("SKIPPING {} {}", main_command, subcommand); + } else { + info!("executing {} {}", main_command, subcommand); + let output = Command::new(main_command).arg(subcommand).output()?; + if !output.stdout.is_empty() { + writeln!(file, "Example output:")?; + write_output_to_file(file, output)?; + } else { + debug!("empty stdout - nothing to write"); + if !&output.stderr.is_empty() { + debug!("stderr: {:#?}", String::from_utf8_lossy(&output.stderr)); + } + } + } + } + Ok(()) +} + +fn write_output_to_file(file: &mut File, output: Output) -> io::Result<()> { + writeln!(file, "```")?; + file.write_all(&output.stdout)?; + writeln!(file, "```")?; + + if !&output.stderr.is_empty() { + debug!("stderr: {:#?}", String::from_utf8_lossy(&output.stderr)); + } + Ok(()) +}