1use proc_macro2::{Span, TokenStream as TokenStream2, TokenTree};
7use quote::quote;
8use quote::quote_spanned;
9use unsynn::*;
10
11struct SpannedError {
13 message: String,
14 span: Span,
15 help: Option<String>,
17}
18
19keyword! {
24 KKrate = "krate";
25 KEnumName = "enum_name";
26 KVariantName = "variant_name";
27 KStructName = "struct_name";
28 KFields = "fields";
29 KInput = "input";
30}
31
32operator! {
33 At = "@";
34 Col = ":";
35}
36
37unsynn! {
38 struct BuildStructFieldsInput {
40 krate_section: KrateSection,
41 enum_name_section: EnumNameSection,
42 variant_name_section: VariantNameSection,
43 struct_name_section: StructNameSection,
44 fields_section: FieldsSection,
45 input_section: InputSection,
46 }
47
48 struct KrateSection {
50 _at: At,
51 _kw: KKrate,
52 content: BraceGroup,
53 }
54
55 struct EnumNameSection {
57 _at: At,
58 _kw: KEnumName,
59 content: BraceGroupContaining<Ident>,
60 }
61
62 struct VariantNameSection {
64 _at: At,
65 _kw: KVariantName,
66 content: BraceGroupContaining<Ident>,
67 }
68
69 struct StructNameSection {
71 _at: At,
72 _kw: KStructName,
73 content: BraceGroupContaining<Ident>,
74 }
75
76 struct FieldsSection {
79 _at: At,
80 _kw: KFields,
81 content: BraceGroup,
83 }
84
85 struct InputSection {
87 _at: At,
88 _kw: KInput,
89 content: BraceGroup,
90 }
91}
92
93struct ParsedBuildInput {
98 krate_path: TokenStream2,
99 enum_name: Ident,
100 variant_name: Ident,
101 struct_name: Ident,
102 fields: Vec<ParsedFieldDef>,
103 input: TokenStream2,
104}
105
106#[derive(Clone)]
107struct ParsedFieldDef {
108 name: Ident,
109 kind: FieldKind,
110 doc: Option<String>,
112}
113
114#[derive(Clone, Copy, PartialEq)]
115enum FieldKind {
116 Bool,
117 String,
118 OptString,
119 OptBool,
120 OptChar,
121 I64,
122 OptI64,
123 ListString,
124 ListI64,
125 Ident,
127}
128
129struct ParsedField {
131 name: String,
132 #[allow(dead_code)]
133 name_span: Span,
134 value: FieldValue,
135}
136
137enum FieldValue {
138 String(String),
140 Bool(bool),
142 Char(char),
144 I64(i64),
146 ListString(Vec<String>),
148 ListI64(Vec<i64>),
150 Ident(String),
152 Flag,
154}
155
156impl BuildStructFieldsInput {
157 fn to_parsed(&self) -> std::result::Result<ParsedBuildInput, String> {
158 let krate_path = self.krate_section.content.0.stream();
159 let enum_name = self.enum_name_section.content.content.clone();
160 let variant_name = self.variant_name_section.content.content.clone();
161 let struct_name = self.struct_name_section.content.content.clone();
162
163 let fields = parse_field_defs_with_docs(&self.fields_section.content.0.stream())?;
165
166 let input = self.input_section.content.0.stream();
167
168 Ok(ParsedBuildInput {
169 krate_path,
170 enum_name,
171 variant_name,
172 struct_name,
173 fields,
174 input,
175 })
176 }
177}
178
179fn parse_field_defs_with_docs(
181 tokens: &TokenStream2,
182) -> std::result::Result<Vec<ParsedFieldDef>, String> {
183 let tokens: Vec<TokenTree> = tokens.clone().into_iter().collect();
184 let mut fields = Vec::new();
185 let mut i = 0;
186 let mut current_doc: Option<String> = None;
187
188 while i < tokens.len() {
189 if let TokenTree::Punct(p) = &tokens[i]
191 && p.as_char() == ','
192 {
193 i += 1;
194 continue;
195 }
196
197 if let TokenTree::Punct(p) = &tokens[i]
199 && p.as_char() == '#'
200 && i + 1 < tokens.len()
201 && let TokenTree::Group(g) = &tokens[i + 1]
202 && g.delimiter() == proc_macro2::Delimiter::Bracket
203 && let Some(doc) = extract_doc_from_attr(&g.stream())
204 {
205 let trimmed = doc.trim();
207 if let Some(existing) = &mut current_doc {
208 existing.push(' ');
209 existing.push_str(trimmed);
210 } else {
211 current_doc = Some(trimmed.to_string());
212 }
213 i += 2;
214 continue;
215 }
216
217 let name = match &tokens[i] {
219 TokenTree::Ident(ident) => ident.clone(),
220 other => return Err(format!("expected field name, found `{other}`")),
221 };
222 i += 1;
223
224 if i >= tokens.len() {
226 return Err(format!("expected `:` after field name `{name}`"));
227 }
228 if let TokenTree::Punct(p) = &tokens[i] {
229 if p.as_char() != ':' {
230 return Err(format!(
231 "expected `:` after field name `{name}`, found `{p}`"
232 ));
233 }
234 } else {
235 return Err(format!("expected `:` after field name `{name}`"));
236 }
237 i += 1;
238
239 if i >= tokens.len() {
241 return Err(format!("expected field kind after `{name}:`"));
242 }
243 let kind_ident = match &tokens[i] {
244 TokenTree::Ident(ident) => ident.clone(),
245 other => return Err(format!("expected field kind, found `{other}`")),
246 };
247 i += 1;
248
249 let kind_str = kind_ident.to_string();
250 let kind = match kind_str.as_str() {
251 "bool" => FieldKind::Bool,
252 "string" => FieldKind::String,
253 "opt_string" => FieldKind::OptString,
254 "opt_bool" => FieldKind::OptBool,
255 "opt_char" => FieldKind::OptChar,
256 "i64" => FieldKind::I64,
257 "opt_i64" => FieldKind::OptI64,
258 "list_string" => FieldKind::ListString,
259 "list_i64" => FieldKind::ListI64,
260 "ident" => FieldKind::Ident,
261 _ => return Err(format!("unknown field kind: {kind_str}")),
262 };
263
264 fields.push(ParsedFieldDef {
265 name,
266 kind,
267 doc: current_doc.take(),
268 });
269 }
270
271 Ok(fields)
272}
273
274fn unescape_string(s: &str) -> String {
276 let mut out = String::with_capacity(s.len());
277 let mut chars = s.chars().peekable();
278
279 while let Some(c) = chars.next() {
280 if c == '\\' {
281 match chars.next() {
282 Some('\\') => out.push('\\'),
283 Some('"') => out.push('"'),
284 Some('\'') => out.push('\''),
285 Some('n') => out.push('\n'),
286 Some('r') => out.push('\r'),
287 Some('t') => out.push('\t'),
288 Some('0') => out.push('\0'),
289 Some(other) => {
290 out.push('\\');
292 out.push(other);
293 }
294 None => out.push('\\'),
295 }
296 } else {
297 out.push(c);
298 }
299 }
300 out
301}
302
303fn extract_doc_from_attr(tokens: &TokenStream2) -> Option<String> {
304 let tokens: Vec<TokenTree> = tokens.clone().into_iter().collect();
305
306 if tokens.len() >= 3
308 && let TokenTree::Ident(ident) = &tokens[0]
309 && *ident == "doc"
310 && let TokenTree::Punct(p) = &tokens[1]
311 && p.as_char() == '='
312 && let TokenTree::Literal(lit) = &tokens[2]
313 {
314 let lit_str = lit.to_string();
315 if lit_str.starts_with('"') && lit_str.ends_with('"') {
317 let inner = &lit_str[1..lit_str.len() - 1];
318 return Some(unescape_string(inner.trim_start()));
319 }
320 }
321 None
322}
323
324pub fn build_struct_fields(input: TokenStream2) -> TokenStream2 {
330 let mut iter = input.to_token_iter();
331
332 let parsed_input: BuildStructFieldsInput = match iter.parse() {
333 Ok(i) => i,
334 Err(e) => {
335 let msg = e.to_string();
336 return quote! { compile_error!(#msg); };
337 }
338 };
339
340 let input = match parsed_input.to_parsed() {
341 Ok(i) => i,
342 Err(e) => {
343 return quote! { compile_error!(#e); };
344 }
345 };
346
347 match build_struct_fields_impl(&input) {
348 Ok(tokens) => tokens,
349 Err(err) => emit_error(err),
350 }
351}
352
353fn emit_error(err: SpannedError) -> TokenStream2 {
354 let message = err.message;
355 let span = err.span;
356 let help = err.help;
357
358 let full_message = if let Some(help_text) = help {
360 format!("{message}\n = help: {help_text}")
361 } else {
362 message
363 };
364 quote_spanned! { span => compile_error!(#full_message) }
365}
366
367fn build_struct_fields_impl(
368 input: &ParsedBuildInput,
369) -> std::result::Result<TokenStream2, SpannedError> {
370 let krate_path = &input.krate_path;
371 let enum_name = &input.enum_name;
372 let variant_name = &input.variant_name;
373 let struct_name = &input.struct_name;
374
375 let parsed_fields = parse_input_fields(&input.input, &input.fields)?;
377
378 let field_values: Vec<TokenStream2> = input
380 .fields
381 .iter()
382 .map(|field_def| {
383 let field_name = &field_def.name;
384 let field_name_str = field_name.to_string();
385
386 let parsed = parsed_fields.iter().find(|p| p.name == field_name_str);
388
389 let value = match (parsed, field_def.kind) {
390 (Some(p), FieldKind::String) => match &p.value {
391 FieldValue::String(s) => quote! { #s },
392 _ => quote! { "" }, },
394 (Some(p), FieldKind::OptString) => match &p.value {
395 FieldValue::String(s) => quote! { Some(#s) },
396 _ => quote! { None },
397 },
398 (Some(p), FieldKind::Bool) => match &p.value {
399 FieldValue::Bool(b) => quote! { #b },
400 FieldValue::Flag => quote! { true },
401 _ => quote! { false },
402 },
403 (Some(p), FieldKind::OptBool) => match &p.value {
404 FieldValue::Bool(b) => quote! { Some(#b) },
405 FieldValue::Flag => quote! { Some(true) },
406 _ => quote! { None },
407 },
408 (Some(p), FieldKind::OptChar) => match &p.value {
409 FieldValue::Char(c) => quote! { Some(#c) },
410 _ => quote! { None },
411 },
412 (Some(p), FieldKind::I64) => match &p.value {
413 FieldValue::I64(n) => quote! { #n },
414 _ => quote! { 0 }, },
416 (Some(p), FieldKind::OptI64) => match &p.value {
417 FieldValue::I64(n) => quote! { Some(#n) },
418 _ => quote! { None },
419 },
420 (Some(p), FieldKind::ListString) => match &p.value {
421 FieldValue::ListString(items) => quote! { &[#(#items),*] },
422 _ => quote! { &[] },
423 },
424 (Some(p), FieldKind::ListI64) => match &p.value {
425 FieldValue::ListI64(items) => quote! { &[#(#items),*] },
426 _ => quote! { &[] },
427 },
428 (Some(p), FieldKind::Ident) => match &p.value {
429 FieldValue::Ident(s) => quote! { #s },
430 _ => quote! { "" },
431 },
432 (None, FieldKind::String) => quote! { "" },
433 (None, FieldKind::OptString) => quote! { None },
434 (None, FieldKind::Bool) => quote! { false },
435 (None, FieldKind::OptBool) => quote! { None },
436 (None, FieldKind::OptChar) => quote! { None },
437 (None, FieldKind::I64) => quote! { 0 },
438 (None, FieldKind::OptI64) => quote! { None },
439 (None, FieldKind::ListString) => quote! { &[] },
440 (None, FieldKind::ListI64) => quote! { &[] },
441 (None, FieldKind::Ident) => quote! { "" },
442 };
443
444 quote! { #field_name: #value }
445 })
446 .collect();
447
448 Ok(quote! {
449 #krate_path::#enum_name::#variant_name(#krate_path::#struct_name {
450 #(#field_values),*
451 })
452 })
453}
454
455fn parse_input_fields(
456 input: &TokenStream2,
457 field_defs: &[ParsedFieldDef],
458) -> std::result::Result<Vec<ParsedField>, SpannedError> {
459 let tokens: Vec<TokenTree> = input.clone().into_iter().collect();
460 let mut parsed = Vec::new();
461 let mut i = 0;
462
463 while i < tokens.len() {
464 if let TokenTree::Punct(p) = &tokens[i]
466 && p.as_char() == ','
467 {
468 i += 1;
469 continue;
470 }
471
472 let field_name = match &tokens[i] {
474 TokenTree::Ident(ident) => ident.clone(),
475 other => {
476 return Err(SpannedError {
477 message: format!("expected field name, found `{other}`"),
478 span: other.span(),
479 help: None,
480 });
481 }
482 };
483 let field_name_str = field_name.to_string();
484 let field_span = field_name.span();
485 i += 1;
486
487 if parsed
489 .iter()
490 .any(|p: &ParsedField| p.name == field_name_str)
491 {
492 return Err(SpannedError {
493 message: format!(
494 "duplicate field `{field_name_str}`; each field can only be specified once"
495 ),
496 span: field_span,
497 help: None,
498 });
499 }
500
501 let field_def = field_defs.iter().find(|f| f.name == field_name_str);
503 if field_def.is_none() {
504 let known_names: Vec<_> = field_defs.iter().map(|f| f.name.to_string()).collect();
506 let suggestion = find_closest(&field_name_str, &known_names);
507 let msg = if let Some(s) = suggestion {
508 format!(
509 "unknown field `{}`; did you mean `{}`? Known fields: {}",
510 field_name_str,
511 s,
512 known_names.join(", ")
513 )
514 } else {
515 format!(
516 "unknown field `{}`; known fields: {}",
517 field_name_str,
518 known_names.join(", ")
519 )
520 };
521 return Err(SpannedError {
522 message: msg,
523 span: field_span,
524 help: None,
525 });
526 }
527 let field_def = field_def.unwrap();
528
529 if i >= tokens.len() {
531 match field_def.kind {
533 FieldKind::Bool | FieldKind::OptBool => {
534 parsed.push(ParsedField {
535 name: field_name_str,
536 name_span: field_span,
537 value: FieldValue::Flag,
538 });
539 }
540 FieldKind::String | FieldKind::OptString => {
541 return Err(SpannedError {
542 message: format!(
543 "`{field_name_str}` requires a string value: `{field_name_str} = \"value\"`"
544 ),
545 span: field_span,
546 help: field_def.doc.clone(),
547 });
548 }
549 FieldKind::OptChar => {
550 return Err(SpannedError {
551 message: format!(
552 "`{field_name_str}` requires a char value: `{field_name_str} = 'v'`"
553 ),
554 span: field_span,
555 help: field_def.doc.clone(),
556 });
557 }
558 FieldKind::I64 | FieldKind::OptI64 => {
559 return Err(SpannedError {
560 message: format!(
561 "`{field_name_str}` requires an integer value: `{field_name_str} = 42`"
562 ),
563 span: field_span,
564 help: field_def.doc.clone(),
565 });
566 }
567 FieldKind::ListString => {
568 return Err(SpannedError {
569 message: format!(
570 "`{field_name_str}` requires a list value: `{field_name_str} = [\"a\", \"b\"]`"
571 ),
572 span: field_span,
573 help: field_def.doc.clone(),
574 });
575 }
576 FieldKind::ListI64 => {
577 return Err(SpannedError {
578 message: format!(
579 "`{field_name_str}` requires a list value: `{field_name_str} = [1, 2, 3]`"
580 ),
581 span: field_span,
582 help: field_def.doc.clone(),
583 });
584 }
585 FieldKind::Ident => {
586 return Err(SpannedError {
587 message: format!(
588 "`{field_name_str}` requires an identifier value: `{field_name_str} = some_value`"
589 ),
590 span: field_span,
591 help: field_def.doc.clone(),
592 });
593 }
594 }
595 continue;
596 }
597
598 if let TokenTree::Punct(p) = &tokens[i] {
600 if p.as_char() == '=' {
601 i += 1;
602 if i >= tokens.len() {
604 return Err(SpannedError {
605 message: format!("`{field_name_str}` requires a value after `=`"),
606 span: field_span,
607 help: field_def.doc.clone(),
608 });
609 }
610
611 let value_token = &tokens[i];
612 i += 1;
613
614 match field_def.kind {
615 FieldKind::String | FieldKind::OptString => {
616 if let TokenTree::Literal(lit) = value_token {
618 let lit_str = lit.to_string();
619 if lit_str.starts_with('\"') && lit_str.ends_with('\"') {
621 let inner = lit_str[1..lit_str.len() - 1].to_string();
622 parsed.push(ParsedField {
623 name: field_name_str,
624 name_span: field_span,
625 value: FieldValue::String(inner),
626 });
627 } else {
628 return Err(SpannedError {
629 message: format!(
630 "`{field_name_str}` expects a string literal: `{field_name_str} = \"value\"`"
631 ),
632 span: value_token.span(),
633 help: field_def.doc.clone(),
634 });
635 }
636 } else if let TokenTree::Ident(ident) = value_token {
637 return Err(SpannedError {
639 message: format!(
640 "`{field_name_str}` expects a string literal, not an identifier; \
641 try `{field_name_str} = \"{ident}\"` (with quotes)"
642 ),
643 span: value_token.span(),
644 help: field_def.doc.clone(),
645 });
646 } else {
647 return Err(SpannedError {
648 message: format!(
649 "`{field_name_str}` expects a string literal: `{field_name_str} = \"value\"`"
650 ),
651 span: value_token.span(),
652 help: field_def.doc.clone(),
653 });
654 }
655 }
656 FieldKind::Bool | FieldKind::OptBool => {
657 if let TokenTree::Ident(ident) = value_token {
659 let ident_str = ident.to_string();
660 match ident_str.as_str() {
661 "true" => {
662 parsed.push(ParsedField {
663 name: field_name_str,
664 name_span: field_span,
665 value: FieldValue::Bool(true),
666 });
667 }
668 "false" => {
669 parsed.push(ParsedField {
670 name: field_name_str,
671 name_span: field_span,
672 value: FieldValue::Bool(false),
673 });
674 }
675 _ => {
676 return Err(SpannedError {
677 message: format!(
678 "`{field_name_str}` expects `true` or `false`: `{field_name_str} = true`"
679 ),
680 span: value_token.span(),
681 help: field_def.doc.clone(),
682 });
683 }
684 }
685 } else if let TokenTree::Literal(lit) = value_token {
686 let lit_str = lit.to_string();
688 if lit_str.starts_with('"') && lit_str.ends_with('"') {
689 let inner = &lit_str[1..lit_str.len() - 1];
690 let suggestion = match inner {
692 "true" | "yes" | "1" | "on" => "true",
693 "false" | "no" | "0" | "off" => "false",
694 _ => "true",
695 };
696 return Err(SpannedError {
697 message: format!(
698 "`{field_name_str}` expects `true` or `false`, not a string; \
699 try `{field_name_str} = {suggestion}` (without quotes)"
700 ),
701 span: value_token.span(),
702 help: field_def.doc.clone(),
703 });
704 }
705 return Err(SpannedError {
706 message: format!(
707 "`{field_name_str}` expects `true` or `false`: `{field_name_str} = true`"
708 ),
709 span: value_token.span(),
710 help: field_def.doc.clone(),
711 });
712 } else {
713 return Err(SpannedError {
714 message: format!(
715 "`{field_name_str}` expects `true` or `false`: `{field_name_str} = true`"
716 ),
717 span: value_token.span(),
718 help: field_def.doc.clone(),
719 });
720 }
721 }
722 FieldKind::OptChar => {
723 if let TokenTree::Literal(lit) = value_token {
725 let lit_str = lit.to_string();
726 if lit_str.starts_with('\'')
728 && lit_str.ends_with('\'')
729 && lit_str.len() >= 3
730 {
731 let inner = &lit_str[1..lit_str.len() - 1];
733 let c = if inner.starts_with('\\') {
734 match inner.chars().nth(1) {
736 Some('n') => '\n',
737 Some('r') => '\r',
738 Some('t') => '\t',
739 Some('\\') => '\\',
740 Some('\'') => '\'',
741 Some('0') => '\0',
742 Some(c) => c,
743 None => {
744 return Err(SpannedError {
745 message: format!(
746 "`{field_name_str}` has invalid escape sequence in char literal"
747 ),
748 span: value_token.span(),
749 help: field_def.doc.clone(),
750 });
751 }
752 }
753 } else {
754 inner.chars().next().unwrap_or(' ')
755 };
756 parsed.push(ParsedField {
757 name: field_name_str,
758 name_span: field_span,
759 value: FieldValue::Char(c),
760 });
761 } else {
762 return Err(SpannedError {
763 message: format!(
764 "`{field_name_str}` expects a char literal: `{field_name_str} = 'v'`"
765 ),
766 span: value_token.span(),
767 help: field_def.doc.clone(),
768 });
769 }
770 } else if let TokenTree::Ident(ident) = value_token {
771 return Err(SpannedError {
773 message: format!(
774 "`{field_name_str}` expects a char literal, not an identifier; \
775 try `{field_name_str} = '{ident}'` (with single quotes)"
776 ),
777 span: value_token.span(),
778 help: field_def.doc.clone(),
779 });
780 } else {
781 return Err(SpannedError {
782 message: format!(
783 "`{field_name_str}` expects a char literal: `{field_name_str} = 'v'`"
784 ),
785 span: value_token.span(),
786 help: field_def.doc.clone(),
787 });
788 }
789 }
790 FieldKind::I64 | FieldKind::OptI64 => {
791 let (value, advance) =
793 parse_integer_value(&tokens[i - 1..], value_token, &field_name_str)?;
794 parsed.push(ParsedField {
795 name: field_name_str,
796 name_span: field_span,
797 value: FieldValue::I64(value),
798 });
799 i += advance;
801 }
802 FieldKind::ListString => {
803 if let TokenTree::Group(g) = value_token {
805 if g.delimiter() == proc_macro2::Delimiter::Bracket {
806 let items = parse_string_list(&g.stream())?;
807 parsed.push(ParsedField {
808 name: field_name_str,
809 name_span: field_span,
810 value: FieldValue::ListString(items),
811 });
812 } else {
813 let bracket_name = match g.delimiter() {
815 proc_macro2::Delimiter::Brace => "curly braces `{}`",
816 proc_macro2::Delimiter::Parenthesis => "parentheses `()`",
817 _ => "wrong delimiters",
818 };
819 return Err(SpannedError {
820 message: format!(
821 "`{field_name_str}` expects square brackets `[]`, not {bracket_name}; \
822 try `{field_name_str} = [\"a\", \"b\"]`"
823 ),
824 span: value_token.span(),
825 help: field_def.doc.clone(),
826 });
827 }
828 } else if let TokenTree::Literal(lit) = value_token {
829 let lit_str = lit.to_string();
831 if lit_str.starts_with('"') && lit_str.ends_with('"') {
832 let inner = &lit_str[1..lit_str.len() - 1];
833 return Err(SpannedError {
834 message: format!(
835 "`{field_name_str}` expects a list, not a single string; \
836 try `{field_name_str} = [\"{inner}\"]`"
837 ),
838 span: value_token.span(),
839 help: field_def.doc.clone(),
840 });
841 }
842 return Err(SpannedError {
843 message: format!(
844 "`{field_name_str}` expects a list: `{field_name_str} = [\"a\", \"b\"]`"
845 ),
846 span: value_token.span(),
847 help: field_def.doc.clone(),
848 });
849 } else {
850 return Err(SpannedError {
851 message: format!(
852 "`{field_name_str}` expects a list: `{field_name_str} = [\"a\", \"b\"]`"
853 ),
854 span: value_token.span(),
855 help: field_def.doc.clone(),
856 });
857 }
858 }
859 FieldKind::ListI64 => {
860 if let TokenTree::Group(g) = value_token {
862 if g.delimiter() == proc_macro2::Delimiter::Bracket {
863 let items = parse_i64_list(&g.stream())?;
864 parsed.push(ParsedField {
865 name: field_name_str,
866 name_span: field_span,
867 value: FieldValue::ListI64(items),
868 });
869 } else {
870 let bracket_name = match g.delimiter() {
872 proc_macro2::Delimiter::Brace => "curly braces `{}`",
873 proc_macro2::Delimiter::Parenthesis => "parentheses `()`",
874 _ => "wrong delimiters",
875 };
876 return Err(SpannedError {
877 message: format!(
878 "`{field_name_str}` expects square brackets `[]`, not {bracket_name}; \
879 try `{field_name_str} = [1, 2, 3]`"
880 ),
881 span: value_token.span(),
882 help: field_def.doc.clone(),
883 });
884 }
885 } else if let TokenTree::Literal(lit) = value_token {
886 let lit_str = lit.to_string();
888 if lit_str.chars().all(|c| c.is_ascii_digit() || c == '-') {
889 return Err(SpannedError {
890 message: format!(
891 "`{field_name_str}` expects a list, not a single value; \
892 try `{field_name_str} = [{lit_str}]`"
893 ),
894 span: value_token.span(),
895 help: field_def.doc.clone(),
896 });
897 }
898 return Err(SpannedError {
899 message: format!(
900 "`{field_name_str}` expects a list: `{field_name_str} = [1, 2, 3]`"
901 ),
902 span: value_token.span(),
903 help: field_def.doc.clone(),
904 });
905 } else {
906 return Err(SpannedError {
907 message: format!(
908 "`{field_name_str}` expects a list: `{field_name_str} = [1, 2, 3]`"
909 ),
910 span: value_token.span(),
911 help: field_def.doc.clone(),
912 });
913 }
914 }
915 FieldKind::Ident => {
916 if let TokenTree::Ident(ident) = value_token {
918 parsed.push(ParsedField {
919 name: field_name_str,
920 name_span: field_span,
921 value: FieldValue::Ident(ident.to_string()),
922 });
923 } else if let TokenTree::Literal(lit) = value_token {
924 let lit_str = lit.to_string();
926 if lit_str.starts_with('"') && lit_str.ends_with('"') {
927 let inner = &lit_str[1..lit_str.len() - 1];
928 return Err(SpannedError {
929 message: format!(
930 "`{field_name_str}` expects a bare identifier, not a string; \
931 try `{field_name_str} = {inner}` (without quotes)"
932 ),
933 span: value_token.span(),
934 help: field_def.doc.clone(),
935 });
936 }
937 return Err(SpannedError {
938 message: format!(
939 "`{field_name_str}` expects an identifier: `{field_name_str} = some_value`"
940 ),
941 span: value_token.span(),
942 help: field_def.doc.clone(),
943 });
944 } else {
945 return Err(SpannedError {
946 message: format!(
947 "`{field_name_str}` expects an identifier: `{field_name_str} = some_value`"
948 ),
949 span: value_token.span(),
950 help: field_def.doc.clone(),
951 });
952 }
953 }
954 }
955 } else if p.as_char() == ',' {
956 match field_def.kind {
958 FieldKind::Bool | FieldKind::OptBool => {
959 parsed.push(ParsedField {
960 name: field_name_str,
961 name_span: field_span,
962 value: FieldValue::Flag,
963 });
964 }
965 FieldKind::String | FieldKind::OptString => {
966 return Err(SpannedError {
967 message: format!(
968 "`{field_name_str}` requires a string value: `{field_name_str} = \"value\"`"
969 ),
970 span: field_span,
971 help: field_def.doc.clone(),
972 });
973 }
974 FieldKind::OptChar => {
975 return Err(SpannedError {
976 message: format!(
977 "`{field_name_str}` requires a char value: `{field_name_str} = 'v'`"
978 ),
979 span: field_span,
980 help: field_def.doc.clone(),
981 });
982 }
983 FieldKind::I64 | FieldKind::OptI64 => {
984 return Err(SpannedError {
985 message: format!(
986 "`{field_name_str}` requires an integer value: `{field_name_str} = 42`"
987 ),
988 span: field_span,
989 help: field_def.doc.clone(),
990 });
991 }
992 FieldKind::ListString => {
993 return Err(SpannedError {
994 message: format!(
995 "`{field_name_str}` requires a list value: `{field_name_str} = [\"a\", \"b\"]`"
996 ),
997 span: field_span,
998 help: field_def.doc.clone(),
999 });
1000 }
1001 FieldKind::ListI64 => {
1002 return Err(SpannedError {
1003 message: format!(
1004 "`{field_name_str}` requires a list value: `{field_name_str} = [1, 2, 3]`"
1005 ),
1006 span: field_span,
1007 help: field_def.doc.clone(),
1008 });
1009 }
1010 FieldKind::Ident => {
1011 return Err(SpannedError {
1012 message: format!(
1013 "`{field_name_str}` requires an identifier value: `{field_name_str} = some_value`"
1014 ),
1015 span: field_span,
1016 help: field_def.doc.clone(),
1017 });
1018 }
1019 }
1020 i += 1;
1021 } else {
1022 return Err(SpannedError {
1023 message: format!("expected `=` or `,` after field name `{field_name_str}`"),
1024 span: p.span(),
1025 help: None,
1026 });
1027 }
1028 } else {
1029 match field_def.kind {
1032 FieldKind::Bool | FieldKind::OptBool => {
1033 parsed.push(ParsedField {
1034 name: field_name_str,
1035 name_span: field_span,
1036 value: FieldValue::Flag,
1037 });
1038 }
1039 FieldKind::String | FieldKind::OptString => {
1040 return Err(SpannedError {
1041 message: format!(
1042 "`{field_name_str}` requires a string value: `{field_name_str} = \"value\"`"
1043 ),
1044 span: field_span,
1045 help: field_def.doc.clone(),
1046 });
1047 }
1048 FieldKind::OptChar => {
1049 return Err(SpannedError {
1050 message: format!(
1051 "`{field_name_str}` requires a char value: `{field_name_str} = 'v'`"
1052 ),
1053 span: field_span,
1054 help: field_def.doc.clone(),
1055 });
1056 }
1057 FieldKind::I64 | FieldKind::OptI64 => {
1058 return Err(SpannedError {
1059 message: format!(
1060 "`{field_name_str}` requires an integer value: `{field_name_str} = 42`"
1061 ),
1062 span: field_span,
1063 help: field_def.doc.clone(),
1064 });
1065 }
1066 FieldKind::ListString => {
1067 return Err(SpannedError {
1068 message: format!(
1069 "`{field_name_str}` requires a list value: `{field_name_str} = [\"a\", \"b\"]`"
1070 ),
1071 span: field_span,
1072 help: field_def.doc.clone(),
1073 });
1074 }
1075 FieldKind::ListI64 => {
1076 return Err(SpannedError {
1077 message: format!(
1078 "`{field_name_str}` requires a list value: `{field_name_str} = [1, 2, 3]`"
1079 ),
1080 span: field_span,
1081 help: field_def.doc.clone(),
1082 });
1083 }
1084 FieldKind::Ident => {
1085 return Err(SpannedError {
1086 message: format!(
1087 "`{field_name_str}` requires an identifier value: `{field_name_str} = some_value`"
1088 ),
1089 span: field_span,
1090 help: field_def.doc.clone(),
1091 });
1092 }
1093 }
1094 }
1095 }
1096
1097 Ok(parsed)
1098}
1099
1100fn parse_integer_value(
1102 tokens: &[TokenTree],
1103 value_token: &TokenTree,
1104 field_name: &str,
1105) -> std::result::Result<(i64, usize), SpannedError> {
1106 if let TokenTree::Punct(p) = value_token
1108 && p.as_char() == '-'
1109 {
1110 if tokens.len() > 1
1112 && let TokenTree::Literal(lit) = &tokens[1]
1113 {
1114 let lit_str = lit.to_string();
1115 if let Ok(n) = lit_str.parse::<i64>() {
1116 return Ok((-n, 1)); }
1118 }
1119 return Err(SpannedError {
1120 message: "expected integer literal after `-`".to_string(),
1121 span: p.span(),
1122 help: None,
1123 });
1124 }
1125
1126 if let TokenTree::Literal(lit) = value_token {
1128 let lit_str = lit.to_string();
1129 if let Ok(n) = lit_str.parse::<i64>() {
1131 return Ok((n, 0));
1132 }
1133 let cleaned = lit_str.trim_end_matches(|c: char| c.is_alphabetic() || c == '_');
1135 if let Ok(n) = cleaned.parse::<i64>() {
1136 return Ok((n, 0));
1137 }
1138 if cleaned.chars().all(|c| c.is_ascii_digit()) {
1140 return Err(SpannedError {
1141 message: format!(
1142 "`{}` value `{}` is too large; this field accepts i64 (range {} to {})",
1143 field_name,
1144 cleaned,
1145 i64::MIN,
1146 i64::MAX
1147 ),
1148 span: lit.span(),
1149 help: None,
1150 });
1151 }
1152 return Err(SpannedError {
1153 message: format!("`{field_name}` expected integer literal, got `{lit_str}`"),
1154 span: lit.span(),
1155 help: None,
1156 });
1157 }
1158
1159 Err(SpannedError {
1160 message: format!("`{field_name}` expected integer literal, got `{value_token}`"),
1161 span: value_token.span(),
1162 help: None,
1163 })
1164}
1165
1166fn parse_string_list(stream: &TokenStream2) -> std::result::Result<Vec<String>, SpannedError> {
1168 let tokens: Vec<TokenTree> = stream.clone().into_iter().collect();
1169 let mut items = Vec::new();
1170
1171 let mut i = 0;
1172 while i < tokens.len() {
1173 if let TokenTree::Punct(p) = &tokens[i]
1175 && p.as_char() == ','
1176 {
1177 i += 1;
1178 continue;
1179 }
1180
1181 if let TokenTree::Literal(lit) = &tokens[i] {
1183 let lit_str = lit.to_string();
1184 if lit_str.starts_with('\"') && lit_str.ends_with('\"') {
1185 let inner = lit_str[1..lit_str.len() - 1].to_string();
1186 items.push(inner);
1187 i += 1;
1188 } else {
1189 return Err(SpannedError {
1190 message: format!("expected string literal in list, got `{lit_str}`"),
1191 span: lit.span(),
1192 help: None,
1193 });
1194 }
1195 } else {
1196 return Err(SpannedError {
1197 message: format!("expected string literal in list, got `{}`", tokens[i]),
1198 span: tokens[i].span(),
1199 help: None,
1200 });
1201 }
1202 }
1203
1204 Ok(items)
1205}
1206
1207fn parse_i64_list(stream: &TokenStream2) -> std::result::Result<Vec<i64>, SpannedError> {
1209 let tokens: Vec<TokenTree> = stream.clone().into_iter().collect();
1210 let mut items = Vec::new();
1211
1212 let mut i = 0;
1213 while i < tokens.len() {
1214 if let TokenTree::Punct(p) = &tokens[i] {
1216 if p.as_char() == ',' {
1217 i += 1;
1218 continue;
1219 }
1220 if p.as_char() == '-' {
1222 if i + 1 < tokens.len()
1223 && let TokenTree::Literal(lit) = &tokens[i + 1]
1224 {
1225 let lit_str = lit.to_string();
1226 if let Ok(n) = lit_str.parse::<i64>() {
1227 items.push(-n);
1228 i += 2;
1229 continue;
1230 }
1231 }
1232 return Err(SpannedError {
1233 message: "expected integer after `-`".to_string(),
1234 span: p.span(),
1235 help: None,
1236 });
1237 }
1238 }
1239
1240 if let TokenTree::Literal(lit) = &tokens[i] {
1242 let lit_str = lit.to_string();
1243 if let Ok(n) = lit_str.parse::<i64>() {
1244 items.push(n);
1245 i += 1;
1246 } else {
1247 let cleaned = lit_str.trim_end_matches(|c: char| c.is_alphabetic() || c == '_');
1249 if let Ok(n) = cleaned.parse::<i64>() {
1250 items.push(n);
1251 i += 1;
1252 } else {
1253 return Err(SpannedError {
1254 message: format!("expected integer literal in list, got `{lit_str}`"),
1255 span: lit.span(),
1256 help: None,
1257 });
1258 }
1259 }
1260 } else {
1261 return Err(SpannedError {
1262 message: format!("expected integer literal in list, got `{}`", tokens[i]),
1263 span: tokens[i].span(),
1264 help: None,
1265 });
1266 }
1267 }
1268
1269 Ok(items)
1270}
1271
1272#[cfg(feature = "helpful-derive")]
1273fn find_closest<'a>(target: &str, candidates: &'a [String]) -> Option<&'a str> {
1274 candidates
1275 .iter()
1276 .filter_map(|c| {
1277 let dist = strsim::levenshtein(target, c);
1278 if dist <= 3 {
1279 Some((c.as_str(), dist))
1280 } else {
1281 None
1282 }
1283 })
1284 .min_by_key(|(_, d)| *d)
1285 .map(|(s, _)| s)
1286}
1287
1288#[cfg(not(feature = "helpful-derive"))]
1289fn find_closest<'a>(_target: &str, _candidates: &'a [String]) -> Option<&'a str> {
1290 None
1291}