use serde::{Deserialize, Serialize};
pub const DEFAULT_RUST_DEV_TOOLS: &[&str] = &[
"cargo-edit",
"cargo-sort",
"cargo-machete",
"cargo-deny",
"cargo-llvm-cov",
];
const DEFAULT_PYTHON_PM: &str = "uv";
const DEFAULT_NODE_PM: &str = "pnpm";
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ToolsConfig {
#[serde(default)]
pub python_package_manager: Option<String>,
#[serde(default)]
pub node_package_manager: Option<String>,
#[serde(default)]
pub rust_dev_tools: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
pub struct LangContext<'a> {
pub tools: &'a ToolsConfig,
pub run_wrapper: Option<&'a str>,
pub extra_lint_paths: &'a [String],
pub project_file: Option<&'a str>,
}
impl<'a> LangContext<'a> {
pub fn default(tools: &'a ToolsConfig) -> Self {
Self {
tools,
run_wrapper: None,
extra_lint_paths: &[],
project_file: None,
}
}
}
pub fn wrap_command(cmd: String, wrapper: Option<&str>) -> String {
match wrapper {
Some(w) => format!("{w} {cmd}"),
None => cmd,
}
}
pub fn append_paths(cmd: String, paths: &[String]) -> String {
if paths.is_empty() {
cmd
} else {
format!("{} {}", cmd, paths.join(" "))
}
}
pub fn require_tool(tool: &str) -> String {
format!("command -v {tool} >/dev/null 2>&1")
}
pub fn require_tools(tools: &[&str]) -> String {
tools.iter().map(|t| require_tool(t)).collect::<Vec<_>>().join(" && ")
}
impl ToolsConfig {
pub fn python_pm(&self) -> &str {
self.python_package_manager.as_deref().unwrap_or(DEFAULT_PYTHON_PM)
}
pub fn node_pm(&self) -> &str {
self.node_package_manager.as_deref().unwrap_or(DEFAULT_NODE_PM)
}
pub fn rust_tools(&self) -> Vec<&str> {
match self.rust_dev_tools.as_deref() {
Some(list) => list.iter().map(String::as_str).collect(),
None => DEFAULT_RUST_DEV_TOOLS.to_vec(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_match_documented_values() {
let cfg = ToolsConfig::default();
assert_eq!(cfg.python_pm(), "uv");
assert_eq!(cfg.node_pm(), "pnpm");
assert_eq!(
cfg.rust_tools(),
vec![
"cargo-edit",
"cargo-sort",
"cargo-machete",
"cargo-deny",
"cargo-llvm-cov"
]
);
}
#[test]
fn getters_return_user_value_when_set() {
let cfg = ToolsConfig {
python_package_manager: Some("pip".to_string()),
node_package_manager: Some("yarn".to_string()),
rust_dev_tools: Some(vec!["cargo-foo".to_string(), "cargo-bar".to_string()]),
};
assert_eq!(cfg.python_pm(), "pip");
assert_eq!(cfg.node_pm(), "yarn");
assert_eq!(cfg.rust_tools(), vec!["cargo-foo", "cargo-bar"]);
}
#[test]
fn empty_rust_dev_tools_is_respected() {
let cfg = ToolsConfig {
rust_dev_tools: Some(vec![]),
..Default::default()
};
assert!(cfg.rust_tools().is_empty());
}
#[test]
fn deserializes_from_toml() {
let toml_str = r#"
python_package_manager = "poetry"
node_package_manager = "npm"
rust_dev_tools = ["cargo-edit"]
"#;
let cfg: ToolsConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.python_pm(), "poetry");
assert_eq!(cfg.node_pm(), "npm");
assert_eq!(cfg.rust_tools(), vec!["cargo-edit"]);
}
#[test]
fn require_tool_emits_command_v() {
assert_eq!(require_tool("ruff"), "command -v ruff >/dev/null 2>&1");
}
#[test]
fn require_tools_joins_with_and() {
assert_eq!(
require_tools(&["go", "gofmt"]),
"command -v go >/dev/null 2>&1 && command -v gofmt >/dev/null 2>&1"
);
}
#[test]
fn empty_toml_uses_defaults() {
let cfg: ToolsConfig = toml::from_str("").unwrap();
assert_eq!(cfg.python_pm(), "uv");
assert_eq!(cfg.node_pm(), "pnpm");
}
}