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