use std::collections::{BTreeSet, HashSet, VecDeque};
use std::error::Error;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub(crate) struct Block {
pub commands: Vec<Command>,
pub literal: String,
pub line_number: u32,
}
#[derive(Clone, PartialEq)]
#[non_exhaustive]
pub struct Command {
pub name: String,
pub args: Vec<Argument>,
pub prefix: Option<String>,
pub tags: HashSet<String>,
pub silent: bool,
pub fail: bool,
pub line_number: u32,
}
impl std::fmt::Debug for Command {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Command")
.field("name", &self.name)
.field("args", &self.args)
.field("prefix", &self.prefix)
.field("tags", &BTreeSet::from_iter(&self.tags))
.field("silent", &self.silent)
.field("fail", &self.fail)
.field("line_number", &self.line_number)
.finish()
}
}
impl Command {
pub fn consume_args(&self) -> ArgumentConsumer<'_> {
ArgumentConsumer::new(&self.args)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Argument {
pub key: Option<String>,
pub value: String,
}
impl Argument {
pub fn name(&self) -> &str {
match self.key.as_deref() {
Some(key) => key,
None => &self.value,
}
}
pub fn parse<T>(&self) -> Result<T, Box<dyn Error>>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
self.value.parse().map_err(|e| format!("invalid argument '{}': {e}", self.value).into())
}
}
pub struct ArgumentConsumer<'a> {
args: VecDeque<&'a Argument>,
}
impl<'a> Iterator for ArgumentConsumer<'a> {
type Item = &'a Argument;
fn next(&mut self) -> Option<Self::Item> {
self.args.pop_front()
}
}
impl<'a> ArgumentConsumer<'a> {
fn new(args: &'a [Argument]) -> Self {
Self { args: VecDeque::from_iter(args.iter()) }
}
pub fn lookup(&mut self, key: &str) -> Option<&'a Argument> {
let arg = self.args.iter().rev().find(|a| a.key.as_deref() == Some(key)).copied();
if arg.is_some() {
self.args.retain(|a| a.key.as_deref() != Some(key))
}
arg
}
pub fn lookup_parse<T>(&mut self, key: &str) -> Result<Option<T>, Box<dyn Error>>
where
T: std::str::FromStr,
<T as std::str::FromStr>::Err: std::fmt::Display,
{
let value = self
.args
.iter()
.rev()
.find(|a| a.key.as_deref() == Some(key))
.map(|a| a.parse())
.transpose()?;
if value.is_some() {
self.args.retain(|a| a.key.as_deref() != Some(key))
}
Ok(value)
}
pub fn next_key(&mut self) -> Option<&'a Argument> {
self.args.iter().position(|a| a.key.is_some()).map(|i| self.args.remove(i).unwrap())
}
pub fn next_pos(&mut self) -> Option<&'a Argument> {
self.args.iter().position(|a| a.key.is_none()).map(|i| self.args.remove(i).unwrap())
}
pub fn reject_rest(&self) -> Result<(), Box<dyn Error>> {
if let Some(arg) = self.args.front() {
return Err(format!("invalid argument '{}'", arg.name()).into());
}
Ok(())
}
pub fn rest(&mut self) -> Vec<&'a Argument> {
self.args.drain(..).collect()
}
pub fn rest_key(&mut self) -> Vec<&'a Argument> {
let keyed: Vec<_> = self.args.iter().filter(|a| a.key.is_some()).copied().collect();
if !keyed.is_empty() {
self.args.retain(|a| a.key.is_none());
}
keyed
}
pub fn rest_pos(&mut self) -> Vec<&'a Argument> {
let pos: Vec<_> = self.args.iter().filter(|a| a.key.is_none()).copied().collect();
if !pos.is_empty() {
self.args.retain(|a| a.key.is_some());
}
pos
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! arg {
($value:expr) => {
Argument { key: None, value: $value.to_string() }
};
($key:expr => $value:expr) => {
Argument { key: Some($key.to_string()), value: $value.to_string() }
};
}
macro_rules! cmd {
($input:expr) => {{
crate::parser::parse_command(&format!("{}\n", $input)).expect("invalid command")
}};
}
#[test]
fn argument_name() {
assert_eq!(arg!("value").name(), "value");
assert_eq!(arg!("key" => "value").name(), "key");
}
#[test]
fn argument_parse() {
assert_eq!(arg!("-1").parse::<i64>().unwrap(), -1_i64);
assert_eq!(arg!("0").parse::<i64>().unwrap(), 0_i64);
assert_eq!(arg!("1").parse::<i64>().unwrap(), 1_i64);
assert_eq!(
arg!("").parse::<i64>().unwrap_err().to_string(),
"invalid argument '': cannot parse integer from empty string"
);
assert_eq!(
arg!("foo").parse::<i64>().unwrap_err().to_string(),
"invalid argument 'foo': invalid digit found in string"
);
assert!(!arg!("false").parse::<bool>().unwrap());
assert!(arg!("true").parse::<bool>().unwrap());
assert_eq!(
arg!("").parse::<bool>().unwrap_err().to_string(),
"invalid argument '': provided string was not `true` or `false`"
);
}
#[test]
fn command_consume_args() {
let cmd = cmd!("cmd foo key=value bar");
assert_eq!(cmd.consume_args().rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2]]);
}
#[test]
fn argument_consumer_lookup() {
let cmd = cmd!("cmd value key=value foo=bar key=other");
let mut args = cmd.consume_args();
assert_eq!(args.lookup("unknown"), None);
assert_eq!(args.lookup("value"), None);
assert_eq!(args.rest().len(), 4);
let mut args = cmd.consume_args();
assert_eq!(args.lookup("key"), Some(&cmd.args[3]));
assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[2]]);
let mut args = cmd.consume_args();
assert_eq!(args.lookup("foo"), Some(&cmd.args[2]));
assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[3]]);
}
#[test]
fn argument_consumer_lookup_parse() {
let cmd = cmd!("cmd value key=1 foo=bar key=2");
let mut args = cmd.consume_args();
assert_eq!(args.lookup_parse::<String>("unknown").unwrap(), None);
assert_eq!(args.lookup_parse::<String>("value").unwrap(), None);
assert_eq!(args.rest().len(), 4);
let mut args = cmd.consume_args();
assert_eq!(args.lookup_parse("key").unwrap(), Some(2));
assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[2]]);
let mut args = cmd.consume_args();
assert_eq!(args.lookup_parse("foo").unwrap(), Some("bar".to_string()));
assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[3]]);
let mut args = cmd.consume_args();
assert!(args.lookup_parse::<bool>("key").is_err());
assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2], &cmd.args[3]]);
}
#[test]
fn argument_consumer_next() {
let cmd = cmd!("cmd foo key=1 key=2 bar");
let mut args = cmd.consume_args();
assert_eq!(args.next(), Some(&cmd.args[0]));
assert_eq!(args.next(), Some(&cmd.args[1]));
assert_eq!(args.next(), Some(&cmd.args[2]));
assert_eq!(args.next(), Some(&cmd.args[3]));
assert_eq!(args.next(), None);
assert!(args.rest().is_empty());
let mut args = cmd.consume_args();
assert_eq!(args.next_key(), Some(&cmd.args[1]));
assert_eq!(args.next_key(), Some(&cmd.args[2]));
assert_eq!(args.next_key(), None);
assert_eq!(args.next(), Some(&cmd.args[0]));
assert_eq!(args.next(), Some(&cmd.args[3]));
assert_eq!(args.next(), None);
assert!(args.rest().is_empty());
let mut args = cmd.consume_args();
assert_eq!(args.next_pos(), Some(&cmd.args[0]));
assert_eq!(args.next_pos(), Some(&cmd.args[3]));
assert_eq!(args.next_pos(), None);
assert_eq!(args.next(), Some(&cmd.args[1]));
assert_eq!(args.next(), Some(&cmd.args[2]));
assert_eq!(args.next(), None);
assert!(args.rest().is_empty());
}
#[test]
fn argument_consumer_reject_rest() {
let cmd = cmd!("cmd");
assert!(cmd.consume_args().reject_rest().is_ok());
let cmd = cmd!("cmd value");
let mut args = cmd.consume_args();
assert_eq!(args.reject_rest().unwrap_err().to_string(), "invalid argument 'value'");
assert!(!args.rest().is_empty());
let cmd = cmd!("cmd key=value");
let mut args = cmd.consume_args();
assert_eq!(args.reject_rest().unwrap_err().to_string(), "invalid argument 'key'");
assert!(!args.rest().is_empty());
}
#[test]
fn argument_consumer_rest() {
let cmd = cmd!("cmd foo key=1 key=2 bar");
let mut args = cmd.consume_args();
assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[1], &cmd.args[2], &cmd.args[3]]);
assert!(args.rest().is_empty());
let mut args = cmd.consume_args();
assert_eq!(args.rest_pos(), vec![&cmd.args[0], &cmd.args[3]]);
assert!(args.rest_pos().is_empty());
assert_eq!(args.rest(), vec![&cmd.args[1], &cmd.args[2]]);
let mut args = cmd.consume_args();
assert_eq!(args.rest_key(), vec![&cmd.args[1], &cmd.args[2]]);
assert!(args.rest_key().is_empty());
assert_eq!(args.rest(), vec![&cmd.args[0], &cmd.args[3]]);
}
}