#![allow(dead_code)]
use proc_macro2::Delimiter;
use syn::parse::discouraged::AnyDelimiter;
use syn::{Ident, LitStr, Token, parse::ParseStream};
pub fn parse_comma_list<T, F>(input: ParseStream, mut parser: F) -> syn::Result<Vec<T>>
where
F: FnMut(ParseStream) -> syn::Result<T>,
{
let mut items = Vec::new();
while !input.is_empty() {
items.push(parser(input)?);
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
} else {
break;
}
}
Ok(items)
}
pub fn parse_bracketed_list<T, F>(input: ParseStream, parser: F) -> syn::Result<Vec<T>>
where
F: Fn(ParseStream) -> syn::Result<T>,
{
let (delim, _span, content) = input.parse_any_delimiter()?;
if delim != Delimiter::Bracket {
return Err(content.error("expected brackets"));
}
parse_comma_list(&content, parser)
}
pub fn try_parse_key(input: ParseStream) -> syn::Result<Option<String>> {
let lookahead = input.lookahead1();
if lookahead.peek(Ident) {
let ident: Ident = input.parse()?;
Ok(Some(ident.to_string()))
} else if lookahead.peek(LitStr) {
Ok(None)
} else {
Err(lookahead.error())
}
}
pub fn parse_key_value_list<F>(input: ParseStream, mut handler: F) -> syn::Result<()>
where
F: FnMut(String, ParseStream) -> syn::Result<()>,
{
while !input.is_empty() {
let lookahead = input.lookahead1();
if lookahead.peek(Ident) {
let ident: Ident = input.parse()?;
let key = ident.to_string();
handler(key, input)?;
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
} else {
break;
}
} else if lookahead.peek(LitStr) {
break;
} else {
return Err(lookahead.error());
}
}
Ok(())
}
pub fn try_consume_comma(input: ParseStream) -> bool {
if input.peek(Token![,]) {
let _ = input.parse::<Token![,]>();
true
} else {
false
}
}
#[cfg(test)]
mod tests {
use syn::parse::Parser;
use super::*;
#[test]
fn test_parse_comma_list_single() {
let parser = |input: ParseStream| {
parse_comma_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!("a", "b", "c");
let result = parser.parse2(tokens);
assert!(result.is_ok());
let items: Vec<String> = result.unwrap();
assert_eq!(items, vec!["a", "b", "c"]);
}
#[test]
fn test_parse_comma_list_with_trailing_comma() {
let parser = |input: ParseStream| {
parse_comma_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!("x", "y",);
let result = parser.parse2(tokens);
assert!(result.is_ok());
let items: Vec<String> = result.unwrap();
assert_eq!(items, vec!["x", "y"]);
}
#[test]
fn test_parse_bracketed_list_strings() {
let parser = |input: ParseStream| {
parse_bracketed_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!(["a", "b", "c"]);
let result = parser.parse2(tokens);
assert!(result.is_ok());
let items: Vec<String> = result.unwrap();
assert_eq!(items, vec!["a", "b", "c"]);
}
#[test]
fn test_try_parse_key_ident() {
let parser = |input: ParseStream| try_parse_key(input);
let tokens = quote::quote!(title);
let result = parser.parse2(tokens);
assert!(result.is_ok());
let key = result.unwrap();
assert_eq!(key, Some("title".to_string()));
}
#[test]
fn test_try_consume_comma_logic() {
let parser = |input: ParseStream| {
let has_comma = try_consume_comma(input);
Ok(has_comma)
};
let tokens = quote::quote!(,);
let result = parser.parse2(tokens);
assert!(result.is_ok());
assert!(result.unwrap());
}
#[test]
fn test_try_parse_key_litstr() {
let parser = |input: ParseStream| {
let result = try_parse_key(input)?;
let _: LitStr = input.parse()?;
Ok(result)
};
let tokens = quote::quote!("some_string");
let result = parser.parse2(tokens);
assert!(result.is_ok());
assert_eq!(result.unwrap(), None);
}
#[test]
fn test_try_parse_key_invalid_token() {
let parser = |input: ParseStream| try_parse_key(input);
let tokens = quote::quote!(42);
let result = parser.parse2(tokens);
assert!(result.is_err());
}
#[test]
fn test_try_consume_comma_no_comma() {
let parser = |input: ParseStream| {
let has_comma = try_consume_comma(input);
let _: Ident = input.parse()?;
Ok(has_comma)
};
let tokens = quote::quote!(foo);
let result = parser.parse2(tokens);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[test]
fn test_parse_key_value_handler() {
let parser = |input: ParseStream| {
let mut title = None;
let mut version = None;
parse_key_value_list(input, |key, input| {
match key.as_str() {
"title" => {
input.parse::<Token![=]>()?;
title = Some(input.parse::<LitStr>()?.value());
}
"version" => {
input.parse::<Token![=]>()?;
version = Some(input.parse::<LitStr>()?.value());
}
_ => {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"unknown key",
));
}
}
Ok(())
})?;
Ok((title, version))
};
let tokens = quote::quote!(title = "Test", version = "1.0");
let result = parser.parse2(tokens);
assert!(result.is_ok());
let (title, version) = result.unwrap();
assert_eq!(title, Some("Test".to_string()));
assert_eq!(version, Some("1.0".to_string()));
}
#[test]
fn test_parse_key_value_list_litstr_breaks() {
let parser = |input: ParseStream| {
let mut keys = Vec::new();
parse_key_value_list(input, |key, input| {
input.parse::<Token![=]>()?;
let _: LitStr = input.parse()?;
keys.push(key);
Ok(())
})?;
let _: LitStr = input.parse()?;
Ok(keys)
};
let tokens = quote::quote!(title = "Test", "remaining");
let result = parser.parse2(tokens);
assert!(result.is_ok());
assert_eq!(result.unwrap(), vec!["title"]);
}
#[test]
fn test_parse_key_value_list_invalid_token() {
let parser = |input: ParseStream| {
parse_key_value_list(input, |_key, input| {
input.parse::<Token![=]>()?;
let _: LitStr = input.parse()?;
Ok(())
})
};
let tokens = quote::quote!(42);
let result = parser.parse2(tokens);
assert!(result.is_err());
}
#[test]
fn test_parse_bracketed_list_empty() {
let parser = |input: ParseStream| {
parse_bracketed_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!([]);
let result = parser.parse2(tokens);
assert!(result.is_ok());
let items: Vec<String> = result.unwrap();
assert!(items.is_empty());
}
#[test]
fn test_parse_bracketed_list_single_item() {
let parser = |input: ParseStream| {
parse_bracketed_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!(["single"]);
let result = parser.parse2(tokens);
assert!(result.is_ok());
let items: Vec<String> = result.unwrap();
assert_eq!(items, vec!["single"]);
}
#[test]
fn test_parse_bracketed_list_with_trailing_comma() {
let parser = |input: ParseStream| {
parse_bracketed_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!(["a", "b",]);
let result = parser.parse2(tokens);
assert!(result.is_ok());
let items: Vec<String> = result.unwrap();
assert_eq!(items, vec!["a", "b"]);
}
#[test]
fn test_parse_bracketed_list_integers() {
use syn::LitInt;
let parser = |input: ParseStream| {
parse_bracketed_list(input, |input| {
input
.parse::<LitInt>()
.and_then(|lit| lit.base10_parse::<i32>())
})
};
let tokens = quote::quote!([1, 2, 3]);
let result = parser.parse2(tokens);
assert!(result.is_ok());
let items: Vec<i32> = result.unwrap();
assert_eq!(items, vec![1, 2, 3]);
}
#[test]
fn test_parse_bracketed_list_wrong_delimiter_parens() {
let parser = |input: ParseStream| {
parse_bracketed_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!(("a", "b"));
let result = parser.parse2(tokens);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("expected brackets"));
}
#[test]
fn test_parse_bracketed_list_wrong_delimiter_braces() {
let parser = |input: ParseStream| {
parse_bracketed_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!({"a", "b"});
let result = parser.parse2(tokens);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("expected brackets"));
}
#[test]
fn test_parse_bracketed_list_no_delimiter() {
let parser = |input: ParseStream| {
parse_bracketed_list(input, |input| {
input.parse::<LitStr>().map(|lit| lit.value())
})
};
let tokens = quote::quote!("just_a_string");
let result = parser.parse2(tokens);
assert!(result.is_err());
}
}