1extern crate proc_macro;
2
3use proc_macro2::Ident;
4use proc_macro2::TokenStream;
5use proc_macro_error2::OptionExt;
6use proc_macro_error2::{abort, proc_macro_error};
7use quote::quote;
8use syn::{
9 AngleBracketedGenericArguments,
10 AttrStyle::Outer,
11 Attribute, DeriveInput,
12 Expr::Lit,
13 ExprLit, Field, Fields,
14 Fields::Named,
15 GenericArgument,
16 Lit::Str,
17 Meta::{List, NameValue},
18 MetaList, MetaNameValue, PathArguments, PathSegment, Result, Type, TypePath,
19};
20mod case;
21
22struct Intermediate {
23 struct_name: Ident,
24 struct_doc: String,
25 field_example: String,
26}
27
28struct AttrMeta {
29 docs: Vec<String>,
30 doc_skip_prefix: Vec<String>,
31 help: Option<String>,
32 default_source: Option<DefaultSource>,
33 nesting_format: Option<NestingFormat>,
34 require: bool,
35 skip: bool,
36 is_enum: bool,
37 flatten: bool,
38 rename: Option<String>,
39 rename_rule: case::RenameRule,
40}
41
42struct ParsedField {
43 docs: Vec<String>,
44 doc_skip_prefix: Vec<String>,
45 default: DefaultSource,
46 nesting_format: Option<NestingFormat>,
47 skip: bool,
48 is_enum: bool,
49 flatten: bool,
50 name: String,
51 optional: bool,
52 ty: Option<String>,
53}
54
55impl ParsedField {
56 fn push_doc_to_string(&self, s: &mut String) {
57 push_doc_string(s, &self.docs, &self.doc_skip_prefix);
58 }
59
60 fn default_key(&self) -> String {
62 if let DefaultSource::DefaultValue(v) = &self.default {
63 let key = v.trim_matches('\"').replace(' ', "").replace('.', "-");
64 if !key.is_empty() {
65 return key;
66 }
67 }
68 "example".into()
69 }
70
71 fn label(&self) -> String {
72 let label = match self.nesting_format {
73 Some(NestingFormat::Section(NestingType::Dict)) => {
74 if self.flatten {
75 self.default_key()
76 } else {
77 format!("{}.{}", self.name, self.default_key())
78 }
79 }
80 Some(NestingFormat::Prefix) => String::new(),
81 _ => {
82 if self.flatten {
83 String::new()
84 } else {
85 self.name.to_string()
86 }
87 }
88 };
89 if label.is_empty() {
90 String::from("label")
91 } else {
92 format!(
93 "
94 if label.is_empty() {{
95 \"{label}\".to_string()
96 }} else {{
97 label.to_string() + \".\" + \"{label}\"
98 }}
99 "
100 )
101 }
102 }
103
104 fn label_format(&self) -> (&str, &str) {
105 match self.nesting_format {
106 Some(NestingFormat::Section(NestingType::Vec)) => {
107 if self.flatten {
108 abort!(
109 "flatten",
110 format!(
111 "Only structs and maps can be flattened! \
112 (But field `{}` is a collection)",
113 self.name
114 )
115 )
116 }
117 ("[[", "]]")
118 }
119 Some(NestingFormat::Section(NestingType::Dict)) => ("[", "]"),
120 Some(NestingFormat::Prefix) => ("", ""),
121 _ => {
122 if self.flatten {
123 ("", "")
124 } else {
125 ("[", "]")
126 }
127 }
128 }
129 }
130
131 fn prefix(&self) -> String {
132 let opt_prefix = if self.optional {
133 "# ".to_string()
134 } else {
135 String::new()
136 };
137 if self.nesting_format == Some(NestingFormat::Prefix) {
138 format!("{}{}.", opt_prefix, self.name)
139 } else {
140 opt_prefix
141 }
142 }
143}
144
145#[derive(Debug)]
146enum DefaultSource {
147 DefaultValue(String),
148 DefaultFn(Option<String>),
149 #[allow(dead_code)]
150 SerdeDefaultFn(String),
151}
152
153#[derive(PartialEq)]
154enum NestingType {
155 None,
156 Vec,
157 Dict,
158}
159
160#[derive(PartialEq)]
161enum NestingFormat {
162 Section(NestingType),
163 Prefix,
164}
165
166fn default_value(ty: String) -> String {
167 match ty.as_str() {
168 "usize" | "u8" | "u16" | "u32" | "u64" | "u128" | "isize" | "i8" | "i16" | "i32"
169 | "i64" | "i128" => "0",
170 "f32" | "f64" => "0.0",
171 _ => "\"\"",
172 }
173 .to_string()
174}
175
176fn parse_type(
178 ty: &Type,
179 default: &mut String,
180 optional: &mut bool,
181 nesting_format: &mut Option<NestingFormat>,
182) -> Option<String> {
183 let mut r#type = None;
184 if let Type::Path(TypePath { path, .. }) = ty {
185 if let Some(PathSegment { ident, arguments }) = path.segments.last() {
186 let id = ident.to_string();
187 if arguments.is_none() {
188 r#type = Some(id.clone());
189 *default = default_value(id);
190 } else if id == "Option" {
191 *optional = true;
192 if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
193 args, ..
194 }) = arguments
195 {
196 if let Some(GenericArgument::Type(ty)) = args.first() {
197 r#type = parse_type(ty, default, &mut false, nesting_format);
198 }
199 }
200 } else if id == "Vec" || id == "HashSet" || id == "BTreeSet" {
201 if nesting_format.is_some() {
202 *nesting_format = Some(NestingFormat::Section(NestingType::Vec));
203 }
204 if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
205 args, ..
206 }) = arguments
207 {
208 if let Some(GenericArgument::Type(ty)) = args.first() {
209 let mut item_default_value = String::new();
210 r#type = parse_type(ty, &mut item_default_value, &mut false, &mut None);
211 *default = if item_default_value.is_empty() {
212 "[ ]".to_string()
213 } else {
214 format!("[ {item_default_value:}, ]")
215 }
216 }
217 }
218 } else if id == "HashMap" || id == "BTreeMap" {
219 if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
220 args, ..
221 }) = arguments
222 {
223 if let Some(GenericArgument::Type(ty)) = args.last() {
224 let mut item_default_value = String::new();
225 r#type = parse_type(ty, &mut item_default_value, &mut false, &mut None);
226 }
227 }
228 if nesting_format.is_some() {
229 *nesting_format = Some(NestingFormat::Section(NestingType::Dict));
230 }
231 }
232 }
234 }
235 r#type
236}
237
238fn parse_attrs(attrs: &[Attribute]) -> AttrMeta {
239 let mut docs = Vec::new();
240 let mut doc_skip_prefix = Vec::new();
241 let mut help = None;
242 let mut default_source = None;
243 let mut nesting_format = None;
244 let mut require = false;
245 let mut skip = false;
246 let mut is_enum = false;
247 let mut flatten = false;
248 #[allow(unused_mut)]
250 let mut rename = None;
251 #[allow(unused_mut)]
253 let mut rename_rule = case::RenameRule::None;
254
255 for attr in attrs.iter() {
256 match (attr.style, &attr.meta) {
257 (Outer, NameValue(MetaNameValue { path, value, .. })) => {
258 for seg in path.segments.iter() {
259 if seg.ident == "doc" {
260 if let Lit(ExprLit {
261 lit: Str(lit_str), ..
262 }) = value
263 {
264 docs.push(lit_str.value());
265 }
266 }
267 }
268 }
269 (
270 Outer,
271 List(MetaList {
272 path,
273 tokens: _tokens,
274 ..
275 }),
276 ) if path.segments.last().is_some_and(|s| s.ident == "serde") => {
277 #[cfg(feature = "serde")]
278 {
279 let token_str = _tokens.to_string();
280 for attribute in token_str.split(find_unenclosed_char(',')).map(str::trim) {
281 if attribute.starts_with("default") {
282 if let Some((_, s)) = attribute.split_once('=') {
283 default_source = Some(DefaultSource::SerdeDefaultFn(
284 s.trim().trim_matches('"').into(),
285 ));
286 } else {
287 default_source = Some(DefaultSource::DefaultFn(None));
288 }
289 }
290 if attribute == "skip_deserializing" || attribute == "skip" {
291 skip = true;
292 }
293 if attribute == "flatten" {
294 flatten = true;
295 }
296 if attribute.starts_with("rename") {
297 if attribute.starts_with("rename_all") {
298 if let Some((_, s)) = attribute.split_once('=') {
299 rename_rule = if let Ok(r) =
300 case::RenameRule::from_str(s.trim().trim_matches('"'))
301 {
302 r
303 } else {
304 abort!(&_tokens, "unsupported rename rule")
305 }
306 }
307 } else if let Some((_, s)) = attribute.split_once('=') {
308 rename = Some(s.trim().trim_matches('"').into());
309 }
310 }
311 }
312 }
313 }
314 (Outer, List(MetaList { path, tokens, .. }))
315 if path
316 .segments
317 .last()
318 .map(|s| s.ident == "toml_example")
319 .unwrap_or_default() =>
320 {
321 let token_str = tokens.to_string();
322 for attribute in token_str.split(find_unenclosed_char(',')).map(str::trim) {
323 if attribute.starts_with("doc_skip_prefix") {
324 if let Some((_, s)) = attribute.split_once('=') {
325 let s = s.trim();
326 if let Ok(lit_str) = syn::parse_str::<syn::LitStr>(s) {
327 doc_skip_prefix.push(lit_str.value());
328 } else {
329 abort!(
330 &attr,
331 "doc_skip_prefix expects a quoted string as value, try \
332 toml_example(doc_skip_prefix = \"\\dev-doc\")"
333 )
334 }
335 } else {
336 abort!(
337 &attr,
338 "doc_skip_prefix expects a value, try \
339 toml_example(doc_skip_prefix = \"\\dev-doc\")"
340 )
341 }
342 } else if attribute.starts_with("default") {
343 if let Some((_, s)) = attribute.split_once('=') {
344 default_source = Some(DefaultSource::DefaultValue(s.trim().into()));
345 } else {
346 default_source = Some(DefaultSource::DefaultFn(None));
347 }
348 } else if attribute.starts_with("nesting") {
349 if let Some((_, s)) = attribute.split_once('=') {
350 nesting_format = match s.trim() {
351 "prefix" => Some(NestingFormat::Prefix),
352 "section" => Some(NestingFormat::Section(NestingType::None)),
353 _ => {
354 abort!(&attr, "please use prefix or section for nesting derive")
355 }
356 }
357 } else {
358 nesting_format = Some(NestingFormat::Section(NestingType::None));
359 }
360 } else if attribute == "require" {
361 require = true;
362 } else if attribute == "skip" {
363 skip = true;
364 } else if attribute == "is_enum" || attribute == "enum" {
365 is_enum = true;
366 } else if attribute == "flatten" {
367 flatten = true;
368 } else if attribute.starts_with("help") {
369 if let Some((_, s)) = attribute.split_once('=') {
370 let s = s.trim();
371 if let Ok(lit_str) = syn::parse_str::<syn::LitStr>(s) {
372 help = Some(lit_str.value());
373 } else {
374 abort!(
375 &attr,
376 "help expects a quoted string as value, try \
377 toml_example(help = \"This is a help text\")"
378 )
379 }
380 } else {
381 abort!(
382 &attr,
383 "help expects a value, try \
384 toml_example(help = \"This is a help text\")"
385 )
386 }
387 } else {
388 abort!(&attr, format!("{} is not allowed attribute", attribute))
389 }
390 }
391 }
392 _ => (),
393 }
394 }
395
396 AttrMeta {
397 docs,
398 doc_skip_prefix,
399 help,
400 default_source,
401 nesting_format,
402 require,
403 skip,
404 is_enum,
405 flatten,
406 rename,
407 rename_rule,
408 }
409}
410
411fn parse_field(
412 mut all_doc_skip_prefix: Vec<String>,
413 struct_default: Option<&DefaultSource>,
414 field: &Field,
415 rename_rule: case::RenameRule,
416) -> ParsedField {
417 let mut default_value = String::new();
418 let mut optional = false;
419 let AttrMeta {
420 docs,
421 doc_skip_prefix,
422 help,
423 default_source,
424 mut nesting_format,
425 skip,
426 is_enum,
427 flatten,
428 rename,
429 require,
430 ..
431 } = parse_attrs(&field.attrs);
432 all_doc_skip_prefix.extend(doc_skip_prefix);
433 let docs = help.map(|h| help_to_docs(&h)).unwrap_or(docs);
434 let ty = parse_type(
435 &field.ty,
436 &mut default_value,
437 &mut optional,
438 &mut nesting_format,
439 );
440 let default = match default_source {
441 Some(DefaultSource::DefaultFn(_)) => DefaultSource::DefaultFn(ty.clone()),
442 Some(DefaultSource::SerdeDefaultFn(f)) => DefaultSource::SerdeDefaultFn(f),
443 Some(DefaultSource::DefaultValue(v)) => DefaultSource::DefaultValue(v),
444 _ if struct_default.is_some() => DefaultSource::DefaultFn(None),
445 _ => DefaultSource::DefaultValue(default_value),
446 };
447 let name = if let Some(field_name) = field.ident.as_ref().map(|i| i.to_string()) {
448 rename.unwrap_or(rename_rule.apply_to_field(&field_name))
449 } else {
450 abort!(&field, "The field should has name")
451 };
452 ParsedField {
453 docs,
454 doc_skip_prefix: all_doc_skip_prefix,
455 default,
456 nesting_format,
457 skip,
458 is_enum,
459 flatten,
460 name,
461 optional: optional && !require,
462 ty,
463 }
464}
465
466fn push_doc_string(example: &mut String, docs: &[String], doc_skip_prefix: &[String]) {
467 for doc in docs.iter() {
468 if doc_skip_prefix
469 .iter()
470 .any(|prefix| doc.trim().starts_with(prefix))
471 {
472 continue;
473 }
474 example.push('#');
475 example.push_str(doc);
476 example.push('\n');
477 }
478}
479
480fn help_to_docs(help: &str) -> Vec<String> {
481 help.lines()
482 .map(|l| {
483 if l.is_empty() {
484 String::new()
485 } else {
486 format!(" {}", l)
487 }
488 })
489 .collect()
490}
491
492#[proc_macro_derive(TomlExample, attributes(toml_example))]
493#[proc_macro_error]
494pub fn derive(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
495 Intermediate::from_ast(syn::parse_macro_input!(item as syn::DeriveInput))
496 .unwrap()
497 .to_token_stream()
498 .unwrap()
499 .into()
500}
501
502impl Intermediate {
504 pub fn from_ast(
505 DeriveInput {
506 ident, data, attrs, ..
507 }: syn::DeriveInput,
508 ) -> Result<Intermediate> {
509 let struct_name = ident.clone();
510
511 let AttrMeta {
512 docs,
513 doc_skip_prefix,
514 help,
515 default_source,
516 rename_rule,
517 ..
518 } = parse_attrs(&attrs);
519
520 let struct_doc = {
521 let mut doc = String::new();
522 let docs = help.as_ref().map(|h| help_to_docs(h)).unwrap_or(docs);
523 push_doc_string(&mut doc, &docs, &doc_skip_prefix);
524 doc
525 };
526
527 let fields = if let syn::Data::Struct(syn::DataStruct { fields, .. }) = &data {
528 fields
529 } else {
530 abort!(ident, "TomlExample derive only use for struct")
531 };
532
533 let field_example =
534 Self::parse_field_examples(ident, doc_skip_prefix, default_source, fields, rename_rule);
535
536 Ok(Intermediate {
537 struct_name,
538 struct_doc,
539 field_example,
540 })
541 }
542
543 pub fn to_token_stream(&self) -> Result<TokenStream> {
544 let Intermediate {
545 struct_name,
546 struct_doc,
547 field_example,
548 } = self;
549
550 let field_example_stream: proc_macro2::TokenStream = field_example.parse()?;
551
552 Ok(quote! {
553 impl toml_example::TomlExample for #struct_name {
554 fn toml_example() -> String {
555 #struct_name::toml_example_with_prefix("", ("", ""), "")
556 }
557 fn toml_example_with_prefix(label: &str, label_format: (&str, &str), prefix: &str)
558 -> String {
559 let wrapped_label = if label_format.0.is_empty() {
560 String::new()
561 } else {
562 label_format.0.to_string() + label + label_format.1
563 };
564 #struct_doc.to_string() + &wrapped_label + &#field_example_stream
565 }
566 }
567 })
568 }
569
570 fn parse_field_examples(
571 struct_ty: Ident,
572 doc_skip_prefix: Vec<String>,
573 struct_default: Option<DefaultSource>,
574 fields: &Fields,
575 rename_rule: case::RenameRule,
576 ) -> String {
577 let mut field_example = "r##\"".to_string();
578 let mut nesting_field_example = "".to_string();
579
580 if let Named(named_fields) = fields {
581 for f in named_fields.named.iter() {
582 let field = parse_field(
583 doc_skip_prefix.clone(),
584 struct_default.as_ref(),
585 f,
586 rename_rule,
587 );
588 if field.skip {
589 continue;
590 }
591
592 if field.nesting_format.is_some() {
593 let (example, nesting_section_newline) =
597 if field.nesting_format == Some(NestingFormat::Prefix) {
598 (&mut field_example, "")
599 } else if field.flatten {
600 (
601 &mut field_example,
602 if field.nesting_format
603 == Some(NestingFormat::Section(NestingType::None))
604 {
605 ""
606 } else {
607 "\n"
608 },
609 )
610 } else {
611 (&mut nesting_field_example, "\n")
612 };
613
614 field.push_doc_to_string(example);
615 if let Some(ref field_type) = field.ty {
616 example.push_str("\"##.to_string()");
617 let (before, after) = field.label_format();
618 let label_format = format!(
619 "(\"{}{before}\", \"{after}{nesting_section_newline}\")",
620 if field.optional && field.nesting_format != Some(NestingFormat::Prefix)
621 {
622 "# "
623 } else {
624 ""
625 }
626 );
627 example.push_str(&format!(
628 " + &{field_type}::toml_example_with_prefix(\
629 &{}, {}, \"{}\"\
630 )",
631 field.label(),
632 label_format,
633 field.prefix()
634 ));
635 example.push_str(" + &r##\"");
636 } else {
637 abort!(&f.ident, "nesting only work on inner structure")
638 }
639 } else {
640 field.push_doc_to_string(&mut field_example);
642 if field.optional {
643 field_example.push_str("# ");
644 }
645 field_example.push_str("\"##.to_string() + prefix + &r##\"");
646 field_example.push_str(field.name.trim_start_matches("r#"));
647 match field.default {
648 DefaultSource::DefaultValue(default) => {
649 field_example.push_str(" = ");
650 if field.optional {
651 field_example.push_str(&default.replace('\n', "\n# "));
652 } else {
653 field_example.push_str(&default);
654 }
655 field_example.push('\n');
656 }
657 DefaultSource::DefaultFn(None) => match struct_default {
658 Some(DefaultSource::DefaultFn(None)) => {
659 let suffix = format!(
660 ".{}",
661 f.ident
662 .as_ref()
663 .expect_or_abort("Named fields always have and ident")
664 );
665 handle_default_fn_source(
666 &mut field_example,
667 field.is_enum,
668 struct_ty.to_string(),
669 Some(suffix),
670 );
671 }
672 Some(DefaultSource::SerdeDefaultFn(ref fn_str)) => {
673 let suffix = format!(
674 ".{}",
675 f.ident
676 .as_ref()
677 .expect_or_abort("Named fields always have an ident")
678 );
679 handle_serde_default_fn_source(
680 &mut field_example,
681 field.is_enum,
682 fn_str,
683 Some(suffix),
684 );
685 }
686 Some(DefaultSource::DefaultValue(_)) => abort!(
687 f.ident,
688 "Setting a default value on a struct is not supported!"
689 ),
690 _ => field_example.push_str(" = \"\"\n"),
691 },
692 DefaultSource::DefaultFn(Some(ty)) => {
693 handle_default_fn_source(&mut field_example, field.is_enum, ty, None)
694 }
695 DefaultSource::SerdeDefaultFn(ref fn_str) => {
696 handle_serde_default_fn_source(
697 &mut field_example,
698 field.is_enum,
699 fn_str,
700 None,
701 )
702 }
703 }
704 field_example.push('\n');
705 }
706 }
707 }
708 field_example += &nesting_field_example;
709 field_example.push_str("\"##.to_string()");
710
711 field_example
712 }
713}
714
715fn handle_default_fn_source(
716 field_example: &mut String,
717 is_enum: bool,
718 type_ident: String,
719 suffix: Option<String>,
720) {
721 let suffix = suffix.unwrap_or_default();
722 field_example.push_str(" = \"##.to_string()");
723 if is_enum {
724 field_example.push_str(&format!(
725 " + &format!(\"\\\"{{:?}}\\\"\", {type_ident}::default(){suffix})"
726 ));
727 } else {
728 field_example.push_str(&format!(
729 " + &format!(\"{{:?}}\", {type_ident}::default(){suffix})"
730 ));
731 }
732 field_example.push_str(" + &r##\"\n");
733}
734
735fn handle_serde_default_fn_source(
736 field_example: &mut String,
737 is_enum: bool,
738 fn_str: &String,
739 suffix: Option<String>,
740) {
741 let suffix = suffix.unwrap_or_default();
742 field_example.push_str(" = \"##.to_string()");
743 if is_enum {
744 field_example.push_str(&format!(
745 " + &format!(\"\\\"{{:?}}\\\"\", {fn_str}(){suffix})"
746 ));
747 } else {
748 field_example.push_str(&format!(" + &format!(\"{{:?}}\", {fn_str}(){suffix})"));
749 }
750 field_example.push_str("+ &r##\"\n");
751}
752
753fn find_unenclosed_char(pat: char) -> impl FnMut(char) -> bool {
756 let mut quotes = 0;
757 let mut single_quotes = 0;
758 let mut brackets = 0;
759 let mut braces = 0;
760 let mut parenthesis = 0;
761 let mut is_escaped = false;
762 move |char| -> bool {
763 if is_escaped {
764 is_escaped = false;
765 return false;
766 } else if char == '\\' {
767 is_escaped = true;
768 } else if (quotes % 2 == 1 && char != '"') || (single_quotes % 2 == 1 && char != '\'') {
769 return false;
770 } else {
771 match char {
772 '"' => quotes += 1,
773 '\'' => single_quotes += 1,
774 '[' => brackets += 1,
775 ']' => brackets -= 1,
776 '{' => braces += 1,
777 '}' => braces -= 1,
778 '(' => parenthesis += 1,
779 ')' => parenthesis -= 1,
780 _ => {}
781 }
782 }
783 char == pat
784 && quotes % 2 == 0
785 && single_quotes % 2 == 0
786 && brackets == 0
787 && braces == 0
788 && parenthesis == 0
789 }
790}