1use convert_case::{Case, Casing};
52use proc_macro::{TokenStream, TokenTree};
53use std::collections::HashMap;
54
55#[proc_macro]
56pub fn paths(struct_path_stream: TokenStream) -> TokenStream {
57 let mut current_struct_name: Option<String> = None;
58 let mut current_struct_fields: Vec<String> = Vec::with_capacity(16);
59
60 let mut opened_struct = false;
61 let mut colons_counter = 0;
62 let mut options_opened = false;
63
64 let mut current_field_path: Option<String> = None;
65
66 let mut current_option_name: Option<String> = None;
67 let mut expect_option_value: bool = false;
68
69 let mut options: HashMap<String, String> = HashMap::new();
70 let mut found_structs: Vec<(String, Vec<String>)> = Vec::new();
71
72 for token_tree in struct_path_stream.into_iter() {
73 match token_tree {
74 TokenTree::Ident(id) if current_struct_name.is_none() => {
75 current_struct_name = Some(id.to_string());
76 }
77 TokenTree::Punct(punct)
78 if current_struct_name.is_some()
79 && !opened_struct
80 && punct == ':'
81 && colons_counter < 2 =>
82 {
83 colons_counter += 1;
84 if colons_counter > 1 {
85 opened_struct = true;
86 }
87 }
88 TokenTree::Ident(id) if opened_struct => {
89 colons_counter = 0;
90 if let Some(ref mut field_path) = &mut current_field_path {
91 field_path.push_str(id.to_string().as_str())
92 } else {
93 current_field_path = Some(id.to_string());
94 }
95 }
96 TokenTree::Punct(punct)
97 if current_struct_name.is_some()
98 && opened_struct
99 && punct == ':'
100 && colons_counter < 2 =>
101 {
102 colons_counter += 1;
103 opened_struct = false;
104 if let Some(ref mut field_path) = current_field_path.take() {
105 if let Some(ref mut struct_name) = &mut current_struct_name {
106 struct_name.push_str("::");
107 struct_name.push_str(field_path);
108 }
109 }
110 }
111 TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
112 if let Some(ref mut field_path) = &mut current_field_path {
113 field_path.push(punct.as_char());
114 } else {
115 panic!(
116 "Unexpected punctuation input for struct path group parameters: {:?}",
117 punct
118 )
119 }
120 }
121 TokenTree::Group(group) if opened_struct && current_field_path.is_none() => {
122 parse_multiple_fields(group.stream(), &mut current_struct_fields)
123 }
124 TokenTree::Punct(punct) if !options_opened && opened_struct && punct == ',' => {
125 opened_struct = false;
126 colons_counter = 0;
127 if let Some(struct_name) = current_struct_name.take() {
128 if let Some(field_path) = current_field_path.take() {
129 current_struct_fields.push(field_path);
130 }
131 if !current_struct_fields.is_empty() {
132 found_structs
133 .push((struct_name, std::mem::take(&mut current_struct_fields)));
134 } else {
135 panic!("Unexpected comma with empty fields for {}!", struct_name);
136 }
137 } else {
138 panic!("Unexpected comma with empty definitions!");
139 }
140 }
141 TokenTree::Punct(punct) if punct == ';' && opened_struct && !options_opened => {
142 options_opened = true;
143 opened_struct = false;
144 }
145 TokenTree::Ident(id) if options_opened && !expect_option_value => {
146 current_option_name = Some(id.to_string())
147 }
148 TokenTree::Ident(id) if options_opened && expect_option_value => {
149 expect_option_value = false;
150 match current_option_name.take() {
151 Some(option_name) => {
152 options.insert(option_name, id.to_string());
153 }
154 _ => {
155 panic!("Wrong options format")
156 }
157 }
158 }
159 TokenTree::Literal(lit) if options_opened && expect_option_value => {
160 expect_option_value = false;
161 match current_option_name.take() {
162 Some(option_name) => {
163 let lit_str = lit.to_string();
164 options.insert(
165 option_name,
166 lit_str.as_str()[1..lit_str.len() - 1].to_string(),
167 );
168 }
169 _ => {
170 panic!("Wrong options format")
171 }
172 }
173 }
174 TokenTree::Punct(punct) if options_opened && punct == '=' => {
175 expect_option_value = true;
176 }
177 TokenTree::Punct(punct) if options_opened && punct == ',' => {
178 expect_option_value = false;
179 }
180 others => {
181 panic!("Unexpected input for struct path parameters: {:?}", others)
182 }
183 }
184 }
185
186 if let Some(field_path) = current_field_path.take() {
187 current_struct_fields.push(field_path);
188 }
189
190 if let Some(struct_name) = current_struct_name.take() {
191 if let Some(field_path) = current_field_path.take() {
192 current_struct_fields.push(field_path);
193 }
194 if !current_struct_fields.is_empty() {
195 found_structs.push((struct_name, std::mem::take(&mut current_struct_fields)));
196 } else {
197 panic!("Unexpected comma with empty fields for {}!", struct_name);
198 }
199 } else {
200 panic!("Unexpected comma with empty definitions!");
201 }
202
203 let all_check_functions = generate_checks_code_for(&found_structs);
204
205 let mut all_final_fields: Vec<String> = Vec::with_capacity(16);
206
207 for (_, struct_fields) in &found_structs {
208 for field_path in struct_fields {
209 let mut final_field_path = field_path.clone().replace('~', ".");
210 if !options.is_empty() {
211 final_field_path = apply_options(&options, final_field_path);
212 }
213 all_final_fields.push(format!("\"{}\"", final_field_path))
214 }
215 }
216
217 if !all_final_fields.is_empty() {
218 format!(
219 "{{{}\n[{}]}}",
220 all_check_functions,
221 all_final_fields.join(",")
222 )
223 .parse()
224 .unwrap()
225 } else {
226 panic!("Empty struct fields")
227 }
228}
229
230#[inline]
231fn parse_multiple_fields(group_stream: TokenStream, found_struct_fields: &mut Vec<String>) {
232 let mut current_field_path: Option<String> = None;
233
234 for token_tree in group_stream.into_iter() {
235 match token_tree {
236 TokenTree::Ident(id) => {
237 if let Some(ref mut field_path) = &mut current_field_path {
238 field_path.push_str(id.to_string().as_str())
239 } else {
240 current_field_path = Some(id.to_string());
241 }
242 }
243 TokenTree::Punct(punct) if punct == ',' => {
244 if let Some(field_path) = current_field_path.take() {
245 found_struct_fields.push(field_path);
246 current_field_path = None;
247 } else {
248 panic!(
249 "Unexpected punctuation input for struct path group parameters: {:?}",
250 punct
251 )
252 }
253 }
254 TokenTree::Punct(punct) if punct == '.' => {
255 if let Some(ref mut field_path) = &mut current_field_path {
256 field_path.push('.');
257 } else {
258 panic!(
259 "Unexpected punctuation input for struct path group parameters: {:?}",
260 punct
261 )
262 }
263 }
264 others => {
265 panic!(
266 "Unexpected input for struct path group parameters: {:?}",
267 others
268 )
269 }
270 }
271 }
272
273 if let Some(field_path) = current_field_path.take() {
274 found_struct_fields.push(field_path);
275 }
276}
277
278#[proc_macro]
279pub fn path(struct_path_stream: TokenStream) -> TokenStream {
280 let mut current_struct_name: Option<String> = None;
281
282 let mut opened_struct = false;
283 let mut colons_counter = 0;
284 let mut options_opened = false;
285
286 let mut current_field_path: Option<String> = None;
287 let mut current_full_field_path: Option<String> = None;
288
289 let mut current_option_name: Option<String> = None;
290 let mut expect_option_value: bool = false;
291
292 let mut options: HashMap<String, String> = HashMap::new();
293 let mut found_structs: Vec<(String, Vec<String>)> = Vec::new();
294
295 for token_tree in struct_path_stream.into_iter() {
296 match token_tree {
297 TokenTree::Ident(id) if current_struct_name.is_none() => {
298 current_struct_name = Some(id.to_string());
299 }
300 TokenTree::Punct(punct)
301 if current_struct_name.is_some()
302 && !opened_struct
303 && punct == ':'
304 && colons_counter < 2 =>
305 {
306 colons_counter += 1;
307 if colons_counter > 1 {
308 opened_struct = true;
309 }
310 }
311 TokenTree::Ident(id) if opened_struct => {
312 colons_counter = 0;
313 if let Some(ref mut field_path) = &mut current_field_path {
314 field_path.push_str(id.to_string().as_str())
315 } else {
316 current_field_path = Some(id.to_string());
317 }
318 }
319 TokenTree::Punct(punct)
320 if current_struct_name.is_some()
321 && opened_struct
322 && punct == ':'
323 && colons_counter < 2 =>
324 {
325 colons_counter += 1;
326 opened_struct = false;
327 if let Some(ref mut field_path) = current_field_path.take() {
328 if let Some(ref mut struct_name) = &mut current_struct_name {
329 struct_name.push_str("::");
330 struct_name.push_str(field_path);
331 }
332 }
333 }
334 TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
335 if let Some(ref mut field_path) = &mut current_field_path {
336 field_path.push(punct.as_char());
337 } else {
338 panic!(
339 "Unexpected punctuation input for struct path group parameters: {:?}",
340 punct
341 )
342 }
343 }
344 TokenTree::Punct(punct) if !options_opened && opened_struct && punct == ',' => {
345 opened_struct = false;
346 colons_counter = 0;
347 if let Some(struct_name) = current_struct_name.take() {
348 if let Some(field_path) = current_field_path.take() {
349 found_structs.push((struct_name, vec![field_path.clone()]));
350
351 if let Some(full_field_path) = &mut current_full_field_path {
352 full_field_path.push('.');
353 full_field_path.push_str(field_path.as_str());
354 } else {
355 current_full_field_path = Some(field_path)
356 }
357 } else {
358 panic!("Unexpected comma with empty fields for {}!", struct_name);
359 }
360 } else {
361 panic!("Unexpected comma with empty definitions!");
362 }
363 }
364 TokenTree::Punct(punct) if punct == ';' && opened_struct && !options_opened => {
365 options_opened = true;
366 opened_struct = false;
367 }
368 TokenTree::Ident(id) if options_opened && !expect_option_value => {
369 current_option_name = Some(id.to_string())
370 }
371 TokenTree::Ident(id) if options_opened && expect_option_value => {
372 expect_option_value = false;
373 match current_option_name.take() {
374 Some(option_name) => {
375 options.insert(option_name, id.to_string());
376 }
377 _ => {
378 panic!("Wrong options format")
379 }
380 }
381 }
382 TokenTree::Literal(lit) if options_opened && expect_option_value => {
383 expect_option_value = false;
384 match current_option_name.take() {
385 Some(option_name) => {
386 let lit_str = lit.to_string();
387 options.insert(
388 option_name,
389 lit_str.as_str()[1..lit_str.len() - 1].to_string(),
390 );
391 }
392 _ => {
393 panic!("Wrong options format")
394 }
395 }
396 }
397 TokenTree::Punct(punct) if options_opened && punct == '=' => {
398 expect_option_value = true;
399 }
400 TokenTree::Punct(punct) if options_opened && punct == ',' => {
401 expect_option_value = false;
402 }
403 others => {
404 panic!("Unexpected input for struct path parameters: {:?}", others)
405 }
406 }
407 }
408
409 if let Some(struct_name) = current_struct_name.take() {
410 if let Some(field_path) = current_field_path.take() {
411 found_structs.push((struct_name, vec![field_path.clone()]));
412
413 if let Some(full_field_path) = &mut current_full_field_path {
414 full_field_path.push('.');
415 full_field_path.push_str(field_path.as_str());
416 } else {
417 current_full_field_path = Some(field_path)
418 }
419 }
420 }
421
422 if let Some(full_field_path) = current_full_field_path.take() {
423 let all_check_functions = generate_checks_code_for(&found_structs);
424 let final_field_path = apply_options(&options, full_field_path).replace('~', ".");
425 let result_str = format!("{{{}\n\"{}\"}}", all_check_functions, final_field_path);
426 result_str.parse().unwrap()
427 } else {
428 panic!("Unexpected empty path definition!");
429 }
430}
431
432#[inline]
433fn generate_checks_code_for(found_structs: &Vec<(String, Vec<String>)>) -> String {
434 let mut all_check_functions = String::new();
435
436 for (struct_name, struct_fields) in found_structs {
437 let check_functions = struct_fields
438 .iter()
439 .map(|field_path| {
440 let field_path_result = field_path.replace('~', ".iter().next().unwrap().");
441 format!(
442 r#"
443 {{
444 #[allow(dead_code, unused_variables)]
445 #[cold]
446 fn _check_sp(test_struct: &{}) {{
447 let _t = &test_struct.{};
448 }}
449 }}
450 "#,
451 struct_name, field_path_result
452 )
453 })
454 .collect::<Vec<String>>()
455 .join("\n");
456
457 all_check_functions.push_str(&check_functions);
458 }
459 all_check_functions
460}
461
462#[inline]
463fn apply_options(options: &HashMap<String, String>, field_path: String) -> String {
464 let delim = options
465 .get("delim")
466 .as_ref()
467 .map(|s| s.as_str())
468 .unwrap_or_else(|| ".");
469 let case = options.get("case");
470 field_path
471 .split('.')
472 .map(|field_name| {
473 if let Some(case_value) = case {
474 match case_value.as_str() {
475 "camel" => field_name.from_case(Case::Snake).to_case(Case::Camel),
476 "pascal" => field_name.from_case(Case::Snake).to_case(Case::Pascal),
477 another => panic!("Unknown case is specified: {}", another),
478 }
479 } else {
480 field_name.to_string()
481 }
482 })
483 .collect::<Vec<String>>()
484 .join(delim)
485}