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() {
Some((_, ch)) if ch == '{' => {
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 == '}' {
let placeholder = &self.input[1..next_idx];
self.input = &self.input[next_idx + 1..];
return Some(Ok(TokenBorrowed::Placeholder(placeholder)));
}
}
Some(Err(Error::EndOfInputInPlaceholder))
}
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)))
}
None => None,
}
}
}
#[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(); }
Some(Ok(Token::Literal(literal)))
}
Some(Err(e)) => Some(Err(e)),
None => None,
}
}
}
pub(crate) fn parse_url<'src>(
input: &'src str,
) -> JoiningIter<'src> {
JoiningIter::new(input)
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Token {
Literal(String),
Placeholder(Ident),
}
impl Token {
pub fn is_placeholder(&self) -> bool {
if let Token::Placeholder(_) = self {
true
} else {
false
}
}
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 {
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() {
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)));
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)));
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() }))
);
}
}