use crate::Result;
use serde_json::Value;
use std::path::PathBuf;
#[allow(clippy::exhaustive_structs)] pub struct Context {
pub dry_run: bool,
pub job_id: String,
pub working_dir: PathBuf,
}
impl Context {
#[must_use]
pub fn new(dry_run: bool, job_id: String) -> Self {
Self {
dry_run,
job_id,
working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")),
}
}
#[must_use]
pub fn with_working_dir(dry_run: bool, job_id: String, working_dir: PathBuf) -> Self {
Self {
dry_run,
job_id,
working_dir,
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[allow(clippy::exhaustive_structs)] pub struct Output {
pub success: bool,
pub data: Value,
pub message: Option<String>,
}
pub trait Capability: Send + Sync {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn schema(&self) -> Value;
fn validate(&self, args: &Value) -> Result<()>;
fn execute(&self, args: &Value, ctx: &Context) -> Result<Output>;
}
pub struct CapabilityRegistry {
capabilities: std::collections::HashMap<String, Box<dyn Capability>>,
}
impl CapabilityRegistry {
#[must_use]
pub fn new() -> Self {
Self {
capabilities: std::collections::HashMap::new(),
}
}
pub fn register<C: Capability + 'static>(&mut self, capability: C) {
let name = capability.name().to_string();
self.capabilities.insert(name, Box::new(capability));
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&dyn Capability> {
if let Some(cap) = self.capabilities.get(name) {
return Some(cap.as_ref());
}
let name_lower = name.to_lowercase();
for (key, cap) in &self.capabilities {
if key.to_lowercase() == name_lower {
return Some(cap.as_ref());
}
}
None
}
#[must_use]
pub fn list(&self) -> Vec<&str> {
self.capabilities.keys().map(|s| s.as_str()).collect()
}
}
impl Default for CapabilityRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use serde_json::Value;
struct TestCap {
name: &'static str,
}
impl Capability for TestCap {
fn name(&self) -> &'static str {
self.name
}
fn description(&self) -> &'static str {
"test capability"
}
fn schema(&self) -> Value {
serde_json::json!({})
}
fn validate(&self, _args: &Value) -> crate::Result<()> {
Ok(())
}
fn execute(&self, _args: &Value, _ctx: &Context) -> crate::Result<Output> {
Ok(Output {
success: true,
data: serde_json::json!({}),
message: None,
})
}
}
#[test]
fn test_registry_register_and_get() {
let mut reg = CapabilityRegistry::new();
reg.register(TestCap { name: "Alpha" });
let cap = reg.get("Alpha");
assert!(cap.is_some(), "Should find registered capability");
assert_eq!(cap.unwrap().name(), "Alpha");
}
#[test]
fn test_registry_duplicate_name_replaces() {
let mut reg = CapabilityRegistry::new();
reg.register(TestCap { name: "Beta" });
reg.register(TestCap { name: "Beta" });
let cap = reg.get("Beta");
assert!(
cap.is_some(),
"Should still find capability after duplicate registration"
);
assert_eq!(cap.unwrap().name(), "Beta");
}
#[test]
fn test_registry_case_insensitive_lookup() {
let mut reg = CapabilityRegistry::new();
reg.register(TestCap { name: "ShellExec" });
assert!(reg.get("ShellExec").is_some());
assert!(
reg.get("shellexec").is_some(),
"Case-insensitive lookup should find ShellExec"
);
assert!(
reg.get("SHELLEXEC").is_some(),
"Uppercase lookup should find ShellExec"
);
assert!(
reg.get("ShellExec").is_some(),
"Exact-case lookup should find ShellExec"
);
}
#[test]
fn test_registry_unregistered_lookup_returns_none() {
let mut reg = CapabilityRegistry::new();
reg.register(TestCap { name: "Delta" });
assert!(reg.get("NoSuchCap").is_none());
assert!(reg.get("").is_none());
assert!(reg.get("gamma").is_none());
}
#[test]
fn test_registry_list() {
let mut reg = CapabilityRegistry::new();
assert!(reg.list().is_empty());
reg.register(TestCap { name: "A" });
reg.register(TestCap { name: "B" });
reg.register(TestCap { name: "C" });
let list = reg.list();
assert_eq!(list.len(), 3);
assert!(list.contains(&"A"));
assert!(list.contains(&"B"));
assert!(list.contains(&"C"));
}
}