pub mod help;
mod lint;
pub mod quit;
pub use lint::*;
use crate::looper::Looper;
use crate::terminal::{AccessTerminalError, Terminal};
use std::borrow::Cow;
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::fmt::{Debug, Display};
use thiserror::Error;
#[derive(Debug, Clone, Error)]
pub enum ApplyCommandError<E> {
#[error("application: {0}")]
Application(E),
#[error("access terminal: {0}")]
AccessTerminal(#[from] AccessTerminalError),
}
impl<E> ApplyCommandError<E> {
pub fn application(self) -> Option<E> {
match self {
ApplyCommandError::Application(err) => Some(err),
ApplyCommandError::AccessTerminal(_) => None,
}
}
pub fn access_terminal(self) -> Option<AccessTerminalError> {
match self {
ApplyCommandError::Application(_) => None,
ApplyCommandError::AccessTerminal(err) => Some(err),
}
}
}
pub trait Command<T: Terminal> {
type Context;
type Error;
fn apply(&mut self, looper: &mut Looper<Self::Context, Self::Error, T>)
-> Result<ApplyOutcome, ApplyCommandError<Self::Error>>;
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplyOutcome {
Applied,
Skipped,
}
pub trait NamedCommandParser<T> {
type Context;
type Error;
#[allow(clippy::type_complexity)]
fn parse(&self, s: &str) -> Result<Box<dyn Command<T, Context = Self::Context, Error = Self::Error>>, ParseCommandError>;
fn shorthand(&self) -> Option<Cow<'static, str>>;
fn name(&self) -> Cow<'static, str>;
fn description(&self) -> Description;
#[allow(clippy::type_complexity)]
fn parse_no_args<M>(
&self,
s: &str,
ctor: impl FnOnce() -> M,
) -> Result<Box<dyn Command<T, Context = Self::Context, Error = Self::Error>>, ParseCommandError>
where
M: Command<T, Context = Self::Context, Error = Self::Error> + 'static,
T: Terminal,
Self: Sized,
{
if s.is_empty() {
Ok(Box::new(ctor()))
} else {
Err(ParseCommandError(
format!("invalid arguments to '{}': '{s}'", self.name()).into(),
))
}
}
}
#[derive(Debug, Clone)]
pub struct Description {
pub purpose: Cow<'static, str>,
pub usage: Cow<'static, str>,
pub examples: Vec<Example>,
}
#[derive(Debug, Clone)]
pub struct Example {
pub scenario: Cow<'static, str>,
pub command: Cow<'static, str>,
}
impl Example {
fn assert_parsable<C, E, T, P: NamedCommandParser<T, Context = C, Error = E> + ?Sized>(
&self,
parser: &P,
) -> Result<(), ParseCommandError> {
parser.parse(&self.command)?;
Ok(())
}
}
pub struct Commander<C, E, T> {
parsers: Vec<Box<dyn NamedCommandParser<T, Context = C, Error = E>>>,
by_shorthand: BTreeMap<String, usize>,
by_name: BTreeMap<String, usize>,
}
impl<C, E, T> Commander<C, E, T> {
pub fn new(parsers: Vec<Box<dyn NamedCommandParser<T, Context = C, Error = E>>>) -> Self {
parsers.try_into().unwrap()
}
pub fn parsers(&self) -> impl Iterator<Item = &Box<dyn NamedCommandParser<T, Context = C, Error = E>>> {
self.by_name.values().map(|&idx| &self.parsers[idx])
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{0}")]
pub struct InvalidCommandParserSpec(String);
#[derive(Debug, Clone, PartialEq, Eq, Error)]
#[error("{0}")]
pub struct ParseCommandError(pub Cow<'static, str>);
impl ParseCommandError {
#[allow(clippy::needless_pass_by_value)]
pub fn convert<E: ToString>(err: E) -> Self {
Self(err.to_string().into())
}
}
impl<C, E, T> TryFrom<Vec<Box<dyn NamedCommandParser<T, Context = C , Error = E>>>> for Commander<C, E, T> {
type Error = InvalidCommandParserSpec;
fn try_from(parsers: Vec<Box<dyn NamedCommandParser<T, Context = C , Error = E>>>) -> Result<Self, Self::Error> {
fn insert<N: Ord + Display>(
key: N,
value: usize,
map: &mut BTreeMap<N, usize>,
) -> Result<(), InvalidCommandParserSpec> {
match map.entry(key) {
Entry::Vacant(entry) => {
entry.insert(value);
Ok(())
}
Entry::Occupied(entry) => duplicate_error(entry.key()),
}
}
fn check<N: Ord + Display>(
key: &N,
map: &BTreeMap<N, usize>,
) -> Result<(), InvalidCommandParserSpec> {
if map.contains_key(key) {
duplicate_error(key)
} else {
Ok(())
}
}
fn duplicate_error<N: Display>(key: &N) -> Result<(), InvalidCommandParserSpec> {
Err(InvalidCommandParserSpec(format!(
"duplicate command parser for '{key}'"
)))
}
let mut by_shorthand = BTreeMap::default();
let mut by_name = BTreeMap::default();
for (index, parser) in parsers.iter().enumerate() {
for example in &parser.description().examples {
example.assert_parsable(&**parser).map_err(|err| {
InvalidCommandParserSpec(format!(
"unparsable example command '{}': {err}",
example.command
))
})?;
}
if let Some(shorthand) = parser.shorthand() {
let shorthand = shorthand.into_owned();
check(&shorthand, &by_name)?;
insert(shorthand, index, &mut by_shorthand)?;
}
if parser.name().len() < 2 {
return Err(InvalidCommandParserSpec(format!(
"invalid command name '{}': must contain at least 2 characters",
parser.name()
)));
}
let name = parser.name().into_owned();
check(&name, &by_shorthand)?;
insert(name, index, &mut by_name)?;
}
Ok(Self {
parsers,
by_shorthand,
by_name,
})
}
}
impl<C, E, T> Commander<C, E, T> {
pub fn parse(&self, s: &str) -> Result<Box<dyn Command<T, Context = C , Error = E>>, ParseCommandError> {
if s.is_empty() {
return Err(ParseCommandError("empty command string".into()));
}
let index = s.find(' ').unwrap_or(s.len());
let name = &s[..index];
let &parser_idx = self
.by_shorthand
.get(name)
.or_else(|| self.by_name.get(name))
.ok_or_else(|| ParseCommandError(format!("no command parser for '{name}'").into()))?;
let command_frag = if index == s.len() {
""
} else {
&s[index + 1..]
};
self.parsers[parser_idx].parse(command_frag)
}
}
pub(crate) fn read_command<C, E, T: Terminal>(
looper: &mut Looper<C, E, T>,
prompt: &str,
) -> Result<Box<dyn Command<T, Context = C , Error = E>>, AccessTerminalError> {
let (terminal, commander, _) = looper.split();
terminal.read_value(prompt, |str| commander.parse(str))
}
#[cfg(test)]
mod tests;