parse-display 0.10.0

Procedural macro to implement Display and FromStr using common settings.
Documentation
use core::mem;
use std::borrow::Cow;
use std::collections::HashMap;

use regex::Regex;
use regex_syntax::ast::{Ast, Flags, GroupKind};

use crate::FromStrFormat;

pub use regex;

#[track_caller]
pub fn to_regex<T, E>(format: &dyn FromStrFormat<T, Err = E>) -> Option<String> {
    format.regex()
}

#[track_caller]
pub fn to_ast<T, E>(format: &dyn FromStrFormat<T, Err = E>) -> Option<(String, Ast)> {
    let s = format.regex()?;
    let ast = ast_from_str(&s);
    Some((s, ast))
}

#[track_caller]
fn ast_from_str(s: &str) -> Ast {
    let Ok(mut ast) = regex_syntax::ast::parse::Parser::new().parse(s) else {
        panic!("invalid regex: {s}")
    };
    let e = replace_ast(&mut ast, &mut |ast| {
        if let Ast::Group(g) = ast {
            match &g.kind {
                GroupKind::CaptureIndex(_) => {
                    g.kind = GroupKind::NonCapturing(Flags {
                        span: g.span,
                        items: vec![],
                    });
                }
                GroupKind::CaptureName { name, .. } => {
                    return Err(format!(
                        "named capture group is not supported: `{}`",
                        name.name
                    ));
                }
                GroupKind::NonCapturing(_) => {}
            }
        }
        Ok(true)
    });
    if let Err(e) = e {
        panic!("{e}");
    }
    ast
}

pub struct Parser {
    pub re: Regex,
    pub ss: Vec<Option<String>>,
}
impl Parser {
    #[track_caller]
    pub fn new(s: &str, with: &mut [(&str, Option<(String, Ast)>)]) -> Self {
        let mut asts: HashMap<&str, &Ast> = HashMap::new();
        let mut ss = Vec::new();
        for (capture_name, item) in with {
            if let Some((item_s, item_ast)) = item {
                asts.insert(capture_name, item_ast);
                ss.push(Some(mem::take(item_s)));
            } else {
                ss.push(None);
            }
        }
        let re = if asts.is_empty() {
            Cow::Borrowed(s)
        } else {
            let mut ast = regex_syntax::ast::parse::Parser::new().parse(s).unwrap();
            let e = replace_ast(&mut ast, &mut |ast| {
                if let Ast::Group(g) = ast {
                    if let GroupKind::CaptureName { name, .. } = &g.kind {
                        if let Some(ast) = asts.get(name.name.as_str()) {
                            g.ast = Box::new((*ast).clone());
                            return Ok(false);
                        }
                    }
                }
                Ok(true)
            });
            if let Err(e) = e {
                panic!("{e}");
            }
            Cow::Owned(ast.to_string())
        };
        Self {
            re: Regex::new(&re).unwrap(),
            ss,
        }
    }
}

#[track_caller]
pub fn build_regex(s: &str, with: &[(&str, Option<Ast>)]) -> Regex {
    let with: HashMap<&str, &Ast> = with
        .iter()
        .filter_map(|(name, ast)| Some((*name, ast.as_ref()?)))
        .collect();
    let re = if with.is_empty() {
        Cow::Borrowed(s)
    } else {
        let mut ast = regex_syntax::ast::parse::Parser::new().parse(s).unwrap();
        let e = replace_ast(&mut ast, &mut |ast| {
            if let Ast::Group(g) = ast {
                if let GroupKind::CaptureName { name, .. } = &g.kind {
                    if let Some(ast) = with.get(name.name.as_str()) {
                        g.ast = Box::new((*ast).clone());
                        return Ok(false);
                    }
                }
            }
            Ok(true)
        });
        if let Err(e) = e {
            panic!("{e}");
        }
        Cow::Owned(ast.to_string())
    };
    Regex::new(&re).unwrap()
}

fn replace_asts(
    asts: &mut Vec<Ast>,
    f: &mut impl FnMut(&mut Ast) -> ReplaceAstResult<bool>,
) -> ReplaceAstResult {
    for ast in asts {
        replace_ast(ast, f)?;
    }
    Ok(())
}

fn replace_ast(
    ast: &mut Ast,
    f: &mut impl FnMut(&mut Ast) -> ReplaceAstResult<bool>,
) -> ReplaceAstResult {
    if !f(ast)? {
        return Ok(());
    }
    match ast {
        Ast::Empty(..)
        | Ast::Flags(..)
        | Ast::Literal(..)
        | Ast::Dot(..)
        | Ast::Assertion(..)
        | Ast::ClassUnicode(..)
        | Ast::ClassPerl(..)
        | Ast::ClassBracketed(..) => Ok(()),
        Ast::Repetition(rep) => replace_ast(&mut rep.ast, f),
        Ast::Group(g) => replace_ast(&mut g.ast, f),
        Ast::Alternation(alt) => replace_asts(&mut alt.asts, f),
        Ast::Concat(c) => replace_asts(&mut c.asts, f),
    }
}

type ReplaceAstResult<T = ()> = Result<T, String>;