diff --git a/README.md b/README.md index 43488f0..7c87fa5 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ Config::to_toml_example("example.toml"); // write example to a file let example = Config::toml_example(); ``` -Toml example base on the doc string of each field +Toml example base on the docstring of each field ```toml # Config is to arrange something or change the controls on a computer or other device # so that it can be used in a particular way @@ -135,8 +135,7 @@ Please add `#[toml_example(nesting)]`, or `#[toml_example(nesting = prefix)]` on #[allow(dead_code)] struct Node { /// Services are running in the node - #[toml_example(nesting)] - #[toml_example(default = http)] + #[toml_example(default = http, nesting)] services: HashMap, } ``` @@ -149,43 +148,52 @@ Please add `#[toml_example(nesting)]`, or `#[toml_example(nesting = prefix)]` on port = 80 ``` -If you want an optional field become a required field in example, -place the `#[toml_example(require)]` on the field. -If you want to skip some field you can use `#[toml_example(skip)]`, -the `#[serde(skip)]`, `#[serde(skip_deserializing)]` also works. + +## Flattening +Flattening means treating the fields of a nested struct as if they were defined directly in the wrapping struct. ```rust -use toml_example::TomlExample; #[derive(TomlExample)] -struct Config { - /// Config.a is an optional number - #[toml_example(require)] - a: Option, - /// Config.b is an optional string - #[toml_example(require)] - b: Option, - #[toml_example(require)] - #[toml_example(default = "third")] - c: Option, - #[toml_example(skip)] - d: usize, +struct ItemWrapper { + #[toml_example(flatten, nesting)] + item: Item, +} +#[derive(TomlExample)] +struct Item { + value: String, } + +assert_eq!(ItemWrapper::toml_example(), Item::toml_example()); ``` -```toml -# Config.a is an optional number -a = 0 -# Config.b is an optional string -b = "" +This works with maps too! -c = "third" +```rust +#[derive(TomlExample, Deserialize)] +struct MainConfig { + #[serde(flatten)] + #[toml_example(nesting)] + nested: HashMap, +} +#[derive(TomlExample, Deserialize)] +struct ConfigItem { + #[toml_example(default = false)] + enabled: bool, +} +let example = MainConfig::toml_example(); +assert!(toml::from_str::(&example).is_ok()); +println!("{example}"); +``` +```toml +[example] +enabled = false ``` ## Enum Field You can also use fieldless enums, but you have to annotate them with `#[toml_example(enum)]` or `#[toml_example(is_enum)]` if you mind the keyword highlight you likely get when writing "enum". When annotating a field with `#[toml_example(default)]` it will use the [Debug](core::fmt::Debug) implementation. -However for non-TOML datatypes like enums, this does not work as the value needs to be treated as a string in TOML. +However for non-TOML data types like enums, this does not work as the value needs to be treated as a string in TOML. The `#[toml_example(enum)]` attribute just adds the needed quotes around the [Debug](core::fmt::Debug) implementation and can be omitted if a custom [Debug](core::fmt::Debug) already includes those. @@ -194,8 +202,7 @@ use toml_example::TomlExample; #[derive(TomlExample)] struct Config { /// Config.priority is an enum - #[toml_example(default)] - #[toml_example(enum)] + #[toml_example(enum, default)] priority: Priority, } #[derive(Debug, Default)] @@ -211,6 +218,38 @@ priority = "Important" "#) ``` +## More +If you want an optional field become a required field in example, +place the `#[toml_example(require)]` on the field. +If you want to skip some field you can use `#[toml_example(skip)]`, +the `#[serde(skip)]`, `#[serde(skip_deserializing)]` also works. +```rust +use toml_example::TomlExample; +#[derive(TomlExample)] +struct Config { + /// Config.a is an optional number + #[toml_example(require)] + a: Option, + /// Config.b is an optional string + #[toml_example(require)] + b: Option, + #[toml_example(require, default = "third")] + c: Option, + #[toml_example(skip)] + d: usize, +} +``` +```toml +# Config.a is an optional number +a = 0 + +# Config.b is an optional string +b = "" + +c = "third" + +``` + [crates-badge]: https://img.shields.io/crates/v/toml-example.svg [crate-url]: https://crates.io/crates/toml-example [mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 158f72c..49e949c 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -32,6 +32,7 @@ struct AttrMeta { require: bool, skip: bool, is_enum: bool, + flatten: bool, rename: Option, rename_rule: case::RenameRule, } @@ -42,6 +43,7 @@ struct ParsedField { nesting_format: Option, skip: bool, is_enum: bool, + flatten: bool, name: String, optional: bool, ty: Option, @@ -66,13 +68,34 @@ impl ParsedField { fn label(&self) -> String { match self.nesting_format { Some(NestingFormat::Section(NestingType::Vec)) => { + if self.flatten { + abort!( + "flatten", + format!( + "Only structs and maps can be flattened! \ + (But field `{}` is a collection)", + self.name + ) + ) + } self.prefix() + &format!("[[{}]]", self.name) } Some(NestingFormat::Section(NestingType::Dict)) => { - self.prefix() + &format!("[{}.{}]", self.name, self.default_key()) + self.prefix() + + &if self.flatten { + format!("[{}]", self.default_key()) + } else { + format!("[{}.{}]", self.name, self.default_key()) + } } Some(NestingFormat::Prefix) => "".to_string(), - _ => self.prefix() + &format!("[{}]", self.name), + _ => { + if self.flatten { + self.prefix() + } else { + self.prefix() + &format!("[{}]", self.name) + } + } } } @@ -190,6 +213,7 @@ fn parse_attrs(attrs: &[Attribute]) -> AttrMeta { let mut require = false; let mut skip = false; let mut is_enum = false; + let mut flatten = false; // mut in serde feature #[allow(unused_mut)] let mut rename = None; @@ -222,31 +246,36 @@ fn parse_attrs(attrs: &[Attribute]) -> AttrMeta { #[cfg(feature = "serde")] { let token_str = _tokens.to_string(); - if token_str.starts_with("default") { - if let Some((_, s)) = token_str.split_once('=') { - default_source = Some(DefaultSource::SerdeDefaultFn( - s.trim().trim_matches('"').into(), - )); - } else { - default_source = Some(DefaultSource::DefaultFn(None)); + for attribute in token_str.split(find_unenclosed_char(',')).map(str::trim) { + if attribute.starts_with("default") { + if let Some((_, s)) = attribute.split_once('=') { + default_source = Some(DefaultSource::SerdeDefaultFn( + s.trim().trim_matches('"').into(), + )); + } else { + default_source = Some(DefaultSource::DefaultFn(None)); + } } - } - if token_str == "skip_deserializing" || token_str == "skip" { - skip = true; - } - if token_str.starts_with("rename") { - if token_str.starts_with("rename_all") { - if let Some((_, s)) = token_str.split_once('=') { - rename_rule = if let Ok(r) = - case::RenameRule::from_str(s.trim().trim_matches('"')) - { - r - } else { - abort!(&_tokens, "unsupported rename rule") + if attribute == "skip_deserializing" || attribute == "skip" { + skip = true; + } + if attribute == "flatten" { + flatten = true; + } + if attribute.starts_with("rename") { + if attribute.starts_with("rename_all") { + if let Some((_, s)) = attribute.split_once('=') { + rename_rule = if let Ok(r) = + case::RenameRule::from_str(s.trim().trim_matches('"')) + { + r + } else { + abort!(&_tokens, "unsupported rename rule") + } } + } else if let Some((_, s)) = attribute.split_once('=') { + rename = Some(s.trim().trim_matches('"').into()); } - } else if let Some((_, s)) = token_str.split_once('=') { - rename = Some(s.trim().trim_matches('"').into()); } } } @@ -259,30 +288,36 @@ fn parse_attrs(attrs: &[Attribute]) -> AttrMeta { .unwrap_or_default() => { let token_str = tokens.to_string(); - if token_str.starts_with("default") { - if let Some((_, s)) = token_str.split_once('=') { - default_source = Some(DefaultSource::DefaultValue(s.trim().into())); - } else { - default_source = Some(DefaultSource::DefaultFn(None)); - } - } else if token_str.starts_with("nesting") { - if let Some((_, s)) = token_str.split_once('=') { - nesting_format = match s.trim() { - "prefix" => Some(NestingFormat::Prefix), - "section" => Some(NestingFormat::Section(NestingType::None)), - _ => abort!(&attr, "please use prefix or section for nesting derive"), + for attribute in token_str.split(find_unenclosed_char(',')).map(str::trim) { + if attribute.starts_with("default") { + if let Some((_, s)) = attribute.split_once('=') { + default_source = Some(DefaultSource::DefaultValue(s.trim().into())); + } else { + default_source = Some(DefaultSource::DefaultFn(None)); } + } else if attribute.starts_with("nesting") { + if let Some((_, s)) = attribute.split_once('=') { + nesting_format = match s.trim() { + "prefix" => Some(NestingFormat::Prefix), + "section" => Some(NestingFormat::Section(NestingType::None)), + _ => { + abort!(&attr, "please use prefix or section for nesting derive") + } + } + } else { + nesting_format = Some(NestingFormat::Section(NestingType::None)); + } + } else if attribute == "require" { + require = true; + } else if attribute == "skip" { + skip = true; + } else if attribute == "is_enum" || attribute == "enum" { + is_enum = true; + } else if attribute == "flatten" { + flatten = true; } else { - nesting_format = Some(NestingFormat::Section(NestingType::None)); + abort!(&attr, format!("{} is not allowed attribute", attribute)) } - } else if token_str == "require" { - require = true; - } else if token_str == "skip" { - skip = true; - } else if token_str == "is_enum" || token_str == "enum" { - is_enum = true; - } else { - abort!(&attr, format!("{} is not allowed attribute", token_str)) } } _ => (), @@ -296,6 +331,7 @@ fn parse_attrs(attrs: &[Attribute]) -> AttrMeta { require, skip, is_enum, + flatten, rename, rename_rule, } @@ -314,6 +350,7 @@ fn parse_field( mut nesting_format, skip, is_enum, + flatten, rename, require, .. @@ -342,6 +379,7 @@ fn parse_field( nesting_format, skip, is_enum, + flatten, name, optional: optional && !require, ty, @@ -448,7 +486,17 @@ impl Intermediate { if field.nesting_format == Some(NestingFormat::Prefix) { (&mut field_example, "") } else { - (&mut nesting_field_example, "\n") + ( + &mut nesting_field_example, + if field.flatten + && field.nesting_format + == Some(NestingFormat::Section(NestingType::None)) + { + "" + } else { + "\n" + }, + ) }; field.push_doc_to_string(example); @@ -573,3 +621,42 @@ fn handle_serde_default_fn_source( } field_example.push_str("+ &r##\"\n"); } + +/// A [Pattern](std::str::pattern::Pattern) to find a char that is not enclosed in quotes, braces +/// or the like +fn find_unenclosed_char(pat: char) -> impl FnMut(char) -> bool { + let mut quotes = 0; + let mut single_quotes = 0; + let mut brackets = 0; + let mut braces = 0; + let mut parenthesis = 0; + let mut is_escaped = false; + move |char| -> bool { + if is_escaped { + is_escaped = false; + return false; + } else if char == '\\' { + is_escaped = true; + } else if (quotes % 2 == 1 && char != '"') || (single_quotes % 2 == 1 && char != '\'') { + return false; + } else { + match char { + '"' => quotes += 1, + '\'' => single_quotes += 1, + '[' => brackets += 1, + ']' => brackets -= 1, + '{' => braces += 1, + '}' => braces -= 1, + '(' => parenthesis += 1, + ')' => parenthesis -= 1, + _ => {} + } + } + char == pat + && quotes % 2 == 0 + && single_quotes % 2 == 0 + && brackets == 0 + && braces == 0 + && parenthesis == 0 + } +} diff --git a/lib/src/lib.rs b/lib/src/lib.rs index 5ced2c6..8318c92 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -1,6 +1,6 @@ //! This crate provides the [`TomlExample`] trait and an accompanying derive macro. //! -//! Deriving [`TomlExample`] on a struct will generate functions `toml_example()`, `to_toml_example(file_name)` for generating toml example content. +//! Deriving [`TomlExample`] on a struct will generate functions `toml_example()`, `to_toml_example(file_name)` for generating toml example content. //! //! The following code shows how `toml-example` can be used. //! ```rust @@ -50,7 +50,7 @@ //! Also, toml-example will use `#[serde(default)]`, `#[serde(default = "default_fn")]` for the //! example value. //! -//! With nestring structure, `#[toml_example(nesting)]` should set on the field as following +//! With nesting structure, `#[toml_example(nesting)]` should set on the field as following //! example. //! //! ```rust @@ -68,8 +68,7 @@ //! #[allow(dead_code)] //! struct Node { //! /// Services are running in the node -//! #[toml_example(nesting)] -//! #[toml_example(default = http)] +//! #[toml_example(default = http, nesting)] //! services: HashMap, //! } //! @@ -83,6 +82,51 @@ //! "#); //! ``` //! +//! Flattened items are supported as well. +//! +//! ```rust +//! use toml_example::TomlExample; +//! +//! #[derive(TomlExample)] +//! struct ItemWrapper { +//! #[toml_example(flatten, nesting)] +//! item: Item, +//! } +//! #[derive(TomlExample)] +//! struct Item { +//! value: String, +//! } +//! +//! assert_eq!(ItemWrapper::toml_example(), Item::toml_example()); +//! ``` +//! +//! Flattening works with maps too! +//! +//! ```rust +//! use serde::Deserialize; +//! use toml_example::TomlExample; +//! # use std::collections::HashMap; +//! +//! #[derive(TomlExample, Deserialize)] +//! struct MainConfig { +//! #[serde(flatten)] +//! #[toml_example(nesting)] +//! nested: HashMap, +//! } +//! #[derive(TomlExample, Deserialize)] +//! struct ConfigItem { +//! #[toml_example(default = false)] +//! enabled: bool, +//! } +//! +//! let example = MainConfig::toml_example(); +//! assert!(toml::from_str::(&example).is_ok()); +//! assert_eq!(example, r#"[example] +//! enabled = false +//! +//! "#); +//! ``` +//! //! The fields of a struct can inherit their defaults from the parent struct when the //! `#[toml_example(default)]`, `#[serde(default)]` or `#[serde(default = "default_fn")]` //! attribute is set as an outer attribute of the parent struct: @@ -127,8 +171,7 @@ //! /// Config.b is an optional string //! #[toml_example(require)] //! b: Option, -//! #[toml_example(require)] -//! #[toml_example(default = "third")] +//! #[toml_example(require, default = "third")] //! c: Option, //! #[toml_example(skip)] //! d: usize, @@ -150,7 +193,7 @@ //! "enum".
//! When annotating a field with `#[toml_example(default)]` it will use the //! [Debug](core::fmt::Debug) implementation. -//! However for non-TOML datatypes like enums, this does not work as the value needs to be treated +//! However for non-TOML data types like enums, this does not work as the value needs to be treated //! as a string in TOML. The `#[toml_example(enum)]` attribute just adds the needed quotes around //! the [Debug](core::fmt::Debug) implementation and can be omitted if a custom //! [Debug](core::fmt::Debug) already includes those. @@ -159,8 +202,7 @@ //! #[derive(TomlExample)] //! struct Config { //! /// Config.priority is an enum -//! #[toml_example(default)] -//! #[toml_example(enum)] +//! #[toml_example(default, enum)] //! priority: Priority, //! } //! #[derive(Debug, Default)] @@ -951,8 +993,7 @@ a = 0 #[allow(dead_code)] struct Config { /// Config.ab is an enum - #[toml_example(enum)] - #[toml_example(default)] + #[toml_example(enum, default)] ab: AB, /// Config.ab2 is an enum too #[toml_example(is_enum)] @@ -980,6 +1021,41 @@ ab2 = "A" # Config.ab3 is an enum as well ab3 = "B" +"# + ); + } + + #[test] + fn flatten() { + #[derive(TomlExample)] + struct ItemWrapper { + #[toml_example(flatten, nesting)] + _item: Item, + } + #[derive(TomlExample)] + struct Item { + _value: String, + } + assert_eq!(ItemWrapper::toml_example(), Item::toml_example()); + } + + #[test] + fn multi_attr_escaping() { + #[derive(TomlExample, Deserialize, PartialEq)] + struct ConfigWrapper { + #[toml_example(default = ["hello", "{nice :)\""], require)] + vec: Option>, + + #[toml_example(require, default = ["\"\\\n}])", "super (fancy\\! :-) )"])] + list: Option<[String; 2]>, + } + + assert_eq!( + ConfigWrapper::toml_example(), + r#"vec = ["hello", "{nice :)\""] + +list = ["\"\\\n}])", "super (fancy\\! :-) )"] + "# ); }