irongit

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

241 lines9.3 KBRust
1#![doc=include_str!("../README.md")]
2
3extern crate proc_macro;
4use proc_macro::TokenStream;
5use quote::quote;
6use std::fs;
7use syn::{parse_macro_input, Ident, LitStr};
8
9#[proc_macro]
10#[doc = include_str!("../docs/load_regex_files.md")]
11pub fn load_regex_files(input: TokenStream) -> TokenStream {
12 let folder_path = parse_macro_input!(input as LitStr).value();
13
14 let folder_path =
15 std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()).join(folder_path);
16
17 // Read files from the directory
18 let files = fs::read_dir(folder_path)
19 .expect("Failed to read directory")
20 .filter_map(|entry| {
21 let entry = entry.ok()?;
22 let path = entry.path();
23
24 if path.extension().and_then(|ext| ext.to_str()) == Some("re") {
25 Some(path)
26 } else {
27 None
28 }
29 })
30 .collect::<Vec<_>>();
31
32 // Generate enum variants and compiled regex objects
33 let variants = files.iter().map(|path| {
34 let base = path.file_stem().unwrap().to_str().unwrap();
35 let variant_name = &format!("{}RE", base);
36 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
37 quote! {
38 pub #ident: regex::Regex
39 }
40 });
41
42 // Generate struct initialization
43 let init_fields = files.iter().map(|path| {
44 let base = path.file_stem().unwrap().to_str().unwrap();
45 let variant_name = &format!("{}RE", base);
46 let ident = Ident::new(variant_name, proc_macro2::Span::call_site());
47 let file_path = path.to_str().unwrap();
48 quote! {
49 #ident: regex::Regex::new(&format!(r"(?mx){}", include_str!(#file_path))).unwrap()
50 }
51 });
52
53 // Generate the enum and regex accessor
54 let expanded = quote! {
55 pub struct RegexFile {
56 #(#variants),*
57 }
58
59 impl RegexFile {
60 pub fn new() -> Self {
61 RegexFile {
62 #(#init_fields),*
63 }
64 }
65 }
66
67 lazy_static::lazy_static! {
68 pub static ref RE: RegexFile = RegexFile::new();
69 }
70
71
72 };
73
74 let structs = files.iter().map(|path| {
75 let base = path.file_stem().unwrap().to_str().unwrap();
76 let variant_name = &format!("{}RE", base);
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 pub val: String,
111 #(#fields),*
112 }
113
114 impl std::fmt::Display for #ident {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 write!(f, "{}", serde_json::to_string_pretty(&self).unwrap())
117 }
118 }
119
120 impl #ident {
121 /// Gets the raw captures
122 pub fn captures(text:&str) -> Option<regex::Captures<'_>> {
123 let re = &RE.#ident;
124 re.captures(text)
125 }
126
127 /// Gets the raw captures from the contents of a file
128 /// Buffer is required because captures use references to the original text.
129 pub fn captures_from_file<'a>(buf: &'a mut String, filename: &str) -> Result<Option<regex::Captures<'a>>, std::io::Error> {
130 buf.clear();
131 buf.push_str(&std::fs::read_to_string(filename)?);
132 Ok(Self::captures(buf))
133 }
134
135 /// Gets the raw captures iter
136 pub fn captures_iter(text: &str) -> impl Iterator<Item = regex::Captures<'_>> + '_ {
137 let re = &RE.#ident;
138 re.captures_iter(text)
139 }
140
141 /// Gets the raw captures iter from the contents of a file
142 /// Buffer is required because captures use references to the original text.
143 pub fn captures_iter_from_file<'a>(buf: &'a mut String, filename: &str) -> Result<impl Iterator<Item = regex::Captures<'a>> + 'a, std::io::Error> {
144 buf.clear();
145 buf.push_str(&std::fs::read_to_string(filename)?);
146 Ok(Self::captures_iter(buf))
147 }
148
149 /// Extracts the first regex match for the given string
150 /// This match contains `start_pos`, `end_pos`, and `val`
151 pub fn from_str(text: &str) -> Option<Self> {
152 if let Some(captures) = Self::captures(text) {
153 Some(Self {
154 start_pos: captures.get(0).unwrap().start(),
155 end_pos: captures.get(0).unwrap().end(),
156 val: captures.get(0).unwrap().as_str().to_string(),
157 #(#field_initializers),*
158 })
159 } else {
160 None
161 }
162 }
163
164 /// Extracts the first regex match for the text content of the given filename
165 /// This match contains `start_pos`, `end_pos`, and `val`
166 pub fn from_file(filename: &str) -> Result<Option<Self>, std::io::Error> {
167 let text = std::fs::read_to_string(filename)?;
168 Ok(Self::from_str(&text))
169 }
170
171 /// Extracts all regex matches for the given string
172 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
173 pub fn iter_from_str(text: &str) -> impl Iterator<Item = Self> + '_ {
174 Self::captures_iter(text)
175 .map(|captures| {
176 Self {
177 start_pos: captures.get(0).unwrap().start(),
178 end_pos: captures.get(0).unwrap().end(),
179 val: captures.get(0).unwrap().as_str().to_string(),
180 #(#field_initializers),*
181 }
182 })
183 }
184
185 /// Extracts all regex matches for the given string
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 vec_from_str(text: &str) -> Vec<Self> {
188 Self::iter_from_str(text).collect()
189 }
190
191 /// Extracts all regex matches for the text contents of a given file
192 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
193 /// Buffer is required because the iterator refers to the original text.
194 pub fn iter_from_file<'a>(buf: &'a mut String, filename: &str) -> Result<impl Iterator<Item = Self> + 'a, std::io::Error> {
195 buf.clear();
196 buf.push_str(&std::fs::read_to_string(filename)?);
197 Ok(Self::iter_from_str(buf))
198 }
199
200 /// Extracts all regex matches for the text contents of a given file
201 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
202 pub fn vec_from_file(filename: &str) -> Result<Vec<Self>, std::io::Error> {
203 let text = std::fs::read_to_string(filename)?;
204 Ok(Self::vec_from_str(&text))
205 }
206 }
207 }
208 });
209
210 let expanded = quote! {
211 #expanded
212 #(#structs)*
213
214 #[derive(Debug, Clone, serde::Serialize)]
215 pub struct SimpleMatch {
216 pub start_pos: usize,
217 pub end_pos: usize,
218 pub val: String,
219 }
220
221 impl SimpleMatch {
222 pub fn from_match(m: Option<regex::Match<'_>>) -> Option<Self> {
223 if let Some(m) = m {
224 Some(Self {
225 start_pos: m.start(),
226 end_pos: m.end(),
227 val: m.as_str().to_string(),
228 })
229 } else {
230 None
231 }
232 }
233
234 pub fn to<T: std::str::FromStr>(&self) -> Option<T> {
235 self.val.parse::<T>().ok()
236 }
237 }
238 };
239
240 TokenStream::from(expanded)
241}