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}