irongit

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

233 lines8.3 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.
14///
15/// Event Example:
16/// ```text
17/// ./
18/// # This will create a struct called EventRE with the fields start, end, ampm, and activity
19///
20/// # Get the start
21/// ^(?<start>\d{1,2}:\d{1,2})
22/// .
23/// # Get the end time
24/// (?<end>\d{1,2}:\d{1,2})
25/// \s{0,2}
26///
27/// # Get the AM or PM after the times
28/// (?<ampm>[AMPM]{2})
29/// .
30/// # Get the activity string
31/// (?<activity>.+)
32/// ```
33pub fn load_regex_files(input: TokenStream) -> TokenStream {
34 let folder_path = parse_macro_input!(input as LitStr).value();
35
36 let folder_path =
37 std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join(folder_path);
38
39 // Read files from the directory
40 let files = fs::read_dir(folder_path)
41 .expect("Failed to read directory")
42 .filter_map(|entry| {
43 let entry = entry.ok()?;
44 let path = entry.path();
45
46 if path.extension().and_then(|ext| ext.to_str()) == Some("re") {
47 Some(path)
48 } else {
49 None
50 }
51 })
52 .collect::<Vec<_>>();
53
54 // Generate enum variants and compiled regex objects
55 let variants = files.iter().map(|path| {
56 let base = path.file_stem().unwrap().to_str().unwrap();
57 let variant_name = &format!("{}RE", base);
58 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
59 quote! {
60 pub #ident: regex::Regex
61 }
62 });
63
64 // Generate struct initialization
65 let init_fields = files.iter().map(|path| {
66 let base = path.file_stem().unwrap().to_str().unwrap();
67 let variant_name = &format!("{}RE", base);
68 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
69 let file_path = path.to_str().unwrap();
70 quote! {
71 #ident: regex::Regex::new(&format!(r"(?mx){}", include_str!(#file_path))).unwrap()
72 }
73 });
74
75 // Generate the enum and regex accessor
76 let expanded = quote! {
77 pub struct RegexFile {
78 #(#variants),*
79 }
80
81 impl RegexFile {
82 pub fn new() -> Self {
83 RegexFile {
84 #(#init_fields),*
85 }
86 }
87 }
88
89 lazy_static::lazy_static! {
90 pub static ref RE: RegexFile = RegexFile::new();
91 }
92
93
94 };
95
96 let structs = files.iter().map(|path| {
97 let base = path.file_stem().unwrap().to_str().unwrap();
98 let variant_name = &format!("{}RE", base);
99 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
100 let file_text = fs::read_to_string(path).unwrap();
101 let re_str = format!(r"(?mx){}", file_text);
102 let re = regex::Regex::new(&re_str).unwrap();
103
104 // Collect fields into a Vec so they can be used multiple times
105 let fields: Vec<_> = re
106 .capture_names()
107 .flatten()
108 .map(|name| {
109 let ident = Ident::new(name, proc_macro2::Span::call_site());
110 quote! {
111 pub #ident: Option<SimpleMatch>
112 }
113 })
114 .collect();
115
116 let field_initializers: Vec<_> = re
117 .capture_names()
118 .flatten()
119 .map(|name| {
120 let ident = Ident::new(name, proc_macro2::Span::call_site());
121 quote! {
122 #ident: SimpleMatch::from_match(captures.name(#name))
123 }
124 })
125 .collect();
126
127 quote! {
128 #[derive(Debug, Clone, serde::Serialize)]
129 pub struct #ident {
130 pub start_pos: usize,
131 pub end_pos: usize,
132 #(#fields),*
133 }
134
135 impl std::fmt::Display for #ident {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 write!(f, "{}", serde_json::to_string_pretty(&self).unwrap())
138 }
139 }
140
141 impl #ident {
142 /// Extracts the first regex match for the given string
143 /// This match contains `start_pos`, `end_pos`, and `val`
144 pub fn from_str(text: &str) -> Option<Self> {
145 let re = &RE.#ident;
146 let captures = re.captures(text);
147 if let Some(captures) = captures {
148 Some(Self {
149 start_pos: captures.get(0).unwrap().start(),
150 end_pos: captures.get(0).unwrap().end(),
151 #(#field_initializers),*
152 })
153 } else {
154 None
155 }
156 }
157
158 /// Extracts the first regex match for the text content of the given filename
159 /// This match contains `start_pos`, `end_pos`, and `val`
160 pub fn from_file(filename: &str) -> Result<Option<Self>, std::io::Error> {
161 let text = std::fs::read_to_string(filename)?;
162 Ok(Self::from_str(&text))
163 }
164
165 /// Extracts all regex matches for the given string
166 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
167 pub fn iter_from_str(text: &str) -> impl Iterator<Item = Self> + '_ {
168 let re = &RE.#ident;
169 re.captures_iter(text)
170 .map(|captures| {
171 Self {
172 start_pos: captures.get(0).unwrap().start(),
173 end_pos: captures.get(0).unwrap().end(),
174 #(#field_initializers),*
175 }
176 })
177 }
178
179 /// Extracts all regex matches for the given string
180 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
181 pub fn vec_from_str(text: &str) -> Vec<Self> {
182 Self::iter_from_str(text).collect()
183 }
184
185 /// Extracts all regex matches for the text contents of a given file
186 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
187 pub fn iter_from_file(filename: &str) -> Result<impl Iterator<Item = Self> + '_, std::io::Error> {
188 let text = std::fs::read_to_string(filename)?;
189 Ok(Self::iter_from_str(Box::leak(text.into_boxed_str())))
190 }
191
192 /// Extracts all regex matches for the text contents of a given file
193 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
194 pub fn vec_from_file(filename: &str) -> Result<Vec<Self>, std::io::Error> {
195 let text = std::fs::read_to_string(filename)?;
196 Ok(Self::vec_from_str(&text))
197 }
198 }
199 }
200 });
201
202 let expanded = quote! {
203 #expanded
204 #(#structs)*
205
206 #[derive(Debug, Clone, serde::Serialize)]
207 pub struct SimpleMatch {
208 pub start_pos: usize,
209 pub end_pos: usize,
210 pub val: String,
211 }
212
213 impl SimpleMatch {
214 pub fn from_match(m: Option<regex::Match<'_>>) -> Option<Self> {
215 if let Some(m) = m {
216 Some(Self {
217 start_pos: m.start(),
218 end_pos: m.end(),
219 val: m.as_str().to_string(),
220 })
221 } else {
222 None
223 }
224 }
225
226 pub fn to<T: std::str::FromStr>(&self) -> Option<T> {
227 self.val.parse::<T>().ok()
228 }
229 }
230 };
231
232 TokenStream::from(expanded)
233}