soar_core/
toml.rs

1use std::any::type_name;
2
3use documented::{Documented, DocumentedFields};
4use toml_edit::{ArrayOfTables, Decor, Item, RawString, Table};
5use tracing::warn;
6
7use crate::error::ConfigError;
8
9/// Appends documentation lines as TOML comments to the given `Decor`.
10///
11/// This function transforms each line in the provided documentation string
12/// into a TOML comment (prefixed with `#`) and appends it to the existing
13/// comment prefix in `decor`, preserving formatting.
14///
15/// # Arguments
16/// * `decor` - Mutable reference to the TOML `Decor` where comments should be inserted.
17/// * `docs` - The documentation string to convert to TOML comments.
18pub fn append_docs_as_toml_comments(decor: &mut Decor, docs: &str) {
19    let old_prefix = decor.prefix().and_then(RawString::as_str);
20    let last_line = old_prefix.and_then(|prefix| prefix.lines().last());
21
22    let comments = docs
23        .lines()
24        .map(|l| {
25            if l.is_empty() {
26                "#\n".into()
27            } else {
28                format!("# {l}\n")
29            }
30        })
31        .collect();
32
33    let new_prefix = match (old_prefix, last_line) {
34        (None | Some(""), None) => comments,
35        (None, Some(_)) => unreachable!(),
36        (Some(_), None) => unreachable!(),
37        (Some(prefix), Some("")) => format!("{prefix}{comments}"),
38        (Some(prefix), Some(_)) => format!("{prefix}#\n{comments}"),
39    };
40    decor.set_prefix(new_prefix);
41}
42
43/// Annotates a TOML `Table` with documentation extracted from a struct `T` that implements
44/// the `Documented` and `DocumentedFields` traits.
45///
46/// This adds documentation comments above each key in the table based on field-level documentation,
47/// and optionally includes the struct-level documentation if `is_root` is false.
48///
49/// # Arguments
50/// * `table` - Mutable reference to the TOML table to annotate.
51/// * `is_root` - Whether this table is the root; root tables don't get container-level doc comments.
52///
53/// # Returns
54/// Returns `Ok(())` if successful, or a `ConfigError` if a TOML item is unexpectedly `None`.
55pub fn annotate_toml_table<T>(table: &mut Table, is_root: bool) -> Result<(), ConfigError>
56where
57    T: Documented + DocumentedFields,
58{
59    if !is_root {
60        append_docs_as_toml_comments(table.decor_mut(), T::DOCS);
61    }
62
63    for (mut key_mut, value_item) in table.iter_mut() {
64        let key_str = key_mut.get();
65        match T::get_field_docs(key_str) {
66            Ok(docs) => match value_item {
67                Item::None => {
68                    return Err(ConfigError::Custom(format!(
69                        "Encountered TomlEditItem::None for key '{key_str}' unexpectedly",
70                    )))
71                }
72                Item::Value(_) => append_docs_as_toml_comments(key_mut.leaf_decor_mut(), docs),
73                Item::Table(sub_table) => append_docs_as_toml_comments(sub_table.decor_mut(), docs),
74                Item::ArrayOfTables(array) => {
75                    let first_table = array
76                        .iter_mut()
77                        .next()
78                        .expect("Array of table should not be empty");
79                    append_docs_as_toml_comments(first_table.decor_mut(), docs);
80                }
81            },
82            Err(_) => {
83                warn!(
84                    "Field '{}' found in TOML but not in struct '{}' for documentation lookup, or it's a complex case like flatten.",
85                    key_str,
86                    type_name::<T>()
87                );
88            }
89        }
90    }
91
92    Ok(())
93}
94
95/// Annotates the first table in a TOML `ArrayOfTables` using documentation from the given struct `T`.
96///
97/// This assumes that the structure of all tables in the array is the same, so only the first table is annotated.
98///
99/// # Arguments
100/// * `array` - Mutable reference to the TOML array of tables to annotate.
101///
102/// # Returns
103/// Returns `Ok(())` if annotation succeeds, or a `ConfigError` if annotation fails on the first table.
104pub fn annotate_toml_array_of_tables<T>(array: &mut ArrayOfTables) -> Result<(), ConfigError>
105where
106    T: Documented + DocumentedFields,
107{
108    if let Some(first_table) = array.iter_mut().next() {
109        annotate_toml_table::<T>(first_table, false).map_err(|err| {
110            ConfigError::Custom(format!("Failed to annotate first table in array: {err}"))
111        })?;
112    }
113    Ok(())
114}