Skip to main content

forge_system_one/
config.rs

1/// The decisions endpoint for a configured base. A base already carrying
2/// `/v1` appends only `/systemone` (pydantic-ai's rule, so an SDK-style
3/// base and a host-style base both land on the same route).
4pub fn systemone_url(base_url: &str) -> String {
5    let base = base_url.trim_end_matches('/');
6    if base.ends_with("/v1") { format!("{base}/systemone") } else { format!("{base}/v1/systemone") }
7}
8
9/// The per-request timeout a section gets when it names none.
10pub const DEFAULT_TIMEOUT_MS: u64 = 30_000;
11
12/// The raw `[systemone]` section as written in forge.toml.
13#[derive(Clone, serde::Deserialize)]
14#[serde(deny_unknown_fields)]
15pub struct SystemOneSection {
16    pub enabled: Option<bool>,
17    pub base_url: Option<String>,
18    pub api_key: Option<String>,
19    pub model: Option<String>,
20    pub timeout_ms: Option<u64>,
21}
22
23/// Hand-written because the raw API key must never be printed.
24impl std::fmt::Debug for SystemOneSection {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("SystemOneSection")
27            .field("enabled", &self.enabled)
28            .field("base_url", &self.base_url)
29            .field("api_key", &self.api_key.as_ref().map(|_| "[redacted]"))
30            .field("model", &self.model)
31            .field("timeout_ms", &self.timeout_ms)
32            .finish()
33    }
34}
35
36/// An enabled, validated `[systemone]` section.
37#[derive(Clone, PartialEq, Eq)]
38pub struct SystemOneConfig {
39    pub base_url: String,
40    pub api_key: Option<String>,
41    pub model: String,
42    pub timeout_ms: u64,
43}
44
45/// Hand-written because the API key must never be printed.
46impl std::fmt::Debug for SystemOneConfig {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.debug_struct("SystemOneConfig")
49            .field("base_url", &self.base_url)
50            .field("api_key", &self.api_key.as_ref().map(|_| "[redacted]"))
51            .field("model", &self.model)
52            .field("timeout_ms", &self.timeout_ms)
53            .finish()
54    }
55}
56
57impl SystemOneSection {
58    /// `Ok(None)` is the dormant state (disabled, or nothing to start);
59    /// `Err` is enabled-but-unusable, and the caller prefixes the
60    /// section name into the message.
61    pub fn into_config(self) -> Result<Option<SystemOneConfig>, String> {
62        if !self.enabled.unwrap_or(true) {
63            return Ok(None);
64        }
65        let base_url = self
66            .base_url
67            .filter(|b| !b.trim().is_empty())
68            .ok_or("`base_url` is required when `[systemone]` is enabled")?;
69        let model = self
70            .model
71            .filter(|m| !m.trim().is_empty())
72            .ok_or("`model` is required when `[systemone]` is enabled")?;
73        let timeout_ms = self.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS);
74        if timeout_ms == 0 {
75            return Err("`timeout_ms` must be greater than zero".to_owned());
76        }
77        Ok(Some(SystemOneConfig {
78            base_url: base_url.trim().to_owned(),
79            api_key: self.api_key.filter(|k| !k.trim().is_empty()),
80            model: model.trim().to_owned(),
81            timeout_ms,
82        }))
83    }
84}
85
86#[cfg(test)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn systemone_url_handles_base_shapes() {
92        assert_eq!(
93            systemone_url("https://api.typesafe.ai"),
94            "https://api.typesafe.ai/v1/systemone"
95        );
96        assert_eq!(
97            systemone_url("https://api.typesafe.ai/"),
98            "https://api.typesafe.ai/v1/systemone"
99        );
100        assert_eq!(
101            systemone_url("https://api.typesafe.ai/v1"),
102            "https://api.typesafe.ai/v1/systemone"
103        );
104        assert_eq!(
105            systemone_url("https://openrouter.ai/api"),
106            "https://openrouter.ai/api/v1/systemone"
107        );
108        assert_eq!(systemone_url("http://localhost:11434/"), "http://localhost:11434/v1/systemone");
109    }
110
111    #[test]
112    fn disabled_section_resolves_to_none() {
113        let section = SystemOneSection {
114            enabled: Some(false),
115            base_url: None,
116            api_key: None,
117            model: None,
118            timeout_ms: None,
119        };
120        assert_eq!(section.into_config(), Ok(None));
121    }
122
123    #[test]
124    fn missing_base_url_is_refused_when_enabled() {
125        let section = SystemOneSection {
126            enabled: None,
127            base_url: None,
128            api_key: None,
129            model: Some("jev-latest".to_owned()),
130            timeout_ms: None,
131        };
132        assert_eq!(
133            section.into_config(),
134            Err("`base_url` is required when `[systemone]` is enabled".to_owned())
135        );
136    }
137
138    #[test]
139    fn whitespace_base_url_is_treated_as_missing() {
140        let section = SystemOneSection {
141            enabled: None,
142            base_url: Some("  ".to_owned()),
143            api_key: None,
144            model: Some("jev-latest".to_owned()),
145            timeout_ms: None,
146        };
147        assert_eq!(
148            section.into_config(),
149            Err("`base_url` is required when `[systemone]` is enabled".to_owned())
150        );
151    }
152
153    #[test]
154    fn missing_model_is_refused_when_enabled() {
155        let section = SystemOneSection {
156            enabled: None,
157            base_url: Some("https://api.typesafe.ai".to_owned()),
158            api_key: None,
159            model: None,
160            timeout_ms: None,
161        };
162        assert_eq!(
163            section.into_config(),
164            Err("`model` is required when `[systemone]` is enabled".to_owned())
165        );
166    }
167
168    #[test]
169    fn zero_timeout_is_refused() {
170        let section = SystemOneSection {
171            enabled: None,
172            base_url: Some("https://api.typesafe.ai".to_owned()),
173            api_key: None,
174            model: Some("jev-latest".to_owned()),
175            timeout_ms: Some(0),
176        };
177        assert_eq!(section.into_config(), Err("`timeout_ms` must be greater than zero".to_owned()));
178    }
179
180    #[test]
181    fn full_section_resolves_with_defaults() {
182        let section = SystemOneSection {
183            enabled: None,
184            base_url: Some("https://api.typesafe.ai/".to_owned()),
185            api_key: Some("k".to_owned()),
186            model: Some("jev-latest".to_owned()),
187            timeout_ms: None,
188        };
189        assert_eq!(
190            section.into_config(),
191            Ok(Some(SystemOneConfig {
192                base_url: "https://api.typesafe.ai/".to_owned(),
193                api_key: Some("k".to_owned()),
194                model: "jev-latest".to_owned(),
195                timeout_ms: DEFAULT_TIMEOUT_MS,
196            }))
197        );
198    }
199
200    #[test]
201    fn blank_api_key_resolves_to_none() {
202        let section = SystemOneSection {
203            enabled: None,
204            base_url: Some("https://api.typesafe.ai".to_owned()),
205            api_key: Some("   ".to_owned()),
206            model: Some("jev-latest".to_owned()),
207            timeout_ms: None,
208        };
209        let cfg = section.into_config().expect("an enabled section with url and model resolves");
210        assert_eq!(cfg.expect("resolved").api_key, None);
211    }
212
213    #[test]
214    fn unknown_key_in_section_is_refused() {
215        let result: Result<SystemOneSection, _> = toml::from_str(
216            "enabled = true\nbase_uri = \"https://api.typesafe.ai\"\nmodel = \"jev-latest\"\n",
217        );
218        let err = result.expect_err("a mistyped key refuses the section");
219        assert!(err.to_string().contains("base_uri"), "{err}");
220    }
221
222    #[test]
223    fn section_deserializes_from_toml() {
224        let section: SystemOneSection = toml::from_str(
225            "enabled = false\nbase_url = \"https://api.typesafe.ai\"\nmodel = \"jev-latest\"\n",
226        )
227        .expect("the section parses");
228        assert_eq!(section.enabled, Some(false));
229        assert_eq!(section.base_url.as_deref(), Some("https://api.typesafe.ai"));
230        assert_eq!(section.into_config(), Ok(None));
231    }
232}