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