1use convert_case::{Case, Casing};
57use proc_macro::{Delimiter, Group, Span, TokenStream, TokenTree};
58use std::collections::HashMap;
59
60fn compile_error_for(message: String) -> TokenStream {
64 format!("compile_error!({:?})", message)
65 .parse()
66 .expect("a compile_error! invocation with an escaped string literal always parses")
67}
68
69type FoundStructs = Vec<(Vec<TokenTree>, Vec<Vec<TokenTree>>)>;
75
76#[proc_macro]
77pub fn paths(struct_path_stream: TokenStream) -> TokenStream {
78 match paths_impl(struct_path_stream) {
79 Ok(stream) => stream,
80 Err(message) => compile_error_for(message),
81 }
82}
83
84fn paths_impl(struct_path_stream: TokenStream) -> Result<TokenStream, String> {
85 let mut current_struct_name_tokens: Vec<TokenTree> = Vec::new();
86 let mut current_struct_fields: Vec<Vec<TokenTree>> = Vec::with_capacity(16);
87
88 let mut opened_struct = false;
89 let mut colons_counter = 0;
90 let mut options_opened = false;
91
92 let mut current_field_tokens: Vec<TokenTree> = Vec::new();
93
94 let mut current_option_name: Option<String> = None;
95 let mut expect_option_value: bool = false;
96
97 let mut options: HashMap<String, String> = HashMap::new();
98 let mut found_structs: FoundStructs = Vec::new();
99
100 for token_tree in struct_path_stream.into_iter() {
101 match token_tree {
102 TokenTree::Ident(id) if current_struct_name_tokens.is_empty() => {
103 current_struct_name_tokens.push(TokenTree::Ident(id));
104 }
105 TokenTree::Punct(punct)
106 if !current_struct_name_tokens.is_empty()
107 && !opened_struct
108 && punct == ':'
109 && colons_counter < 2 =>
110 {
111 colons_counter += 1;
112 if colons_counter > 1 {
113 opened_struct = true;
114 }
115 }
116 TokenTree::Ident(id) if opened_struct => {
117 colons_counter = 0;
118 current_field_tokens.push(TokenTree::Ident(id));
119 }
120 TokenTree::Punct(punct)
121 if !current_struct_name_tokens.is_empty()
122 && opened_struct
123 && punct == ':'
124 && colons_counter < 2 =>
125 {
126 colons_counter += 1;
127 opened_struct = false;
128 let field_tokens = std::mem::take(&mut current_field_tokens);
129 if !fold_field_into_struct_name(field_tokens, &mut current_struct_name_tokens) {
130 return Err(format!(
131 "Unexpected punctuation input for struct path group parameters: {:?}",
132 punct
133 ));
134 }
135 }
136 TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
137 if current_field_tokens.is_empty() {
138 return Err(format!(
139 "Unexpected punctuation input for struct path group parameters: {:?}",
140 punct
141 ));
142 }
143 current_field_tokens.push(TokenTree::Punct(punct));
144 }
145 TokenTree::Group(group) if opened_struct && current_field_tokens.is_empty() => {
146 parse_multiple_fields(group.stream(), &mut current_struct_fields)?
147 }
148 TokenTree::Punct(punct) if !options_opened && opened_struct && punct == ',' => {
149 opened_struct = false;
150 colons_counter = 0;
151 if current_struct_name_tokens.is_empty() {
152 return Err("Unexpected comma with empty definitions!".to_string());
153 }
154 if !current_field_tokens.is_empty() {
155 current_struct_fields.push(std::mem::take(&mut current_field_tokens));
156 }
157 if current_struct_fields.is_empty() {
158 return Err(format!(
159 "Unexpected comma with empty fields for {}!",
160 render_text(¤t_struct_name_tokens)
161 ));
162 }
163 found_structs.push((
164 std::mem::take(&mut current_struct_name_tokens),
165 std::mem::take(&mut current_struct_fields),
166 ));
167 }
168 TokenTree::Punct(punct) if punct == ';' && opened_struct && !options_opened => {
169 options_opened = true;
170 opened_struct = false;
171 }
172 TokenTree::Ident(id) if options_opened && !expect_option_value => {
173 current_option_name = Some(id.to_string())
174 }
175 TokenTree::Ident(id) if options_opened && expect_option_value => {
176 expect_option_value = false;
177 match current_option_name.take() {
178 Some(option_name) => {
179 options.insert(option_name, id.to_string());
180 }
181 _ => {
182 return Err("Wrong options format".to_string());
183 }
184 }
185 }
186 TokenTree::Literal(lit) if options_opened && expect_option_value => {
187 expect_option_value = false;
188 match current_option_name.take() {
189 Some(option_name) => {
190 options.insert(option_name, unquote_literal(&lit)?);
191 }
192 _ => {
193 return Err("Wrong options format".to_string());
194 }
195 }
196 }
197 TokenTree::Punct(punct) if options_opened && punct == '=' => {
198 expect_option_value = true;
199 }
200 TokenTree::Punct(punct) if options_opened && punct == ',' => {
201 expect_option_value = false;
202 }
203 others => {
204 return Err(format!(
205 "Unexpected input for struct path parameters: {:?}",
206 others
207 ));
208 }
209 }
210 }
211
212 if !current_field_tokens.is_empty() {
213 current_struct_fields.push(std::mem::take(&mut current_field_tokens));
214 }
215 if current_struct_name_tokens.is_empty() {
216 return Err("Unexpected comma with empty definitions!".to_string());
217 }
218 if current_struct_fields.is_empty() {
219 return Err(format!(
220 "Unexpected comma with empty fields for {}!",
221 render_text(¤t_struct_name_tokens)
222 ));
223 }
224 found_structs.push((current_struct_name_tokens, current_struct_fields));
225
226 let all_check_functions = generate_checks_code_for(&found_structs);
227
228 let mut all_final_fields: Vec<String> = Vec::with_capacity(16);
229
230 for (_, fields) in &found_structs {
231 for field_tokens in fields {
232 let mut final_field_path = render_text(field_tokens).replace('~', ".");
233 if !options.is_empty() {
234 final_field_path = apply_options(&options, final_field_path)?;
235 }
236 all_final_fields.push(format!("\"{}\"", final_field_path))
237 }
238 }
239
240 let fields_str = format!("[{}]", all_final_fields.join(","));
241 let fields_tokens: TokenStream = fields_str
242 .parse()
243 .map_err(|_| format!("Generated code failed to parse: {}", fields_str))?;
244 let mut block_contents = all_check_functions;
245 block_contents.extend(fields_tokens);
246 Ok(TokenStream::from(TokenTree::Group(Group::new(
247 Delimiter::Brace,
248 block_contents,
249 ))))
250}
251
252fn fold_field_into_struct_name(
259 field_tokens: Vec<TokenTree>,
260 struct_name_tokens: &mut Vec<TokenTree>,
261) -> bool {
262 let is_single_ident = matches!(field_tokens.as_slice(), [TokenTree::Ident(_)]);
263 if is_single_ident {
264 struct_name_tokens.extend("::".parse::<TokenStream>().expect("`::` always parses"));
265 struct_name_tokens.extend(field_tokens);
266 }
267 is_single_ident
268}
269
270fn render_text(tokens: &[TokenTree]) -> String {
273 tokens.iter().map(|token| token.to_string()).collect()
274}
275
276fn parse_multiple_fields(
277 group_stream: TokenStream,
278 found_struct_fields: &mut Vec<Vec<TokenTree>>,
279) -> Result<(), String> {
280 let mut current_field_tokens: Vec<TokenTree> = Vec::new();
281
282 for token_tree in group_stream.into_iter() {
283 match token_tree {
284 TokenTree::Ident(id) => current_field_tokens.push(TokenTree::Ident(id)),
285 TokenTree::Punct(punct) if punct == ',' => {
286 if current_field_tokens.is_empty() {
287 return Err(format!(
288 "Unexpected punctuation input for struct path group parameters: {:?}",
289 punct
290 ));
291 }
292 found_struct_fields.push(std::mem::take(&mut current_field_tokens));
293 }
294 TokenTree::Punct(punct) if punct == '.' => {
295 if current_field_tokens.is_empty() {
296 return Err(format!(
297 "Unexpected punctuation input for struct path group parameters: {:?}",
298 punct
299 ));
300 }
301 current_field_tokens.push(TokenTree::Punct(punct));
302 }
303 others => {
304 return Err(format!(
305 "Unexpected input for struct path group parameters: {:?}",
306 others
307 ));
308 }
309 }
310 }
311
312 if !current_field_tokens.is_empty() {
313 found_struct_fields.push(current_field_tokens);
314 }
315 Ok(())
316}
317
318#[proc_macro]
319pub fn path(struct_path_stream: TokenStream) -> TokenStream {
320 match path_impl(struct_path_stream) {
321 Ok(stream) => stream,
322 Err(message) => compile_error_for(message),
323 }
324}
325
326fn path_impl(struct_path_stream: TokenStream) -> Result<TokenStream, String> {
327 let mut current_struct_name_tokens: Vec<TokenTree> = Vec::new();
328
329 let mut opened_struct = false;
330 let mut colons_counter = 0;
331 let mut options_opened = false;
332
333 let mut current_field_tokens: Vec<TokenTree> = Vec::new();
334
335 let mut current_option_name: Option<String> = None;
336 let mut expect_option_value: bool = false;
337
338 let mut options: HashMap<String, String> = HashMap::new();
339 let mut found_structs: FoundStructs = Vec::new();
340
341 for token_tree in struct_path_stream.into_iter() {
342 match token_tree {
343 TokenTree::Ident(id) if current_struct_name_tokens.is_empty() => {
344 current_struct_name_tokens.push(TokenTree::Ident(id));
345 }
346 TokenTree::Punct(punct)
347 if !current_struct_name_tokens.is_empty()
348 && !opened_struct
349 && punct == ':'
350 && colons_counter < 2 =>
351 {
352 colons_counter += 1;
353 if colons_counter > 1 {
354 opened_struct = true;
355 }
356 }
357 TokenTree::Ident(id) if opened_struct => {
358 colons_counter = 0;
359 current_field_tokens.push(TokenTree::Ident(id));
360 }
361 TokenTree::Punct(punct)
362 if !current_struct_name_tokens.is_empty()
363 && opened_struct
364 && punct == ':'
365 && colons_counter < 2 =>
366 {
367 colons_counter += 1;
368 opened_struct = false;
369 let field_tokens = std::mem::take(&mut current_field_tokens);
370 if !fold_field_into_struct_name(field_tokens, &mut current_struct_name_tokens) {
371 return Err(format!(
372 "Unexpected punctuation input for struct path group parameters: {:?}",
373 punct
374 ));
375 }
376 }
377 TokenTree::Punct(punct) if opened_struct && (punct == '.' || punct == '~') => {
378 if current_field_tokens.is_empty() {
379 return Err(format!(
380 "Unexpected punctuation input for struct path group parameters: {:?}",
381 punct
382 ));
383 }
384 current_field_tokens.push(TokenTree::Punct(punct));
385 }
386 TokenTree::Punct(punct) if !options_opened && opened_struct && punct == ',' => {
387 opened_struct = false;
388 colons_counter = 0;
389 if current_struct_name_tokens.is_empty() {
390 return Err("Unexpected comma with empty definitions!".to_string());
391 }
392 if current_field_tokens.is_empty() {
393 return Err(format!(
394 "Unexpected comma with empty fields for {}!",
395 render_text(¤t_struct_name_tokens)
396 ));
397 }
398 found_structs.push((
399 std::mem::take(&mut current_struct_name_tokens),
400 vec![std::mem::take(&mut current_field_tokens)],
401 ));
402 }
403 TokenTree::Punct(punct) if punct == ';' && opened_struct && !options_opened => {
404 options_opened = true;
405 opened_struct = false;
406 }
407 TokenTree::Ident(id) if options_opened && !expect_option_value => {
408 current_option_name = Some(id.to_string())
409 }
410 TokenTree::Ident(id) if options_opened && expect_option_value => {
411 expect_option_value = false;
412 match current_option_name.take() {
413 Some(option_name) => {
414 options.insert(option_name, id.to_string());
415 }
416 _ => {
417 return Err("Wrong options format".to_string());
418 }
419 }
420 }
421 TokenTree::Literal(lit) if options_opened && expect_option_value => {
422 expect_option_value = false;
423 match current_option_name.take() {
424 Some(option_name) => {
425 options.insert(option_name, unquote_literal(&lit)?);
426 }
427 _ => {
428 return Err("Wrong options format".to_string());
429 }
430 }
431 }
432 TokenTree::Punct(punct) if options_opened && punct == '=' => {
433 expect_option_value = true;
434 }
435 TokenTree::Punct(punct) if options_opened && punct == ',' => {
436 expect_option_value = false;
437 }
438 others => {
439 return Err(format!(
440 "Unexpected input for struct path parameters: {:?}",
441 others
442 ));
443 }
444 }
445 }
446
447 if !current_struct_name_tokens.is_empty() && !current_field_tokens.is_empty() {
448 found_structs.push((current_struct_name_tokens, vec![current_field_tokens]));
449 }
450
451 if found_structs.is_empty() {
452 return Err("Unexpected empty path definition!".to_string());
453 }
454
455 let all_check_functions = generate_checks_code_for(&found_structs);
456 let full_field_path = found_structs
461 .iter()
462 .flat_map(|(_, fields)| fields.iter())
463 .map(|tokens| render_text(tokens))
464 .collect::<Vec<_>>()
465 .join(".")
466 .replace('~', ".");
467 let final_field_path = apply_options(&options, full_field_path)?;
468 let fields_str = format!("\"{}\"", final_field_path);
469 let fields_tokens: TokenStream = fields_str
470 .parse()
471 .map_err(|_| format!("Generated code failed to parse: {}", fields_str))?;
472 let mut block_contents = all_check_functions;
473 block_contents.extend(fields_tokens);
474 Ok(TokenStream::from(TokenTree::Group(Group::new(
475 Delimiter::Brace,
476 block_contents,
477 ))))
478}
479
480fn generate_checks_code_for(found_structs: &FoundStructs) -> TokenStream {
486 let mut all_check_functions = TokenStream::new();
487 for (struct_name_tokens, struct_fields) in found_structs {
488 let struct_name: TokenStream = struct_name_tokens.iter().cloned().collect();
489 for field_tokens in struct_fields {
490 let field_path: TokenStream = field_tokens
491 .iter()
492 .map(|token| match token {
493 TokenTree::Punct(punct) if *punct == '~' => {
494 fill(".iter().next().unwrap().", punct.span(), &[])
495 }
496 token => token.clone().into(),
497 })
498 .collect();
499 all_check_functions.extend(fill(
500 "const _: fn(&STRUCT) = |t: &STRUCT| { let _ = &t.FIELD; };",
501 Span::call_site(),
502 &[("STRUCT", &struct_name), ("FIELD", &field_path)],
503 ));
504 }
505 }
506 all_check_functions
507}
508
509fn fill(template: &str, span: Span, substitutions: &[(&str, &TokenStream)]) -> TokenStream {
513 fn walk(stream: TokenStream, span: Span, subs: &[(&str, &TokenStream)]) -> TokenStream {
514 stream
515 .into_iter()
516 .map(|mut token| {
517 match &token {
518 TokenTree::Ident(ident) => {
519 let name = ident.to_string();
520 if let Some((_, tokens)) = subs.iter().find(|(n, _)| *n == name) {
521 return (*tokens).clone();
522 }
523 }
524 TokenTree::Group(group) => {
525 let stream = walk(group.stream(), span, subs);
526 token = TokenTree::Group(Group::new(group.delimiter(), stream));
527 }
528 _ => {}
529 }
530 token.set_span(span);
531 token.into()
532 })
533 .collect()
534 }
535 walk(
536 template.parse().expect("templates are valid tokens"),
537 span,
538 substitutions,
539 )
540}
541
542fn unquote_literal(lit: &proc_macro::Literal) -> Result<String, String> {
549 let text = lit.to_string();
550 let bytes = text.as_bytes();
551 let is_quoted = bytes.len() >= 2
552 && ((bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
553 || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''));
554 if is_quoted {
555 Ok(text[1..text.len() - 1].to_string())
556 } else {
557 Err(format!(
558 "Unsupported option value {}: expected a string (\"...\") or char ('.') literal",
559 text
560 ))
561 }
562}
563
564fn apply_options(options: &HashMap<String, String>, field_path: String) -> Result<String, String> {
565 let delim = options
566 .get("delim")
567 .as_ref()
568 .map(|s| s.as_str())
569 .unwrap_or(".");
570 let case = options.get("case");
571 let segments = field_path
572 .split('.')
573 .map(|field_name| {
574 if let Some(case_value) = case {
575 match case_value.as_str() {
576 "camel" => Ok(field_name.from_case(Case::Snake).to_case(Case::Camel)),
577 "pascal" => Ok(field_name.from_case(Case::Snake).to_case(Case::Pascal)),
578 another => Err(format!("Unknown case is specified: {}", another)),
579 }
580 } else {
581 Ok(field_name.to_string())
582 }
583 })
584 .collect::<Result<Vec<String>, String>>()?;
585 Ok(segments.join(delim))
586}