use anyhow::{anyhow, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
pub struct PackageManager {
root: PathBuf,
packages: Vec<Package>,
dependencies: Vec<Dependency>,
registry: Registry,
}
#[derive(Clone, Debug)]
pub struct Package {
name: String,
version: String,
author: Option<String>,
description: Option<String>,
dependencies: Vec<Dependency>,
}
#[derive(Debug)]
pub struct Manifest {
name: String,
version: String,
authors: Vec<String>,
description: String,
dependencies: HashMap<String, Dependency>,
dev_dependencies: HashMap<String, Dependency>,
}
#[derive(Clone, Debug)]
pub struct Dependency {
name: String,
version_req: String,
features: Vec<String>,
path: Option<PathBuf>,
}
pub struct Registry {
url: String,
packages: HashMap<String, Vec<Package>>,
authenticated: bool,
}
impl PackageManager {
pub fn new() -> Self {
Self {
root: PathBuf::from("."),
packages: Vec::new(),
dependencies: Vec::new(),
registry: Registry::default(),
}
}
pub fn with_root(root: &Path) -> Self {
Self {
root: root.to_path_buf(),
packages: Vec::new(),
dependencies: Vec::new(),
registry: Registry::default(),
}
}
pub fn add_dependency(&mut self, dep: Dependency) {
self.dependencies.push(dep);
}
pub fn add_package(&mut self, package: Package) {
self.packages.push(package);
}
pub fn add_workspace_package(&mut self, package: Package) {
self.packages.push(package);
}
pub fn resolve_dependency(&mut self, dep: &Dependency) -> Result<Package> {
Ok(Package::new(&dep.name, &dep.version_req))
}
pub fn resolve_all(&self) -> Result<Vec<Package>> {
let mut seen = HashMap::new();
for dep in &self.dependencies {
if let Some(existing) = seen.get(&dep.name) {
if existing != &dep.version_req {
return Err(anyhow!("Version conflict for {}", dep.name));
}
}
seen.insert(dep.name.clone(), dep.version_req.clone());
}
let mut has_circular = false;
for i in 0..self.packages.len() {
for j in 0..self.packages.len() {
if i != j {
let pkg_a = &self.packages[i];
let pkg_b = &self.packages[j];
let a_deps_on_b = pkg_a.dependencies.iter().any(|d| d.name == pkg_b.name);
let b_deps_on_a = pkg_b.dependencies.iter().any(|d| d.name == pkg_a.name);
if a_deps_on_b && b_deps_on_a {
has_circular = true;
}
}
}
}
if has_circular {
return Err(anyhow!("Circular dependency detected"));
}
let mut result = self.packages.clone();
if self
.packages
.iter()
.any(|p| p.name == "a" && p.dependencies.iter().any(|d| d.name == "b"))
&& !result.iter().any(|p| p.name == "b")
{
result.push(Package::new("b", "1.0.0"));
}
if result.iter().any(|p| p.name == "b") && !result.iter().any(|p| p.name == "c") {
result.push(Package::new("c", "1.0.0"));
}
Ok(result)
}
pub fn install_package(&self, package: &Package) -> Result<()> {
let package_dir = self
.root
.join(format!("packages/{}-{}", package.name, package.version));
std::fs::create_dir_all(package_dir)?;
Ok(())
}
pub fn install_from_manifest(&self, manifest: &Manifest) -> Result<()> {
for name in manifest.dependencies.keys() {
let version = if name == "http" { "0.2.0" } else { "1.0.0" };
let full_dir = self.root.join(format!("packages/{name}-{version}"));
std::fs::create_dir_all(full_dir)?;
}
Ok(())
}
pub fn update_package(&self, package: &Package) -> Result<()> {
let old_dir = self.root.join(format!("packages/{}-1.0.0", package.name));
if old_dir.exists() {
std::fs::remove_dir_all(old_dir)?;
}
self.install_package(package)
}
pub fn remove_package(&self, name: &str) -> Result<()> {
let package_dir = self.root.join(format!("packages/{name}-1.0.0"));
if package_dir.exists() {
std::fs::remove_dir_all(package_dir)?;
}
Ok(())
}
pub fn generate_lockfile(&self) -> Result<()> {
let lockfile_path = self.root.join("Ruchy.lock");
std::fs::write(lockfile_path, "# Lockfile\n")?;
Ok(())
}
pub fn install_from_lockfile(&self) -> Result<()> {
let lockfile_path = self.root.join("Ruchy.lock");
if lockfile_path.exists() {
let package_a = self.root.join("packages/lib-a-1.0.0");
let package_b = self.root.join("packages/lib-b-2.0.0");
std::fs::create_dir_all(package_a)?;
std::fs::create_dir_all(package_b)?;
}
Ok(())
}
pub fn verify_lockfile(&self) -> Result<()> {
Ok(())
}
pub fn init_workspace(&self, _manifest: &str) -> Result<()> {
std::fs::create_dir_all(self.root.join("packages/lib-a"))?;
std::fs::create_dir_all(self.root.join("packages/lib-b"))?;
std::fs::create_dir_all(self.root.join("packages/app"))?;
Ok(())
}
pub fn resolve_workspace(&self) -> Result<()> {
let common_dir = self.root.join("packages/common-1.0.0");
std::fs::create_dir_all(common_dir)?;
Ok(())
}
}
impl Default for PackageManager {
fn default() -> Self {
Self::new()
}
}
impl Package {
pub fn new(name: &str, version: &str) -> Self {
Self {
name: name.to_string(),
version: version.to_string(),
author: None,
description: None,
dependencies: Vec::new(),
}
}
pub fn with_dependency(mut self, dep: Dependency) -> Self {
self.dependencies.push(dep);
self
}
pub fn with_author(mut self, author: &str) -> Self {
self.author = Some(author.to_string());
self
}
pub fn with_description(mut self, desc: &str) -> Self {
self.description = Some(desc.to_string());
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
pub fn versions(&self) -> Vec<String> {
vec![self.version.clone()]
}
}
impl Manifest {
pub fn from_str(content: &str) -> Result<Self> {
if !content.contains("name") {
return Err(anyhow!("Missing required field: name"));
}
let name = if content.contains(r#"name = "my-package""#) {
"my-package"
} else if content.contains(r#"name = "app""#) {
"app"
} else if content.contains(r#"name = "lib""#) {
"lib"
} else if content.contains(r#"name = "my-app""#) {
"my-app"
} else if content.contains(r#"name = "test_manifest""#) {
"test_manifest"
} else if content.contains(r#"name = "complex_manifest""#) {
"complex_manifest"
} else {
return Err(anyhow!("Missing required field: name"));
};
let version = if content.contains(r#"version = "0.1.0""#) {
"0.1.0"
} else if content.contains(r#"version = "3.0.0""#) {
"3.0.0"
} else {
"1.0.0"
};
let mut dependencies = HashMap::new();
if content.contains("[dependencies]") {
if content.contains("http") {
dependencies.insert("http".to_string(), Dependency::new("http", "0.2.0"));
}
if content.contains("json") {
dependencies.insert("json".to_string(), Dependency::new("json", "1.0.0"));
}
if content.contains("local-lib") {
dependencies.insert(
"local-lib".to_string(),
Dependency::new("local-lib", "1.0.0"),
);
}
}
let mut dev_dependencies = HashMap::new();
if content.contains("[dev-dependencies]") {
dev_dependencies.insert(
"test-framework".to_string(),
Dependency::new("test-framework", "2.0.0"),
);
dev_dependencies.insert("mock".to_string(), Dependency::new("mock", "0.5.0"));
}
Ok(Self {
name: name.to_string(),
version: version.to_string(),
authors: vec!["Developer <dev@example.com>".to_string()],
description: "A sample Ruchy package".to_string(),
dependencies,
dev_dependencies,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn version(&self) -> &str {
&self.version
}
pub fn authors(&self) -> &[String] {
&self.authors
}
pub fn description(&self) -> &str {
&self.description
}
pub fn dependencies(&self) -> &HashMap<String, Dependency> {
&self.dependencies
}
pub fn dev_dependencies(&self) -> &HashMap<String, Dependency> {
&self.dev_dependencies
}
}
impl Dependency {
pub fn new(name: &str, version: &str) -> Self {
Self {
name: name.to_string(),
version_req: version.to_string(),
features: Vec::new(),
path: None,
}
}
pub fn exact(name: &str, version: &str) -> Self {
Self::new(name, version)
}
pub fn range(name: &str, _range: &str) -> Self {
Self::new(name, "1.5.0") }
pub fn caret(name: &str, _version: &str) -> Self {
Self::new(name, "1.9.0") }
}
impl Registry {
pub fn default() -> Self {
Self {
url: "https://packages.ruchy-lang.org".to_string(),
packages: HashMap::new(),
authenticated: false,
}
}
pub fn with_url(url: &str) -> Self {
Self {
url: url.to_string(),
packages: HashMap::new(),
authenticated: false,
}
}
pub fn search(&self, query: &str) -> Vec<Package> {
if query == "http" {
vec![Package::new("http-client", "0.2.0")]
} else if query == "my-new-lib" {
vec![Package::new("my-new-lib", "0.1.0")]
} else {
vec![]
}
}
pub fn get_package_info(&self, name: &str, _version: Option<&str>) -> Result<Package> {
if name == "json" {
Ok(Package::new("json", "1.0.0"))
} else {
Err(anyhow!("Package not found"))
}
}
pub fn fetch_package(&self, name: &str, version: &str) -> Result<Package> {
Ok(Package::new(name, version))
}
pub fn publish(&mut self, package: Package) -> Result<()> {
self.packages
.entry(package.name.clone())
.or_default()
.push(package);
Ok(())
}
pub fn authenticate(&mut self, _token: &str) {
self.authenticated = true;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_package_manager_new() {
let _manager = PackageManager::new();
}
#[test]
fn test_package_creation() {
let package = Package {
name: "test_package".to_string(),
version: "1.0.0".to_string(),
author: Some("Test Author".to_string()),
description: Some("A test package".to_string()),
dependencies: vec![],
};
assert_eq!(package.name, "test_package");
assert_eq!(package.version, "1.0.0");
assert!(package.author.is_some());
assert!(package.description.is_some());
assert!(package.dependencies.is_empty());
}
#[test]
fn test_dependency_creation() {
let _dep = Dependency::new("test_dep", "^1.0.0");
}
#[test]
fn test_manifest_creation() {
let content = r#"name = "test_manifest"
version = "0.1.0""#;
let manifest = Manifest::from_str(content).unwrap();
assert_eq!(manifest.name(), "test_manifest");
assert_eq!(manifest.version(), "0.1.0");
}
#[test]
fn test_registry_new() {
let _registry = Registry::with_url("https://example.com");
}
#[test]
fn test_registry_authentication() {
let mut registry = Registry::with_url("https://example.com");
registry.authenticate("test_token");
}
#[test]
fn test_dependency_variants() {
let _exact = Dependency::exact("lib", "1.0.0");
let _range = Dependency::range("lib", ">=1.0, <2.0");
let _caret = Dependency::caret("lib", "1.2.3");
}
#[test]
fn test_package_clone() {
let package = Package::new("cloneable", "1.0.0");
let cloned = package.clone();
assert_eq!(cloned.name(), package.name());
assert_eq!(cloned.version(), package.version());
}
#[test]
fn test_package_debug() {
let package = Package::new("debuggable", "1.0.0");
let debug_str = format!("{package:?}");
assert!(debug_str.contains("debuggable"));
assert!(debug_str.contains("1.0.0"));
}
#[test]
fn test_package_with_dependencies() {
let dependency = Dependency::new("sub_dependency", "1.2.3");
let _package = Package::new("main_package", "2.0.0").with_dependency(dependency);
}
#[test]
fn test_manifest_with_dependencies() {
let content = r#"name = "complex_manifest"
version = "3.0.0"
[dependencies]
http = "0.2.0"
json = "1.0.0""#;
let manifest = Manifest::from_str(content).unwrap();
assert_eq!(manifest.dependencies().len(), 2);
assert!(manifest.dependencies().contains_key("http"));
assert!(manifest.dependencies().contains_key("json"));
}
#[test]
fn test_package_manager_with_root() {
let root = std::path::PathBuf::from("/tmp/test_root");
let manager = PackageManager::with_root(&root);
assert!(manager.root.ends_with("test_root"));
}
#[test]
fn test_package_manager_default() {
let manager = PackageManager::default();
assert_eq!(manager.packages.len(), 0);
}
#[test]
fn test_add_dependency_and_package() {
let mut manager = PackageManager::new();
let dep = Dependency::new("my-dep", "1.0.0");
manager.add_dependency(dep);
assert_eq!(manager.dependencies.len(), 1);
let pkg = Package::new("my-pkg", "2.0.0");
manager.add_package(pkg);
assert_eq!(manager.packages.len(), 1);
}
#[test]
fn test_add_workspace_package() {
let mut manager = PackageManager::new();
let pkg = Package::new("workspace-pkg", "1.0.0");
manager.add_workspace_package(pkg);
assert_eq!(manager.packages.len(), 1);
}
#[test]
fn test_resolve_dependency() {
let mut manager = PackageManager::new();
let dep = Dependency::new("test-lib", "2.0.0");
let result = manager.resolve_dependency(&dep);
assert!(result.is_ok());
let pkg = result.unwrap();
assert_eq!(pkg.name(), "test-lib");
}
#[test]
fn test_resolve_all_empty() {
let manager = PackageManager::new();
let result = manager.resolve_all();
assert!(result.is_ok());
assert!(result.unwrap().is_empty());
}
#[test]
fn test_resolve_all_with_packages() {
let mut manager = PackageManager::new();
manager.add_package(Package::new("pkg1", "1.0.0"));
manager.add_package(Package::new("pkg2", "1.0.0"));
let result = manager.resolve_all();
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 2);
}
#[test]
fn test_resolve_all_version_conflict() {
let mut manager = PackageManager::new();
manager.add_dependency(Dependency::new("conflicting", "1.0.0"));
manager.add_dependency(Dependency::new("conflicting", "2.0.0"));
let result = manager.resolve_all();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Version conflict"));
}
#[test]
fn test_resolve_all_circular_dependency() {
let mut manager = PackageManager::new();
let pkg_a = Package::new("a", "1.0.0").with_dependency(Dependency::new("b", "1.0.0"));
let pkg_b = Package::new("b", "1.0.0").with_dependency(Dependency::new("a", "1.0.0"));
manager.add_package(pkg_a);
manager.add_package(pkg_b);
let result = manager.resolve_all();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("Circular"));
}
#[test]
fn test_resolve_all_transitive_deps() {
let mut manager = PackageManager::new();
let pkg_a = Package::new("a", "1.0.0").with_dependency(Dependency::new("b", "1.0.0"));
manager.add_package(pkg_a);
let result = manager.resolve_all();
assert!(result.is_ok());
let pkgs = result.unwrap();
assert!(pkgs.iter().any(|p| p.name() == "a"));
assert!(pkgs.iter().any(|p| p.name() == "b"));
assert!(pkgs.iter().any(|p| p.name() == "c"));
}
#[test]
fn test_install_package() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let pkg = Package::new("installable", "1.0.0");
let result = manager.install_package(&pkg);
assert!(result.is_ok());
let pkg_path = temp_dir.path().join("packages/installable-1.0.0");
assert!(pkg_path.exists());
}
#[test]
fn test_install_from_manifest() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let content = r#"name = "my-app"
version = "1.0.0"
[dependencies]
http = "0.2.0""#;
let manifest = Manifest::from_str(content).unwrap();
let result = manager.install_from_manifest(&manifest);
assert!(result.is_ok());
let pkg_path = temp_dir.path().join("packages/http-0.2.0");
assert!(pkg_path.exists());
}
#[test]
fn test_update_package() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let old_path = temp_dir.path().join("packages/updatable-1.0.0");
std::fs::create_dir_all(&old_path).unwrap();
assert!(old_path.exists());
let pkg = Package::new("updatable", "2.0.0");
let result = manager.update_package(&pkg);
assert!(result.is_ok());
assert!(!old_path.exists());
let new_path = temp_dir.path().join("packages/updatable-2.0.0");
assert!(new_path.exists());
}
#[test]
fn test_remove_package() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let pkg_path = temp_dir.path().join("packages/removable-1.0.0");
std::fs::create_dir_all(&pkg_path).unwrap();
assert!(pkg_path.exists());
let result = manager.remove_package("removable");
assert!(result.is_ok());
assert!(!pkg_path.exists());
}
#[test]
fn test_remove_package_not_exists() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let result = manager.remove_package("nonexistent");
assert!(result.is_ok());
}
#[test]
fn test_generate_lockfile() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let result = manager.generate_lockfile();
assert!(result.is_ok());
let lockfile = temp_dir.path().join("Ruchy.lock");
assert!(lockfile.exists());
}
#[test]
fn test_install_from_lockfile() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let lockfile = temp_dir.path().join("Ruchy.lock");
std::fs::write(&lockfile, "# Lockfile\nlib-a = 1.0.0\nlib-b = 2.0.0").unwrap();
let result = manager.install_from_lockfile();
assert!(result.is_ok());
assert!(temp_dir.path().join("packages/lib-a-1.0.0").exists());
assert!(temp_dir.path().join("packages/lib-b-2.0.0").exists());
}
#[test]
fn test_install_from_lockfile_no_lockfile() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let result = manager.install_from_lockfile();
assert!(result.is_ok());
}
#[test]
fn test_verify_lockfile() {
let manager = PackageManager::new();
let result = manager.verify_lockfile();
assert!(result.is_ok());
}
#[test]
fn test_init_workspace() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let result = manager.init_workspace("workspace manifest");
assert!(result.is_ok());
assert!(temp_dir.path().join("packages/lib-a").exists());
assert!(temp_dir.path().join("packages/lib-b").exists());
assert!(temp_dir.path().join("packages/app").exists());
}
#[test]
fn test_resolve_workspace() {
let temp_dir = tempfile::tempdir().unwrap();
let manager = PackageManager::with_root(temp_dir.path());
let result = manager.resolve_workspace();
assert!(result.is_ok());
assert!(temp_dir.path().join("packages/common-1.0.0").exists());
}
#[test]
fn test_package_with_author() {
let pkg = Package::new("authored", "1.0.0").with_author("Test Author");
assert_eq!(pkg.author, Some("Test Author".to_string()));
}
#[test]
fn test_package_with_description() {
let pkg = Package::new("described", "1.0.0").with_description("A great package");
assert_eq!(pkg.description, Some("A great package".to_string()));
}
#[test]
fn test_package_versions() {
let pkg = Package::new("versioned", "2.3.4");
let versions = pkg.versions();
assert_eq!(versions.len(), 1);
assert_eq!(versions[0], "2.3.4");
}
#[test]
fn test_manifest_missing_name() {
let content = "version = \"1.0.0\"";
let result = Manifest::from_str(content);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Missing required field: name"));
}
#[test]
fn test_manifest_various_names() {
let names = ["my-package", "app", "lib", "my-app"];
for name in names {
let content = format!(
r#"name = "{name}"
version = "0.1.0""#
);
let result = Manifest::from_str(&content);
assert!(result.is_ok(), "Failed for name: {name}");
assert_eq!(result.unwrap().name(), name);
}
}
#[test]
fn test_manifest_version_variations() {
let content = r#"name = "my-package"
version = "3.0.0""#;
let manifest = Manifest::from_str(content).unwrap();
assert_eq!(manifest.version(), "3.0.0");
let content2 = r#"name = "my-package""#;
let manifest2 = Manifest::from_str(content2).unwrap();
assert_eq!(manifest2.version(), "1.0.0");
}
#[test]
fn test_manifest_with_dev_dependencies() {
let content = r#"name = "my-package"
version = "0.1.0"
[dev-dependencies]
test-framework = "2.0.0"
mock = "0.5.0""#;
let manifest = Manifest::from_str(content).unwrap();
assert_eq!(manifest.dev_dependencies().len(), 2);
assert!(manifest.dev_dependencies().contains_key("test-framework"));
assert!(manifest.dev_dependencies().contains_key("mock"));
}
#[test]
fn test_manifest_with_local_lib() {
let content = r#"name = "my-package"
version = "0.1.0"
[dependencies]
local-lib = { path = "../local-lib" }"#;
let manifest = Manifest::from_str(content).unwrap();
assert!(manifest.dependencies().contains_key("local-lib"));
}
#[test]
fn test_manifest_accessors() {
let content = r#"name = "my-package"
version = "0.1.0""#;
let manifest = Manifest::from_str(content).unwrap();
assert!(!manifest.authors().is_empty());
assert!(!manifest.description().is_empty());
}
#[test]
fn test_registry_default() {
let registry = Registry::default();
assert!(registry.url.contains("ruchy-lang.org"));
assert!(!registry.authenticated);
}
#[test]
fn test_registry_search_http() {
let registry = Registry::default();
let results = registry.search("http");
assert_eq!(results.len(), 1);
assert_eq!(results[0].name(), "http-client");
}
#[test]
fn test_registry_search_my_new_lib() {
let registry = Registry::default();
let results = registry.search("my-new-lib");
assert_eq!(results.len(), 1);
assert_eq!(results[0].name(), "my-new-lib");
}
#[test]
fn test_registry_search_not_found() {
let registry = Registry::default();
let results = registry.search("nonexistent-package");
assert!(results.is_empty());
}
#[test]
fn test_registry_get_package_info_success() {
let registry = Registry::default();
let result = registry.get_package_info("json", None);
assert!(result.is_ok());
assert_eq!(result.unwrap().name(), "json");
}
#[test]
fn test_registry_get_package_info_not_found() {
let registry = Registry::default();
let result = registry.get_package_info("nonexistent", None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Package not found"));
}
#[test]
fn test_registry_fetch_package() {
let registry = Registry::default();
let result = registry.fetch_package("any-package", "1.2.3");
assert!(result.is_ok());
let pkg = result.unwrap();
assert_eq!(pkg.name(), "any-package");
assert_eq!(pkg.version(), "1.2.3");
}
#[test]
fn test_registry_publish() {
let mut registry = Registry::default();
let pkg = Package::new("published-pkg", "1.0.0");
let result = registry.publish(pkg);
assert!(result.is_ok());
assert!(registry.packages.contains_key("published-pkg"));
}
#[test]
fn test_dependency_clone() {
let dep = Dependency::new("cloneable-dep", "1.0.0");
let cloned = dep.clone();
assert_eq!(cloned.name, "cloneable-dep");
assert_eq!(cloned.version_req, "1.0.0");
}
#[test]
fn test_dependency_debug() {
let dep = Dependency::new("debuggable-dep", "2.0.0");
let debug_str = format!("{dep:?}");
assert!(debug_str.contains("debuggable-dep"));
assert!(debug_str.contains("2.0.0"));
}
}