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 = "default_on_missing_tool_binary")]
36 pub on_missing_tool_binary: OnMissing,
37
38 #[serde(default = "default_timeout")]
40 #[schemars(schema_with = "schema_timeout")]
41 pub timeout: u64,
42
43 #[serde(default)]
45 pub languages: BTreeMap<String, LanguageToolConfig>,
46
47 #[serde(default)]
50 pub language_aliases: BTreeMap<String, String>,
51
52 #[serde(default)]
54 pub tools: BTreeMap<String, ToolDefinition>,
55
56 #[serde(skip)]
65 #[schemars(skip)]
66 pub values_withheld: bool,
67}
68
69fn default_timeout() -> u64 {
70 30_000
71}
72
73fn default_on_missing_tool_binary() -> OnMissing {
74 OnMissing::Warn
75}
76
77fn schema_timeout(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
79 schemars::json_schema!({
80 "type": "integer",
81 "minimum": 0
82 })
83}
84
85impl Default for CodeBlockToolsConfig {
86 fn default() -> Self {
87 Self {
88 enabled: false,
89 normalize_language: NormalizeLanguage::default(),
90 on_error: OnError::default(),
91 on_missing_language_definition: OnMissing::default(),
92 on_missing_tool_binary: default_on_missing_tool_binary(),
93 timeout: default_timeout(),
94 languages: BTreeMap::new(),
95 language_aliases: BTreeMap::new(),
96 tools: BTreeMap::new(),
97 values_withheld: false,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
104#[serde(rename_all = "kebab-case")]
105pub enum NormalizeLanguage {
106 #[default]
108 Linguist,
109 Exact,
111}
112
113#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
115#[serde(rename_all = "kebab-case")]
116pub enum OnError {
117 #[default]
119 Fail,
120 Skip,
122 Warn,
124}
125
126#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
128#[serde(rename_all = "kebab-case")]
129pub enum OnMissing {
130 #[default]
132 Ignore,
133 Warn,
141 Fail,
143 FailFast,
145}
146
147impl OnMissing {
148 pub fn skips_the_block(self) -> bool {
155 matches!(self, OnMissing::Ignore | OnMissing::Warn)
156 }
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
161#[serde(rename_all = "kebab-case")]
162pub struct LanguageToolConfig {
163 #[serde(default = "default_true")]
167 pub enabled: bool,
168
169 #[serde(default)]
171 pub lint: Vec<String>,
172
173 #[serde(default)]
175 pub format: Vec<String>,
176
177 #[serde(default)]
179 pub on_error: Option<OnError>,
180}
181
182impl Default for LanguageToolConfig {
183 fn default() -> Self {
184 Self {
185 enabled: true,
186 lint: Vec::new(),
187 format: Vec::new(),
188 on_error: None,
189 }
190 }
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
197#[serde(rename_all = "kebab-case")]
198pub struct ToolDefinition {
199 pub command: Vec<String>,
201
202 #[serde(default = "default_true")]
204 pub stdin: bool,
205
206 #[serde(default = "default_true")]
208 pub stdout: bool,
209
210 #[serde(default)]
212 pub lint_args: Vec<String>,
213
214 #[serde(default)]
216 pub format_args: Vec<String>,
217}
218
219fn default_true() -> bool {
220 true
221}
222
223impl Default for ToolDefinition {
224 fn default() -> Self {
225 Self {
226 command: Vec::new(),
227 stdin: true,
228 stdout: true,
229 lint_args: Vec::new(),
230 format_args: Vec::new(),
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_default_config() {
241 let config = CodeBlockToolsConfig::default();
242 assert!(!config.enabled);
243 assert_eq!(config.normalize_language, NormalizeLanguage::Linguist);
244 assert_eq!(config.on_error, OnError::Fail);
245 assert_eq!(config.on_missing_language_definition, OnMissing::Ignore);
246 assert_eq!(config.on_missing_tool_binary, OnMissing::Warn);
247 assert_eq!(config.timeout, 30_000);
248 assert!(config.languages.is_empty());
249 assert!(config.language_aliases.is_empty());
250 assert!(config.tools.is_empty());
251 }
252
253 #[test]
254 fn test_deserialize_config() {
255 let toml = r#"
256enabled = true
257normalize-language = "exact"
258on-error = "skip"
259timeout = 60000
260
261[languages.python]
262lint = ["ruff:check"]
263format = ["ruff:format"]
264
265[languages.json]
266format = ["prettier"]
267on-error = "warn"
268
269[language-aliases]
270py = "python"
271bash = "shell"
272
273[tools.custom-tool]
274command = ["my-tool", "--format"]
275stdin = true
276stdout = true
277"#;
278
279 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
280
281 assert!(config.enabled);
282 assert_eq!(config.normalize_language, NormalizeLanguage::Exact);
283 assert_eq!(config.on_error, OnError::Skip);
284 assert_eq!(config.timeout, 60_000);
285
286 let python = config.languages.get("python").expect("Missing python config");
287 assert_eq!(python.lint, vec!["ruff:check"]);
288 assert_eq!(python.format, vec!["ruff:format"]);
289 assert_eq!(python.on_error, None);
290
291 let json = config.languages.get("json").expect("Missing json config");
292 assert!(json.lint.is_empty());
293 assert_eq!(json.format, vec!["prettier"]);
294 assert_eq!(json.on_error, Some(OnError::Warn));
295
296 assert_eq!(config.language_aliases.get("py").map(String::as_str), Some("python"));
297 assert_eq!(config.language_aliases.get("bash").map(String::as_str), Some("shell"));
298
299 let tool = config.tools.get("custom-tool").expect("Missing custom tool");
300 assert_eq!(tool.command, vec!["my-tool", "--format"]);
301 assert!(tool.stdin);
302 assert!(tool.stdout);
303 }
304
305 #[test]
306 fn test_serialize_config() {
307 let mut config = CodeBlockToolsConfig {
308 enabled: true,
309 ..Default::default()
310 };
311 config.languages.insert(
312 "rust".to_string(),
313 LanguageToolConfig {
314 format: vec!["rustfmt".to_string()],
315 ..Default::default()
316 },
317 );
318
319 let toml = toml::to_string_pretty(&config).expect("Failed to serialize");
320 assert!(toml.contains("enabled = true"));
321 assert!(toml.contains("[languages.rust]"));
322 assert!(toml.contains("rustfmt"));
323 }
324
325 #[test]
326 fn test_on_missing_options() {
327 let toml = r#"
328enabled = true
329on-missing-language-definition = "fail"
330on-missing-tool-binary = "fail-fast"
331"#;
332
333 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
334
335 assert_eq!(config.on_missing_language_definition, OnMissing::Fail);
336 assert_eq!(config.on_missing_tool_binary, OnMissing::FailFast);
337 }
338
339 #[test]
340 fn test_on_missing_defaults() {
341 let toml = r#"
342enabled = true
343"#;
344
345 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
346
347 assert_eq!(config.on_missing_language_definition, OnMissing::Ignore);
350 assert_eq!(config.on_missing_tool_binary, OnMissing::Warn);
353 }
354
355 #[test]
356 fn test_on_missing_all_variants() {
357 for (input, expected) in [
359 ("ignore", OnMissing::Ignore),
360 ("warn", OnMissing::Warn),
361 ("fail", OnMissing::Fail),
362 ("fail-fast", OnMissing::FailFast),
363 ] {
364 let toml = format!(
365 r#"
366enabled = true
367on-missing-language-definition = "{input}"
368"#
369 );
370 let config: CodeBlockToolsConfig = toml::from_str(&toml).expect("Failed to parse TOML");
371 assert_eq!(
372 config.on_missing_language_definition, expected,
373 "Failed for variant: {input}"
374 );
375 }
376 }
377
378 #[test]
379 fn test_language_config_enabled_defaults_to_true() {
380 let toml = r#"
382lint = ["ruff:check"]
383"#;
384 let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
385 assert!(config.enabled);
386 assert_eq!(config.lint, vec!["ruff:check"]);
387 assert!(config.format.is_empty());
388 }
389
390 #[test]
391 fn test_language_config_enabled_false() {
392 let toml = r#"
394enabled = false
395"#;
396 let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
397 assert!(!config.enabled);
398 assert!(config.lint.is_empty());
399 assert!(config.format.is_empty());
400 }
401
402 #[test]
403 fn test_language_config_enabled_false_with_tools() {
404 let toml = r#"
406enabled = false
407lint = ["ruff:check"]
408format = ["ruff:format"]
409"#;
410 let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
411 assert!(!config.enabled);
412 assert_eq!(config.lint, vec!["ruff:check"]);
413 assert_eq!(config.format, vec!["ruff:format"]);
414 }
415
416 #[test]
417 fn test_language_config_enabled_in_full_config() {
418 let toml = r#"
420enabled = true
421on-missing-language-definition = "fail"
422
423[languages.python]
424lint = ["ruff:check"]
425
426[languages.plaintext]
427enabled = false
428"#;
429 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
430
431 let python = config.languages.get("python").expect("Missing python config");
432 assert!(python.enabled);
433 assert_eq!(python.lint, vec!["ruff:check"]);
434
435 let plaintext = config.languages.get("plaintext").expect("Missing plaintext config");
436 assert!(!plaintext.enabled);
437 assert!(plaintext.lint.is_empty());
438 }
439
440 #[test]
441 fn test_language_config_default_trait() {
442 let config = LanguageToolConfig::default();
443 assert!(config.enabled);
444 assert!(config.lint.is_empty());
445 assert!(config.format.is_empty());
446 assert!(config.on_error.is_none());
447 }
448
449 #[test]
450 fn test_language_config_serialize_enabled_false() {
451 let config = LanguageToolConfig {
452 enabled: false,
453 ..Default::default()
454 };
455 let toml = toml::to_string_pretty(&config).expect("Failed to serialize");
456 assert!(toml.contains("enabled = false"));
457 }
458}