Change structopt to clap, remove markdown dependency

This commit is contained in:
Andrei Vasiliu 2022-01-21 17:34:21 +02:00
parent cc3ef1a8be
commit 57979da28c
3 changed files with 156 additions and 53 deletions

75
Cargo.lock generated
View file

@ -269,6 +269,33 @@ dependencies = [
"libloading", "libloading",
] ]
[[package]]
name = "clap"
version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a30c3bf9ff12dfe5dae53f0a96e0febcd18420d1c0e7fad77796d9d5c4b5375"
dependencies = [
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"textwrap",
]
[[package]]
name = "clap_derive"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "517358c28fcef6607bf6f76108e02afad7e82297d132a6b846dcc1fc3efcd153"
dependencies = [
"heck 0.4.0",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "color_quant" name = "color_quant"
version = "1.1.0" version = "1.1.0"
@ -281,6 +308,7 @@ version = "0.2.0"
dependencies = [ dependencies = [
"base64 0.13.0", "base64 0.13.0",
"bytes", "bytes",
"clap",
"crossbeam", "crossbeam",
"directories", "directories",
"heed", "heed",
@ -630,7 +658,7 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
dependencies = [ dependencies = [
"heck", "heck 0.3.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -902,6 +930,12 @@ dependencies = [
"unicode-segmentation", "unicode-segmentation",
] ]
[[package]]
name = "heck"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
[[package]] [[package]]
name = "heed" name = "heed"
version = "0.10.6" version = "0.10.6"
@ -1570,6 +1604,15 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "os_str_bytes"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "page_size" name = "page_size"
version = "0.4.2" version = "0.4.2"
@ -1728,6 +1771,30 @@ dependencies = [
"toml", "toml",
] ]
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]] [[package]]
name = "proc-macro-hack" name = "proc-macro-hack"
version = "0.5.19" version = "0.5.19"
@ -2863,6 +2930,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.30" version = "1.0.30"

View file

@ -86,8 +86,7 @@ thread_local = "1.1.3"
hmac = "0.11.0" hmac = "0.11.0"
sha-1 = "0.9.8" sha-1 = "0.9.8"
# used for conduit's CLI and admin room command parsing # used for conduit's CLI and admin room command parsing
structopt = { version = "0.3.25", default-features = false } clap = { version = "3.0.10", default-features = false, features = ["std", "derive"] }
pulldown-cmark = "0.9.1"
[features] [features]
default = ["conduit_bin", "backend_sqlite", "backend_rocksdb"] default = ["conduit_bin", "backend_sqlite", "backend_rocksdb"]

View file

