oberst 0.1.0

A type-safe command parser and dispatcher inspired by Brigadier and written in Rust.
Documentation
use std::collections::HashMap;

pub mod parser;
pub use oberst_proc::define_command;

/// Helper type used internally by `define_command!`.
pub type Parse<Context> = for<'a> fn(
    &mut parser::CommandParser<'a>,
) -> Result<Execute<'a, Context>, parser::ParseError<'a>>;

/// Helper type used internally by `define_command!`.
pub type Execute<'a, Context> = Box<dyn FnOnce(&Context) -> CommandResult<'a>>;

/// Any error that can occur while parsing or executing a command.
#[derive(Debug)]
pub enum CommandError<'a> {
    Parse(parser::ParseError<'a>),
    Dispatch(Box<dyn std::error::Error + 'a>),
}

/// The result of a command execution.
pub type CommandResult<'a> = std::result::Result<i32, CommandError<'a>>;

impl<'a, E> From<E> for CommandError<'a>
where
    E: std::error::Error + 'a,
{
    fn from(error: E) -> Self {
        CommandError::Dispatch(Box::new(error))
    }
}

/// Contains the name and possible usages of a command.
/// Generated automatically.
pub struct CommandUsage {
    pub name: &'static str,
    pub usage: &'static [&'static str],
    pub description: Option<&'static str>,
}

struct Command<Context: 'static> {
    usage: &'static CommandUsage,
    dispatchers: &'static [CommandDispatch<Context>],
}

/// Helper struct generated by `define_command!`
pub struct CommandDispatch<Context> {
    pub parser: Parse<Context>,
}

/// The core of `oberst`. This struct manages commands and allows them to be dispatched.
pub struct CommandSource<Context: 'static> {
    commands: HashMap<&'static str, Command<Context>>,
    context: Context,
}

impl<Context: 'static> CommandSource<Context> {
    /// Create a new `CommandSource` with the given context.
    /// The context will be passed to all commands.
    pub fn new(context: Context) -> Self {
        Self {
            commands: HashMap::new(),
            context,
        }
    }

    /// Register a command with the given name, usage and dispatchers.
    /// Use the `register_command!` macro instead of calling this method directly.
    pub fn register(
        &mut self,
        name: &'static str,
        usage: &'static CommandUsage,
        dispatchers: &'static [CommandDispatch<Context>],
    ) {
        assert!(!dispatchers.is_empty());
        debug_assert!(name.chars().all(char::is_alphabetic));
        self.commands.insert(name, Command { usage, dispatchers });
    }

    /// Get the usage information for the given command.
    pub fn get_usage(&self, command: &str) -> Option<&CommandUsage> {
        self.commands.get(command).map(|command| command.usage)
    }

    /// Dispatch a command described by the string in `command`.
    pub fn dispatch<'a>(&'a self, command: &'a str) -> CommandResult {
        let mut parser = parser::CommandParser::new(command);
        let command = parser.read_while(|c| c.is_alphabetic());
        let command = self.commands.get(&command).ok_or(CommandError::Parse(
            parser.error(parser::ParseErrorKind::UnknownCommand),
        ))?;

        let mut last_error = None;

        for dispatch in command.dispatchers {
            let mut branch = parser.branch();
            match (dispatch.parser)(&mut branch) {
                Ok(execute) => {
                    return (execute)(&self.context);
                }
                Err(error) => {
                    last_error = Some(error);
                }
            }
        }

        Err(CommandError::Parse(
            last_error.expect("Expected at least one dispatch"),
        ))
    }
}

/// Helper macro to register a command to a `CommandSource`.
#[macro_export]
macro_rules! register_command {
    ($source:expr, $name:ident) => {
        ($source).register(stringify!($name), &$name::USAGE, $name::DISPATCHERS)
    };
}