use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use crate::error::{PkgError, PkgResult};
use crate::lockfile::Lockfile;
use crate::manifest::Manifest;
use crate::resolver::PackageRegistry;
use crate::tree;
pub const MANIFEST_FILE: &str = "bock.package";
pub const LOCKFILE: &str = "bock.lock";
pub fn find_manifest(start_dir: &Path) -> PkgResult<PathBuf> {
let mut dir = start_dir.to_path_buf();
loop {
let candidate = dir.join(MANIFEST_FILE);
if candidate.exists() {
return Ok(candidate);
}
if !dir.pop() {
return Err(PkgError::Io(format!(
"no {MANIFEST_FILE} found in {start_dir:?} or any parent directory"
)));
}
}
}
pub fn init(dir: &Path, name: &str) -> PkgResult<PathBuf> {
let manifest_path = dir.join(MANIFEST_FILE);
if manifest_path.exists() {
return Err(PkgError::Io(format!(
"{MANIFEST_FILE} already exists in {}",
dir.display()
)));
}
let manifest = Manifest {
package: crate::manifest::PackageSection {
name: name.to_string(),
version: "0.1.0".to_string(),
targets: None,
},
dependencies: Default::default(),
dev_dependencies: BTreeMap::new(),
features: BTreeMap::new(),
};
let content = manifest.to_toml_string()?;
std::fs::write(&manifest_path, content).map_err(|e| PkgError::Io(e.to_string()))?;
Ok(manifest_path)
}
pub fn add(manifest_path: &Path, name: &str, version: Option<&str>) -> PkgResult<()> {
let mut manifest = Manifest::from_file(manifest_path)?;
let version_str = version.unwrap_or("*").to_string();
manifest.add_dependency(name.to_string(), version_str.clone());
let content = manifest.to_toml_string()?;
std::fs::write(manifest_path, content).map_err(|e| PkgError::Io(e.to_string()))?;
Ok(())
}
pub fn remove(manifest_path: &Path, name: &str) -> PkgResult<()> {
let mut manifest = Manifest::from_file(manifest_path)?;
if !manifest.remove_dependency(name) {
return Err(PkgError::PackageNotFound(format!(
"'{name}' is not listed in [dependencies]"
)));
}
let content = manifest.to_toml_string()?;
std::fs::write(manifest_path, content).map_err(|e| PkgError::Io(e.to_string()))?;
Ok(())
}
pub fn resolve_and_lock(manifest_path: &Path, registry: &PackageRegistry) -> PkgResult<Lockfile> {
let manifest = Manifest::from_file(manifest_path)?;
let lock_path = manifest_path
.parent()
.unwrap_or(Path::new("."))
.join(LOCKFILE);
let direct_deps: BTreeMap<String, String> = manifest
.dependencies
.iter()
.filter_map(|(name, spec)| spec.version_req().map(|v| (name.clone(), v.to_string())))
.collect();
let resolved = registry.resolve(
&manifest.package.name,
&manifest.package.version,
&direct_deps,
)?;
let lockfile = Lockfile::from_resolved(&resolved);
lockfile.write_to_file(&lock_path)?;
Ok(lockfile)
}
pub fn show_tree(manifest_path: &Path, registry: &PackageRegistry) -> PkgResult<String> {
let manifest = Manifest::from_file(manifest_path)?;
let lock_path = manifest_path
.parent()
.unwrap_or(Path::new("."))
.join(LOCKFILE);
let direct_deps: BTreeMap<String, String> = manifest
.dependencies
.iter()
.filter_map(|(name, spec)| spec.version_req().map(|v| (name.clone(), v.to_string())))
.collect();
let resolved = if lock_path.exists() {
let lockfile = Lockfile::from_file(&lock_path)?;
lockfile.to_resolved()?
} else {
registry.resolve(
&manifest.package.name,
&manifest.package.version,
&direct_deps,
)?
};
Ok(tree::render_tree(
&manifest.package.name,
&manifest.package.version,
&direct_deps,
&resolved,
registry,
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn init_creates_manifest() {
let dir = tempfile::tempdir().unwrap();
let path = init(dir.path(), "test-project").unwrap();
assert!(path.exists());
let manifest = Manifest::from_file(&path).unwrap();
assert_eq!(manifest.package.name, "test-project");
assert_eq!(manifest.package.version, "0.1.0");
}
#[test]
fn init_fails_if_exists() {
let dir = tempfile::tempdir().unwrap();
init(dir.path(), "test").unwrap();
let result = init(dir.path(), "test");
assert!(result.is_err());
}
#[test]
fn add_dependency_to_manifest() {
let dir = tempfile::tempdir().unwrap();
let path = init(dir.path(), "my-app").unwrap();
add(&path, "foo", Some("^1.0")).unwrap();
let manifest = Manifest::from_file(&path).unwrap();
assert!(manifest.dependencies.contains_key("foo"));
assert_eq!(manifest.dependencies["foo"].version_req(), Some("^1.0"));
}
#[test]
fn remove_dependency_from_manifest() {
let dir = tempfile::tempdir().unwrap();
let path = init(dir.path(), "my-app").unwrap();
add(&path, "foo", Some("^1.0")).unwrap();
remove(&path, "foo").unwrap();
let manifest = Manifest::from_file(&path).unwrap();
assert!(!manifest.dependencies.contains_key("foo"));
}
#[test]
fn remove_nonexistent_returns_error() {
let dir = tempfile::tempdir().unwrap();
let path = init(dir.path(), "my-app").unwrap();
let result = remove(&path, "nonexistent");
assert!(matches!(result, Err(PkgError::PackageNotFound(_))));
}
#[test]
fn resolve_and_lock_creates_lockfile() {
let dir = tempfile::tempdir().unwrap();
let path = init(dir.path(), "my-app").unwrap();
add(&path, "foo", Some("^1.0")).unwrap();
let mut registry = PackageRegistry::new();
registry.register("foo", "1.2.0", BTreeMap::new()).unwrap();
let lockfile = resolve_and_lock(&path, ®istry).unwrap();
assert_eq!(lockfile.get_version("foo"), Some("1.2.0"));
let lock_path = dir.path().join(LOCKFILE);
assert!(lock_path.exists());
}
#[test]
fn show_tree_renders_deps() {
let dir = tempfile::tempdir().unwrap();
let path = init(dir.path(), "my-app").unwrap();
add(&path, "foo", Some("^1.0")).unwrap();
let mut registry = PackageRegistry::new();
registry.register("foo", "1.0.0", BTreeMap::new()).unwrap();
resolve_and_lock(&path, ®istry).unwrap();
let tree_output = show_tree(&path, ®istry).unwrap();
assert!(tree_output.contains("my-app v0.1.0"));
assert!(tree_output.contains("foo v1.0.0"));
}
}