use nom::combinator::rest; use nom::error::ErrorKind; use nom::{Err, IResult}; use std::collections::HashMap; use std::fmt::{self, Display}; use std::marker::PhantomData; #[derive(Debug, PartialEq, Eq, Clone)] pub struct Path<'a> { head: &'a str, tail: Vec<&'a str>, } impl<'a> Path<'a> { fn new(head: &'a str, tail: Vec<&'a str>) -> Self { Path { head, tail } } } impl<'a> Display for Path<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.head)?; for part in &self.tail { write!(f, ".{}", part)?; } Ok(()) } } // named!(path_ident, map_res!(is_not!(".}"), std::str::from_utf8)); fn path_ident<'a>(input: &'a str) -> IResult<&'a str, &'a str> { take_till!(input, |c| c == '.' || c == '}') } fn path<'a>(input: &'a str) -> IResult<&'a str, Path<'a>> { map!( input, tuple!( path_ident, many0!(complete!(preceded!(char!('.'), path_ident))) ), |(h, t)| Path::new(h, t) ) } #[derive(Debug, PartialEq, Eq, Clone)] pub enum TemplateToken<'a> { Literal(&'a str), Substitution(Path<'a>), } fn token_substitution<'a>( input: &'a str, ) -> IResult<&'a str, TemplateToken<'a>> { map!( input, delimited!(tag!("{{"), path, tag!("}}")), TemplateToken::Substitution ) } fn template_token<'a>(input: &'a str) -> IResult<&'a str, TemplateToken<'a>> { alt!( input, token_substitution | map!( alt!(complete!(take_until!("{{")) | complete!(rest)), TemplateToken::Literal ) ) } #[derive(Debug, PartialEq, Eq, Clone)] pub struct Template<'a> { tokens: Vec>, } impl<'a> Template<'a> { pub fn new(tokens: Vec>) -> Self { Template { tokens } } } pub struct TemplateVisitor<'a> { marker: PhantomData Template<'a>>, } impl<'a> TemplateVisitor<'a> { pub fn new() -> Self { TemplateVisitor { marker: PhantomData, } } } impl<'a> serde::de::Visitor<'a> for TemplateVisitor<'a> { type Value = Template<'a>; fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str("a valid template string") } fn visit_borrowed_str( self, v: &'a str, ) -> Result { Template::parse(v).map_err(|_| { serde::de::Error::invalid_value( serde::de::Unexpected::Str(v), &"a valid template string", ) }) } } impl<'a> serde::Deserialize<'a> for Template<'a> { fn deserialize>( deserializer: D, ) -> Result { deserializer.deserialize_str(TemplateVisitor::new()) } } impl<'a> Template<'a> { pub fn parse( input: &'a str, ) -> Result, Err<(&'a str, ErrorKind)>> { let (remaining, res) = template(input)?; if !remaining.is_empty() { unreachable!(); } Ok(res) } pub fn format( &self, params: &TemplateParams<'a>, ) -> Result> { use TemplateToken::*; let mut res = String::new(); for token in &self.tokens { match token { Literal(s) => res.push_str(s), Substitution(p) => match params.get(p.clone()) { Some(s) => res.push_str(s), None => return Err(TemplateError::MissingParam(p.clone())), }, } } Ok(res) } } #[derive(Debug, PartialEq, Eq)] pub enum TemplateError<'a> { MissingParam(Path<'a>), } impl<'a> Display for TemplateError<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use TemplateError::*; match self { MissingParam(path) => { write!(f, "Missing template parameter: {}", path) } } } } #[derive(Debug, PartialEq, Eq)] pub enum TemplateParams<'a> { Direct(&'a str), Nested(HashMap<&'a str, TemplateParams<'a>>), } impl<'a> TemplateParams<'a> { fn get(&self, path: Path<'a>) -> Option<&'a str> { use TemplateParams::*; match self { Direct(_) => None, Nested(m) => m.get(path.head).and_then(|next| { if path.tail.is_empty() { match next { Direct(s) => Some(*s), _ => None, } } else { next.get(Path { head: path.tail[0], tail: path.tail[1..].to_vec(), }) } }), } } } #[macro_export] macro_rules! template_params { (@count $head: expr => $hv: tt, $($rest:tt)+) => { 1 + template_params!(@count $($rest)+) }; (@count $one:expr => $($ov: tt)*) => { 1 }; (@inner $ret: ident, ($key: expr => {$($v:tt)*}, $($r:tt)*)) => { $ret.insert($key, template_params!({ $($v)* })); template_params!(@inner $ret, ($($r)*)); }; (@inner $ret: ident, ($key: expr => $value: expr, $($r:tt)*)) => { $ret.insert($key, template_params!($value)); template_params!(@inner $ret, ($($r)*)); }; (@inner $ret: ident, ()) => {}; ({ $($body: tt)* }) => {{ let _cap = template_params!(@count $($body)*); let mut _m = ::std::collections::HashMap::with_capacity(_cap); template_params!(@inner _m, ($($body)*)); TemplateParams::Nested(_m) }}; ($direct:expr) => { TemplateParams::Direct($direct) }; () => { TemplateParams::Nested(::std::collections::HashMap::new()) }; } fn template<'a>(input: &'a str) -> IResult<&'a str, Template<'a>> { complete!( input, map!(many1!(complete!(template_token)), Template::new) ) } #[cfg(test)] mod tests { use super::*; #[test] fn test_parse_path_ident() { assert_eq!(path_ident("foo}}"), Ok(("}}", "foo"))); assert_eq!(path_ident("foo.bar}}"), Ok((".bar}}", "foo"))); } #[test] fn test_parse_path() { assert_eq!(path("foo}}"), Ok(("}}", Path::new("foo", vec![])))); assert_eq!( path("foo.bar}}"), Ok(("}}", Path::new("foo", vec!["bar"]))) ); assert_eq!( path("foo.bar.baz}}"), Ok(("}}", Path::new("foo", vec!["bar", "baz"]))) ); } #[test] fn test_parse_template_token() { assert_eq!( template_token("foo bar"), Ok(("", TemplateToken::Literal("foo bar"))) ); assert_eq!( template_token("foo bar {{baz}}"), Ok(("{{baz}}", TemplateToken::Literal("foo bar "))) ); assert_eq!( template_token("{{baz}}"), Ok(( "", TemplateToken::Substitution(Path::new("baz", Vec::new())) )) ); assert_eq!( template_token("{{baz}} foo bar"), Ok(( " foo bar", TemplateToken::Substitution(Path::new("baz", Vec::new())) )) ); } #[test] fn test_parse_template() { assert_eq!( template("foo bar"), Ok(( "", Template { tokens: vec![TemplateToken::Literal("foo bar")] } )) ); assert_eq!( template("foo bar {{baz}} qux"), Ok(( "", Template { tokens: vec![ TemplateToken::Literal("foo bar "), TemplateToken::Substitution(Path::new( "baz", Vec::new() )), TemplateToken::Literal(" qux"), ] } )) ); } #[test] fn test_template_params_literal() { // trace_macros!(true); let expected = template_params!({ "direct" => "hi", "other" => "here", "nested" => { "one" => "1", "two" => "2", "double" => { "three" => "3", }, }, }); // trace_macros!(false); assert_eq!( TemplateParams::Nested(hashmap! { "direct" => TemplateParams::Direct("hi"), "other" => TemplateParams::Direct("here"), "nested" => TemplateParams::Nested(hashmap!{ "one" => TemplateParams::Direct("1"), "two" => TemplateParams::Direct("2"), "double" => TemplateParams::Nested(hashmap!{ "three" => TemplateParams::Direct("3"), }) }) }), expected, ) } #[test] fn test_format_template() { assert_eq!( "foo bar baz qux", Template::parse("foo {{x}} {{y.z}} {{y.w.z}}") .unwrap() .format(&template_params!({ "x" => "bar", "y" => { "z" => "baz", "w" => { "z" => "qux", }, }, })) .unwrap() ) } }