fastpasta_toml_macro_derive/
lib.rs

1//! # Description
2//! Procedural derive macro for serializing a struct into a TOML template with field descriptions that is easily edited and deserialized.
3//!
4//! Nested structs are not currently supported.
5//!
6//! # Purpose
7//! Make it easy to write a struct that defines a `TOML` template for optional configuration of an executable. Once the struct is deserialized with the derive macro implemented `to_string_pretty_toml()` function, it can be written to a (TOML) file, the file should be understandable without knowing any details of the binary. Deserializing the produced TOML file with no edits produceses the original struct with all optional fields `None`. Editing the produced TOML file will then deserialize into the original struct with those edited values.
8//!
9//! # Table of Contents
10//! - [Description](#description)
11//! - [Purpose](#purpose)
12//! - [Table of Contents](#table-of-contents)
13//! - [Guide](#guide)
14//!   - [What is derived?](#what-is-derived)
15//!   - [Example use in fastPASTA](#example-use-in-fastpasta)
16//!     - [Implementing](#implementing)
17//!     - [Serializing](#serializing)
18//!     - [Deserializing](#deserializing)
19//!
20//! # Guide
21//!
22//! ## What is derived?
23//! A `pub trait` named `TomlConfig` with a single function with the signature:  `fn to_string_pretty_toml(&self) -> String`
24//!
25//! ```rust
26//! pub trait TomlConfig {
27//!     fn to_string_pretty_toml(&self) -> String;
28//! }
29//! ```
30//!
31//! ## Example use in fastPASTA
32//! This macro was originally made for use in the [fastPASTA](https://crates.io/crates/fastpasta) crate.
33//! The example is based on how the macro is used in `fastPASTA`.
34//!
35//! ### Implementing
36//! The struct `CustomChecks` is implemented like this:
37//!
38//! ```rust
39//! use fastpasta_toml_macro_derive::TomlConfig;
40//! use serde_derive::{Deserialize, Serialize};
41//!
42//! pub trait TomlConfig {
43//!     fn to_string_pretty_toml(&self) -> String;
44//! }
45//!
46//! // Deriving the `TomlConfig` macro which implements the `TomlConfig` trait.
47//! #[derive(TomlConfig, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
48//! pub struct CustomChecks {
49//!     // Use the `description` field attribute of the macro
50//!     #[description = "Number of CRU Data Packets expected in the data"]
51//!     // Use the `example` field attribute of the macro to show some example values
52//!     #[example = "20, 500532"]
53//!     cdps: Option<u32>,
54//!
55//!     #[description = "Number of Physics (PhT) Triggers expected in the data"]
56//!     #[example = "0, 10"]
57//!     triggers_pht: Option<u32>,
58//!
59//!     #[description = "Legal Chip ordering for Outer Barrel (ML/OL). Needs to be a list of two lists of 7 chip IDs"]
60//!     #[example = "[[0, 1, 2, 3, 4, 5, 6], [8, 9, 10, 11, 12, 13, 14]]"]
61//!     chip_orders_ob: Option<(Vec<u8>, Vec<u8>)>,
62//! }
63//! ```
64//! ### Serializing
65//!
66//! The template file is generated e.g. like this.
67//! ```rust
68//! let toml = CustomChecks::default().to_string_pretty_toml();
69//! std::fs::write("custom_checks.toml", toml).unwrap();
70//! ```
71//! The contents of "custom_checks.toml" is now:
72//! ```toml
73//! # Number of CRU Data Packets expected in the data
74//! # Example: 20, 500532
75//! #cdps = None [ u32 ] # (Uncomment and set to enable this check)
76//!
77//! # Number of Physics (PhT) Triggers expected in the data
78//! # Example: 0, 10
79//! #triggers_pht = None [ u32 ] # (Uncomment and set to enable this check)
80//!
81//! # Legal Chip ordering for Outer Barrel (ML/OL). Needs to be a list of two lists of 7 chip IDs
82//! # Example: [[0, 1, 2, 3, 4, 5, 6], [8, 9, 10, 11, 12, 13, 14]]
83//! #chip_orders_ob = None [ (Vec < u8 >, Vec < u8 >) ] # (Uncomment and set to enable this check)
84//! ```
85//! Editing all the fields to contain `Some` values could look like this:
86//! ```toml
87//! # Number of CRU Data Packets expected in the data
88//! # Example: 20, 500532
89//! cdps = 20
90//!
91//! # Number of Physics (PhT) Triggers expected in the data
92//! # Example: 0, 10
93//! triggers_pht = 0
94//!
95//! # Legal Chip ordering for Outer Barrel (ML/OL). Needs to be a list of two lists of 7 chip IDs
96//! # Example: [[0, 1, 2, 3, 4, 5, 6], [8, 9, 10, 11, 12, 13, 14]]
97//! chip_orders_ob = [[0, 1, 2, 3, 4, 5, 6], [8, 9, 10, 11, 12, 13, 14]]
98//! ```
99//! ### Deserializing
100//!
101//! Deserializing from a TOML file is the same method as with any other TOML file, using `serde_derive`:
102//! ```rust
103//! let toml = std::fs::read_to_string("custom_checks.toml").unwrap();
104//! let custom_checks = toml::from_str(&toml).unwrap();
105//! ```
106//!
107//! A user that is already familiar with the configuration file might simply write
108//! ```toml
109//! cdps = 10
110//! ```
111//! And input it to the binary. Which would deserialize into a struct with the `cdps` field containing `Some(10)`, and the rest of the fields are `None`.
112
113use proc_macro::TokenStream;
114
115use quote::{quote, ToTokens};
116use syn::{Attribute, DeriveInput};
117
118#[proc_macro_derive(TomlConfig, attributes(description, example))]
119pub fn derive_signature(input: TokenStream) -> TokenStream {
120    // Construct a representation of Rust code as a syntax tree
121    // that we can manipulate
122    let ast = syn::parse_macro_input!(input as DeriveInput);
123    // Build the trait implementation
124    impl_toml_config(&ast)
125}
126
127fn impl_toml_config(ast: &syn::DeriveInput) -> TokenStream {
128    const DESCRIPTION_ATTR_NAME: &str = "description";
129    const EXAMPLE_ATTR_NAME: &str = "example";
130
131    let fields = if let syn::Data::Struct(syn::DataStruct {
132        fields: syn::Fields::Named(ref fields),
133        ..
134    }) = ast.data
135    {
136        fields
137    } else {
138        panic!("This macro only works on structs")
139    };
140
141    let mut descriptions: Vec<String> = Vec::new();
142    let mut examples: Vec<String> = Vec::new();
143    let mut field_ids: Vec<quote::__private::TokenStream> = Vec::new();
144    let mut field_values: Vec<&Option<syn::Ident>> = Vec::new();
145    let mut types: Vec<String> = Vec::new();
146
147    for field in fields.named.iter() {
148        if let Some(desc) = get_attribute(DESCRIPTION_ATTR_NAME, field) {
149            descriptions.push(attribute_value_as_string(desc));
150        } else {
151            panic!("Every custom check field needs a description!")
152        }
153
154        if let Some(example) = get_attribute(EXAMPLE_ATTR_NAME, field) {
155            examples.push(attribute_value_as_string(example));
156        } else {
157            panic!("Every custom check field needs an example!")
158        }
159
160        let literal_key_str: syn::LitStr = field_name_to_key_literal(field.ident.as_ref().unwrap());
161        field_ids.push(quote! { #literal_key_str  });
162
163        field_values.push(&field.ident);
164        types.push(field_option_type_to_inner_type_string(&field.ty));
165    }
166
167    let struct_name = &ast.ident;
168    let generated_code_token_stream: quote::__private::TokenStream = generate_impl(
169        struct_name,
170        descriptions,
171        examples,
172        field_ids,
173        field_values,
174        types,
175    );
176    // Return the generated impl as a proc_macro::TokenStream instead of the TokenStream type that quote returns
177    generated_code_token_stream.into()
178}
179
180/// Generate the implementation of the [TomlConfig] trait
181fn generate_impl(
182    struct_name: &syn::Ident,
183    descriptions: Vec<String>,
184    examples: Vec<String>,
185    field_ids: Vec<quote::__private::TokenStream>,
186    field_values: Vec<&Option<syn::Ident>>,
187    types: Vec<String>,
188) -> quote::__private::TokenStream {
189    quote! {
190        impl TomlConfig for #struct_name {
191            fn to_string_pretty_toml(&self) -> String {
192
193                let mut toml_string = String::new();
194
195                #(
196                    toml_string.push_str(&format!("# {description_comment}\n", description_comment = #descriptions));
197                    toml_string.push_str(&format!("# Example: {example}\n", example = #examples));
198
199                    if let Some(field_val) = &self.#field_values {
200                         // If the type is `String` the value needs to be in quotes in TOML format
201                        let formatted_field_val = if #types.contains(&"String") {
202                                format!("\"{field_val:?}\"")
203                        } else {
204                            // If the type is a tuple struct, the value needs to be in square brackets in TOML format
205                            // This is safe as we know by now that the type is not a `String`
206                            let field_val_string = format!("{field_val:?}");
207                            field_val_string.chars().map(|c| match c {
208                                '(' => '[',
209                                ')' => ']',
210                                _ => c
211                            }).collect::<String>()
212                        };
213                        toml_string.push_str(&format!("{field_name} = {field_value} # [{type_name}]\n\n",
214                            field_name = #field_ids,
215                            field_value = formatted_field_val,
216                            type_name = #types
217                        ));
218                    } else {
219                        toml_string.push_str(&format!("#{field_name} = None [{type_name}] # (Uncomment and set to enable)\n\n",
220                            field_name = #field_ids,
221                            type_name = #types
222                        ));
223                    }
224                )*
225
226                toml_string
227            }
228        }
229    }
230}
231
232fn get_attribute<'a>(attr_name: &'a str, field: &'a syn::Field) -> Option<&'a syn::Attribute> {
233    field.attrs.iter().find(|a| a.path().is_ident(attr_name))
234}
235
236fn attribute_value_as_string(attr: &Attribute) -> String {
237    let attr_description_as_string = attr
238        .meta
239        .require_name_value()
240        .unwrap()
241        .value
242        .to_token_stream()
243        .to_string();
244    let mut as_char_iter = attr_description_as_string.chars();
245    as_char_iter.next();
246    as_char_iter.next_back();
247    as_char_iter.as_str().to_owned()
248}
249
250fn field_name_to_key_literal(field_name: &syn::Ident) -> syn::LitStr {
251    let name: String = field_name.to_string();
252    syn::LitStr::new(&name, field_name.span())
253}
254
255// Convert a fields type of type Option<InnerType> to InnerType as a string
256fn field_option_type_to_inner_type_string(field_option_type: &syn::Type) -> String {
257    let type_name = field_option_type.to_token_stream().to_string();
258    let mut type_as_char = type_name.chars();
259    for _ in 0..=7 {
260        type_as_char.next();
261    }
262    type_as_char.next_back();
263    type_as_char.as_str().to_string()
264}