@ -5,6 +5,7 @@ use crate::{
pdu::PduBuilder, pdu::PduBuilder,
server_server, Database, PduEvent, server_server, Database, PduEvent,
}; };
use clap::Parser;
use regex::Regex; use regex::Regex;
use rocket::{ use rocket::{
futures::{channel::mpsc, stream::StreamExt}, futures::{channel::mpsc, stream::StreamExt},
@ -15,7 +16,6 @@ use ruma::{
EventId, RoomId, RoomVersionId, UserId, EventId, RoomId, RoomVersionId, UserId,
}; };
use serde_json::value::to_raw_value; use serde_json::value::to_raw_value;
use structopt::StructOpt;
use tokio::sync::{MutexGuard, RwLock, RwLockReadGuard}; use tokio::sync::{MutexGuard, RwLock, RwLockReadGuard};
use tracing::warn; use tracing::warn;
@ -155,7 +155,7 @@ pub fn parse_admin_command(db: &Database, command_line: &str, body: Vec<&str>) -
Some(command) => *command, Some(command) => *command,
None => { None => {
let markdown_message = "No command given. Use `help` for a list of commands."; let markdown_message = "No command given. Use `help` for a list of commands.";
let html_message = markdown_to_html(&markdown_message); let html_message = "No command given. Use <code>help</code> for a list of commands.";
return AdminCommand::SendMessage(RoomMessageEventContent::text_html( return AdminCommand::SendMessage(RoomMessageEventContent::text_html(
markdown_message, markdown_message,
@ -164,10 +164,17 @@ pub fn parse_admin_command(db: &Database, command_line: &str, body: Vec<&str>) -
} }
}; };
// Replace `help command` with `command --help`
// Clap has a help subcommand, but it omits the long help description.
if argv[0] == "help" {
argv.remove(0);
argv.push("--help");
}
// Backwards compatibility with `register_appservice`-style commands // Backwards compatibility with `register_appservice`-style commands
let command_with_dashes; let command_with_dashes;
if command_line.contains("_") { if argv[0].contains("_") {
command_with_dashes = command_name.replace("_", "-"); command_with_dashes = argv[0].replace("_", "-");
argv[0] = &command_with_dashes; argv[0] = &command_with_dashes;
} }
@ -179,7 +186,11 @@ pub fn parse_admin_command(db: &Database, command_line: &str, body: Vec<&str>) -
```\n{}\n```", ```\n{}\n```",
command_name, error, command_name, error,
); );
let html_message = markdown_to_html(&markdown_message); let html_message = format!(
"Encountered an error while handling the <code>{}</code> command:\n\
<pre>\n{}\n</pre>",
command_name, error,
);
AdminCommand::SendMessage(RoomMessageEventContent::text_html( AdminCommand::SendMessage(RoomMessageEventContent::text_html(
markdown_message, markdown_message,
@ -189,9 +200,10 @@ pub fn parse_admin_command(db: &Database, command_line: &str, body: Vec<&str>) -
} }
} }
#[derive(StructOpt)] #[derive(Parser)]
#[clap(name = "@conduit:example.com", version = env!("CARGO_PKG_VERSION"))]
enum AdminCommands { enum AdminCommands {
#[structopt(verbatim_doc_comment)] #[clap(verbatim_doc_comment)]
/// Register an appservice using its registration YAML /// Register an appservice using its registration YAML
/// ///
/// This command needs a YAML generated by an appservice (such as a bridge), /// This command needs a YAML generated by an appservice (such as a bridge),
@ -200,25 +212,25 @@ enum AdminCommands {
/// Registering a new bridge using the ID of an existing bridge will replace /// Registering a new bridge using the ID of an existing bridge will replace
/// the old one. /// the old one.
/// ///
/// Example: /// [add-yaml-block-to-usage]
/// ````
/// @conduit:example.com: register-appservice
/// ```
/// yaml content here
/// ```
/// ````
RegisterAppservice, RegisterAppservice,
/// Unregister an appservice using its ID /// Unregister an appservice using its ID
/// ///
/// You can find the ID using the `list-appservices` command. /// You can find the ID using the `list-appservices` command.
UnregisterAppservice { appservice_identifier: String }, UnregisterAppservice {
/// The appservice to unregister
appservice_identifier: String,
},
/// List all the currently registered appservices /// List all the currently registered appservices
ListAppservices, ListAppservices,
/// Get the auth_chain of a PDU /// Get the auth_chain of a PDU
GetAuthChain { event_id: Box<EventId> }, GetAuthChain {
/// An event ID (the $ character followed by the base64 reference hash)
event_id: Box<EventId>,
},
/// Parse and print a PDU from a JSON /// Parse and print a PDU from a JSON
/// ///
@ -227,7 +239,10 @@ enum AdminCommands {
ParsePdu, ParsePdu,
/// Retrieve and print a PDU by ID from the Conduit database /// Retrieve and print a PDU by ID from the Conduit database
GetPdu { event_id: Box<EventId> }, GetPdu {
/// An event ID (a $ followed by the base64 reference hash)
event_id: Box<EventId>,
},
/// Print database memory usage statistics /// Print database memory usage statistics
DatabaseMemoryUsage, DatabaseMemoryUsage,
@ -239,16 +254,16 @@ pub fn try_parse_admin_command(
body: Vec<&str>, body: Vec<&str>,
) -> Result<AdminCommand> { ) -> Result<AdminCommand> {
argv.insert(0, "@conduit:example.com:"); argv.insert(0, "@conduit:example.com:");
let command = match AdminCommands::from_iter_safe(argv) { let command = match AdminCommands::try_parse_from(argv) {
Ok(command) => command, Ok(command) => command,
Err(error) => { Err(error) => {
println!("Before:\n{}\n", error.to_string()); let message = error
let markdown_message = usage_to_markdown(&error.to_string()) .to_string()
.replace("example.com", db.globals.server_name().as_str()); .replace("example.com", db.globals.server_name().as_str());
let html_message = markdown_to_html(&markdown_message); let html_message = usage_to_html(&message);
return Ok(AdminCommand::SendMessage( return Ok(AdminCommand::SendMessage(
RoomMessageEventContent::text_html(markdown_message, html_message), RoomMessageEventContent::text_html(message, html_message),
)); ));
} }
}; };
@ -380,42 +395,58 @@ pub fn try_parse_admin_command(
Ok(admin_command) Ok(admin_command)
} }
// Utility to turn structopt's `--help` text to markdown. // Utility to turn clap's `--help` text to HTML.
fn usage_to_markdown(text: &str) -> String { fn usage_to_html(text: &str) -> String {
// For the conduit admin room, subcommands become main commands // For the conduit admin room, subcommands become main commands
let text = text.replace("SUBCOMMAND", "COMMAND"); let text = text.replace("SUBCOMMAND", "COMMAND");
let text = text.replace("subcommand", "command"); let text = text.replace("subcommand", "command");
// Put the first line (command name and version text) on its own paragraph // Escape option names (e.g. `<element-id>`) since they look like HTML tags
let text = text.replace("<", "&lt;").replace(">", "&gt;");
// Italicize the first line (command name and version text)
let re = Regex::new("^(.*?)\n").expect("Regex compilation should not fail"); let re = Regex::new("^(.*?)\n").expect("Regex compilation should not fail");
let text = re.replace_all(&text, "*$1*\n\n"); let text = re.replace_all(&text, "<em>$1</em>\n");
// Wrap command names in backticks // Unmerge wrapped lines
let text = text.replace("\n ", " ");
// Wrap option names in backticks. The lines look like:
// -V, --version Prints version information
// And are converted to:
// <code>-V, --version</code>: Prints version information
// (?m) enables multi-line mode for ^ and $ // (?m) enables multi-line mode for ^ and $
let re = Regex::new("(?m)^ ([a-z-]+) +(.*)$").expect("Regex compilation should not fail"); let re = Regex::new("(?m)^ (([a-zA-Z_&;-]+(, )?)+) +(.*)$")
let text = re.replace_all(&text, " `$1`: $2"); .expect("Regex compilation should not fail");
let text = re.replace_all(&text, "<code>$1</code>: $4");
// Add * to list items // // Enclose examples in code blocks
let re = Regex::new("(?m)^ (.*)$").expect("Regex compilation should not fail"); // // (?ms) enables multi-line mode and dot-matches-all
let text = re.replace_all(&text, "* $1"); // let re =
// Regex::new("(?ms)^Example:\n(.*?)\nUSAGE:$").expect("Regex compilation should not fail");
// let text = re.replace_all(&text, "EXAMPLE:\n<pre>$1</pre>\nUSAGE:");
// Turn section names to headings let has_yaml_block_marker = text.contains("\n[add-yaml-block-to-usage]\n");
let re = Regex::new("(?m)^([A-Z-]+):$").expect("Regex compilation should not fail"); let text = text.replace("\n[add-yaml-block-to-usage]\n", "");
let text = re.replace_all(&text, "#### $1");
// Add HTML line-breaks
let text = text.replace("\n", "<br>\n");
let text = if !has_yaml_block_marker {
// Wrap the usage line in code tags
let re = Regex::new("(?m)^USAGE:<br>\n (@conduit:.*)<br>$")
.expect("Regex compilation should not fail");
re.replace_all(&text, "USAGE:<br>\n<code>$1</code><br>")
} else {
// Wrap the usage line in a code block, and add a yaml block example
// This makes the usage of e.g. `register-appservice` more accurate
let re = Regex::new("(?m)^USAGE:<br>\n (.*?)<br>\n<br>\n")
.expect("Regex compilation should not fail");
re.replace_all(
&text,
"USAGE:<br>\n<pre>$1\n```\nyaml content here\n```</pre>",
)
};
text.to_string() text.to_string()
} }
// Convert markdown to HTML using the CommonMark flavor
fn markdown_to_html(text: &str) -> String {
// CommonMark's spec allows HTML tags; however, CLI required arguments look
// very much like tags so escape them.
let text = text.replace("<", "&lt;").replace(">", "&gt;");
let mut html_output = String::new();
let parser = pulldown_cmark::Parser::new(&text);
pulldown_cmark::html::push_html(&mut html_output, parser);
html_output
}