irongit

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

214 lines7.9 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 base = path.file_stem().unwrap().to_str().unwrap();
38 let variant_name = &format!("{}RE", base);
39 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
40 quote! {
41 pub #ident: regex::Regex
42 }
43 });
44
45 // Generate struct initialization
46 let init_fields = files.iter().map(|path| {
47 let base = path.file_stem().unwrap().to_str().unwrap();
48 let variant_name = &format!("{}RE", base);
49 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
50 let file_path = path.to_str().unwrap();
51 quote! {
52 #ident: regex::Regex::new(&format!(r"(?mx){}", include_str!(#file_path))).unwrap()
53 }
54 });
55
56 // Generate the enum and regex accessor
57 let expanded = quote! {
58 pub struct RegexFile {
59 #(#variants),*
60 }
61
62 impl RegexFile {
63 pub fn new() -> Self {
64 RegexFile {
65 #(#init_fields),*
66 }
67 }
68 }
69
70 lazy_static::lazy_static! {
71 pub static ref RE: RegexFile = RegexFile::new();
72 }
73
74
75 };
76
77 let structs = files.iter().map(|path| {
78 let base = path.file_stem().unwrap().to_str().unwrap();
79 let variant_name = &format!("{}RE", base);
80 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
81 let file_text = fs::read_to_string(path).unwrap();
82 let re_str = format!(r"(?mx){}", file_text);
83 let re = regex::Regex::new(&re_str).unwrap();
84
85 // Collect fields into a Vec so they can be used multiple times
86 let fields: Vec<_> = re
87 .capture_names()
88 .flatten()
89 .map(|name| {
90 let ident = Ident::new(name, proc_macro2::Span::call_site());
91 quote! {
92 pub #ident: Option<SimpleMatch>
93 }
94 })
95 .collect();
96
97 let field_initializers: Vec<_> = re
98 .capture_names()
99 .flatten()
100 .map(|name| {
101 let ident = Ident::new(name, proc_macro2::Span::call_site());
102 quote! {
103 #ident: SimpleMatch::from_match(captures.name(#name))
104 }
105 })
106 .collect();
107
108 quote! {
109 #[derive(Debug, Clone, serde::Serialize)]
110 pub struct #ident {
111 pub start_pos: usize,
112 pub end_pos: usize,
113 #(#fields),*
114 }
115
116 impl std::fmt::Display for #ident {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 write!(f, "{}", serde_json::to_string_pretty(&self).unwrap())
119 }
120 }
121
122 impl #ident {
123 /// Extracts the first regex match for the given string
124 /// This match contains `start_pos`, `end_pos`, and `val`
125 pub fn from_str(text: &str) -> Option<Self> {
126 let re = &RE.#ident;
127 let captures = re.captures(text);
128 if let Some(captures) = captures {
129 Some(Self {
130 start_pos: captures.get(0).unwrap().start(),
131 end_pos: captures.get(0).unwrap().end(),
132 #(#field_initializers),*
133 })
134 } else {
135 None
136 }
137 }
138
139 /// Extracts the first regex match for the text content of the given filename
140 /// This match contains `start_pos`, `end_pos`, and `val`
141 pub fn from_file(filename: &str) -> Result<Option<Self>, std::io::Error> {
142 let text = std::fs::read_to_string(filename)?;
143 Ok(Self::from_str(&text))
144 }
145
146 /// Extracts all regex matches for the given string
147 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
148 pub fn iter_from_str(text: &str) -> impl Iterator<Item = Self> + '_ {
149 let re = &RE.#ident;
150 re.captures_iter(text)
151 .map(|captures| {
152 Self {
153 start_pos: captures.get(0).unwrap().start(),
154 end_pos: captures.get(0).unwrap().end(),
155 #(#field_initializers),*
156 }
157 })
158 }
159
160 /// Extracts all regex matches for the given string
161 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
162 pub fn vec_from_str(text: &str) -> Vec<Self> {
163 Self::iter_from_str(text).collect()
164 }
165
166 /// Extracts all regex matches for the text contents of a given file
167 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
168 pub fn iter_from_file(filename: &str) -> Result<impl Iterator<Item = Self> + '_, std::io::Error> {
169 let text = std::fs::read_to_string(filename)?;
170 Ok(Self::iter_from_str(Box::leak(text.into_boxed_str())))
171 }
172
173 /// Extracts all regex matches for the text contents of a given file
174 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
175 pub fn vec_from_file(filename: &str) -> Result<Vec<Self>, std::io::Error> {
176 let text = std::fs::read_to_string(filename)?;
177 Ok(Self::vec_from_str(&text))
178 }
179 }
180 }
181 });
182
183 let expanded = quote! {
184 #expanded
185 #(#structs)*
186
187 #[derive(Debug, Clone, serde::Serialize)]
188 pub struct SimpleMatch {
189 pub start_pos: usize,
190 pub end_pos: usize,
191 pub val: String,
192 }
193
194 impl SimpleMatch {
195 pub fn from_match(m: Option<regex::Match<'_>>) -> Option<Self> {
196 if let Some(m) = m {
197 Some(Self {
198 start_pos: m.start(),
199 end_pos: m.end(),
200 val: m.as_str().to_string(),
201 })
202 } else {
203 None
204 }
205 }
206
207 pub fn to<T: std::str::FromStr>(&self) -> Option<T> {
208 self.val.parse::<T>().ok()
209 }
210 }
211 };
212
213 TokenStream::from(expanded)
214}