use regex::Regex;
pub struct CommandLopper<'a> {
current_slice: &'a str,
next_idx: usize,
tokens: Vec<&'a str>,
}
impl<'a> CommandLopper<'a> {
pub fn new(tokens: Vec<&'a str>) -> Self {
Self {
current_slice: "",
next_idx: 0,
tokens,
}
}
}
impl CommandLopper<'_> {
fn check_next_slice(&mut self) {
if self.current_slice.is_empty() {
if self.next_idx < self.tokens.len() {
self.current_slice = self.tokens[self.next_idx];
self.next_idx += 1;
}
}
}
pub fn next_enum(&mut self, enums: &[&str]) -> Option<String> {
self.check_next_slice();
if self.current_slice.is_empty() {
return None;
}
for enum_str in enums {
if self.current_slice.starts_with(enum_str) {
self.current_slice = self.current_slice.strip_prefix(enum_str).unwrap_or("");
return Some(enum_str.to_string());
}
}
None
}
pub fn cut_plain_text(&mut self, text: &str) -> bool {
self.check_next_slice();
if self.current_slice.is_empty() {
return false;
}
if self.current_slice == text {
self.current_slice = "";
return true;
}
false
}
pub fn next_number(&mut self) -> Option<String> {
self.check_next_slice();
if self.current_slice.is_empty() {
return None;
}
let re = Regex::new(r"^\d+([.]\d+)?").unwrap();
let caps = re.captures(self.current_slice);
if caps.is_none() {
return None;
}
let caps = caps.unwrap();
let num = caps.get(0).unwrap().as_str().to_string();
self.current_slice = self.current_slice.strip_prefix(num.as_str()).unwrap_or("");
Some(num)
}
pub fn cut_text_to_space(&mut self) -> Option<String> {
self.check_next_slice();
if self.current_slice.is_empty() {
return None;
}
let result = self.current_slice.to_string();
self.current_slice = "";
Some(result)
}
pub fn cut_text_to_end(&mut self) -> Option<String> {
self.check_next_slice();
if self.current_slice.is_empty() {
return None;
}
let mut buffer = self.current_slice.to_string();
self.current_slice = "";
loop {
self.check_next_slice();
if self.current_slice.is_empty() {
break;
}
buffer += " ";
buffer += self.current_slice;
self.current_slice = "";
}
Some(buffer)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cut_plain_text_exact_match() {
let mut lopper = CommandLopper::new(vec!["runbot"]);
assert!(!lopper.cut_plain_text("run"), "runbot should not match run");
let mut lopper = CommandLopper::new(vec!["runbot"]);
assert!(lopper.cut_plain_text("runbot"), "runbot should match runbot");
}
#[test]
fn test_cut_plain_text_multi_token() {
let mut lopper = CommandLopper::new(vec!["run", "bot"]);
assert!(lopper.cut_plain_text("run"), "first token should match");
assert!(!lopper.current_slice.is_empty() || lopper.next_idx < lopper.tokens.len(),
"should have remaining tokens");
let mut lopper = CommandLopper::new(vec!["run", "bot"]);
assert!(lopper.cut_plain_text("run"));
lopper.check_next_slice();
assert!(lopper.cut_plain_text("bot"));
}
#[test]
fn test_cut_plain_text_single_token() {
let mut lopper = CommandLopper::new(vec!["run"]);
assert!(lopper.cut_plain_text("run"), "run should match run");
assert!(lopper.current_slice.is_empty(), "should consume the token");
}
#[test]
fn test_cut_plain_text_no_match() {
let mut lopper = CommandLopper::new(vec!["hello"]);
assert!(!lopper.cut_plain_text("world"), "hello should not match world");
}
#[test]
fn test_cut_plain_text_empty() {
let mut lopper = CommandLopper::new(vec![]);
assert!(!lopper.cut_plain_text("run"), "empty should not match");
}
#[test]
fn test_next_enum_prefix_match() {
let mut lopper = CommandLopper::new(vec!["-runbot"]);
assert_eq!(lopper.next_enum(&["-", "/", "~"]), Some("-".to_string()));
assert_eq!(lopper.current_slice, "runbot");
let mut lopper = CommandLopper::new(vec!["/test"]);
assert_eq!(lopper.next_enum(&["-", "/", "~"]), Some("/".to_string()));
}
#[test]
fn test_next_enum_no_match() {
let mut lopper = CommandLopper::new(vec!["runbot"]);
assert_eq!(lopper.next_enum(&["-", "/", "~"]), None);
}
#[test]
fn test_next_number() {
let mut lopper = CommandLopper::new(vec!["123"]);
assert_eq!(lopper.next_number(), Some("123".to_string()));
let mut lopper = CommandLopper::new(vec!["123.45"]);
assert_eq!(lopper.next_number(), Some("123.45".to_string()));
let mut lopper = CommandLopper::new(vec!["abc"]);
assert_eq!(lopper.next_number(), None);
}
#[test]
fn test_cut_text_to_space() {
let mut lopper = CommandLopper::new(vec!["hello", "world"]);
assert_eq!(lopper.cut_text_to_space(), Some("hello".to_string()));
assert_eq!(lopper.cut_text_to_space(), Some("world".to_string()));
}
#[test]
fn test_cut_text_to_end() {
let mut lopper = CommandLopper::new(vec!["hello", "world", "test"]);
assert_eq!(lopper.cut_text_to_end(), Some("hello world test".to_string()));
}
#[test]
fn test_complex_scenario_run_vs_runbot() {
let mut lopper = CommandLopper::new(vec!["-", "runbot"]);
assert_eq!(lopper.next_enum(&["-", "/", "~"]), Some("-".to_string()));
assert!(!lopper.cut_plain_text("run"), "runbot should not match run");
let mut lopper = CommandLopper::new(vec!["-", "runbot"]);
assert_eq!(lopper.next_enum(&["-", "/", "~"]), Some("-".to_string()));
assert!(lopper.cut_plain_text("runbot"), "runbot should match runbot");
}
#[test]
fn test_complex_scenario_run_bot() {
let mut lopper = CommandLopper::new(vec!["-", "run", "bot"]);
assert_eq!(lopper.next_enum(&["-", "/", "~"]), Some("-".to_string()));
assert!(lopper.cut_plain_text("run"));
lopper.check_next_slice();
assert!(!lopper.current_slice.is_empty(), "should have remaining token 'bot'");
let mut lopper = CommandLopper::new(vec!["-", "run", "bot"]);
assert_eq!(lopper.next_enum(&["-", "/", "~"]), Some("-".to_string()));
assert!(lopper.cut_plain_text("run"));
lopper.check_next_slice();
assert!(lopper.cut_plain_text("bot"));
}
#[test]
fn test_whitespace_handling() {
let input = "run bot ";
let tokens: Vec<&str> = input.trim().split_ascii_whitespace().collect();
assert_eq!(tokens, vec!["run", "bot"], "trailing spaces should be ignored");
let mut lopper = CommandLopper::new(tokens);
assert!(lopper.cut_plain_text("run"), "should match 'run'");
lopper.check_next_slice();
assert!(lopper.cut_plain_text("bot"), "should match 'bot'");
}
#[test]
fn test_whitespace_variations() {
let input1 = "run bot ";
let tokens1: Vec<&str> = input1.trim().split_ascii_whitespace().collect();
assert_eq!(tokens1, vec!["run", "bot"]);
let mut lopper1 = CommandLopper::new(tokens1);
assert!(lopper1.cut_plain_text("run"));
lopper1.check_next_slice();
assert!(lopper1.cut_plain_text("bot"));
let input2 = " run bot";
let tokens2: Vec<&str> = input2.trim().split_ascii_whitespace().collect();
assert_eq!(tokens2, vec!["run", "bot"]);
let mut lopper2 = CommandLopper::new(tokens2);
assert!(lopper2.cut_plain_text("run"));
lopper2.check_next_slice();
assert!(lopper2.cut_plain_text("bot"));
let input3 = "run bot";
let tokens3: Vec<&str> = input3.trim().split_ascii_whitespace().collect();
assert_eq!(tokens3, vec!["run", "bot"]);
let mut lopper3 = CommandLopper::new(tokens3);
assert!(lopper3.cut_plain_text("run"));
lopper3.check_next_slice();
assert!(lopper3.cut_plain_text("bot"));
let input4 = " run bot ";
let tokens4: Vec<&str> = input4.trim().split_ascii_whitespace().collect();
assert_eq!(tokens4, vec!["run", "bot"]);
let mut lopper4 = CommandLopper::new(tokens4);
assert!(lopper4.cut_plain_text("run"));
lopper4.check_next_slice();
assert!(lopper4.cut_plain_text("bot"));
}
#[test]
fn test_whitespace_with_prefix() {
let input = "-run bot ";
let tokens: Vec<&str> = input.trim().split_ascii_whitespace().collect();
assert_eq!(tokens, vec!["-run", "bot"]);
let input2 = "- run bot ";
let tokens2: Vec<&str> = input2.trim().split_ascii_whitespace().collect();
assert_eq!(tokens2, vec!["-", "run", "bot"]);
let mut lopper = CommandLopper::new(tokens2);
assert_eq!(lopper.next_enum(&["-", "/", "~"]), Some("-".to_string()));
assert!(lopper.cut_plain_text("run"));
lopper.check_next_slice();
assert!(lopper.cut_plain_text("bot"));
}
}