rooty_derive 0.1.4

see the `rooty` crate
Documentation
//! Little parser/formatter for our needs
//!
//! We use the same format as for rust string formatting (`{placeholder}` captures `placeholder`,
//! `{{`, `}}` are literal brackets).
use std::{fmt, iter::Peekable};
use syn::Ident;

#[derive(Debug, Eq, PartialEq)]
pub enum Error {
    InvalidPlaceholder { ident: String },
    UnexpectedRParen,
    EndOfInputInPlaceholder,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::InvalidPlaceholder { ident } => {
                write!(f, "the value \"{}\" is not a valid placeholder", ident)
            }
            Error::UnexpectedRParen => write!(f, "Found an unmatched right parentesis ('}}')"),
            Error::EndOfInputInPlaceholder => write!(
                f,
                "Reached end of input when searching for match for left parenthesis ('{{')"
            ),
        }
    }
}

impl std::error::Error for Error {}

#[derive(Debug)]
struct Lexer<'src> {
    input: &'src str,
}

impl<'src> Lexer<'src> {
    fn new(input: &'src str) -> Self {
        Lexer { input }
    }
}

impl<'src> Iterator for Lexer<'src> {
    type Item = Result<TokenBorrowed<'src>, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        let mut char_indices = self.input.char_indices().peekable();
        match char_indices.next() {
            // Either start of placeholder or literal
            Some((_, ch)) if ch == '{' => {
                // handle lparen literal
                if let Some((_, ch)) = char_indices.peek() {
                    if *ch == '{' {
                        self.input = &self.input[2..];
                        return Some(Ok(TokenBorrowed::Literal("{")));
                    }
                }
                while let Some((next_idx, next_ch)) = char_indices.next() {
                    if next_ch == '}' {
                        // this shouldn't panic because '}' and '{' are always length 1
                        let placeholder = &self.input[1..next_idx];
                        self.input = &self.input[next_idx + 1..];
                        return Some(Ok(TokenBorrowed::Placeholder(placeholder)));
                    }
                }
                // if we got to here we're at the end of the input
                Some(Err(Error::EndOfInputInPlaceholder))
            }
            // unmatched closing brace
            Some((_, ch)) if ch == '}' => match char_indices.next() {
                Some((_, ch)) if ch == '}' => {
                    self.input = &self.input[2..];
                    Some(Ok(TokenBorrowed::Literal("}")))
                }
                _ => Some(Err(Error::UnexpectedRParen)),
            },
            Some((mut idx, ch)) => {
                idx = idx + ch.len_utf8();
                while let Some((next_idx, ch)) = char_indices.next() {
                    if ch == '{' || ch == '}' {
                        break;
                    }
                    idx = next_idx + ch.len_utf8();
                }
                let literal = &self.input[..idx];
                self.input = &self.input[idx..];
                Some(Ok(TokenBorrowed::Literal(literal)))
            }
            // end of input
            None => None,
        }
    }
}

// Iterator that joins sections together. Allocates to do this.
#[derive(Debug)]
pub struct JoiningIter<'src>(Peekable<Lexer<'src>>);

impl<'src> JoiningIter<'src> {
    fn new(inner: &'src str) -> Self {
        JoiningIter(Lexer::new(inner).peekable())
    }
}

impl<'src> Iterator for JoiningIter<'src> {
    type Item = Result<Token, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        let tok = self.0.next();
        match tok {
            Some(Ok(TokenBorrowed::Placeholder(placeholder))) => {
                let ident: Ident = match syn::parse_str(placeholder) {
                    Ok(ident) => ident,
                    Err(_) => {
                        return Some(Err(Error::InvalidPlaceholder {
                            ident: placeholder.to_owned(),
                        }))
                    }
                };
                Some(Ok(Token::Placeholder(ident)))
            }
            Some(Ok(TokenBorrowed::Literal(literal))) => {
                let mut literal = literal.to_owned();
                while let Some(Ok(TokenBorrowed::Literal(next_literal))) = self.0.peek() {
                    literal.push_str(next_literal);
                    self.0.next(); // consume the token
                }
                Some(Ok(Token::Literal(literal)))
            }
            Some(Err(e)) => Some(Err(e)),
            None => None,
        }
    }
}

pub(crate) fn parse_url<'src>(
    input: &'src str,
    //) -> impl Iterator<Item = Result<Token, Error>> + 'src {
) -> JoiningIter<'src> {
    JoiningIter::new(input)
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Token {
    Literal(String),
    Placeholder(Ident),
}

impl Token {
    /// Is this token a placeholder (not a literal)
    pub fn is_placeholder(&self) -> bool {
        if let Token::Placeholder(_) = self {
            true
        } else {
            false
        }
    }
    /// If this token is a placeholder, unwrap it.
    pub fn to_placeholder(self) -> Option<Ident> {
        match self {
            Token::Placeholder(ident) => Some(ident),
            _ => None,
        }
    }
    pub fn begins_with_forward_slash(&self) -> bool {
        match self {
            // I think we can assume the literal string is none-empty by now
            Token::Literal(s) => s.chars().next().unwrap() == '/',
            Token::Placeholder(_) => false,
        }
    }
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum TokenBorrowed<'src> {
    Literal(&'src str),
    Placeholder(&'src str),
}

#[cfg(test)]
mod tests {
    use super::*;
    use proc_macro2::Span;

    #[test]
    fn parses_url() {
        let url_to_parse = "/";
        let mut parser = parse_url(url_to_parse);
        assert_eq!(parser.next(), Some(Ok(Token::Literal("/".into()))));
        assert_eq!(parser.next(), None);

        let url_to_parse = "/this/is/the/{thing}/url/}}{{";
        let mut parser = parse_url(url_to_parse);
        assert_eq!(
            parser.next(),
            Some(Ok(Token::Literal("/this/is/the/".into())))
        );
        assert_eq!(
            parser.next(),
            Some(Ok(Token::Placeholder(Ident::new(
                "thing",
                Span::call_site()
            ))))
        );
        assert_eq!(parser.next(), Some(Ok(Token::Literal("/url/}{".into()))));
        assert_eq!(parser.next(), None);
    }

    #[test]
    fn fails_bad_urls() {
        // unmatched lparen
        let url_to_parse = "bad{url";
        let mut parser = parse_url(url_to_parse);
        assert_eq!(parser.next(), Some(Ok(Token::Literal("bad".into()))));
        assert_eq!(parser.next(), Some(Err(Error::EndOfInputInPlaceholder)));
        // unmatched rparen
        let url_to_parse = "bad}url";
        let mut parser = parse_url(url_to_parse);
        assert_eq!(parser.next(), Some(Ok(Token::Literal("bad".into()))));
        assert_eq!(parser.next(), Some(Err(Error::UnexpectedRParen)));
        // invalid ident
        let url_to_parse = "bad{struct}";
        let mut parser = parse_url(url_to_parse);
        assert_eq!(parser.next(), Some(Ok(Token::Literal("bad".into()))));
        assert_eq!(
            parser.next(),
            Some(Err(Error::InvalidPlaceholder {
                ident: "struct".into()
            }))
        );
        let url_to_parse = "bad{}";
        let mut parser = parse_url(url_to_parse);
        assert_eq!(parser.next(), Some(Ok(Token::Literal("bad".into()))));
        assert_eq!(
            parser.next(),
            Some(Err(Error::InvalidPlaceholder { ident: "".into() }))
        );
    }
}