use super::version::{Version, VersionError, VersionReq};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct Manifest {
pub package: Package,
pub dependencies: HashMap<String, Dependency>,
pub dev_dependencies: HashMap<String, Dependency>,
pub build_dependencies: HashMap<String, Dependency>,
pub features: HashMap<String, Vec<String>>,
pub default_features: Vec<String>,
pub workspace: Option<Workspace>,
pub bin: Vec<Target>,
pub lib: Option<Target>,
pub example: Vec<Target>,
pub test: Vec<Target>,
pub bench: Vec<Target>,
}
impl Manifest {
pub fn new(name: impl Into<String>, version: Version) -> Self {
Self {
package: Package::new(name, version),
dependencies: HashMap::new(),
dev_dependencies: HashMap::new(),
build_dependencies: HashMap::new(),
features: HashMap::new(),
default_features: Vec::new(),
workspace: None,
bin: Vec::new(),
lib: None,
example: Vec::new(),
test: Vec::new(),
bench: Vec::new(),
}
}
pub fn add_dependency(&mut self, name: impl Into<String>, dep: Dependency) {
self.dependencies.insert(name.into(), dep);
}
pub fn add_dev_dependency(&mut self, name: impl Into<String>, dep: Dependency) {
self.dev_dependencies.insert(name.into(), dep);
}
pub fn from_str(s: &str) -> Result<Self, ManifestError> {
parse_manifest(s)
}
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, ManifestError> {
let content =
std::fs::read_to_string(path.as_ref()).map_err(|e| ManifestError::Io(e.to_string()))?;
Self::from_str(&content)
}
pub fn to_toml(&self) -> String {
let mut output = String::new();
output.push_str("[package]\n");
output.push_str(&format!("name = \"{}\"\n", self.package.name));
output.push_str(&format!("version = \"{}\"\n", self.package.version));
if !self.package.authors.is_empty() {
output.push_str(&format!(
"authors = [{}]\n",
self.package
.authors
.iter()
.map(|a| format!("\"{}\"", a))
.collect::<Vec<_>>()
.join(", ")
));
}
if let Some(ref edition) = self.package.edition {
output.push_str(&format!("edition = \"{}\"\n", edition));
}
if let Some(ref desc) = self.package.description {
output.push_str(&format!("description = \"{}\"\n", desc));
}
if let Some(ref license) = self.package.license {
output.push_str(&format!("license = \"{}\"\n", license));
}
if let Some(ref repo) = self.package.repository {
output.push_str(&format!("repository = \"{}\"\n", repo));
}
output.push('\n');
if !self.dependencies.is_empty() {
output.push_str("[dependencies]\n");
for (name, dep) in &self.dependencies {
output.push_str(&dep.to_toml_line(name));
}
output.push('\n');
}
if !self.dev_dependencies.is_empty() {
output.push_str("[dev-dependencies]\n");
for (name, dep) in &self.dev_dependencies {
output.push_str(&dep.to_toml_line(name));
}
output.push('\n');
}
if !self.features.is_empty() {
output.push_str("[features]\n");
if !self.default_features.is_empty() {
output.push_str(&format!(
"default = [{}]\n",
self.default_features
.iter()
.map(|f| format!("\"{}\"", f))
.collect::<Vec<_>>()
.join(", ")
));
}
for (name, deps) in &self.features {
if name != "default" {
output.push_str(&format!(
"{} = [{}]\n",
name,
deps.iter()
.map(|d| format!("\"{}\"", d))
.collect::<Vec<_>>()
.join(", ")
));
}
}
output.push('\n');
}
output
}
}
#[derive(Debug, Clone)]
pub struct Package {
pub name: String,
pub version: Version,
pub authors: Vec<String>,
pub edition: Option<String>,
pub description: Option<String>,
pub license: Option<String>,
pub license_file: Option<String>,
pub repository: Option<String>,
pub homepage: Option<String>,
pub documentation: Option<String>,
pub readme: Option<String>,
pub keywords: Vec<String>,
pub categories: Vec<String>,
pub exclude: Vec<String>,
pub include: Vec<String>,
pub publish: bool,
}
impl Package {
pub fn new(name: impl Into<String>, version: Version) -> Self {
Self {
name: name.into(),
version,
authors: Vec::new(),
edition: Some("2025".to_string()),
description: None,
license: None,
license_file: None,
repository: None,
homepage: None,
documentation: None,
readme: None,
keywords: Vec::new(),
categories: Vec::new(),
exclude: Vec::new(),
include: Vec::new(),
publish: true,
}
}
}
#[derive(Debug, Clone)]
pub struct Dependency {
pub version: Option<VersionReq>,
pub git: Option<String>,
pub branch: Option<String>,
pub tag: Option<String>,
pub rev: Option<String>,
pub path: Option<String>,
pub registry: Option<String>,
pub features: Vec<String>,
pub default_features: bool,
pub optional: bool,
pub package: Option<String>,
}
impl Dependency {
pub fn version(req: impl Into<String>) -> Result<Self, VersionError> {
let req_str = req.into();
let version_req = req_str.parse()?;
Ok(Self {
version: Some(version_req),
git: None,
branch: None,
tag: None,
rev: None,
path: None,
registry: None,
features: Vec::new(),
default_features: true,
optional: false,
package: None,
})
}
pub fn git(url: impl Into<String>) -> Self {
Self {
version: None,
git: Some(url.into()),
branch: None,
tag: None,
rev: None,
path: None,
registry: None,
features: Vec::new(),
default_features: true,
optional: false,
package: None,
}
}
pub fn path(path: impl Into<String>) -> Self {
Self {
version: None,
git: None,
branch: None,
tag: None,
rev: None,
path: Some(path.into()),
registry: None,
features: Vec::new(),
default_features: true,
optional: false,
package: None,
}
}
pub fn with_feature(mut self, feature: impl Into<String>) -> Self {
self.features.push(feature.into());
self
}
pub fn with_features(mut self, features: Vec<String>) -> Self {
self.features = features;
self
}
pub fn no_default_features(mut self) -> Self {
self.default_features = false;
self
}
pub fn optional(mut self) -> Self {
self.optional = true;
self
}
pub fn with_branch(mut self, branch: impl Into<String>) -> Self {
self.branch = Some(branch.into());
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tag = Some(tag.into());
self
}
fn to_toml_line(&self, name: &str) -> String {
if let Some(ref v) = self.version {
if self.features.is_empty() && self.default_features && !self.optional {
return format!("{} = \"{}\"\n", name, v);
}
}
let mut parts = Vec::new();
if let Some(ref v) = self.version {
parts.push(format!("version = \"{}\"", v));
}
if let Some(ref git) = self.git {
parts.push(format!("git = \"{}\"", git));
}
if let Some(ref branch) = self.branch {
parts.push(format!("branch = \"{}\"", branch));
}
if let Some(ref tag) = self.tag {
parts.push(format!("tag = \"{}\"", tag));
}
if let Some(ref path) = self.path {
parts.push(format!("path = \"{}\"", path));
}
if !self.features.is_empty() {
parts.push(format!(
"features = [{}]",
self.features
.iter()
.map(|f| format!("\"{}\"", f))
.collect::<Vec<_>>()
.join(", ")
));
}
if !self.default_features {
parts.push("default-features = false".to_string());
}
if self.optional {
parts.push("optional = true".to_string());
}
format!("{} = {{ {} }}\n", name, parts.join(", "))
}
}
#[derive(Debug, Clone)]
pub struct Workspace {
pub members: Vec<String>,
pub exclude: Vec<String>,
pub default_members: Vec<String>,
pub resolver: Option<String>,
}
#[derive(Debug, Clone)]
pub struct Target {
pub name: String,
pub path: Option<String>,
pub required_features: Vec<String>,
pub test: bool,
pub doctest: bool,
pub bench: bool,
pub doc: bool,
pub edition: Option<String>,
}
impl Target {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
path: None,
required_features: Vec::new(),
test: true,
doctest: true,
bench: true,
doc: true,
edition: None,
}
}
}
fn parse_manifest(s: &str) -> Result<Manifest, ManifestError> {
let mut manifest = Manifest {
package: Package::new("unknown", Version::new(0, 1, 0)),
dependencies: HashMap::new(),
dev_dependencies: HashMap::new(),
build_dependencies: HashMap::new(),
features: HashMap::new(),
default_features: Vec::new(),
workspace: None,
bin: Vec::new(),
lib: None,
example: Vec::new(),
test: Vec::new(),
bench: Vec::new(),
};
let mut current_section = String::new();
for line in s.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
current_section = line[1..line.len() - 1].to_string();
continue;
}
if let Some(eq_pos) = line.find('=') {
let key = line[..eq_pos].trim();
let value = line[eq_pos + 1..].trim();
match current_section.as_str() {
"package" => {
parse_package_field(&mut manifest.package, key, value)?;
}
"dependencies" => {
let dep = parse_dependency(value)?;
manifest.dependencies.insert(key.to_string(), dep);
}
"dev-dependencies" => {
let dep = parse_dependency(value)?;
manifest.dev_dependencies.insert(key.to_string(), dep);
}
"build-dependencies" => {
let dep = parse_dependency(value)?;
manifest.build_dependencies.insert(key.to_string(), dep);
}
"features" => {
let features = parse_string_array(value)?;
if key == "default" {
manifest.default_features = features;
} else {
manifest.features.insert(key.to_string(), features);
}
}
_ => {}
}
}
}
Ok(manifest)
}
fn parse_package_field(pkg: &mut Package, key: &str, value: &str) -> Result<(), ManifestError> {
let value = value.trim_matches('"');
match key {
"name" => pkg.name = value.to_string(),
"version" => {
pkg.version = value
.parse()
.map_err(|e: VersionError| ManifestError::Parse(e.to_string()))?;
}
"authors" => pkg.authors = parse_string_array(value)?,
"edition" => pkg.edition = Some(value.to_string()),
"description" => pkg.description = Some(value.to_string()),
"license" => pkg.license = Some(value.to_string()),
"license-file" => pkg.license_file = Some(value.to_string()),
"repository" => pkg.repository = Some(value.to_string()),
"homepage" => pkg.homepage = Some(value.to_string()),
"documentation" => pkg.documentation = Some(value.to_string()),
"readme" => pkg.readme = Some(value.to_string()),
"keywords" => pkg.keywords = parse_string_array(value)?,
"categories" => pkg.categories = parse_string_array(value)?,
"publish" => pkg.publish = value == "true",
_ => {}
}
Ok(())
}
fn parse_dependency(value: &str) -> Result<Dependency, ManifestError> {
let value = value.trim();
if value.starts_with('"') && value.ends_with('"') {
let version_str = &value[1..value.len() - 1];
return Dependency::version(version_str).map_err(|e| ManifestError::Parse(e.to_string()));
}
if value.starts_with('{') && value.ends_with('}') {
let inner = &value[1..value.len() - 1];
let mut dep = Dependency {
version: None,
git: None,
branch: None,
tag: None,
rev: None,
path: None,
registry: None,
features: Vec::new(),
default_features: true,
optional: false,
package: None,
};
for part in inner.split(',') {
let part = part.trim();
if let Some(eq_pos) = part.find('=') {
let key = part[..eq_pos].trim();
let val = part[eq_pos + 1..].trim().trim_matches('"');
match key {
"version" => {
dep.version = Some(
val.parse()
.map_err(|e: VersionError| ManifestError::Parse(e.to_string()))?,
);
}
"git" => dep.git = Some(val.to_string()),
"branch" => dep.branch = Some(val.to_string()),
"tag" => dep.tag = Some(val.to_string()),
"rev" => dep.rev = Some(val.to_string()),
"path" => dep.path = Some(val.to_string()),
"features" => dep.features = parse_string_array(val)?,
"default-features" => dep.default_features = val == "true",
"optional" => dep.optional = val == "true",
"package" => dep.package = Some(val.to_string()),
_ => {}
}
}
}
return Ok(dep);
}
Dependency::version(value).map_err(|e| ManifestError::Parse(e.to_string()))
}
fn parse_string_array(value: &str) -> Result<Vec<String>, ManifestError> {
let value = value.trim();
if !value.starts_with('[') || !value.ends_with(']') {
return Ok(vec![value.trim_matches('"').to_string()]);
}
let inner = &value[1..value.len() - 1];
Ok(inner
.split(',')
.map(|s| s.trim().trim_matches('"').to_string())
.filter(|s| !s.is_empty())
.collect())
}
#[derive(Debug)]
pub enum ManifestError {
Io(String),
Parse(String),
MissingField(String),
}
impl std::fmt::Display for ManifestError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ManifestError::Io(e) => write!(f, "I/O error: {}", e),
ManifestError::Parse(e) => write!(f, "parse error: {}", e),
ManifestError::MissingField(field) => write!(f, "missing field: {}", field),
}
}
}
impl std::error::Error for ManifestError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_manifest() {
let toml = r#"
[package]
name = "my-package"
version = "1.0.0"
[dependencies]
serde = "1.0"
"#;
let manifest = Manifest::from_str(toml).unwrap();
assert_eq!(manifest.package.name, "my-package");
assert_eq!(manifest.package.version, Version::new(1, 0, 0));
assert!(manifest.dependencies.contains_key("serde"));
}
#[test]
fn test_dependency_version() {
let dep = Dependency::version("^1.0.0").unwrap();
assert!(dep.version.is_some());
}
#[test]
fn test_dependency_git() {
let dep = Dependency::git("https://github.com/user/repo")
.with_branch("main")
.with_features(vec!["feature1".to_string()]);
assert_eq!(dep.git, Some("https://github.com/user/repo".to_string()));
assert_eq!(dep.branch, Some("main".to_string()));
assert_eq!(dep.features.len(), 1);
}
}