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, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
122#[serde(rename_all = "kebab-case")]
123pub struct LanguageToolConfig {
124 #[serde(default)]
126 pub lint: Vec<String>,
127
128 #[serde(default)]
130 pub format: Vec<String>,
131
132 #[serde(default)]
134 pub on_error: Option<OnError>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
141#[serde(rename_all = "kebab-case")]
142pub struct ToolDefinition {
143 pub command: Vec<String>,
145
146 #[serde(default = "default_true")]
148 pub stdin: bool,
149
150 #[serde(default = "default_true")]
152 pub stdout: bool,
153
154 #[serde(default)]
156 pub lint_args: Vec<String>,
157
158 #[serde(default)]
160 pub format_args: Vec<String>,
161}
162
163fn default_true() -> bool {
164 true
165}
166
167impl Default for ToolDefinition {
168 fn default() -> Self {
169 Self {
170 command: Vec::new(),
171 stdin: true,
172 stdout: true,
173 lint_args: Vec::new(),
174 format_args: Vec::new(),
175 }
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn test_default_config() {
185 let config = CodeBlockToolsConfig::default();
186 assert!(!config.enabled);
187 assert_eq!(config.normalize_language, NormalizeLanguage::Linguist);
188 assert_eq!(config.on_error, OnError::Fail);
189 assert_eq!(config.on_missing_language_definition, OnMissing::Ignore);
190 assert_eq!(config.on_missing_tool_binary, OnMissing::Ignore);
191 assert_eq!(config.timeout, 30_000);
192 assert!(config.languages.is_empty());
193 assert!(config.language_aliases.is_empty());
194 assert!(config.tools.is_empty());
195 }
196
197 #[test]
198 fn test_deserialize_config() {
199 let toml = r#"
200enabled = true
201normalize-language = "exact"
202on-error = "skip"
203timeout = 60000
204
205[languages.python]
206lint = ["ruff:check"]
207format = ["ruff:format"]
208
209[languages.json]
210format = ["prettier"]
211on-error = "warn"
212
213[language-aliases]
214py = "python"
215bash = "shell"
216
217[tools.custom-tool]
218command = ["my-tool", "--format"]
219stdin = true
220stdout = true
221"#;
222
223 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
224
225 assert!(config.enabled);
226 assert_eq!(config.normalize_language, NormalizeLanguage::Exact);
227 assert_eq!(config.on_error, OnError::Skip);
228 assert_eq!(config.timeout, 60_000);
229
230 let python = config.languages.get("python").expect("Missing python config");
231 assert_eq!(python.lint, vec!["ruff:check"]);
232 assert_eq!(python.format, vec!["ruff:format"]);
233 assert_eq!(python.on_error, None);
234
235 let json = config.languages.get("json").expect("Missing json config");
236 assert!(json.lint.is_empty());
237 assert_eq!(json.format, vec!["prettier"]);
238 assert_eq!(json.on_error, Some(OnError::Warn));
239
240 assert_eq!(config.language_aliases.get("py").map(String::as_str), Some("python"));
241 assert_eq!(config.language_aliases.get("bash").map(String::as_str), Some("shell"));
242
243 let tool = config.tools.get("custom-tool").expect("Missing custom tool");
244 assert_eq!(tool.command, vec!["my-tool", "--format"]);
245 assert!(tool.stdin);
246 assert!(tool.stdout);
247 }
248
249 #[test]
250 fn test_serialize_config() {
251 let mut config = CodeBlockToolsConfig {
252 enabled: true,
253 ..Default::default()
254 };
255 config.languages.insert(
256 "rust".to_string(),
257 LanguageToolConfig {
258 lint: vec![],
259 format: vec!["rustfmt".to_string()],
260 on_error: None,
261 },
262 );
263
264 let toml = toml::to_string_pretty(&config).expect("Failed to serialize");
265 assert!(toml.contains("enabled = true"));
266 assert!(toml.contains("[languages.rust]"));
267 assert!(toml.contains("rustfmt"));
268 }
269
270 #[test]
271 fn test_on_missing_options() {
272 let toml = r#"
273enabled = true
274on-missing-language-definition = "fail"
275on-missing-tool-binary = "fail-fast"
276"#;
277
278 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
279
280 assert_eq!(config.on_missing_language_definition, OnMissing::Fail);
281 assert_eq!(config.on_missing_tool_binary, OnMissing::FailFast);
282 }
283
284 #[test]
285 fn test_on_missing_default_ignore() {
286 let toml = r#"
287enabled = true
288"#;
289
290 let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
291
292 assert_eq!(config.on_missing_language_definition, OnMissing::Ignore);
294 assert_eq!(config.on_missing_tool_binary, OnMissing::Ignore);
295 }
296
297 #[test]
298 fn test_on_missing_all_variants() {
299 for (input, expected) in [
301 ("ignore", OnMissing::Ignore),
302 ("fail", OnMissing::Fail),
303 ("fail-fast", OnMissing::FailFast),
304 ] {
305 let toml = format!(
306 r#"
307enabled = true
308on-missing-language-definition = "{input}"
309"#
310 );
311 let config: CodeBlockToolsConfig = toml::from_str(&toml).expect("Failed to parse TOML");
312 assert_eq!(
313 config.on_missing_language_definition, expected,
314 "Failed for variant: {input}"
315 );
316 }
317 }
318}