rumdl_lib/code_block_tools/
config.rs1use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
13#[serde(rename_all = "kebab-case")]
14pub struct CodeBlockToolsConfig {
15 #[serde(default)]
17 pub enabled: bool,
18
19 #[serde(default)]
21 pub normalize_language: NormalizeLanguage,
22
23 #[serde(default)]
25 pub on_error: OnError,
26
27 #[serde(default)]
30 pub on_missing_language_definition: OnMissing,
31
32 #[serde(default)]
34 pub on_missing_tool_binary: OnMissing,
35
36 #[serde(default = "default_timeout")]
38 #[schemars(schema_with = "schema_timeout")]
39 pub timeout: u64,
40
41 #[serde(default)]
43 pub languages: BTreeMap<String, LanguageToolConfig>,
44
45 #[serde(default)]
48 pub language_aliases: BTreeMap<String, String>,
49
50 #[serde(default)]
52 pub tools: BTreeMap<String, ToolDefinition>,
53
54 #[serde(skip)]
63 #[schemars(skip)]
64 pub values_withheld: bool,
65}
66
67fn default_timeout() -> u64 {
68 30_000
69}
70
71fn schema_timeout(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
73 schemars::json_schema!({
74 "type": "integer",
75 "minimum": 0
76 })
77}
78
79impl Default for CodeBlockToolsConfig {
80 fn default() -> Self {
81 Self {
82 enabled: false,
83 normalize_language: NormalizeLanguage::default(),
84 on_error: OnError::default(),
85 on_missing_language_definition: OnMissing::default(),
86 on_missing_tool_binary: OnMissing::default(),
87 timeout: default_timeout(),
88 languages: BTreeMap::new(),
89 language_aliases: BTreeMap::new(),
90 tools: BTreeMap::new(),
91 values_withheld: false,
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
98#[serde(rename_all = "kebab-case")]
99pub enum NormalizeLanguage {
100 #[default]
102 Linguist,
103 Exact,
105}
106
107#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
109#[serde(rename_all = "kebab-case")]
110pub enum OnError {
111 #[default]
113 Fail,
114 Skip,
116 Warn,
118}
119
120#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
122#[serde(rename_all = "kebab-case")]
123pub enum OnMissing {
124 #[default]
126 Ignore,
127 Fail,
129 FailFast,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
135#[serde(rename_all = "kebab-case")]
136pub struct LanguageToolConfig {
137 #[serde(default = "default_true")]
141 pub enabled: bool,
142
143 #[serde(default)]
145 pub lint: Vec<String>,
146
147 #[serde(default)]
149 pub format: Vec<String>,
150
151 #[serde(default)]
153 pub on_error: Option<OnError>,
154}
155
156impl Default for LanguageToolConfig {
157 fn default() -> Self {
158 Self {
159 enabled: true,
160 lint: Vec::new(),
161 format: Vec::new(),
162 on_error: None,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
171#[serde(rename_all = "kebab-case")]
172pub struct ToolDefinition {
173 pub command: Vec<String>,
175
176 #[serde(default = "default_true")]
178 pub stdin: bool,
179
180 #[serde(default = "default_true")]
182 pub stdout: bool,
183
184 #[serde(default)]
186 pub lint_args: Vec<String>,
187
188 #[serde(default)]
190 pub format_args: Vec<String>,
191}
192
193fn default_true() -> bool {
194 true
195}
196
197impl Default for ToolDefinition {
198 fn default() -> Self {
199 Self {
200 command: Vec::new(),
201 stdin: true,
202 stdout: true,
203 lint_args: Vec::new(),
204 format_args: Vec::new(),
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_default_config() {
215 let config = CodeBlockToolsConfig::default();
216 assert!(!config.enabled);
217 assert_eq!(config.normalize_language, NormalizeLanguage::Linguist);
218 assert_eq!(config.on_error, OnError::Fail);
219 assert_eq!(config.on_missing_language_definition, OnMissing::Ignore);
220 assert_eq!(config.on_missing_tool_binary, OnMissing::Ignore);
221 assert_eq!(config.timeout, 30_000);
222 assert!(config.languages.is_empty());
223 assert!(config.language_aliases.is_empty());
224 assert!(config.tools.is_empty());
225 }
226
227 #[test]
228 fn test_deserialize_config() {
229 let toml = r#"
230enabled = true
231normalize-language = "exact"
232on-error = "skip"
233timeout = 60000
234
235[languages.python]
236lint = ["ruff:check"]
237format = ["ruff:format"]
238
239[languages.json]
240format = ["prettier"]
241on-error = "warn"
242
243[language-aliases]
244py = "python"
245bash = "shell"
246
247[tools.custom-tool]
248command = ["my-tool", "--format"]
249stdin = true
250stdout = true
251"#;
252
253 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
254
255 assert!(config.enabled);
256 assert_eq!(config.normalize_language, NormalizeLanguage::Exact);
257 assert_eq!(config.on_error, OnError::Skip);
258 assert_eq!(config.timeout, 60_000);
259
260 let python = config.languages.get("python").expect("Missing python config");
261 assert_eq!(python.lint, vec!["ruff:check"]);
262 assert_eq!(python.format, vec!["ruff:format"]);
263 assert_eq!(python.on_error, None);
264
265 let json = config.languages.get("json").expect("Missing json config");
266 assert!(json.lint.is_empty());
267 assert_eq!(json.format, vec!["prettier"]);
268 assert_eq!(json.on_error, Some(OnError::Warn));
269
270 assert_eq!(config.language_aliases.get("py").map(String::as_str), Some("python"));
271 assert_eq!(config.language_aliases.get("bash").map(String::as_str), Some("shell"));
272
273 let tool = config.tools.get("custom-tool").expect("Missing custom tool");
274 assert_eq!(tool.command, vec!["my-tool", "--format"]);
275 assert!(tool.stdin);
276 assert!(tool.stdout);
277 }
278
279 #[test]
280 fn test_serialize_config() {
281 let mut config = CodeBlockToolsConfig {
282 enabled: true,
283 ..Default::default()
284 };
285 config.languages.insert(
286 "rust".to_string(),
287 LanguageToolConfig {
288 format: vec!["rustfmt".to_string()],
289 ..Default::default()
290 },
291 );
292
293 let toml = toml::to_string_pretty(&config).expect("Failed to serialize");
294 assert!(toml.contains("enabled = true"));
295 assert!(toml.contains("[languages.rust]"));
296 assert!(toml.contains("rustfmt"));
297 }
298
299 #[test]
300 fn test_on_missing_options() {
301 let toml = r#"
302enabled = true
303on-missing-language-definition = "fail"
304on-missing-tool-binary = "fail-fast"
305"#;
306
307 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
308
309 assert_eq!(config.on_missing_language_definition, OnMissing::Fail);
310 assert_eq!(config.on_missing_tool_binary, OnMissing::FailFast);
311 }
312
313 #[test]
314 fn test_on_missing_default_ignore() {
315 let toml = r#"
316enabled = true
317"#;
318
319 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
320
321 assert_eq!(config.on_missing_language_definition, OnMissing::Ignore);
323 assert_eq!(config.on_missing_tool_binary, OnMissing::Ignore);
324 }
325
326 #[test]
327 fn test_on_missing_all_variants() {
328 for (input, expected) in [
330 ("ignore", OnMissing::Ignore),
331 ("fail", OnMissing::Fail),
332 ("fail-fast", OnMissing::FailFast),
333 ] {
334 let toml = format!(
335 r#"
336enabled = true
337on-missing-language-definition = "{input}"
338"#
339 );
340 let config: CodeBlockToolsConfig = toml::from_str(&toml).expect("Failed to parse TOML");
341 assert_eq!(
342 config.on_missing_language_definition, expected,
343 "Failed for variant: {input}"
344 );
345 }
346 }
347
348 #[test]
349 fn test_language_config_enabled_defaults_to_true() {
350 let toml = r#"
352lint = ["ruff:check"]
353"#;
354 let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
355 assert!(config.enabled);
356 assert_eq!(config.lint, vec!["ruff:check"]);
357 assert!(config.format.is_empty());
358 }
359
360 #[test]
361 fn test_language_config_enabled_false() {
362 let toml = r#"
364enabled = false
365"#;
366 let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
367 assert!(!config.enabled);
368 assert!(config.lint.is_empty());
369 assert!(config.format.is_empty());
370 }
371
372 #[test]
373 fn test_language_config_enabled_false_with_tools() {
374 let toml = r#"
376enabled = false
377lint = ["ruff:check"]
378format = ["ruff:format"]
379"#;
380 let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
381 assert!(!config.enabled);
382 assert_eq!(config.lint, vec!["ruff:check"]);
383 assert_eq!(config.format, vec!["ruff:format"]);
384 }
385
386 #[test]
387 fn test_language_config_enabled_in_full_config() {
388 let toml = r#"
390enabled = true
391on-missing-language-definition = "fail"
392
393[languages.python]
394lint = ["ruff:check"]
395
396[languages.plaintext]
397enabled = false
398"#;
399 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
400
401 let python = config.languages.get("python").expect("Missing python config");
402 assert!(python.enabled);
403 assert_eq!(python.lint, vec!["ruff:check"]);
404
405 let plaintext = config.languages.get("plaintext").expect("Missing plaintext config");
406 assert!(!plaintext.enabled);
407 assert!(plaintext.lint.is_empty());
408 }
409
410 #[test]
411 fn test_language_config_default_trait() {
412 let config = LanguageToolConfig::default();
413 assert!(config.enabled);
414 assert!(config.lint.is_empty());
415 assert!(config.format.is_empty());
416 assert!(config.on_error.is_none());
417 }
418
419 #[test]
420 fn test_language_config_serialize_enabled_false() {
421 let config = LanguageToolConfig {
422 enabled: false,
423 ..Default::default()
424 };
425 let toml = toml::to_string_pretty(&config).expect("Failed to serialize");
426 assert!(toml.contains("enabled = false"));
427 }
428}