use proc_macro::TokenStream;
use quote::{format_ident, quote};
use syn::{
Expr, Ident, LitStr, Token,
parse::{Parse, ParseStream},
parse_macro_input,
};
struct Arm {
patterns: Vec<LitStr>,
cond: Option<Expr>,
expr: Expr,
}
impl Parse for Arm {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut patterns = Vec::new();
if input.peek(LitStr) {
patterns.push(input.parse()?);
} else if input.peek(Token![_]) {
input.parse::<Token![_]>()?;
} else {
return Err(input.error("Expected string literal or '_' for default arm"));
}
while input.peek(Token![|]) {
input.parse::<Token![|]>()?;
patterns.push(input.parse()?);
}
let cond = if input.peek(Token![if]) {
input.parse::<Token![if]>()?;
Some(input.parse()?)
} else {
None
};
if input.peek(Token![=>]) {
input.parse::<Token![=>]>()?;
} else if input.peek(Token![:]) {
input.parse::<Token![:]>()?;
} else {
return Err(input.error("Expected '=>' or ':' after patterns"));
}
let expr: Expr = input.parse()?;
Ok(Arm { patterns, expr, cond })
}
}
struct Arms {
arms: Vec<Arm>,
completeness: bool,
}
impl Parse for Arms {
fn parse(input: ParseStream) -> syn::Result<Self> {
let (mut arms, mut completeness) = (Vec::new(), false);
while !input.is_empty() {
let arm = input.parse::<Arm>()?;
if arm.patterns.is_empty() && arm.cond.is_none() {
completeness = true;
}
arms.push(arm);
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
}
Ok(Arms { arms, completeness })
}
}
fn compile_pattern(pattern: &str) -> (String, Vec<String>) {
let mut regex_pattern = String::new();
let mut identifiers = Vec::new();
regex_pattern.push('^');
let mut chars = pattern.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'\\' => {
if let Some(next_ch) = chars.next() {
regex_pattern.push_str(®ex::escape(&next_ch.to_string()));
}
}
'{' => {
let mut ident = String::new();
while let Some(ch2) = chars.next() {
if ch2 == '}' {
break;
} else {
ident.push(ch2);
}
}
if !ident.is_empty() {
identifiers.push(ident.clone());
regex_pattern.push_str(&format!("(?P<{}>[^/]+)", ident));
}
}
':' => {
let mut ident = String::new();
while let Some(&ch2) = chars.peek() {
if ch2.is_alphanumeric() || ch2 == '_' {
ident.push(ch2);
chars.next();
} else {
break;
}
}
if !ident.is_empty() {
identifiers.push(ident.clone());
regex_pattern.push_str(&format!("(?P<{}>[^/]+)", ident));
} else {
regex_pattern.push_str(®ex::escape(&":".to_string()));
}
}
_ => {
regex_pattern.push_str(®ex::escape(&ch.to_string()));
}
}
}
regex_pattern.push('$');
(regex_pattern, identifiers)
}
#[proc_macro]
pub fn path_scan(input: TokenStream) -> TokenStream {
let Arms { arms, completeness } = parse_macro_input!(input as Arms);
let mut arm_match_tokens = Vec::new();
for Arm { patterns, expr, cond } in arms.into_iter() {
let value = if completeness {
quote! { #expr }
} else {
quote! { Some({ #expr }) }
};
if !patterns.is_empty() {
for pat_lit in patterns.into_iter() {
let pattern_str = pat_lit.value();
let (regex_str, idents) = compile_pattern(&pattern_str);
let regex_lit = LitStr::new(®ex_str, pat_lit.span());
let mut bindings = Vec::new();
for ident in idents {
let ident_token = Ident::new(&ident, pat_lit.span());
bindings.push(quote! {
let #ident_token = __caps.name(#ident).map(|m| m.as_str()).unwrap();
});
}
let cond_check = if let Some(cond_expr) = &cond {
quote! { if #cond_expr }
} else {
quote! { if true }
};
arm_match_tokens.push(quote! {
if let Some(__caps) = ::path_scan::regex::Regex::new(#regex_lit).unwrap().captures(input) {
#(#bindings)*
#cond_check {
return #value;
}
}
});
}
} else {
let cond_check = if let Some(cond_expr) = &cond {
quote! { if #cond_expr }
} else {
quote! { if true }
};
arm_match_tokens.push(quote! {
#cond_check {
return #value;
}
});
}
}
let last = if completeness {
quote! {unreachable!()}
} else {
quote! {None}
};
let expanded = quote! {
{
move |input: &str| {
#(#arm_match_tokens)*
#last
}
}
};
TokenStream::from(expanded)
}
struct PathScanValInput {
input_expr: Expr,
arms: Arms,
}
impl Parse for PathScanValInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let input_expr: Expr = input.parse()?;
if input.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
if input.peek(syn::token::Brace) {
let content;
let _brace_token = syn::braced!(content in input);
let arms: Arms = content.parse()?;
Ok(PathScanValInput { input_expr, arms })
} else {
let arms: Arms = input.parse()?;
Ok(PathScanValInput { input_expr, arms })
}
}
}
#[proc_macro]
pub fn path_scan_val(input: TokenStream) -> TokenStream {
let PathScanValInput { input_expr, arms } = parse_macro_input!(input as PathScanValInput);
let Arms { arms, completeness } = arms;
let mut arm_match_tokens = Vec::new();
for (arm_index, Arm { patterns, expr, cond }) in arms.into_iter().enumerate() {
let value = if completeness {
quote! { #expr }
} else {
quote! { Some({ #expr }) }
};
if !patterns.is_empty() {
for (pat_index, pat_lit) in patterns.into_iter().enumerate() {
let pattern_str = pat_lit.value();
let (regex_str, idents) = compile_pattern(&pattern_str);
let regex_lit = LitStr::new(®ex_str, pat_lit.span());
let mut bindings = Vec::new();
let regex_var = format_ident!("__regex_{}_{}", arm_index, pat_index);
for ident in idents {
let ident_token = Ident::new(&ident, pat_lit.span());
bindings.push(quote! {
let #ident_token = __caps.name(#ident).map(|m| m.as_str()).unwrap();
});
}
let cond_check = if let Some(ref cond_expr) = cond {
quote! { if #cond_expr }
} else {
quote! { if true }
};
arm_match_tokens.push(quote! {
static #regex_var: ::path_scan::once_cell::sync::Lazy<::path_scan::regex::Regex> = ::path_scan::once_cell::sync::Lazy::new(|| ::path_scan::regex::Regex::new(#regex_lit).unwrap());
if let Some(__caps) = #regex_var.captures(__input) {
#(#bindings)*
#cond_check {
break 'x #value;
}
}
});
}
} else {
let cond_check = if let Some(ref cond_expr) = cond {
quote! { if #cond_expr }
} else {
quote! { if true }
};
arm_match_tokens.push(quote! {
#cond_check {
break 'x #value;
}
});
}
}
let last = if completeness {
quote! { unreachable!() }
} else {
quote! { None }
};
let expanded = quote! {
{
let __input = #input_expr;
#[allow(unreachable_code)]
let __result = 'x: {
#(#arm_match_tokens)*
break 'x #last;
};
__result
}
};
TokenStream::from(expanded)
}