irongit

A rust macro that allows regular expression creation through independent files. Allows fully typed named groups.

199 lines7.0 KBRust
1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::quote;
4use std::fs;
5use syn::{parse_macro_input, Ident, LitStr};
6
7#[proc_macro]
8/// Loads a folder filled with regular expression files ending with `.re`.
9///
10/// All regex files are loaded with the `m` and `x` flags.
11///
12/// - `m` (multi-line): `^` and `$` match the start and end of lines, not just the entire input.
13/// - `x` (extended): allows whitespace and comments starting with `#` inside the regex.
14pub fn load_regex_files(input: TokenStream) -> TokenStream {
15 let folder_path = parse_macro_input!(input as LitStr).value();
16
17 let folder_path =
18 std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join(folder_path);
19
20 // Read files from the directory
21 let files = fs::read_dir(folder_path)
22 .expect("Failed to read directory")
23 .filter_map(|entry| {
24 let entry = entry.ok()?;
25 let path = entry.path();
26
27 if path.extension().and_then(|ext| ext.to_str()) == Some("re") {
28 Some(path)
29 } else {
30 None
31 }
32 })
33 .collect::<Vec<_>>();
34
35 // Generate enum variants and compiled regex objects
36 let variants = files.iter().map(|path| {
37 let variant_name = path.file_stem().unwrap().to_str().unwrap();
38 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
39 quote! {
40 pub #ident: regex::Regex
41 }
42 });
43
44 // Generate struct initialization
45 let init_fields = files.iter().map(|path| {
46 let variant_name = path.file_stem().unwrap().to_str().unwrap();
47 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
48 let file_path = path.to_str().unwrap();
49 quote! {
50 #ident: regex::Regex::new(&format!(r"(?mx){}", include_str!(#file_path))).unwrap()
51 }
52 });
53
54 // Generate the enum and regex accessor
55 let expanded = quote! {
56 pub struct RegexFile {
57 #(#variants),*
58 }
59
60 impl RegexFile {
61 pub fn new() -> Self {
62 RegexFile {
63 #(#init_fields),*
64 }
65 }
66 }
67
68 lazy_static::lazy_static! {
69 pub static ref RE: RegexFile = RegexFile::new();
70 }
71
72
73 };
74
75 let structs = files.iter().map(|path| {
76 let variant_name = path.file_stem().unwrap().to_str().unwrap();
77 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
78 let file_text = fs::read_to_string(path).unwrap();
79 let re_str = format!(r"(?mx){}", file_text);
80 let re = regex::Regex::new(&re_str).unwrap();
81
82 // Collect fields into a Vec so they can be used multiple times
83 let fields: Vec<_> = re
84 .capture_names()
85 .flatten()
86 .map(|name| {
87 let ident = Ident::new(name, proc_macro2::Span::call_site());
88 quote! {
89 pub #ident: Option<SimpleMatch>
90 }
91 })
92 .collect();
93
94 let field_initializers: Vec<_> = re
95 .capture_names()
96 .flatten()
97 .map(|name| {
98 let ident = Ident::new(name, proc_macro2::Span::call_site());
99 quote! {
100 #ident: SimpleMatch::from_match(captures.name(#name))
101 }
102 })
103 .collect();
104
105 quote! {
106 #[derive(Debug, Clone, serde::Serialize)]
107 pub struct #ident {
108 pub start_pos: usize,
109 pub end_pos: usize,
110 #(#fields),*
111 }
112
113 impl std::fmt::Display for #ident {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 write!(f, "{}", serde_json::to_string_pretty(&self).unwrap())
116 }
117 }
118
119 impl #ident {
120 /// Extracts the first regex match for the given string
121 /// This match contains `start_pos`, `end_pos`, and `val`
122 pub fn from_str(text: &str) -> Option<Self> {
123 let re = &RE.#ident;
124 let captures = re.captures(text);
125 if let Some(captures) = captures {
126 Some(Self {
127 start_pos: captures.get(0).unwrap().start(),
128 end_pos: captures.get(0).unwrap().end(),
129 #(#field_initializers),*
130 })
131 } else {
132 None
133 }
134 }
135
136 /// Extracts the first regex match for the text content of the given filename
137 /// This match contains `start_pos`, `end_pos`, and `val`
138 pub fn from_file(filename: &str) -> Result<Option<Self>, std::io::Error> {
139 let text = std::fs::read_to_string(filename)?;
140 Ok(Self::from_str(&text))
141 }
142
143 /// Extracts all regex matches for the given string
144 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
145 pub fn vec_from_str(text: &str) -> Vec<Self> {
146 let re = &RE.#ident;
147 re.captures_iter(text)
148 .map(|captures| {
149 Self {
150 start_pos: captures.get(0).unwrap().start(),
151 end_pos: captures.get(0).unwrap().end(),
152 #(#field_initializers),*
153 }
154 })
155 .collect()
156 }
157
158 /// Extracts all regex matches for the text contents of a given file
159 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
160 pub fn vec_from_file(filename: &str) -> Result<Vec<Self>, std::io::Error> {
161 let text = std::fs::read_to_string(filename)?;
162 Ok(Self::vec_from_str(&text))
163 }
164 }
165 }
166 });
167
168 let expanded = quote! {
169 #expanded
170 #(#structs)*
171
172 #[derive(Debug, Clone, serde::Serialize)]
173 pub struct SimpleMatch {
174 pub start_pos: usize,
175 pub end_pos: usize,
176 pub val: String,
177 }
178
179 impl SimpleMatch {
180 pub fn from_match(m: Option<regex::Match<'_>>) -> Option<Self> {
181 if let Some(m) = m {
182 Some(Self {
183 start_pos: m.start(),
184 end_pos: m.end(),
185 val: m.as_str().to_string(),
186 })
187 } else {
188 None
189 }
190 }
191
192 pub fn to<T: std::str::FromStr>(&self) -> Option<T> {
193 self.val.parse::<T>().ok()
194 }
195 }
196 };
197
198 TokenStream::from(expanded)
199}