Skip to main content

rumdl_lib/code_block_tools/
config.rs

1//! Configuration types for code block tools.
2//!
3//! This module defines the configuration schema for per-language code block
4//! linting and formatting using external tools.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Master configuration for code block tools.
10///
11/// This is disabled by default for safety - users must explicitly enable it.
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
13#[serde(rename_all = "kebab-case")]
14pub struct CodeBlockToolsConfig {
15    /// Master switch (default: false)
16    #[serde(default)]
17    pub enabled: bool,
18
19    /// Language normalization strategy
20    #[serde(default)]
21    pub normalize_language: NormalizeLanguage,
22
23    /// Global error handling strategy
24    #[serde(default)]
25    pub on_error: OnError,
26
27    /// Timeout per tool execution in milliseconds (default: 30000)
28    #[serde(default = "default_timeout")]
29    pub timeout: u64,
30
31    /// Per-language tool configuration
32    #[serde(default)]
33    pub languages: HashMap<String, LanguageToolConfig>,
34
35    /// User-defined language aliases (override built-in resolution)
36    /// Example: { "py": "python", "bash": "shell" }
37    #[serde(default)]
38    pub language_aliases: HashMap<String, String>,
39
40    /// Custom tool definitions (override built-ins)
41    #[serde(default)]
42    pub tools: HashMap<String, ToolDefinition>,
43}
44
45fn default_timeout() -> u64 {
46    30_000
47}
48
49impl Default for CodeBlockToolsConfig {
50    fn default() -> Self {
51        Self {
52            enabled: false,
53            normalize_language: NormalizeLanguage::default(),
54            on_error: OnError::default(),
55            timeout: default_timeout(),
56            languages: HashMap::new(),
57            language_aliases: HashMap::new(),
58            tools: HashMap::new(),
59        }
60    }
61}
62
63/// Language normalization strategy.
64#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
65#[serde(rename_all = "kebab-case")]
66pub enum NormalizeLanguage {
67    /// Resolve language aliases using GitHub Linguist data (e.g., "py" -> "python")
68    #[default]
69    Linguist,
70    /// Use the language tag exactly as written in the code block
71    Exact,
72}
73
74/// Error handling strategy for tool execution failures.
75#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
76#[serde(rename_all = "kebab-case")]
77pub enum OnError {
78    /// Fail the lint/format operation (propagate error)
79    #[default]
80    Fail,
81    /// Skip the code block and continue processing
82    Skip,
83    /// Log a warning but continue processing
84    Warn,
85}
86
87/// Per-language tool configuration.
88#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
89#[serde(rename_all = "kebab-case")]
90pub struct LanguageToolConfig {
91    /// Tools to run in lint mode (rumdl check)
92    #[serde(default)]
93    pub lint: Vec<String>,
94
95    /// Tools to run in format mode (rumdl check --fix / rumdl fmt)
96    #[serde(default)]
97    pub format: Vec<String>,
98
99    /// Override global on-error setting for this language
100    #[serde(default)]
101    pub on_error: Option<OnError>,
102}
103
104/// Definition of an external tool.
105///
106/// This describes how to invoke a tool and how it communicates.
107#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
108#[serde(rename_all = "kebab-case")]
109pub struct ToolDefinition {
110    /// Command to run (first element is the binary, rest are arguments)
111    pub command: Vec<String>,
112
113    /// Whether the tool reads from stdin (default: true)
114    #[serde(default = "default_true")]
115    pub stdin: bool,
116
117    /// Whether the tool writes to stdout (default: true)
118    #[serde(default = "default_true")]
119    pub stdout: bool,
120
121    /// Additional arguments for lint mode (appended to command)
122    #[serde(default)]
123    pub lint_args: Vec<String>,
124
125    /// Additional arguments for format mode (appended to command)
126    #[serde(default)]
127    pub format_args: Vec<String>,
128}
129
130fn default_true() -> bool {
131    true
132}
133
134impl Default for ToolDefinition {
135    fn default() -> Self {
136        Self {
137            command: Vec::new(),
138            stdin: true,
139            stdout: true,
140            lint_args: Vec::new(),
141            format_args: Vec::new(),
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_default_config() {
152        let config = CodeBlockToolsConfig::default();
153        assert!(!config.enabled);
154        assert_eq!(config.normalize_language, NormalizeLanguage::Linguist);
155        assert_eq!(config.on_error, OnError::Fail);
156        assert_eq!(config.timeout, 30_000);
157        assert!(config.languages.is_empty());
158        assert!(config.language_aliases.is_empty());
159        assert!(config.tools.is_empty());
160    }
161
162    #[test]
163    fn test_deserialize_config() {
164        let toml = r#"
165enabled = true
166normalize-language = "exact"
167on-error = "skip"
168timeout = 60000
169
170[languages.python]
171lint = ["ruff:check"]
172format = ["ruff:format"]
173
174[languages.json]
175format = ["prettier"]
176on-error = "warn"
177
178[language-aliases]
179py = "python"
180bash = "shell"
181
182[tools.custom-tool]
183command = ["my-tool", "--format"]
184stdin = true
185stdout = true
186"#;
187
188        let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
189
190        assert!(config.enabled);
191        assert_eq!(config.normalize_language, NormalizeLanguage::Exact);
192        assert_eq!(config.on_error, OnError::Skip);
193        assert_eq!(config.timeout, 60_000);
194
195        let python = config.languages.get("python").expect("Missing python config");
196        assert_eq!(python.lint, vec!["ruff:check"]);
197        assert_eq!(python.format, vec!["ruff:format"]);
198        assert_eq!(python.on_error, None);
199
200        let json = config.languages.get("json").expect("Missing json config");
201        assert!(json.lint.is_empty());
202        assert_eq!(json.format, vec!["prettier"]);
203        assert_eq!(json.on_error, Some(OnError::Warn));
204
205        assert_eq!(config.language_aliases.get("py").map(String::as_str), Some("python"));
206        assert_eq!(config.language_aliases.get("bash").map(String::as_str), Some("shell"));
207
208        let tool = config.tools.get("custom-tool").expect("Missing custom tool");
209        assert_eq!(tool.command, vec!["my-tool", "--format"]);
210        assert!(tool.stdin);
211        assert!(tool.stdout);
212    }
213
214    #[test]
215    fn test_serialize_config() {
216        let mut config = CodeBlockToolsConfig {
217            enabled: true,
218            ..Default::default()
219        };
220        config.languages.insert(
221            "rust".to_string(),
222            LanguageToolConfig {
223                lint: vec![],
224                format: vec!["rustfmt".to_string()],
225                on_error: None,
226            },
227        );
228
229        let toml = toml::to_string_pretty(&config).expect("Failed to serialize");
230        assert!(toml.contains("enabled = true"));
231        assert!(toml.contains("[languages.rust]"));
232        assert!(toml.contains("rustfmt"));
233    }
234}