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