use super::ast::PureExpr;
use super::convert::ToPure;
use proc_macro2::{TokenStream, TokenTree};
use quote::ToTokens;
const COLLECTION_MACROS: &[&str] = &["vec", "array_vec"];
const MAP_MACROS: &[&str] = &["hashmap", "btreemap", "indexmap"];
pub fn try_extract_exprs(name: &str, tokens: &str) -> Option<Vec<PureExpr>> {
let macro_name = name.rsplit("::").next().unwrap_or(name);
if COLLECTION_MACROS.contains(¯o_name) {
parse_comma_separated(tokens)
} else if MAP_MACROS.contains(¯o_name) {
parse_map_values(tokens)
} else {
None
}
}
pub fn exprs_to_tokens(exprs: &[PureExpr]) -> Result<String, super::to_syn::ToSynError> {
use super::to_syn::ToSyn;
let parts = exprs
.iter()
.map(|e| Ok(e.to_syn()?.to_token_stream().to_string()))
.collect::<Result<Vec<_>, super::to_syn::ToSynError>>()?;
Ok(parts.join(", "))
}
pub fn map_entries_to_tokens(
entries: &[(PureExpr, PureExpr)],
) -> Result<String, super::to_syn::ToSynError> {
use super::to_syn::ToSyn;
let parts = entries
.iter()
.map(|(k, v)| {
Ok(format!(
"{} => {}",
k.to_syn()?.to_token_stream(),
v.to_syn()?.to_token_stream()
))
})
.collect::<Result<Vec<_>, super::to_syn::ToSynError>>()?;
Ok(parts.join(", "))
}
fn parse_comma_separated(tokens: &str) -> Option<Vec<PureExpr>> {
let ts: TokenStream = tokens.parse().ok()?;
let chunks = split_by_comma(ts);
let mut exprs = Vec::new();
for chunk in chunks {
if chunk.is_empty() {
continue;
}
match syn::parse2::<syn::Expr>(chunk) {
Ok(expr) => exprs.push(expr.to_pure()),
Err(_) => {
return None;
}
}
}
Some(exprs)
}
fn parse_map_values(tokens: &str) -> Option<Vec<PureExpr>> {
let ts: TokenStream = tokens.parse().ok()?;
let entries = split_map_entries(ts)?;
let mut exprs = Vec::new();
for (_key, value) in entries {
match syn::parse2::<syn::Expr>(value) {
Ok(expr) => exprs.push(expr.to_pure()),
Err(_) => return None,
}
}
Some(exprs)
}
pub fn try_extract_map_entries(name: &str, tokens: &str) -> Option<Vec<(PureExpr, PureExpr)>> {
let macro_name = name.rsplit("::").next().unwrap_or(name);
if !MAP_MACROS.contains(¯o_name) {
return None;
}
let ts: TokenStream = tokens.parse().ok()?;
let entries = split_map_entries(ts)?;
let mut result = Vec::new();
for (key, value) in entries {
let key_expr = syn::parse2::<syn::Expr>(key).ok()?;
let value_expr = syn::parse2::<syn::Expr>(value).ok()?;
result.push((key_expr.to_pure(), value_expr.to_pure()));
}
Some(result)
}
fn split_by_comma(ts: TokenStream) -> Vec<TokenStream> {
let mut chunks = Vec::new();
let mut current = Vec::new();
for tt in ts {
let is_comma = matches!(&tt, TokenTree::Punct(p) if p.as_char() == ',');
if is_comma {
if !current.is_empty() {
chunks.push(current.drain(..).collect());
}
} else {
current.push(tt);
}
}
if !current.is_empty() {
chunks.push(current.into_iter().collect());
}
chunks
}
fn split_map_entries(ts: TokenStream) -> Option<Vec<(TokenStream, TokenStream)>> {
let chunks = split_by_comma(ts);
let mut entries = Vec::new();
for chunk in chunks {
let (key, value) = split_by_fat_arrow(chunk)?;
entries.push((key, value));
}
Some(entries)
}
fn split_by_fat_arrow(ts: TokenStream) -> Option<(TokenStream, TokenStream)> {
let tokens: Vec<TokenTree> = ts.into_iter().collect();
let mut split_idx = None;
let mut i = 0;
while i < tokens.len().saturating_sub(1) {
if let (TokenTree::Punct(p1), TokenTree::Punct(p2)) = (&tokens[i], &tokens[i + 1]) {
if p1.as_char() == '=' && p2.as_char() == '>' {
split_idx = Some(i);
break;
}
}
i += 1;
}
let idx = split_idx?;
let key: TokenStream = tokens[..idx].iter().cloned().collect();
let value: TokenStream = tokens[idx + 2..].iter().cloned().collect();
Some((key, value))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_vec_simple() {
let tokens = "1, 2, 3";
let exprs = try_extract_exprs("vec", tokens).unwrap();
assert_eq!(exprs.len(), 3);
}
#[test]
fn test_parse_vec_with_structs() {
let tokens =
r#"Config { host: "a".into(), port: 80 }, Config { host: "b".into(), port: 81 }"#;
let exprs = try_extract_exprs("vec", tokens).unwrap();
assert_eq!(exprs.len(), 2);
for expr in &exprs {
assert!(matches!(expr, PureExpr::Struct { path, .. } if path == "Config"));
}
}
#[test]
fn test_parse_hashmap_values() {
let tokens = r#""key1" => Config { port: 80 }, "key2" => Config { port: 81 }"#;
let exprs = try_extract_exprs("hashmap", tokens).unwrap();
assert_eq!(exprs.len(), 2);
}
#[test]
fn test_extract_map_entries() {
let tokens = r#""key1" => Config { port: 80 }, "key2" => Config { port: 81 }"#;
let entries = try_extract_map_entries("hashmap", tokens).unwrap();
assert_eq!(entries.len(), 2);
assert!(matches!(&entries[0].0, PureExpr::Lit(s) if s.contains("key1")));
assert!(matches!(&entries[1].0, PureExpr::Lit(s) if s.contains("key2")));
}
#[test]
fn test_unknown_macro_returns_none() {
let tokens = "1, 2, 3";
assert!(try_extract_exprs("custom_macro", tokens).is_none());
}
#[test]
fn test_exprs_to_tokens() {
let exprs = vec![
PureExpr::Lit("1".to_string()),
PureExpr::Lit("2".to_string()),
];
let result = exprs_to_tokens(&exprs).unwrap();
assert!(result.contains("1"));
assert!(result.contains("2"));
}
}