toml_example_derive/
lib.rs

1extern crate proc_macro;
2
3use proc_macro2::Ident;
4use proc_macro2::TokenStream;
5use proc_macro_error2::{abort, proc_macro_error};
6use quote::quote;
7use syn::{
8    AngleBracketedGenericArguments,
9    AttrStyle::Outer,
10    Attribute, DeriveInput,
11    Expr::Lit,
12    ExprLit, Field, Fields,
13    Fields::Named,
14    GenericArgument,
15    Lit::Str,
16    Meta::{List, NameValue},
17    MetaList, MetaNameValue, PathArguments, PathSegment, Result, Type, TypePath,
18};
19mod case;
20
21struct Intermediate {
22    struct_name: Ident,
23    struct_doc: String,
24    field_example: String,
25}
26
27struct AttrMeta {
28    docs: Vec<String>,
29    default_source: Option<DefaultSource>,
30    nesting_format: Option<NestingFormat>,
31    require: bool,
32    skip: bool,
33    is_enum: bool,
34    rename: Option<String>,
35    rename_rule: case::RenameRule,
36}
37
38struct ParsedField {
39    docs: Vec<String>,
40    default: DefaultSource,
41    nesting_format: Option<NestingFormat>,
42    skip: bool,
43    is_enum: bool,
44    name: String,
45    optional: bool,
46    ty: Option<String>,
47}
48
49impl ParsedField {
50    fn push_doc_to_string(&self, s: &mut String) {
51        push_doc_string(s, &self.docs);
52    }
53
54    // Provide a default key for map-like example
55    fn default_key(&self) -> String {
56        if let DefaultSource::DefaultValue(v) = &self.default {
57            let key = v.trim_matches('\"').replace(' ', "").replace('.', "-");
58            if !key.is_empty() {
59                return key;
60            }
61        }
62        "example".into()
63    }
64
65    fn label(&self) -> String {
66        match self.nesting_format {
67            Some(NestingFormat::Section(NestingType::Vec)) => {
68                self.prefix() + &format!("[[{}]]", self.name)
69            }
70            Some(NestingFormat::Section(NestingType::Dict)) => {
71                self.prefix() + &format!("[{}.{}]", self.name, self.default_key())
72            }
73            Some(NestingFormat::Prefix) => "".to_string(),
74            _ => self.prefix() + &format!("[{}]", self.name),
75        }
76    }
77
78    fn prefix(&self) -> String {
79        let opt_prefix = if self.optional {
80            "# ".to_string()
81        } else {
82            String::new()
83        };
84        if self.nesting_format == Some(NestingFormat::Prefix) {
85            format!("{}{}.", opt_prefix, self.name)
86        } else {
87            opt_prefix
88        }
89    }
90}
91
92#[derive(Debug)]
93enum DefaultSource {
94    DefaultValue(String),
95    DefaultFn(Option<String>),
96    #[allow(dead_code)]
97    SerdeDefaultFn(String),
98}
99
100#[derive(PartialEq)]
101enum NestingType {
102    None,
103    Vec,
104    Dict,
105}
106
107#[derive(PartialEq)]
108enum NestingFormat {
109    Section(NestingType),
110    Prefix,
111}
112
113fn default_value(ty: String) -> String {
114    match ty.as_str() {
115        "usize" | "u8" | "u16" | "u32" | "u64" | "u128" | "isize" | "i8" | "i16" | "i32"
116        | "i64" | "i128" => "0",
117        "f32" | "f64" => "0.0",
118        _ => "\"\"",
119    }
120    .to_string()
121}
122
123/// return type and unwrap with Option and Vec; or return the value type of HashMap and BTreeMap
124fn parse_type(
125    ty: &Type,
126    default: &mut String,
127    optional: &mut bool,
128    nesting_format: &mut Option<NestingFormat>,
129) -> Option<String> {
130    let mut r#type = None;
131    if let Type::Path(TypePath { path, .. }) = ty {
132        if let Some(PathSegment { ident, arguments }) = path.segments.last() {
133            let id = ident.to_string();
134            if arguments.is_none() {
135                r#type = Some(id.clone());
136                *default = default_value(id);
137            } else if id == "Option" {
138                *optional = true;
139                if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
140                    args, ..
141                }) = arguments
142                {
143                    if let Some(GenericArgument::Type(ty)) = args.first() {
144                        r#type = parse_type(ty, default, &mut false, nesting_format);
145                    }
146                }
147            } else if id == "Vec" {
148                if nesting_format.is_some() {
149                    *nesting_format = Some(NestingFormat::Section(NestingType::Vec));
150                }
151                if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
152                    args, ..
153                }) = arguments
154                {
155                    if let Some(GenericArgument::Type(ty)) = args.first() {
156                        let mut item_default_value = String::new();
157                        r#type = parse_type(ty, &mut item_default_value, &mut false, &mut None);
158                        *default = if item_default_value.is_empty() {
159                            "[  ]".to_string()
160                        } else {
161                            format!("[ {item_default_value:}, ]")
162                        }
163                    }
164                }
165            } else if id == "HashMap" || id == "BTreeMap" {
166                if let PathArguments::AngleBracketed(AngleBracketedGenericArguments {
167                    args, ..
168                }) = arguments
169                {
170                    if let Some(GenericArgument::Type(ty)) = args.last() {
171                        let mut item_default_value = String::new();
172                        r#type = parse_type(ty, &mut item_default_value, &mut false, &mut None);
173                    }
174                }
175                if nesting_format.is_some() {
176                    *nesting_format = Some(NestingFormat::Section(NestingType::Dict));
177                }
178            }
179            // TODO else Complex struct in else
180        }
181    }
182    r#type
183}
184
185fn parse_attrs(attrs: &[Attribute]) -> AttrMeta {
186    let mut docs = Vec::new();
187    let mut default_source = None;
188    let mut nesting_format = None;
189    let mut require = false;
190    let mut skip = false;
191    let mut is_enum = false;
192    let mut rename = None;
193    let mut rename_rule = case::RenameRule::None;
194
195    for attr in attrs.iter() {
196        match (attr.style, &attr.meta) {
197            (Outer, NameValue(MetaNameValue { path, value, .. })) => {
198                for seg in path.segments.iter() {
199                    if seg.ident == "doc" {
200                        if let Lit(ExprLit {
201                            lit: Str(lit_str), ..
202                        }) = value
203                        {
204                            docs.push(lit_str.value());
205                        }
206                    }
207                }
208            }
209            (
210                Outer,
211                List(MetaList {
212                    path,
213                    tokens: _tokens,
214                    ..
215                }),
216            ) if path
217                .segments
218                .last()
219                .map(|s| s.ident == "serde")
220                .unwrap_or_default() =>
221            {
222                #[cfg(feature = "serde")]
223                {
224                    let token_str = _tokens.to_string();
225                    if token_str.starts_with("default") {
226                        if let Some((_, s)) = token_str.split_once('=') {
227                            default_source = Some(DefaultSource::SerdeDefaultFn(
228                                s.trim().trim_matches('"').into(),
229                            ));
230                        } else {
231                            default_source = Some(DefaultSource::DefaultFn(None));
232                        }
233                    }
234                    if token_str == "skip_deserializing" || token_str == "skip" {
235                        skip = true;
236                    }
237                    if token_str.starts_with("rename") {
238                        if token_str.starts_with("rename_all") {
239                            if let Some((_, s)) = token_str.split_once('=') {
240                                rename_rule = if let Ok(r) =
241                                    case::RenameRule::from_str(s.trim().trim_matches('"'))
242                                {
243                                    r
244                                } else {
245                                    abort!(&_tokens, "unsupported rename rule")
246                                }
247                            }
248                        } else if let Some((_, s)) = token_str.split_once('=') {
249                            rename = Some(s.trim().trim_matches('"').into());
250                        }
251                    }
252                }
253            }
254            (Outer, List(MetaList { path, tokens, .. }))
255                if path
256                    .segments
257                    .last()
258                    .map(|s| s.ident == "toml_example")
259                    .unwrap_or_default() =>
260            {
261                let token_str = tokens.to_string();
262                if token_str.starts_with("default") {
263                    if let Some((_, s)) = token_str.split_once('=') {
264                        default_source = Some(DefaultSource::DefaultValue(s.trim().into()));
265                    } else {
266                        default_source = Some(DefaultSource::DefaultFn(None));
267                    }
268                } else if token_str.starts_with("nesting") {
269                    if let Some((_, s)) = token_str.split_once('=') {
270                        nesting_format = match s.trim() {
271                            "prefix" => Some(NestingFormat::Prefix),
272                            "section" => Some(NestingFormat::Section(NestingType::None)),
273                            _ => abort!(&attr, "please use prefix or section for nesting derive"),
274                        }
275                    } else {
276                        nesting_format = Some(NestingFormat::Section(NestingType::None));
277                    }
278                } else if token_str == "require" {
279                    require = true;
280                } else if token_str == "skip" {
281                    skip = true;
282                } else if token_str == "is_enum" || token_str == "enum" {
283                    is_enum = true;
284                } else {
285                    abort!(&attr, format!("{} is not allowed attribute", token_str))
286                }
287            }
288            _ => (),
289        }
290    }
291
292    AttrMeta {
293        docs,
294        default_source,
295        nesting_format,
296        require,
297        skip,
298        is_enum,
299        rename,
300        rename_rule,
301    }
302}
303
304fn parse_field(field: &Field, rename_rule: case::RenameRule) -> ParsedField {
305    let mut default_value = String::new();
306    let mut optional = false;
307    let AttrMeta {
308        docs,
309        default_source,
310        mut nesting_format,
311        skip,
312        is_enum,
313        rename,
314        require,
315        ..
316    } = parse_attrs(&field.attrs);
317    let ty = parse_type(
318        &field.ty,
319        &mut default_value,
320        &mut optional,
321        &mut nesting_format,
322    );
323    let default = match default_source {
324        Some(DefaultSource::DefaultFn(_)) => DefaultSource::DefaultFn(ty.clone()),
325        Some(DefaultSource::SerdeDefaultFn(f)) => DefaultSource::SerdeDefaultFn(f),
326        Some(DefaultSource::DefaultValue(v)) => DefaultSource::DefaultValue(v),
327        _ => DefaultSource::DefaultValue(default_value),
328    };
329    let name = if let Some(field_name) = field.ident.as_ref().map(|i| i.to_string()) {
330        rename.unwrap_or(rename_rule.apply_to_field(&field_name))
331    } else {
332        abort!(&field, "The field should has name")
333    };
334    ParsedField {
335        docs,
336        default,
337        nesting_format,
338        skip,
339        is_enum,
340        name,
341        optional: optional && !require,
342        ty,
343    }
344}
345
346fn push_doc_string(example: &mut String, docs: &Vec<String>) {
347    for doc in docs.iter() {
348        example.push('#');
349        example.push_str(doc);
350        example.push('\n');
351    }
352}
353
354#[proc_macro_derive(TomlExample, attributes(toml_example))]
355#[proc_macro_error]
356pub fn derive(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
357    Intermediate::from_ast(syn::parse_macro_input!(item as syn::DeriveInput))
358        .unwrap()
359        .to_token_stream()
360        .unwrap()
361        .into()
362}
363
364// Transient intermediate for structure parsing
365impl Intermediate {
366    pub fn from_ast(
367        DeriveInput {
368            ident, data, attrs, ..
369        }: syn::DeriveInput,
370    ) -> Result<Intermediate> {
371        let struct_name = ident.clone();
372
373        let AttrMeta {
374            docs, rename_rule, ..
375        } = parse_attrs(&attrs);
376
377        let struct_doc = {
378            let mut doc = String::new();
379            push_doc_string(&mut doc, &docs);
380            doc
381        };
382
383        let fields = if let syn::Data::Struct(syn::DataStruct { fields, .. }) = &data {
384            fields
385        } else {
386            abort!(ident, "TomlExample derive only use for struct")
387        };
388
389        let field_example = Self::parse_field_examples(fields, rename_rule);
390
391        Ok(Intermediate {
392            struct_name,
393            struct_doc,
394            field_example,
395        })
396    }
397
398    pub fn to_token_stream(&self) -> Result<TokenStream> {
399        let Intermediate {
400            struct_name,
401            struct_doc,
402            field_example,
403        } = self;
404
405        let field_example_stream: proc_macro2::TokenStream = field_example.parse()?;
406
407        Ok(quote! {
408            impl toml_example::TomlExample for #struct_name {
409                fn toml_example() -> String {
410                    #struct_name::toml_example_with_prefix("", "")
411                }
412                fn toml_example_with_prefix(label: &str, prefix: &str) -> String {
413                    #struct_doc.to_string() + label + &#field_example_stream
414                }
415            }
416        })
417    }
418
419    fn parse_field_examples(fields: &Fields, rename_rule: case::RenameRule) -> String {
420        let mut field_example = "r##\"".to_string();
421        let mut nesting_field_example = "".to_string();
422
423        if let Named(named_fields) = fields {
424            for f in named_fields.named.iter() {
425                let field = parse_field(f, rename_rule);
426                if field.skip {
427                    continue;
428                }
429
430                if field.nesting_format.is_some() {
431                    // Recursively add the toml_example_with_prefix of fields
432                    // If nesting in a section way will attached to the bottom to avoid #18
433                    // else the nesting will just using a prefix ahead the every field of example
434                    let (example, nesting_section_newline) =
435                        if field.nesting_format == Some(NestingFormat::Prefix) {
436                            (&mut field_example, "")
437                        } else {
438                            (&mut nesting_field_example, "\n")
439                        };
440
441                    field.push_doc_to_string(example);
442                    if let Some(ref field_type) = field.ty {
443                        example.push_str("\"##.to_string()");
444                        example.push_str(&format!(
445                            " + &{field_type}::toml_example_with_prefix(\"{}{}\", \"{}\")",
446                            field.label(),
447                            nesting_section_newline,
448                            field.prefix()
449                        ));
450                        example.push_str(" + &r##\"");
451                    } else {
452                        abort!(&f.ident, "nesting only work on inner structure")
453                    }
454                } else {
455                    // The leaf field, writing down the example value based on different default source
456                    field.push_doc_to_string(&mut field_example);
457                    if field.optional {
458                        field_example.push_str("# ");
459                    }
460                    field_example.push_str("\"##.to_string() + prefix + &r##\"");
461                    field_example.push_str(field.name.trim_start_matches("r#"));
462                    match field.default {
463                        DefaultSource::DefaultValue(default) => {
464                            field_example.push_str(" = ");
465                            field_example.push_str(&default);
466                            field_example.push('\n');
467                        }
468                        DefaultSource::DefaultFn(None) => {
469                            field_example.push_str(" = \"\"\n");
470                        }
471                        DefaultSource::DefaultFn(Some(ty)) => {
472                            field_example.push_str(" = \"##.to_string()");
473                            if field.is_enum {
474                                field_example.push_str(&format!(
475                                    " + &format!(\"\\\"{{:?}}\\\"\",  {ty}::default())"
476                                ));
477                            } else {
478                                field_example.push_str(&format!(
479                                    " + &format!(\"{{:?}}\",  {ty}::default())"
480                                ));
481                            }
482                            field_example.push_str(" + &r##\"\n");
483                        }
484                        DefaultSource::SerdeDefaultFn(fn_str) => {
485                            field_example.push_str(" = \"##.to_string()");
486                            if field.is_enum {
487                                field_example.push_str(&format!(
488                                    " + &format!(\"\\\"{{:?}}\\\"\",  {fn_str}())"
489                                ));
490                            } else {
491                                field_example
492                                    .push_str(&format!(" + &format!(\"{{:?}}\",  {fn_str}())"));
493                            }
494                            field_example.push_str("+ &r##\"\n");
495                        }
496                    }
497                    field_example.push('\n');
498                }
499            }
500        }
501        field_example += &nesting_field_example;
502        field_example.push_str("\"##.to_string()");
503
504        field_example
505    }
506}