use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DependencySpec {
pub name: String,
pub version: Option<VersionRequirement>,
pub features: Vec<String>,
pub optional: bool,
pub default_features: bool,
pub git: Option<String>,
pub git_ref: Option<GitRef>,
pub path: Option<String>,
}
impl DependencySpec {
pub fn new<S: Into<String>>(name: S) -> Self {
Self {
name: name.into(),
version: None,
features: Vec::new(),
optional: false,
default_features: true,
git: None,
git_ref: None,
path: None,
}
}
pub fn version<S: Into<String>>(mut self, version: S) -> Self {
self.version = Some(VersionRequirement::parse(version.into()));
self
}
pub fn features<I, S>(mut self, features: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.features = features.into_iter().map(|s| s.into()).collect();
self
}
pub fn feature<S: Into<String>>(mut self, feature: S) -> Self {
self.features.push(feature.into());
self
}
pub fn optional(mut self, optional: bool) -> Self {
self.optional = optional;
self
}
pub fn default_features(mut self, enable: bool) -> Self {
self.default_features = enable;
self
}
pub fn git<S: Into<String>>(mut self, url: S) -> Self {
self.git = Some(url.into());
self
}
pub fn branch<S: Into<String>>(mut self, branch: S) -> Self {
self.git_ref = Some(GitRef::Branch(branch.into()));
self
}
pub fn tag<S: Into<String>>(mut self, tag: S) -> Self {
self.git_ref = Some(GitRef::Tag(tag.into()));
self
}
pub fn rev<S: Into<String>>(mut self, rev: S) -> Self {
self.git_ref = Some(GitRef::Rev(rev.into()));
self
}
pub fn path<S: Into<String>>(mut self, path: S) -> Self {
self.path = Some(path.into());
self
}
pub fn to_toml_string(&self) -> String {
if self.features.is_empty()
&& self.default_features
&& self.version.is_none()
&& self.git.is_none()
&& self.path.is_none()
&& !self.optional
{
return format!("{} = \"*\"", self.name);
}
let mut parts = Vec::new();
if let Some(ref version) = self.version {
parts.push(format!("version = \"{}\"", version));
}
if let Some(ref git) = self.git {
parts.push(format!("git = \"{}\"", git));
if let Some(ref git_ref) = self.git_ref {
match git_ref {
GitRef::Branch(b) => parts.push(format!("branch = \"{}\"", b)),
GitRef::Tag(t) => parts.push(format!("tag = \"{}\"", t)),
GitRef::Rev(r) => parts.push(format!("rev = \"{}\"", r)),
}
}
}
if let Some(ref path) = self.path {
parts.push(format!("path = \"{}\"", path));
}
if !self.features.is_empty() {
let features_str = self
.features
.iter()
.map(|f| format!("\"{}\"", f))
.collect::<Vec<_>>()
.join(", ");
parts.push(format!("features = [{}]", features_str));
}
if self.optional {
parts.push("optional = true".to_string());
}
if !self.default_features {
parts.push("default-features = false".to_string());
}
if parts.is_empty() {
format!("{} = \"*\"", self.name)
} else {
format!("{} = {{ {} }}", self.name, parts.join(", "))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionRequirement {
Any,
Exact(String),
Caret(String),
Tilde(String),
GreaterOrEqual(String),
LessThan(String),
Complex(String),
}
impl VersionRequirement {
pub fn parse(s: String) -> Self {
if s.is_empty() || s == "*" {
return VersionRequirement::Any;
}
if s.starts_with('=') {
return VersionRequirement::Exact(s[1..].trim().to_string());
}
if s.starts_with('^') {
return VersionRequirement::Caret(s[1..].trim().to_string());
}
if s.starts_with('~') {
return VersionRequirement::Tilde(s[1..].trim().to_string());
}
if s.starts_with(">=") {
return VersionRequirement::GreaterOrEqual(s[2..].trim().to_string());
}
if s.starts_with('<') {
return VersionRequirement::LessThan(s[1..].trim().to_string());
}
if s.contains(',') {
return VersionRequirement::Complex(s);
}
VersionRequirement::Caret(s)
}
}
impl std::fmt::Display for VersionRequirement {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VersionRequirement::Any => write!(f, "*"),
VersionRequirement::Exact(v) => write!(f, "={}", v),
VersionRequirement::Caret(v) => write!(f, "^{}", v),
VersionRequirement::Tilde(v) => write!(f, "~{}", v),
VersionRequirement::GreaterOrEqual(v) => write!(f, ">={}", v),
VersionRequirement::LessThan(v) => write!(f, "<{}", v),
VersionRequirement::Complex(v) => write!(f, "{}", v),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum GitRef {
Branch(String),
Tag(String),
Rev(String),
}
#[derive(Debug, Clone, Default)]
pub struct DependencyManager {
dependencies: HashMap<String, DependencySpec>,
}
impl DependencyManager {
pub fn new() -> Self {
Self {
dependencies: HashMap::new(),
}
}
pub fn add(&mut self, dep: DependencySpec) -> &mut Self {
self.dependencies.insert(dep.name.clone(), dep);
self
}
pub fn remove(&mut self, name: &str) -> Option<DependencySpec> {
self.dependencies.remove(name)
}
pub fn get(&self, name: &str) -> Option<&DependencySpec> {
self.dependencies.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.dependencies.contains_key(name)
}
pub fn list(&self) -> Vec<&str> {
self.dependencies.keys().map(|s| s.as_str()).collect()
}
pub fn all(&self) -> &HashMap<String, DependencySpec> {
&self.dependencies
}
pub fn to_toml_section(&self) -> String {
if self.dependencies.is_empty() {
return String::new();
}
let mut lines = vec!["[dependencies]".to_string()];
let mut deps: Vec<_> = self.dependencies.values().collect();
deps.sort_by(|a, b| a.name.cmp(&b.name));
for dep in deps {
lines.push(dep.to_toml_string());
}
lines.join("\n")
}
pub fn merge(&mut self, other: &DependencyManager) {
for (name, dep) in &other.dependencies {
self.dependencies.insert(name.clone(), dep.clone());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_dependency() {
let dep = DependencySpec::new("serde");
assert_eq!(dep.name, "serde");
assert_eq!(dep.version, None);
assert!(dep.features.is_empty());
}
#[test]
fn test_dependency_with_version() {
let dep = DependencySpec::new("serde").version("1.0");
assert_eq!(dep.name, "serde");
assert_eq!(
dep.version,
Some(VersionRequirement::Caret("1.0".to_string()))
);
}
#[test]
fn test_dependency_with_features() {
let dep = DependencySpec::new("tokio").features(vec!["full", "rt"]);
assert_eq!(dep.features, vec!["full", "rt"]);
}
#[test]
fn test_version_requirement_parsing() {
assert_eq!(
VersionRequirement::parse("*".to_string()),
VersionRequirement::Any
);
assert_eq!(
VersionRequirement::parse("=1.0.0".to_string()),
VersionRequirement::Exact("1.0.0".to_string())
);
assert_eq!(
VersionRequirement::parse("^1.0".to_string()),
VersionRequirement::Caret("1.0".to_string())
);
assert_eq!(
VersionRequirement::parse("~1.0".to_string()),
VersionRequirement::Tilde("1.0".to_string())
);
assert_eq!(
VersionRequirement::parse(">=1.0".to_string()),
VersionRequirement::GreaterOrEqual("1.0".to_string())
);
assert_eq!(
VersionRequirement::parse("1.0".to_string()),
VersionRequirement::Caret("1.0".to_string())
);
}
#[test]
fn test_toml_string_simple() {
let dep = DependencySpec::new("serde");
assert_eq!(dep.to_toml_string(), "serde = \"*\"");
}
#[test]
fn test_toml_string_with_version() {
let dep = DependencySpec::new("serde").version("1.0");
assert_eq!(dep.to_toml_string(), "serde = { version = \"^1.0\" }");
}
#[test]
fn test_toml_string_with_features() {
let dep = DependencySpec::new("tokio")
.version("1.0")
.features(vec!["full"]);
assert!(dep.to_toml_string().contains("features = [\"full\"]"));
}
#[test]
fn test_dependency_manager() {
let mut manager = DependencyManager::new();
manager.add(DependencySpec::new("serde").version("1.0"));
manager.add(
DependencySpec::new("tokio")
.version("1.0")
.features(vec!["full"]),
);
assert!(manager.contains("serde"));
assert!(manager.contains("tokio"));
assert_eq!(manager.list().len(), 2);
}
#[test]
fn test_dependency_manager_remove() {
let mut manager = DependencyManager::new();
manager.add(DependencySpec::new("serde"));
assert!(manager.contains("serde"));
manager.remove("serde");
assert!(!manager.contains("serde"));
}
#[test]
fn test_toml_section_generation() {
let mut manager = DependencyManager::new();
manager.add(DependencySpec::new("serde").version("1.0"));
manager.add(DependencySpec::new("tokio").version("1.0"));
let toml = manager.to_toml_section();
assert!(toml.contains("[dependencies]"));
assert!(toml.contains("serde"));
assert!(toml.contains("tokio"));
}
#[test]
fn test_git_dependency() {
let dep = DependencySpec::new("my-crate")
.git("https://github.com/user/repo")
.branch("main");
let toml = dep.to_toml_string();
assert!(toml.contains("git = \"https://github.com/user/repo\""));
assert!(toml.contains("branch = \"main\""));
}
#[test]
fn test_path_dependency() {
let dep = DependencySpec::new("local-crate").path("../local-crate");
let toml = dep.to_toml_string();
assert!(toml.contains("path = \"../local-crate\""));
}
#[test]
fn test_optional_dependency() {
let dep = DependencySpec::new("feature-dep")
.version("1.0")
.optional(true);
let toml = dep.to_toml_string();
assert!(toml.contains("optional = true"));
}
#[test]
fn test_no_default_features() {
let dep = DependencySpec::new("minimal")
.version("1.0")
.default_features(false);
let toml = dep.to_toml_string();
assert!(toml.contains("default-features = false"));
}
}