use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::{DrivenError, Result};
#[cfg(test)]
mod property_tests;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Module {
pub id: String,
pub name: String,
pub version: String,
pub description: String,
pub author: Option<String>,
pub license: Option<String>,
pub dependencies: Vec<ModuleDependency>,
pub agents: Vec<String>,
pub workflows: Vec<String>,
pub templates: Vec<String>,
pub resources: Vec<String>,
#[serde(skip)]
pub install_path: Option<PathBuf>,
}
impl Module {
pub fn new(id: impl Into<String>, name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
version: version.into(),
description: String::new(),
author: None,
license: None,
dependencies: Vec::new(),
agents: Vec::new(),
workflows: Vec::new(),
templates: Vec::new(),
resources: Vec::new(),
install_path: None,
}
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
pub fn with_dependency(mut self, dep: ModuleDependency) -> Self {
self.dependencies.push(dep);
self
}
pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
self.agents.push(agent.into());
self
}
pub fn with_workflow(mut self, workflow: impl Into<String>) -> Self {
self.workflows.push(workflow.into());
self
}
pub fn with_template(mut self, template: impl Into<String>) -> Self {
self.templates.push(template.into());
self
}
pub fn with_resource(mut self, resource: impl Into<String>) -> Self {
self.resources.push(resource.into());
self
}
pub fn namespaced_agent(&self, agent: &str) -> String {
format!("{}:{}", self.id, agent)
}
pub fn namespaced_workflow(&self, workflow: &str) -> String {
format!("{}:{}", self.id, workflow)
}
pub fn namespaced_template(&self, template: &str) -> String {
format!("{}:{}", self.id, template)
}
pub fn satisfies_version(&self, version_req: &str) -> bool {
if version_req == "*" {
return true;
}
if version_req.starts_with('^') {
let req = version_req.trim_start_matches('^');
return self
.version
.starts_with(&req.split('.').next().unwrap_or("0").to_string());
}
if version_req.starts_with('~') {
let req = version_req.trim_start_matches('~');
let parts: Vec<&str> = req.split('.').collect();
let self_parts: Vec<&str> = self.version.split('.').collect();
return parts.first() == self_parts.first() && parts.get(1) == self_parts.get(1);
}
self.version == version_req
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModuleDependency {
pub module_id: String,
pub version_req: String,
#[serde(default)]
pub optional: bool,
}
impl ModuleDependency {
pub fn new(module_id: impl Into<String>, version_req: impl Into<String>) -> Self {
Self {
module_id: module_id.into(),
version_req: version_req.into(),
optional: false,
}
}
pub fn optional(module_id: impl Into<String>, version_req: impl Into<String>) -> Self {
Self {
module_id: module_id.into(),
version_req: version_req.into(),
optional: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModuleStatus {
Installed,
Installing,
UpdateAvailable,
DependencyError,
Disabled,
}
pub struct ModuleManager {
installed: HashMap<String, Module>,
status: HashMap<String, ModuleStatus>,
registry_path: PathBuf,
isolation_enabled: bool,
}
impl ModuleManager {
pub fn new(registry_path: impl Into<PathBuf>) -> Self {
Self {
installed: HashMap::new(),
status: HashMap::new(),
registry_path: registry_path.into(),
isolation_enabled: true,
}
}
pub fn load(&mut self) -> Result<()> {
let modules_dir = self.registry_path.join("installed");
if !modules_dir.exists() {
return Ok(());
}
for entry in std::fs::read_dir(&modules_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
let manifest_path = path.join("module.dx");
if manifest_path.exists() {
match self.load_manifest(&manifest_path) {
Ok(mut module) => {
module.install_path = Some(path.clone());
self.status
.insert(module.id.clone(), ModuleStatus::Installed);
self.installed.insert(module.id.clone(), module);
}
Err(e) => {
eprintln!("Warning: Failed to load module at {:?}: {}", path, e);
}
}
}
}
}
Ok(())
}
pub fn install(&mut self, module_path: &Path) -> Result<()> {
let manifest_path = module_path.join("module.dx");
if !manifest_path.exists() {
return Err(DrivenError::Config(format!(
"Module manifest not found at {:?}",
manifest_path
)));
}
let module = self.load_manifest(&manifest_path)?;
if self.installed.contains_key(&module.id) {
return Err(DrivenError::Config(format!(
"Module '{}' is already installed",
module.id
)));
}
self.resolve_dependencies(&module)?;
self.status
.insert(module.id.clone(), ModuleStatus::Installing);
let install_dir = self.registry_path.join("installed").join(&module.id);
std::fs::create_dir_all(&install_dir)?;
self.copy_module_files(module_path, &install_dir)?;
let mut installed_module = module.clone();
installed_module.install_path = Some(install_dir);
self.status
.insert(installed_module.id.clone(), ModuleStatus::Installed);
self.installed
.insert(installed_module.id.clone(), installed_module);
Ok(())
}
pub fn uninstall(&mut self, module_id: &str) -> Result<()> {
let module = self.installed.get(module_id).ok_or_else(|| {
DrivenError::Config(format!("Module '{}' is not installed", module_id))
})?;
for (id, other) in &self.installed {
if id != module_id {
for dep in &other.dependencies {
if dep.module_id == module_id && !dep.optional {
return Err(DrivenError::Config(format!(
"Cannot uninstall '{}': module '{}' depends on it",
module_id, id
)));
}
}
}
}
if let Some(install_path) = &module.install_path {
if install_path.exists() {
std::fs::remove_dir_all(install_path)?;
}
}
self.installed.remove(module_id);
self.status.remove(module_id);
Ok(())
}
pub fn update(&mut self, module_id: &str, new_module_path: &Path) -> Result<()> {
if !self.installed.contains_key(module_id) {
return Err(DrivenError::Config(format!(
"Module '{}' is not installed",
module_id
)));
}
let manifest_path = new_module_path.join("module.dx");
let new_module = self.load_manifest(&manifest_path)?;
if new_module.id != module_id {
return Err(DrivenError::Config(format!(
"Module ID mismatch: expected '{}', got '{}'",
module_id, new_module.id
)));
}
self.uninstall(module_id)?;
self.install(new_module_path)?;
Ok(())
}
pub fn list_installed(&self) -> Vec<&Module> {
self.installed.values().collect()
}
pub fn get(&self, module_id: &str) -> Option<&Module> {
self.installed.get(module_id)
}
pub fn get_status(&self, module_id: &str) -> Option<ModuleStatus> {
self.status.get(module_id).copied()
}
pub fn resolve_dependencies(&self, module: &Module) -> Result<Vec<&Module>> {
let mut resolved = Vec::new();
let mut to_resolve: Vec<&ModuleDependency> = module.dependencies.iter().collect();
let mut visited = std::collections::HashSet::new();
while let Some(dep) = to_resolve.pop() {
if visited.contains(&dep.module_id) {
continue;
}
visited.insert(dep.module_id.clone());
match self.installed.get(&dep.module_id) {
Some(installed) => {
if !installed.satisfies_version(&dep.version_req) {
return Err(DrivenError::Config(format!(
"Dependency '{}' version {} does not satisfy requirement {}",
dep.module_id, installed.version, dep.version_req
)));
}
resolved.push(installed);
for trans_dep in &installed.dependencies {
to_resolve.push(trans_dep);
}
}
None if !dep.optional => {
return Err(DrivenError::Config(format!(
"Required dependency '{}' ({}) is not installed",
dep.module_id, dep.version_req
)));
}
None => {
}
}
}
Ok(resolved)
}
pub fn is_installed(&self, module_id: &str) -> bool {
self.installed.contains_key(module_id)
}
pub fn enable_isolation(&mut self) {
self.isolation_enabled = true;
}
pub fn disable_isolation(&mut self) {
self.isolation_enabled = false;
}
pub fn is_isolation_enabled(&self) -> bool {
self.isolation_enabled
}
pub fn get_all_agents(&self) -> Vec<String> {
let mut agents = Vec::new();
for module in self.installed.values() {
for agent in &module.agents {
if self.isolation_enabled {
agents.push(module.namespaced_agent(agent));
} else {
agents.push(agent.clone());
}
}
}
agents
}
pub fn get_all_workflows(&self) -> Vec<String> {
let mut workflows = Vec::new();
for module in self.installed.values() {
for workflow in &module.workflows {
if self.isolation_enabled {
workflows.push(module.namespaced_workflow(workflow));
} else {
workflows.push(workflow.clone());
}
}
}
workflows
}
pub fn get_all_templates(&self) -> Vec<String> {
let mut templates = Vec::new();
for module in self.installed.values() {
for template in &module.templates {
if self.isolation_enabled {
templates.push(module.namespaced_template(template));
} else {
templates.push(template.clone());
}
}
}
templates
}
fn load_manifest(&self, path: &Path) -> Result<Module> {
let content = std::fs::read_to_string(path)?;
self.parse_dx_manifest(&content)
}
fn parse_dx_manifest(&self, content: &str) -> Result<Module> {
let mut module = Module::new("", "", "");
for line in content.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if let Some((key, value)) = line.split_once('|') {
let key = key.trim();
let value = value.trim();
match key {
"id" => module.id = value.to_string(),
"nm" | "name" => module.name = value.to_string(),
"v" | "version" => module.version = value.to_string(),
"desc" | "description" => module.description = value.to_string(),
"author" => module.author = Some(value.to_string()),
"license" => module.license = Some(value.to_string()),
key if key.starts_with("dep.") => {
let dep_id = key.strip_prefix("dep.").unwrap();
module
.dependencies
.push(ModuleDependency::new(dep_id, value));
}
key if key.starts_with("agent.") => {
module.agents.push(value.to_string());
}
key if key.starts_with("workflow.") => {
module.workflows.push(value.to_string());
}
key if key.starts_with("template.") => {
module.templates.push(value.to_string());
}
key if key.starts_with("resource.") => {
module.resources.push(value.to_string());
}
_ => {}
}
}
}
if module.id.is_empty() {
return Err(DrivenError::Config(
"Module manifest missing 'id' field".to_string(),
));
}
if module.name.is_empty() {
module.name = module.id.clone();
}
if module.version.is_empty() {
module.version = "0.0.0".to_string();
}
Ok(module)
}
fn copy_module_files(&self, src: &Path, dest: &Path) -> Result<()> {
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dest_path = dest.join(entry.file_name());
if src_path.is_dir() {
std::fs::create_dir_all(&dest_path)?;
self.copy_module_files(&src_path, &dest_path)?;
} else {
std::fs::copy(&src_path, &dest_path)?;
}
}
Ok(())
}
}
impl Default for ModuleManager {
fn default() -> Self {
Self::new(".driven/modules")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_module_creation() {
let module = Module::new("test-module", "Test Module", "1.0.0")
.with_description("A test module")
.with_agent("test-agent")
.with_workflow("test-workflow");
assert_eq!(module.id, "test-module");
assert_eq!(module.name, "Test Module");
assert_eq!(module.version, "1.0.0");
assert_eq!(module.agents.len(), 1);
assert_eq!(module.workflows.len(), 1);
}
#[test]
fn test_version_satisfaction() {
let module = Module::new("test", "Test", "1.2.3");
assert!(module.satisfies_version("1.2.3"));
assert!(module.satisfies_version("*"));
assert!(module.satisfies_version("^1"));
assert!(module.satisfies_version("~1.2"));
assert!(!module.satisfies_version("2.0.0"));
assert!(!module.satisfies_version("^2"));
}
#[test]
fn test_namespacing() {
let module = Module::new("my-module", "My Module", "1.0.0")
.with_agent("agent1")
.with_workflow("workflow1");
assert_eq!(module.namespaced_agent("agent1"), "my-module:agent1");
assert_eq!(
module.namespaced_workflow("workflow1"),
"my-module:workflow1"
);
}
#[test]
fn test_dependency_creation() {
let dep = ModuleDependency::new("other-module", "^1.0.0");
assert_eq!(dep.module_id, "other-module");
assert_eq!(dep.version_req, "^1.0.0");
assert!(!dep.optional);
let opt_dep = ModuleDependency::optional("optional-module", "*");
assert!(opt_dep.optional);
}
#[test]
fn test_module_manager_creation() {
let manager = ModuleManager::new("/tmp/test-modules");
assert!(manager.list_installed().is_empty());
assert!(manager.is_isolation_enabled());
}
}