struct-string-template 0.1.0

Simple string templating language on struct fields
Documentation
use unicode_segmentation::UnicodeSegmentation;

use crate::err::TemplateError;


#[derive(Debug, PartialEq, Eq, Clone)]
pub enum TemplateElement {
    Part {
        literal: String
    },
    Replace {
        selector: String
    },
}

pub struct Formatter {
    pub(crate) elements: Vec<TemplateElement>
}

impl IntoIterator for Formatter {
    type Item = TemplateElement;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.elements.into_iter()
    }
}

impl Formatter {
    pub fn build<S>(fmts: S) -> Result<Self, TemplateError>
        where S: Into<String>
    {
        let as_str = fmts.into();
        let mut chars = as_str.graphemes(true);
        let mut result = Vec::new();
        let mut tmp = String::new();

        while let Some(c) = chars.next() {
            if c != "%" {
                tmp.push_str(c);
                continue;
            }

            match chars.next() {
                Some("%") => {
                    tmp.push_str("%")
                },
                Some("(") => {
                    if !tmp.is_empty() {
                        result.push(TemplateElement::Part { literal: tmp.clone() });
                    }
                    tmp.clear();
                    let mut selector = String::new();
                    loop {
                        match chars.next() {
                            Some(")") => break,
                            Some(c) => selector.push_str(c),
                            None => return Err(TemplateError::UnexpectedEnd { formats: as_str }),
                        }
                    }
                    result.push(TemplateElement::Replace { selector });
                },
                Some(c) => return Err(TemplateError::UnexpectedCharacter {
                    character: c.to_string(),
                    formats: as_str,
                }),
                None => return Err(TemplateError::UnexpectedEnd { formats: as_str }),
            };
        }

        if !tmp.is_empty() {
            result.push(TemplateElement::Part { literal: tmp.clone() });
        }

        Ok(Self { elements: result })
    }
}

#[cfg(test)]
mod tests {
    use crate::formatter::TemplateElement;
    use crate::err::TemplateError;
    use super::Formatter;

    fn repl(s: &str) -> TemplateElement {
        TemplateElement::Replace { selector: s.to_owned() }
    }

    fn part(s: &str) -> TemplateElement {
        TemplateElement::Part { literal: s.to_owned() }
    }

    #[test]
    fn test_valid() {
        let fmts = "%(title) %(id) %%(select)   ";
        let result = Formatter::build(fmts);
        assert!(result.is_ok());
        let formatter = result.ok().unwrap();
        assert_eq!(formatter.elements, vec![
            repl("title"),
            part(" "),
            repl("id"),
            part(" %(select)   "),
        ])
    }

    #[test]
    fn test_valid_empty() {
        let fmts = "";
        let result = Formatter::build(fmts);
        assert!(result.is_ok());
        let formatter = result.ok().unwrap();
        assert_eq!(formatter.elements, vec![])
    }

    #[test]
    fn test_valid_whitespace() {
        let fmts = "\n\t ";
        let result = Formatter::build(fmts);
        assert!(result.is_ok());
        let formatter = result.ok().unwrap();
        assert_eq!(formatter.elements, vec![
            part("\n\t "),
        ])
    }

    #[test]
    fn test_valid_nonalnum_in_selector() {
        let fmts = "%(a.b.c.d)%(p//  q)%((?;:'))";
        let result = Formatter::build(fmts);
        assert!(result.is_ok());
        let formatter = result.ok().unwrap();
        assert_eq!(formatter.elements, vec![
            repl("a.b.c.d"),
            repl("p//  q"),
            repl("(?;:'"),
            part(")"),
        ])
    }

    #[test]
    fn test_valid_marker_literal_simple() {
        let fmts = "%%%%";
        let result = Formatter::build(fmts);
        assert!(result.is_ok());
        let formatter = result.ok().unwrap();
        assert_eq!(formatter.elements, vec![
            part("%%"),
        ])
    }

    #[test]
    fn test_valid_marker_literal_complex() {
        let fmts = "%%%%%%%(a)P%%%%";
        let result = Formatter::build(fmts);
        assert!(result.is_ok());
        let formatter = result.ok().unwrap();
        assert_eq!(formatter.elements, vec![
            part("%%%"),
            repl("a"),
            part("P%%"),
        ])
    }

    #[test]
    fn test_invalid_marker_not_followed_by_open_paren() {
        let fmts = "%%()%l";
        let result = Formatter::build(fmts);
        assert!(result.is_err());
        assert!(matches!(result, Err(TemplateError::UnexpectedCharacter { .. })))
    }

    #[test]
    fn test_invalid_unterminated_selector() {
        let fmts = "%%%(abcd pl";
        let result = Formatter::build(fmts);
        assert!(result.is_err());
        assert!(matches!(result, Err(TemplateError::UnexpectedEnd { .. })))
    }

    #[test]
    fn test_invalid_end_reached_after_marker() {
        let fmts = "%%%%%";
        let result = Formatter::build(fmts);
        assert!(result.is_err());
        assert!(matches!(result, Err(TemplateError::UnexpectedEnd { .. })))
    }
}