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