use std::{borrow::Cow, fmt}; pub struct Amanda<'raw> { tokens: Vec>, set_vars: Vec, } impl<'raw> Amanda<'raw> { pub fn has(&self, var: &str) -> bool { for tok in &self.tokens { if let Token::Variable { name } = tok && var.eq(*name) { return true; } } false } pub fn set>, V: fmt::Display>(&mut self, key: K, value: V) { self.set_vars.push(SetVar { name: key.into(), value: value.to_string(), }); } pub fn set_many> + Copy, V: fmt::Display, I>( &mut self, key: K, slice: I, ) where I: Iterator, { slice.for_each(|item| self.set(key, item)); } pub fn parse(raw: &'raw str) -> Self { let mut tokens = vec![]; let mut curr = raw; loop { if curr.is_empty() { break; } match curr.char_indices().find(|(_, ch)| *ch == '{') { None => { tokens.push(Token::text(curr)); break; } Some((open, _)) => match curr.char_indices().find(|(_, ch)| *ch == '}') { None => { tokens.push(Token::text(curr)); break; } Some((close, _)) => { let before = &curr[..open]; let key = &curr[open + 1..close]; let after = &curr[close + 1..]; if key.contains(char::is_whitespace) { tokens.push(Token::text(&curr[..=close])); curr = after; } else { if !before.is_empty() { tokens.push(Token::text(before)); } tokens.push(Token::var(key)); curr = after; } } }, } } Amanda { tokens, set_vars: vec![], } } } impl<'raw> fmt::Display for Amanda<'raw> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for tok in &self.tokens { match tok { Token::Text(txt) => write!(f, "{txt}")?, Token::Variable { name } => { let mut set_one = false; for set in self.set_vars.iter().filter(|sv| sv.name.eq(name)) { set_one = true; write!(f, "{}", set.value)?; } if !set_one { write!(f, "{{{name}}}")?; } } } } Ok(()) } } struct SetVar { name: Cow<'static, str>, value: String, } #[derive(Copy, Clone, Debug, PartialEq)] enum Token<'raw> { Text(&'raw str), Variable { name: &'raw str }, } impl<'raw> Token<'raw> { pub fn text(text: &'raw str) -> Self { Self::Text(text) } pub fn var(name: &'raw str) -> Self { Self::Variable { name } } } #[cfg(test)] mod test { use crate::amanda::{Amanda, Token}; #[test] fn parses_text() { let raw = "text"; let expected = vec![Token::text("text")]; let actual = Amanda::parse(raw); assert_eq!(actual.tokens, expected) } #[test] fn parses_variable() { let raw = "{foo}"; let expected = vec![Token::var("foo")]; let actual = Amanda::parse(raw); assert_eq!(actual.tokens, expected) } #[test] fn parses_text_and_varaible() { let raw = "text {var}"; let expected = vec![Token::text("text "), Token::var("var")]; let actual = Amanda::parse(raw); assert_eq!(actual.tokens, expected) } #[test] fn parses_sequential_variables() { let raw = "{foo}{bar}{bing}"; let expected = vec![Token::var("foo"), Token::var("bar"), Token::var("bing")]; let actual = Amanda::parse(raw); assert_eq!(actual.tokens, expected) } #[test] fn parses_multiline_text() { let raw = "one line\ntwo line\nthree line"; let expected = vec![Token::text(raw)]; let actual = Amanda::parse(raw); assert_eq!(actual.tokens, expected) } #[test] fn parses_complex() { let raw = "Dear {name},\nI am contacting you today about your cars extended warranty.\n\nSincerely,\n{bad-agency}"; let expected = vec![ Token::text("Dear "), Token::var("name"), Token::text( ",\nI am contacting you today about your cars extended warranty.\n\nSincerely,\n", ), Token::var("bad-agency"), ]; let actual = Amanda::parse(raw); assert_eq!(actual.tokens, expected) } #[test] fn replaces_var_once() { let raw = "{foo}"; let mut am = Amanda::parse(raw); am.set("foo", 27); assert_eq!("27", am.to_string()) } #[test] fn replace_var_many() { let raw = "{foo}"; let mut am = Amanda::parse(raw); am.set_many("foo", ["(one)", "(two)"].iter()); assert_eq!("(one)(two)", am.to_string()) } }