use proc_macro2::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Expr, Ident, LitStr, Result, Token, braced};
#[allow(clippy::struct_field_names)]
pub struct Pattern {
pub name: Option<Ident>,
pub kind: PatternKind,
pub action: Option<Expr>,
}
pub enum PatternKind {
Literal(LitStr),
Regex(LitStr),
Glob(LitStr),
}
impl Parse for Pattern {
fn parse(input: ParseStream) -> Result<Self> {
let name = if input.peek(Ident) && input.peek2(Token![:]) {
let name: Ident = input.parse()?;
let _: Token![:] = input.parse()?;
Some(name)
} else {
None
};
let pattern = if input.peek(Ident) {
let kind: Ident = input.parse()?;
match kind.to_string().as_str() {
"regex" | "re" => {
let content;
syn::parenthesized!(content in input);
let lit: LitStr = content.parse()?;
PatternKind::Regex(lit)
}
"glob" => {
let content;
syn::parenthesized!(content in input);
let lit: LitStr = content.parse()?;
PatternKind::Glob(lit)
}
_ => {
return Err(syn::Error::new(
kind.span(),
format!("unknown pattern type: {kind}"),
));
}
}
} else {
let lit: LitStr = input.parse()?;
PatternKind::Literal(lit)
};
let action = if input.peek(Token![=>]) {
let _: Token![=>] = input.parse()?;
let expr: Expr = input.parse()?;
Some(expr)
} else {
None
};
Ok(Self {
name,
kind: pattern,
action,
})
}
}
pub struct PatternsInput {
pub patterns: Punctuated<Pattern, Token![,]>,
}
impl Parse for PatternsInput {
fn parse(input: ParseStream) -> Result<Self> {
let patterns = if input.peek(syn::token::Brace) {
let content;
braced!(content in input);
Punctuated::parse_terminated(&content)?
} else {
Punctuated::parse_terminated(input)?
};
Ok(Self { patterns })
}
}
pub fn expand(input: PatternsInput) -> TokenStream {
let patterns: Vec<_> = input
.patterns
.into_iter()
.enumerate()
.map(|(idx, pattern)| {
let name = pattern
.name
.map_or_else(|| quote! { None }, |n| quote! { Some(#n.to_string()) });
let pattern_expr = match pattern.kind {
PatternKind::Literal(lit) => {
quote! { rust_expect::pattern::PatternType::Literal(#lit.to_string()) }
}
PatternKind::Regex(lit) => {
let pattern_str = lit.value();
if let Err(e) = regex::Regex::new(&pattern_str) {
return syn::Error::new(lit.span(), format!("invalid regex: {e}"))
.to_compile_error();
}
quote! { rust_expect::pattern::PatternType::Regex(#lit.to_string()) }
}
PatternKind::Glob(lit) => {
quote! { rust_expect::pattern::PatternType::Glob(#lit.to_string()) }
}
};
let action_expr = pattern.action.map_or_else(
|| quote! { None::<Box<dyn Fn(&str)>> },
|a| quote! { Some(Box::new(move |_| { #a })) },
);
quote! {
rust_expect::pattern::Pattern {
name: #name,
index: #idx,
pattern: #pattern_expr,
action: #action_expr,
}
}
})
.collect();
quote! {
rust_expect::pattern::PatternSet::new(vec![#(#patterns),*])
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
#[test]
fn parse_simple_pattern() {
let input: PatternsInput = parse_quote! {
"hello"
};
assert_eq!(input.patterns.len(), 1);
}
#[test]
fn parse_multiple_patterns() {
let input: PatternsInput = parse_quote! {
"hello",
"world"
};
assert_eq!(input.patterns.len(), 2);
}
}