use std::convert::{TryFrom, TryInto};
use std::error::Error;
use std::fmt::{self, Debug};
static COMMAND_LIST_BEGIN: &str = "command_list_ok_begin\n";
static COMMAND_LIST_END: &str = "command_list_end\n";
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Command {
commands: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct CommandError {
pub reason: InvalidCommandReason,
pub list_at: Option<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum InvalidCommandReason {
Empty,
InvalidCharacter(usize, char),
UnncessaryWhitespace,
NestedCommandList,
}
impl Command {
pub fn new<C>(c: C) -> Self
where
C: TryInto<Self>,
<C as TryInto<Self>>::Error: Debug,
{
c.try_into().expect("invalid command")
}
pub fn render(self) -> String {
let mut out;
if self.commands.len() == 1 {
let c = self.commands.first().unwrap();
out = String::with_capacity(c.len() + 1);
out.push_str(c);
out.push('\n');
} else {
assert!(self.commands.len() > 1);
out = String::with_capacity(
COMMAND_LIST_BEGIN.len()
+ self.commands.iter().fold(0, |acc, c| acc + c.len() + 1)
+ COMMAND_LIST_END.len(),
);
out.push_str(COMMAND_LIST_BEGIN);
for c in self.commands {
out.push_str(&c);
out.push('\n');
}
out.push_str(COMMAND_LIST_END);
}
out
}
}
impl TryFrom<&str> for Command {
type Error = CommandError;
fn try_from(c: &str) -> Result<Self, Self::Error> {
let mut c = validate_single_command(c)?.to_owned();
canonicalize_command(&mut c);
Ok(Self { commands: vec![c] })
}
}
impl TryFrom<&[&str]> for Command {
type Error = CommandError;
fn try_from(commands: &[&str]) -> Result<Self, Self::Error> {
if commands.is_empty() {
return Err(CommandError {
reason: InvalidCommandReason::Empty,
list_at: None,
});
}
let mut out = Vec::with_capacity(commands.len());
for (index, c) in commands.iter().enumerate() {
let mut c = validate_single_command(c)
.map_err(|mut e| {
e.list_at = Some(index);
e
})?
.to_owned();
canonicalize_command(&mut c);
if c.starts_with("command_list_") {
return Err(CommandError {
reason: InvalidCommandReason::NestedCommandList,
list_at: Some(index),
});
} else {
out.push(c.to_owned());
}
}
Ok(Self { commands: out })
}
}
fn validate_single_command(command: &str) -> Result<&str, CommandError> {
if command.is_empty() {
return Err(CommandError {
reason: InvalidCommandReason::Empty,
list_at: None,
});
}
if command.chars().nth(0).unwrap().is_ascii_whitespace()
|| command.chars().last().unwrap().is_ascii_whitespace()
{
return Err(CommandError {
reason: InvalidCommandReason::UnncessaryWhitespace,
list_at: None,
});
}
let mut in_command_part = true;
if let Some((index, c)) = command.char_indices().find(|(index, c)| {
if in_command_part {
if is_valid_command_char(*c) {
false
} else {
if *index != 0 && *c == ' ' {
in_command_part = false;
false
} else {
true
}
}
} else {
*c == '\n'
}
}) {
return Err(CommandError {
reason: InvalidCommandReason::InvalidCharacter(index, c),
list_at: None,
});
}
Ok(command)
}
fn canonicalize_command(command: &mut str) {
let command_end = command
.char_indices()
.find(|(_i, c)| !is_valid_command_char(*c))
.map(|(i, _)| i)
.unwrap_or(command.len() - 1);
command[..command_end].make_ascii_lowercase();
}
fn is_valid_command_char(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
impl Error for CommandError {}
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.reason {
InvalidCommandReason::Empty => write!(f, "Command or command list was empty"),
InvalidCommandReason::InvalidCharacter(i, c) => write!(
f,
"Command contained an invalid character: {:?} at position {}",
c, i
),
InvalidCommandReason::UnncessaryWhitespace => {
write!(f, "Command contained leading or trailing whitespace")
}
InvalidCommandReason::NestedCommandList => write!(
f,
"Command attempted to open or close a command list while already in one"
),
}?;
if let Some(i) = self.list_at {
write!(f, " (at command list index {})", i)?;
}
Ok(())
}
}