feat: Improve admin command reference generation
- Change xtasks to use `clap` for argument parsing - Generate admin command reference manually instead of with `clap_markdown` - Split admin command reference into multiple files
This commit is contained in:
parent
60dd6baffd
commit
89be9d1efc
31 changed files with 1297 additions and 5822 deletions
|
|
@ -10,10 +10,12 @@ repository.workspace = true
|
|||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
conduwuit-admin.workspace = true
|
||||
conduwuit.workspace = true
|
||||
clap.workspace = true
|
||||
# Required for working with JSON output from cargo metadata
|
||||
serde.workspace = true
|
||||
serde_json = "1.0"
|
||||
|
||||
askama = "0.15.1"
|
||||
cargo_metadata = "0.23.1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
[package]
|
||||
name = "xtask-generate-commands"
|
||||
authors.workspace = true
|
||||
description.workspace = true
|
||||
edition.workspace = true
|
||||
homepage.workspace = true
|
||||
license.workspace = true
|
||||
readme.workspace = true
|
||||
repository.workspace = true
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
clap-markdown = "0.1.5"
|
||||
clap_builder = { version = "4.5.38", default-features = false }
|
||||
clap_mangen = "0.2"
|
||||
|
||||
conduwuit-admin.workspace = true
|
||||
|
||||
# Hack to prevent rebuilds
|
||||
conduwuit.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
@ -1,113 +0,0 @@
|
|||
use std::{
|
||||
fs::{self, File},
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use clap_builder::{Command, CommandFactory};
|
||||
use conduwuit_admin::AdminCommand;
|
||||
|
||||
enum CommandType {
|
||||
Admin,
|
||||
Server,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut args = std::env::args().skip(1);
|
||||
let command_type = args.next();
|
||||
let task = args.next();
|
||||
|
||||
match (command_type, task) {
|
||||
| (None, _) => {
|
||||
return Err("Missing command type (admin or server)".into());
|
||||
},
|
||||
| (Some(cmd_type), None) => {
|
||||
return Err(format!("Missing task for {cmd_type} command").into());
|
||||
},
|
||||
| (Some(cmd_type), Some(task)) => {
|
||||
let command_type = match cmd_type.as_str() {
|
||||
| "admin" => CommandType::Admin,
|
||||
| "server" => CommandType::Server,
|
||||
| _ => return Err(format!("Invalid command type: {cmd_type}").into()),
|
||||
};
|
||||
|
||||
match task.as_str() {
|
||||
| "man" => match command_type {
|
||||
| CommandType::Admin => {
|
||||
let dir = Path::new("./admin-man");
|
||||
gen_admin_manpages(dir)?;
|
||||
},
|
||||
| CommandType::Server => {
|
||||
let dir = Path::new("./server-man");
|
||||
gen_server_manpages(dir)?;
|
||||
},
|
||||
},
|
||||
| "md" => {
|
||||
match command_type {
|
||||
| CommandType::Admin => {
|
||||
let command = AdminCommand::command().name("admin");
|
||||
|
||||
let res = clap_markdown::help_markdown_command_custom(
|
||||
&command,
|
||||
&clap_markdown::MarkdownOptions::default().show_footer(false),
|
||||
)
|
||||
.replace("\n\r", "\n")
|
||||
.replace("\r\n", "\n")
|
||||
.replace(" \n", "\n");
|
||||
|
||||
let mut file = File::create(Path::new("./docs/admin_reference.md"))?;
|
||||
file.write_all(res.trim_end().as_bytes())?;
|
||||
file.write_all(b"\n")?;
|
||||
},
|
||||
| CommandType::Server => {
|
||||
// Get the server command from the conduwuit crate
|
||||
let command = conduwuit::Args::command();
|
||||
|
||||
let res = clap_markdown::help_markdown_command_custom(
|
||||
&command,
|
||||
&clap_markdown::MarkdownOptions::default().show_footer(false),
|
||||
)
|
||||
.replace("\n\r", "\n")
|
||||
.replace("\r\n", "\n")
|
||||
.replace(" \n", "\n");
|
||||
|
||||
let mut file = File::create(Path::new("./docs/server_reference.md"))?;
|
||||
file.write_all(res.trim_end().as_bytes())?;
|
||||
file.write_all(b"\n")?;
|
||||
},
|
||||
}
|
||||
},
|
||||
| invalid => return Err(format!("Invalid task name: {invalid}").into()),
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn gen_manpage_common(dir: &Path, c: &Command, prefix: Option<&str>) -> Result<(), io::Error> {
|
||||
fs::create_dir_all(dir)?;
|
||||
let sub_name = c.get_display_name().unwrap_or_else(|| c.get_name());
|
||||
let name = if let Some(prefix) = prefix {
|
||||
format!("{prefix}-{sub_name}")
|
||||
} else {
|
||||
sub_name.to_owned()
|
||||
};
|
||||
|
||||
let mut out = File::create(dir.join(format!("{name}.1")))?;
|
||||
let clap_mangen = clap_mangen::Man::new(c.to_owned().disable_help_flag(true));
|
||||
clap_mangen.render(&mut out)?;
|
||||
|
||||
for sub in c.get_subcommands() {
|
||||
gen_manpage_common(&dir.join(sub_name), sub, Some(&name))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn gen_admin_manpages(dir: &Path) -> Result<(), io::Error> {
|
||||
gen_manpage_common(dir, &AdminCommand::command().name("admin"), None)
|
||||
}
|
||||
|
||||
fn gen_server_manpages(dir: &Path) -> Result<(), io::Error> {
|
||||
gen_manpage_common(dir, &conduwuit::Args::command(), None)
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
use std::{env, process::Command};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut child = Command::new("cargo").args(["run", "--package", "xtask-generate-commands", "--"].into_iter().map(ToOwned::to_owned).chain(env::args().skip(2)))
|
||||
// .stdout(Stdio::piped())
|
||||
// .stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("failed to execute child");
|
||||
child.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
26
xtask/src/main.rs
Normal file
26
xtask/src/main.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
mod tasks;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use crate::tasks::Task;
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
struct BaseArgs {
|
||||
#[command(subcommand)]
|
||||
task: Task,
|
||||
#[command(flatten)]
|
||||
args: Args,
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
struct Args {
|
||||
/// Simulate without actually touching the filesystem
|
||||
#[arg(long)]
|
||||
dry_run: bool,
|
||||
}
|
||||
|
||||
fn main() -> impl std::process::Termination {
|
||||
let BaseArgs { task, args } = BaseArgs::parse();
|
||||
|
||||
task.invoke(args)
|
||||
}
|
||||
112
xtask/src/tasks/generate_docs/admin_commands.rs
Normal file
112
xtask/src/tasks/generate_docs/admin_commands.rs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
//! Generates documentation for the various commands that may be used in the admin room and server console.
|
||||
//!
|
||||
//! This generates one index page and several category pages, one for each of the direct subcommands of the top-level
|
||||
//! `!admin` command. Those category pages then list all of the sub-subcommands.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use askama::Template;
|
||||
use clap::{Command, CommandFactory};
|
||||
use conduwuit_admin::AdminCommand;
|
||||
|
||||
use crate::tasks::{TaskResult, generate_docs::FileOutput};
|
||||
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "admin/index.md")]
|
||||
/// The template for the index page, which links to all of the category pages.
|
||||
struct Index {
|
||||
categories: Vec<Category>
|
||||
}
|
||||
|
||||
/// A direct subcommand of the top-level `!admin` command.
|
||||
#[derive(askama::Template)]
|
||||
#[template(path = "admin/category.md")]
|
||||
struct Category {
|
||||
name: String,
|
||||
description: String,
|
||||
commands: Vec<Subcommand>,
|
||||
}
|
||||
|
||||
/// A second-or-deeper level subcommand of the `!admin` command.
|
||||
struct Subcommand {
|
||||
name: String,
|
||||
description: String,
|
||||
/// How deeply nested this command was in the original command tree.
|
||||
/// This determines the header size used for it in the documentation.
|
||||
depth: usize,
|
||||
}
|
||||
|
||||
|
||||
fn flatten_subcommands(command: &Command) -> Vec<Subcommand> {
|
||||
let mut subcommands = Vec::new();
|
||||
let mut name_stack = Vec::new();
|
||||
|
||||
fn flatten(
|
||||
subcommands: &mut Vec<Subcommand>,
|
||||
stack: &mut Vec<String>,
|
||||
command: &Command
|
||||
) {
|
||||
let depth = stack.len();
|
||||
stack.push(command.get_name().to_owned());
|
||||
|
||||
// do not include the root command
|
||||
if depth > 0 {
|
||||
let name = stack.join(" ");
|
||||
|
||||
let description = command
|
||||
.get_long_about()
|
||||
.or_else(|| command.get_about())
|
||||
.map(|about| about.to_string())
|
||||
.unwrap_or("_(no description)_".to_owned());
|
||||
|
||||
subcommands.push(
|
||||
Subcommand {
|
||||
name,
|
||||
description,
|
||||
depth,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for command in command.get_subcommands() {
|
||||
flatten(subcommands, stack, command);
|
||||
}
|
||||
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
flatten(&mut subcommands, &mut name_stack, command);
|
||||
|
||||
subcommands
|
||||
}
|
||||
|
||||
pub(super) fn generate(out: &mut impl FileOutput) -> TaskResult<()> {
|
||||
let admin_commands = AdminCommand::command();
|
||||
|
||||
let categories: Vec<_> = admin_commands
|
||||
.get_subcommands()
|
||||
.map(|command| {
|
||||
Category {
|
||||
name: command.get_name().to_owned(),
|
||||
description: command.get_about().expect("categories should have a docstring").to_string(),
|
||||
commands: flatten_subcommands(command),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let root = Path::new("reference/admin/");
|
||||
|
||||
for category in &categories {
|
||||
out.create_file(
|
||||
root.join(&category.name).with_extension("md"),
|
||||
category.render()?
|
||||
);
|
||||
}
|
||||
|
||||
out.create_file(
|
||||
root.join("index.md"),
|
||||
Index { categories }.render()?,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
67
xtask/src/tasks/generate_docs/mod.rs
Normal file
67
xtask/src/tasks/generate_docs/mod.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
mod admin_commands;
|
||||
|
||||
use std::{collections::HashMap, path::{Path, PathBuf}};
|
||||
|
||||
use cargo_metadata::MetadataCommand;
|
||||
|
||||
use crate::tasks::TaskResult;
|
||||
|
||||
trait FileOutput {
|
||||
fn create_file(&mut self, path: PathBuf, contents: String);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FileQueue {
|
||||
queue: HashMap<PathBuf, String>,
|
||||
}
|
||||
|
||||
impl FileQueue {
|
||||
fn write(self, root: &Path, dry_run: bool) -> std::io::Result<()> {
|
||||
for (path, contents) in self.queue.into_iter() {
|
||||
let path = root.join(&path);
|
||||
|
||||
eprintln!("Writing {}", path.display());
|
||||
if !dry_run {
|
||||
std::fs::write(path, contents)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FileOutput for FileQueue {
|
||||
fn create_file(&mut self, path: PathBuf, contents: String) {
|
||||
assert!(path.is_relative(), "path must be relative");
|
||||
assert!(path.extension().is_some(), "path must not point to a directory");
|
||||
|
||||
if self.queue.contains_key(&path) {
|
||||
panic!("attempted to create an already created file {}", path.display());
|
||||
}
|
||||
|
||||
self.queue.insert(path, contents);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Args)]
|
||||
pub(crate) struct Args {
|
||||
/// The base path of the documentation. Defaults to `docs/` in the crate root.
|
||||
root: Option<PathBuf>,
|
||||
}
|
||||
|
||||
pub(super) fn run(common_args: crate::Args, task_args: Args) -> TaskResult<()> {
|
||||
let mut queue = FileQueue::default();
|
||||
|
||||
let metadata = MetadataCommand::new()
|
||||
.no_deps()
|
||||
.exec()
|
||||
.expect("should have been able to run cargo");
|
||||
|
||||
let root = task_args.root.unwrap_or_else(|| metadata.workspace_root.join_os("docs/"));
|
||||
|
||||
admin_commands::generate(&mut queue)?;
|
||||
|
||||
queue.write(&root, common_args.dry_run)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
37
xtask/src/tasks/mod.rs
Normal file
37
xtask/src/tasks/mod.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
type TaskResult<T> = Result<T, Box<dyn std::error::Error>>;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! tasks {
|
||||
(
|
||||
$(
|
||||
$module:ident: $desc:literal
|
||||
),*
|
||||
) => {
|
||||
$(pub(super) mod $module;)*
|
||||
|
||||
#[derive(clap::Subcommand)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub(super) enum Task {
|
||||
$(
|
||||
#[clap(about = $desc, long_about = None)]
|
||||
$module($module::Args),
|
||||
)*
|
||||
}
|
||||
|
||||
impl Task {
|
||||
pub(super) fn invoke(self, common_args: $crate::Args) -> TaskResult<impl std::process::Termination> {
|
||||
match self {
|
||||
$(
|
||||
Self::$module(task_args) => {
|
||||
$module::run(common_args, task_args)
|
||||
},
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
tasks! {
|
||||
generate_docs: "Generate various documentation files. This is run automatically when compiling the website."
|
||||
}
|
||||
10
xtask/templates/admin/category.md
Normal file
10
xtask/templates/admin/category.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# `!admin {{ name }}`
|
||||
|
||||
{{ description }}
|
||||
|
||||
{% for command in commands %}
|
||||
{% let header = "#".repeat((command.depth + 1).min(3)) -%}
|
||||
{{ header }} `!admin {{ command.name }}`
|
||||
|
||||
{{ command.description }}
|
||||
{% endfor %}
|
||||
7
xtask/templates/admin/index.md
Normal file
7
xtask/templates/admin/index.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Admin Commands
|
||||
|
||||
These are all the admin commands. TODO fill me out
|
||||
|
||||
{%~ for category in categories %}
|
||||
- [`!admin {{ category.name }}`]({{ category.name }}/) {{ category.description }}
|
||||
{%- endfor %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue