use super::Config;
use super::schema::{ConfigSchema, KeySchema};
const HEADER: &str = "\
# lean-ctx configuration — full annotated reference
#
# Generated by `lean-ctx config init --full`. Every key is documented with its
# purpose and, where applicable, its default, allowed values, and the environment
# variable that overrides it. The values below reflect your current configuration.
#
# A key left at its default may be deleted — lean-ctx falls back to the documented
# default. After editing, run `lean-ctx config apply` to reload.
";
pub fn render_annotated_config(cfg: &Config, schema: &ConfigSchema) -> String {
let mut out = String::from(HEADER);
let Ok(body) = toml::to_string_pretty(cfg) else {
return out;
};
let mut current_section = String::from("root");
for line in body.lines() {
let trimmed = line.trim_start();
if let Some(section) = section_header(trimmed) {
append_section(&mut out, schema, §ion, line);
current_section = section;
continue;
}
if let Some(field) = leading_key(trimmed) {
let path = if current_section == "root" {
field.to_string()
} else {
format!("{current_section}.{field}")
};
if let Some(key_schema) = schema.lookup(&path) {
append_key_comment(&mut out, key_schema);
}
}
out.push_str(line);
out.push('\n');
}
out
}
fn section_header(trimmed: &str) -> Option<String> {
if trimmed.starts_with("[[") || !trimmed.starts_with('[') || !trimmed.ends_with(']') {
return None;
}
let inner = trimmed[1..trimmed.len() - 1].trim();
if inner.is_empty() {
None
} else {
Some(inner.to_string())
}
}
fn leading_key(trimmed: &str) -> Option<&str> {
let eq = trimmed.find('=')?;
let key = trimmed[..eq].trim();
if !key.is_empty()
&& key
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
{
Some(key)
} else {
None
}
}
fn append_section(out: &mut String, schema: &ConfigSchema, section: &str, header_line: &str) {
if !out.ends_with("\n\n") {
out.push('\n');
}
if let Some(section_schema) = schema.sections.get(section)
&& !section_schema.description.is_empty()
{
for comment in section_schema.description.lines() {
out.push_str("# ");
out.push_str(comment);
out.push('\n');
}
}
out.push_str(header_line);
out.push('\n');
}
fn append_key_comment(out: &mut String, key_schema: &KeySchema) {
for comment in key_schema.description.lines() {
out.push_str("# ");
out.push_str(comment);
out.push('\n');
}
if !key_schema.default.is_null() {
out.push_str("# default: ");
out.push_str(&key_schema.default.to_string());
out.push('\n');
}
if let Some(values) = &key_schema.values
&& !values.is_empty()
{
out.push_str("# values: ");
out.push_str(&values.join(", "));
out.push('\n');
}
if let Some(env) = &key_schema.env_override {
out.push_str("# env: ");
out.push_str(env);
out.push('\n');
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::CompressionLevel;
fn customized() -> Config {
let mut cfg = Config {
max_ram_percent: 30,
compression_level: CompressionLevel::Standard,
theme: "neon".to_string(),
..Config::default()
};
cfg.proxy.anthropic_upstream = Some("https://upstream.example".to_string());
cfg.gain.display_name = Some("alice".to_string());
cfg
}
#[test]
fn render_round_trips_to_identical_config() {
let cfg = customized();
let schema = ConfigSchema::generate();
let rendered = render_annotated_config(&cfg, &schema);
let expected: toml::Value = toml::from_str(&toml::to_string_pretty(&cfg).unwrap()).unwrap();
let actual: toml::Value = toml::from_str(&rendered).expect("rendered config must parse");
assert_eq!(
actual, expected,
"render → parse must reproduce the config exactly (#443)"
);
}
#[test]
fn render_preserves_customized_values() {
let cfg = customized();
let schema = ConfigSchema::generate();
let parsed: Config = toml::from_str(&render_annotated_config(&cfg, &schema)).unwrap();
assert_eq!(parsed.max_ram_percent, 30);
assert_eq!(parsed.compression_level, CompressionLevel::Standard);
assert_eq!(parsed.theme, "neon");
}
#[test]
fn render_is_deterministic_and_annotated() {
let cfg = Config::default();
let schema = ConfigSchema::generate();
let first = render_annotated_config(&cfg, &schema);
let second = render_annotated_config(&cfg, &schema);
assert_eq!(first, second, "render must be deterministic (#498)");
assert!(first.contains("# lean-ctx configuration"));
assert!(
first.contains("max_ram_percent"),
"documented keys must appear"
);
assert!(
first.matches("# ").count() > 5,
"rendered config must be annotated with comments"
);
}
}