Skip to main content

lean_ctx/core/config/schema/
mod.rs

1//! Auto-generated config schema from `Config` struct metadata.
2//!
3//! Used by `lean-ctx config schema` to emit JSON and by
4//! `lean-ctx config validate` to check user config.toml files.
5
6use serde::Serialize;
7use std::collections::BTreeMap;
8mod sections_advanced;
9mod sections_core;
10mod sections_features;
11
12#[derive(Debug, Clone, Serialize)]
13pub struct ConfigSchema {
14    pub version: u32,
15    pub sections: BTreeMap<String, SectionSchema>,
16}
17
18#[derive(Debug, Clone, Serialize)]
19pub struct SectionSchema {
20    pub description: String,
21    pub keys: BTreeMap<String, KeySchema>,
22}
23
24#[derive(Debug, Clone, Serialize)]
25pub struct KeySchema {
26    #[serde(rename = "type")]
27    pub ty: String,
28    pub default: serde_json::Value,
29    pub description: String,
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub values: Option<Vec<String>>,
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub env_override: Option<String>,
34}
35
36fn clean_f32(v: f32) -> serde_json::Value {
37    let clean: f64 = format!("{v}").parse().unwrap_or(v as f64);
38    serde_json::json!(clean)
39}
40
41fn key(ty: &str, default: serde_json::Value, desc: &str) -> KeySchema {
42    KeySchema {
43        ty: ty.to_string(),
44        default,
45        description: desc.to_string(),
46        values: None,
47        env_override: None,
48    }
49}
50
51fn key_enum(values: &[&str], default: &str, desc: &str) -> KeySchema {
52    KeySchema {
53        ty: "enum".to_string(),
54        default: serde_json::Value::String(default.to_string()),
55        description: desc.to_string(),
56        values: Some(values.iter().map(ToString::to_string).collect()),
57        env_override: None,
58    }
59}
60
61fn key_with_env(ty: &str, default: serde_json::Value, desc: &str, env: &str) -> KeySchema {
62    KeySchema {
63        ty: ty.to_string(),
64        default,
65        description: desc.to_string(),
66        values: None,
67        env_override: Some(env.to_string()),
68    }
69}
70
71fn key_enum_with_env(values: &[&str], default: &str, desc: &str, env: &str) -> KeySchema {
72    KeySchema {
73        ty: "enum".to_string(),
74        default: serde_json::Value::String(default.to_string()),
75        description: desc.to_string(),
76        values: Some(values.iter().map(ToString::to_string).collect()),
77        env_override: Some(env.to_string()),
78    }
79}
80
81impl ConfigSchema {
82    pub fn generate() -> Self {
83        let mut sections = BTreeMap::new();
84        sections_core::build(&mut sections);
85        sections_features::build(&mut sections);
86        sections_advanced::build(&mut sections);
87
88        ConfigSchema {
89            version: 1,
90            sections,
91        }
92    }
93
94    /// Looks up a key schema by its dot-separated TOML path.
95    /// Returns `None` if the key is not part of the schema.
96    pub fn lookup(&self, key: &str) -> Option<&KeySchema> {
97        if let Some(dot_pos) = key.find('.') {
98            let section = &key[..dot_pos];
99            let field = &key[dot_pos + 1..];
100            self.sections.get(section)?.keys.get(field)
101        } else {
102            self.sections.get("root")?.keys.get(key)
103        }
104    }
105
106    /// All known TOML keys (dot-separated) for validation.
107    pub fn known_keys(&self) -> Vec<String> {
108        let mut keys = Vec::new();
109        for (section, schema) in &self.sections {
110            if section == "root" {
111                for key_name in schema.keys.keys() {
112                    keys.push(key_name.clone());
113                }
114            } else {
115                if schema.keys.is_empty() {
116                    keys.push(section.clone());
117                }
118                for key_name in schema.keys.keys() {
119                    keys.push(format!("{section}.{key_name}"));
120                }
121            }
122        }
123        keys
124    }
125}