irongit

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

214 lines7.9 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 /// Extracts the first regex match for the given string
122 /// This match contains `start_pos`, `end_pos`, and `val`
123 pub fn from_str(text: &str) -> Option<Self> {
124 let re = &RE.#ident;
125 let captures = re.captures(text);
126 if let Some(captures) = captures {
127 Some(Self {
128 start_pos: captures.get(0).unwrap().start(),
129 end_pos: captures.get(0).unwrap().end(),
130 val: captures.get(0).unwrap().as_str().to_string(),
131 #(#field_initializers),*
132 })
133 } else {
134 None
135 }
136 }
137
138 /// Extracts the first regex match for the text content of the given filename
139 /// This match contains `start_pos`, `end_pos`, and `val`
140 pub fn from_file(filename: &str) -> Result<Option<Self>, std::io::Error> {
141 let text = std::fs::read_to_string(filename)?;
142 Ok(Self::from_str(&text))
143 }
144
145 /// Extracts all regex matches for the given string
146 /// Each match contains a `start_pos`, `end_pos`, and each field for the given class contains `start_pos`, `end_pos`, and `val`
147 pub fn iter_from_str(text: &str) -> impl Iterator<Item = Self> + '_ {
148 let re = &RE.#ident;
149 re.captures_iter(text)
150 .map(|captures| {
151 Self {
152 start_pos: captures.get(0).unwrap().start(),
153 end_pos: captures.get(0).unwrap().end(),
154 val: captures.get(0).unwrap().as_str().to_string(),
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}