use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub struct CodeBlockToolsConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub normalize_language: NormalizeLanguage,
#[serde(default)]
pub on_error: OnError,
#[serde(default)]
pub on_missing_language_definition: OnMissing,
#[serde(default)]
pub on_missing_tool_binary: OnMissing,
#[serde(default = "default_timeout")]
#[schemars(schema_with = "schema_timeout")]
pub timeout: u64,
#[serde(default)]
pub languages: HashMap<String, LanguageToolConfig>,
#[serde(default)]
pub language_aliases: HashMap<String, String>,
#[serde(default)]
pub tools: HashMap<String, ToolDefinition>,
}
fn default_timeout() -> u64 {
30_000
}
fn schema_timeout(_gen: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"type": "integer",
"minimum": 0
})
}
impl Default for CodeBlockToolsConfig {
fn default() -> Self {
Self {
enabled: false,
normalize_language: NormalizeLanguage::default(),
on_error: OnError::default(),
on_missing_language_definition: OnMissing::default(),
on_missing_tool_binary: OnMissing::default(),
timeout: default_timeout(),
languages: HashMap::new(),
language_aliases: HashMap::new(),
tools: HashMap::new(),
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum NormalizeLanguage {
#[default]
Linguist,
Exact,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum OnError {
#[default]
Fail,
Skip,
Warn,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum OnMissing {
#[default]
Ignore,
Fail,
FailFast,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageToolConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub lint: Vec<String>,
#[serde(default)]
pub format: Vec<String>,
#[serde(default)]
pub on_error: Option<OnError>,
}
impl Default for LanguageToolConfig {
fn default() -> Self {
Self {
enabled: true,
lint: Vec::new(),
format: Vec::new(),
on_error: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, schemars::JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub struct ToolDefinition {
pub command: Vec<String>,
#[serde(default = "default_true")]
pub stdin: bool,
#[serde(default = "default_true")]
pub stdout: bool,
#[serde(default)]
pub lint_args: Vec<String>,
#[serde(default)]
pub format_args: Vec<String>,
}
fn default_true() -> bool {
true
}
impl Default for ToolDefinition {
fn default() -> Self {
Self {
command: Vec::new(),
stdin: true,
stdout: true,
lint_args: Vec::new(),
format_args: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = CodeBlockToolsConfig::default();
assert!(!config.enabled);
assert_eq!(config.normalize_language, NormalizeLanguage::Linguist);
assert_eq!(config.on_error, OnError::Fail);
assert_eq!(config.on_missing_language_definition, OnMissing::Ignore);
assert_eq!(config.on_missing_tool_binary, OnMissing::Ignore);
assert_eq!(config.timeout, 30_000);
assert!(config.languages.is_empty());
assert!(config.language_aliases.is_empty());
assert!(config.tools.is_empty());
}
#[test]
fn test_deserialize_config() {
let toml = r#"
enabled = true
normalize-language = "exact"
on-error = "skip"
timeout = 60000
[languages.python]
lint = ["ruff:check"]
format = ["ruff:format"]
[languages.json]
format = ["prettier"]
on-error = "warn"
[language-aliases]
py = "python"
bash = "shell"
[tools.custom-tool]
command = ["my-tool", "--format"]
stdin = true
stdout = true
"#;
let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
assert!(config.enabled);
assert_eq!(config.normalize_language, NormalizeLanguage::Exact);
assert_eq!(config.on_error, OnError::Skip);
assert_eq!(config.timeout, 60_000);
let python = config.languages.get("python").expect("Missing python config");
assert_eq!(python.lint, vec!["ruff:check"]);
assert_eq!(python.format, vec!["ruff:format"]);
assert_eq!(python.on_error, None);
let json = config.languages.get("json").expect("Missing json config");
assert!(json.lint.is_empty());
assert_eq!(json.format, vec!["prettier"]);
assert_eq!(json.on_error, Some(OnError::Warn));
assert_eq!(config.language_aliases.get("py").map(String::as_str), Some("python"));
assert_eq!(config.language_aliases.get("bash").map(String::as_str), Some("shell"));
let tool = config.tools.get("custom-tool").expect("Missing custom tool");
assert_eq!(tool.command, vec!["my-tool", "--format"]);
assert!(tool.stdin);
assert!(tool.stdout);
}
#[test]
fn test_serialize_config() {
let mut config = CodeBlockToolsConfig {
enabled: true,
..Default::default()
};
config.languages.insert(
"rust".to_string(),
LanguageToolConfig {
format: vec!["rustfmt".to_string()],
..Default::default()
},
);
let toml = toml::to_string_pretty(&config).expect("Failed to serialize");
assert!(toml.contains("enabled = true"));
assert!(toml.contains("[languages.rust]"));
assert!(toml.contains("rustfmt"));
}
#[test]
fn test_on_missing_options() {
let toml = r#"
enabled = true
on-missing-language-definition = "fail"
on-missing-tool-binary = "fail-fast"
"#;
let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
assert_eq!(config.on_missing_language_definition, OnMissing::Fail);
assert_eq!(config.on_missing_tool_binary, OnMissing::FailFast);
}
#[test]
fn test_on_missing_default_ignore() {
let toml = r#"
enabled = true
"#;
let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
assert_eq!(config.on_missing_language_definition, OnMissing::Ignore);
assert_eq!(config.on_missing_tool_binary, OnMissing::Ignore);
}
#[test]
fn test_on_missing_all_variants() {
for (input, expected) in [
("ignore", OnMissing::Ignore),
("fail", OnMissing::Fail),
("fail-fast", OnMissing::FailFast),
] {
let toml = format!(
r#"
enabled = true
on-missing-language-definition = "{input}"
"#
);
let config: CodeBlockToolsConfig = toml::from_str(&toml).expect("Failed to parse TOML");
assert_eq!(
config.on_missing_language_definition, expected,
"Failed for variant: {input}"
);
}
}
#[test]
fn test_language_config_enabled_defaults_to_true() {
let toml = r#"
lint = ["ruff:check"]
"#;
let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
assert!(config.enabled);
assert_eq!(config.lint, vec!["ruff:check"]);
assert!(config.format.is_empty());
}
#[test]
fn test_language_config_enabled_false() {
let toml = r#"
enabled = false
"#;
let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
assert!(!config.enabled);
assert!(config.lint.is_empty());
assert!(config.format.is_empty());
}
#[test]
fn test_language_config_enabled_false_with_tools() {
let toml = r#"
enabled = false
lint = ["ruff:check"]
format = ["ruff:format"]
"#;
let config: LanguageToolConfig = toml::from_str(toml).expect("Failed to parse TOML");
assert!(!config.enabled);
assert_eq!(config.lint, vec!["ruff:check"]);
assert_eq!(config.format, vec!["ruff:format"]);
}
#[test]
fn test_language_config_enabled_in_full_config() {
let toml = r#"
enabled = true
on-missing-language-definition = "fail"
[languages.python]
lint = ["ruff:check"]
[languages.plaintext]
enabled = false
"#;
let config: CodeBlockToolsConfig = toml::from_str(toml).expect("Failed to parse TOML");
let python = config.languages.get("python").expect("Missing python config");
assert!(python.enabled);
assert_eq!(python.lint, vec!["ruff:check"]);
let plaintext = config.languages.get("plaintext").expect("Missing plaintext config");
assert!(!plaintext.enabled);
assert!(plaintext.lint.is_empty());
}
#[test]
fn test_language_config_default_trait() {
let config = LanguageToolConfig::default();
assert!(config.enabled);
assert!(config.lint.is_empty());
assert!(config.format.is_empty());
assert!(config.on_error.is_none());
}
#[test]
fn test_language_config_serialize_enabled_false() {
let config = LanguageToolConfig {
enabled: false,
..Default::default()
};
let toml = toml::to_string_pretty(&config).expect("Failed to serialize");
assert!(toml.contains("enabled = false"));
}
}