#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]
use serde::Deserialize;
#[derive(Debug, Clone, Default, Deserialize, PartialEq)]
pub struct ExtensionManifestFile {
#[serde(default)]
pub extensions: Vec<ExtensionEntry>,
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
pub struct ExtensionEntry {
pub name: String,
pub command: String,
#[serde(default)]
pub args: Vec<String>,
#[serde(default)]
pub env: std::collections::HashMap<String, String>,
#[serde(default)]
pub timeout_ms: Option<u64>,
#[serde(default)]
pub hooks: Vec<String>,
#[serde(default)]
pub commands: Vec<String>,
}
pub fn parse_str(raw: &str) -> Result<ExtensionManifestFile, toml::de::Error> {
toml::from_str(raw)
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn parses_minimal_entry() {
let raw = r#"
[[extensions]]
name = "dirty"
command = "/usr/local/bin/dirty"
"#;
let parsed = parse_str(raw).expect("parse");
assert_eq!(parsed.extensions.len(), 1);
let ext = &parsed.extensions[0];
assert_eq!(ext.name, "dirty");
assert_eq!(ext.command, "/usr/local/bin/dirty");
assert!(ext.args.is_empty());
assert!(ext.env.is_empty());
assert!(ext.hooks.is_empty());
assert!(ext.commands.is_empty());
assert_eq!(ext.timeout_ms, None);
}
#[test]
fn parses_entry_with_all_fields() {
let raw = r#"
[[extensions]]
name = "todo"
command = "python3"
args = ["/tmp/todo.py", "--verbose"]
env = { RUST_LOG = "info", TODO_DIR = "/tmp" }
timeout_ms = 5000
hooks = ["before_user_message"]
commands = ["todo", "tdr"]
"#;
let parsed = parse_str(raw).expect("parse");
let ext = &parsed.extensions[0];
assert_eq!(ext.args, vec!["/tmp/todo.py", "--verbose"]);
assert_eq!(ext.env.get("RUST_LOG").map(|s| s.as_str()), Some("info"));
assert_eq!(ext.timeout_ms, Some(5000));
assert_eq!(ext.hooks, vec!["before_user_message"]);
assert_eq!(ext.commands, vec!["todo", "tdr"]);
}
#[test]
fn parses_multiple_entries() {
let raw = r#"
[[extensions]]
name = "first"
command = "/bin/first"
[[extensions]]
name = "second"
command = "/bin/second"
"#;
let parsed = parse_str(raw).expect("parse");
assert_eq!(parsed.extensions.len(), 2);
assert_eq!(parsed.extensions[0].name, "first");
assert_eq!(parsed.extensions[1].name, "second");
}
#[test]
fn empty_input_yields_empty_extensions() {
let parsed = parse_str("").expect("parse");
assert!(parsed.extensions.is_empty());
}
#[test]
fn missing_required_name_field_errors() {
let raw = r#"
[[extensions]]
command = "/bin/x"
"#;
assert!(parse_str(raw).is_err());
}
#[test]
fn missing_required_command_field_errors() {
let raw = r#"
[[extensions]]
name = "x"
"#;
assert!(parse_str(raw).is_err());
}
#[test]
fn malformed_toml_errors() {
assert!(parse_str("not = valid = toml").is_err());
}
}