exfiltrate 0.3.0

An embeddable debug tool for Rust.
Documentation
// SPDX-License-Identifier: MIT OR Apache-2.0
//! Implements the remote command that returns detailed command help.
use crate::commands::COMMANDS;
use exfiltrate_internal::args::{ArgKind, ArgSpec, usage_block};
use exfiltrate_internal::command::{Command, Response};

/// The remote `help` command.
///
/// Returns the full description of a requested command, followed by a usage
/// block generated from the command's declared parameters. Generating the usage
/// line is the point: a hand-written one drifts away from the code that parses
/// the arguments, and a command that declares its schema can no longer document
/// a signature it does not implement.
pub struct Help;

static ARGS: &[ArgSpec] =
    &[ArgSpec::positional("command", "the command to describe", ArgKind::String).required()];

impl Command for Help {
    fn name(&self) -> &'static str {
        "help"
    }

    fn short_description(&self) -> &'static str {
        "Provides detailed help for a command.  Use this to learn more about an individual command."
    }

    fn full_description(&self) -> &'static str {
        "Provides detailed help for a command."
    }

    fn args(&self) -> &'static [ArgSpec] {
        ARGS
    }

    fn execute(&self, args: Vec<String>) -> Result<Response, Response> {
        let parsed = exfiltrate_internal::args::ParsedArgs::parse(self.args(), args)
            .map_err(|error| Response::String(format!("{error}\n{}", usage_line())))?;
        let requested = parsed.get("command").expect("`command` is required");

        let registry = COMMANDS.lock_sync_read();
        let Some(command) = registry.get(requested) else {
            let mut message = format!("No such command: {requested}\n\nAvailable commands:\n");
            for name in registry.keys() {
                message.push_str("  ");
                message.push_str(name);
                message.push('\n');
            }
            return Err(message.into());
        };
        Ok(describe(command.as_ref()).into())
    }
}

fn usage_line() -> String {
    exfiltrate_internal::args::usage_line("help", ARGS)
}

/// Renders a command's full description followed by its generated usage block.
///
/// A command that declares no parameters gets its description alone; inventing a
/// usage line for a signature nobody declared would be worse than omitting one.
pub(crate) fn describe(command: &dyn Command) -> String {
    let mut out = command.full_description().trim_end().to_string();
    if !command.args().is_empty() {
        out.push_str("\n\n");
        out.push_str(&usage_block(command.name(), command.args()));
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    static DECLARED_ARGS: &[ArgSpec] = &[
        ArgSpec::positional("path", "which file", ArgKind::Path).required(),
        ArgSpec::flag("lines", "how many lines", ArgKind::Integer),
    ];

    struct Declared;

    impl Command for Declared {
        fn name(&self) -> &'static str {
            "declared"
        }
        fn short_description(&self) -> &'static str {
            "a command with a schema"
        }
        fn full_description(&self) -> &'static str {
            "Does a thing."
        }
        fn args(&self) -> &'static [ArgSpec] {
            DECLARED_ARGS
        }
        fn execute(&self, _args: Vec<String>) -> Result<Response, Response> {
            Ok("".into())
        }
    }

    struct Undeclared;

    impl Command for Undeclared {
        fn name(&self) -> &'static str {
            "undeclared"
        }
        fn short_description(&self) -> &'static str {
            "a command without a schema"
        }
        fn full_description(&self) -> &'static str {
            "Does a thing.\nUsage: exfiltrate undeclared [WHATEVER]"
        }
        fn execute(&self, _args: Vec<String>) -> Result<Response, Response> {
            Ok("".into())
        }
    }

    #[test]
    fn a_declared_command_gets_a_generated_usage_block() {
        let text = describe(&Declared);
        assert!(text.starts_with("Does a thing."), "{text}");
        assert!(
            text.contains("Usage: exfiltrate declared <PATH> [--lines <INTEGER>]"),
            "{text}"
        );
        assert!(text.contains("which file"), "{text}");
        assert!(text.contains("how many lines"), "{text}");
    }

    #[test]
    fn an_undeclared_command_keeps_its_hand_written_prose_untouched() {
        let text = describe(&Undeclared);
        assert_eq!(
            text,
            "Does a thing.\nUsage: exfiltrate undeclared [WHATEVER]"
        );
    }

    #[test]
    fn asking_for_nothing_reports_the_usage_rather_than_an_empty_answer() {
        let error = Help.execute(Vec::new()).unwrap_err().to_string();
        assert!(
            error.contains("Usage: exfiltrate help <COMMAND>"),
            "{error}"
        );
    }

    #[test]
    fn an_unknown_command_lists_the_ones_that_do_exist() {
        crate::commands::insert(Box::new(Declared)).ok();
        let error = Help
            .execute(vec!["not_a_command".to_string()])
            .unwrap_err()
            .to_string();
        assert!(error.contains("No such command: not_a_command"), "{error}");
        assert!(error.contains("declared"), "{error}");
    }
}