Skip to main content

toml_example_derive/
lib.rs

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    // Provide a default key for map-like example
61    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
176/// return type and unwrap with Option, Vec, HashSet, BTreeSet; or return the value type of HashMap and BTreeMap
177fn 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            // TODO else Complex struct in else
233        }
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    // mut in serde feature
249    #[allow(unused_mut)]
250    let mut rename = None;
251    // mut in serde feature
252    #[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
502// Transient intermediate for structure parsing
503impl 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                    // Recursively add the toml_example_with_prefix of fields
594                    // If nesting in a section way will attached to the bottom to avoid #18
595                    // else the nesting will just using a prefix ahead the every field of example
596                    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                    // The leaf field, writing down the example value based on different default source
641                    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
753/// A [Pattern](std::str::pattern::Pattern) to find a char that is not enclosed in quotes, braces
754/// or the like
755fn 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}