mod dependency;
mod package;
mod workspace;
use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use std::str::FromStr;
use globset::{Glob, GlobSetBuilder};
use toml_edit::{Array, DocumentMut, Item, Table, value};
use crate::package::manifest::Members;
pub use self::dependency::{
Dependencies, DependenciesMut, Dependency, DependencyMut, DependencyRef,
};
pub use self::package::{Package, PackageMut};
pub use self::workspace::{Workspace, WorkspaceExclude, WorkspaceMembers, WorkspaceMut};
use super::Error;
#[derive(Clone, Debug)]
pub struct CargoManifest(DocumentMut);
impl CargoManifest {
pub fn new_package(name: impl Into<String>) -> Self {
Self({
let mut document = DocumentMut::new();
document.insert(
"package",
Item::Table({
let mut table = Table::new();
table.insert("name", value(name.into()));
table.insert("version", value("0.0.0"));
table.insert("edition", value("2024"));
table
}),
);
document
})
}
pub fn new_workspace() -> Self {
Self({
let mut document = DocumentMut::new();
document.insert(
"workspace",
Item::Table({
let mut table = Table::new();
table.insert("resolver", value("3"));
table.insert("members", value(Array::new()));
table
}),
);
document
})
}
pub fn workspace(&self) -> Option<Workspace<'_>> {
match self.0.get("workspace") {
Some(item) => item.as_table_like().map(Workspace),
None => None,
}
}
pub fn workspace_mut(&mut self) -> WorkspaceMut<'_> {
WorkspaceMut::new(self.0.entry("workspace"))
}
pub fn add_workspace_member(&mut self, path: impl AsRef<Path>) -> &mut Self {
self.workspace_mut().add_member(path);
self
}
pub fn with_workspace_member(mut self, path: impl AsRef<Path>) -> Self {
self.add_workspace_member(path);
self
}
pub fn package(&self) -> Option<Package<'_>> {
match self.0.get("package") {
Some(item) => item.as_table_like().map(Package),
None => None,
}
}
pub fn package_mut(&mut self) -> Option<PackageMut<'_>> {
match self.0.get_mut("package") {
Some(item) => item.as_table_like_mut().map(PackageMut),
None => None,
}
}
pub fn members(&self) -> Result<Members, Error> {
let mut includes = GlobSetBuilder::new();
let mut excludes = Vec::new();
if let Some(workspace) = self.workspace() {
for member in workspace.members() {
includes.add(Glob::new(member.trim_start_matches("./"))?);
}
for path in workspace.exclude() {
excludes.push(PathBuf::from(path));
}
}
let dependencies = self
.dependencies()
.into_iter()
.chain(self.dev_dependencies())
.chain(self.build_dependencies());
for dependency in dependencies {
if let Some(path) = dependency.path() {
includes.add(Glob::new(path.trim_start_matches("./"))?);
}
}
Ok(Members::new(includes.build()?, excludes))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, Error> {
std::str::from_utf8(bytes)?.parse()
}
}
impl CargoManifest {
pub fn add_dependency(&mut self, dependency: impl Into<Dependency>) -> &mut Self {
self.dependencies_mut().insert(dependency);
self
}
pub fn with_dependency(mut self, dependency: impl Into<Dependency>) -> Self {
self.add_dependency(dependency);
self
}
pub fn get_dependency(&self, name: impl AsRef<str>) -> Option<DependencyRef<'_>> {
self.dependencies().get(name)
}
pub fn get_dependency_mut(&mut self, name: impl AsRef<str>) -> Option<DependencyMut<'_>> {
self.dependencies_mut().into_get_mut(name)
}
pub fn dependencies(&self) -> Dependencies<'_> {
self.0
.get("dependencies")
.map(Into::into)
.unwrap_or_default()
}
pub fn dependencies_mut(&mut self) -> DependenciesMut<'_> {
DependenciesMut::new(self.0.entry("dependencies"))
}
}
impl CargoManifest {
pub fn add_dev_dependency(&mut self, dependency: impl Into<Dependency>) -> &mut Self {
self.dev_dependencies_mut().insert(dependency);
self
}
pub fn with_dev_dependency(mut self, dependency: impl Into<Dependency>) -> Self {
self.add_dev_dependency(dependency);
self
}
pub fn get_dev_dependency(&self, name: impl AsRef<str>) -> Option<DependencyRef<'_>> {
self.dev_dependencies().get(name)
}
pub fn get_dev_dependency_mut(&mut self, name: impl AsRef<str>) -> Option<DependencyMut<'_>> {
self.dev_dependencies_mut().into_get_mut(name)
}
pub fn dev_dependencies(&self) -> Dependencies<'_> {
self.0
.get("dev-dependencies")
.map(Into::into)
.unwrap_or_default()
}
pub fn dev_dependencies_mut(&mut self) -> DependenciesMut<'_> {
DependenciesMut::new(self.0.entry("dev-dependencies"))
}
}
impl CargoManifest {
pub fn add_build_dependency(&mut self, dependency: impl Into<Dependency>) -> &mut Self {
self.build_dependencies_mut().insert(dependency);
self
}
pub fn with_build_dependency(mut self, dependency: impl Into<Dependency>) -> Self {
self.add_build_dependency(dependency);
self
}
pub fn get_build_dependency(&self, name: impl AsRef<str>) -> Option<DependencyRef<'_>> {
self.build_dependencies().get(name)
}
pub fn get_build_dependency_mut(&mut self, name: impl AsRef<str>) -> Option<DependencyMut<'_>> {
self.build_dependencies_mut().into_get_mut(name)
}
pub fn build_dependencies(&self) -> Dependencies<'_> {
self.0
.get("build-dependencies")
.map(Into::into)
.unwrap_or_default()
}
pub fn build_dependencies_mut(&mut self) -> DependenciesMut<'_> {
DependenciesMut::new(self.0.entry("build-dependencies"))
}
}
impl Default for CargoManifest {
fn default() -> Self {
Self::new_workspace()
}
}
impl Display for CargoManifest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl PartialEq for CargoManifest {
fn eq(&self, other: &Self) -> bool {
self.0.to_string() == other.0.to_string()
}
}
impl Eq for CargoManifest {}
impl FromStr for CargoManifest {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(s.parse()?))
}
}
#[cfg(test)]
mod tests {
use std::path::Path;
use semver::Version;
use toml_edit::{DocumentMut, value};
use crate::package::manifest::cargo::Dependency;
use super::CargoManifest;
#[test]
fn test_package_version() {
let mut document = DocumentMut::new();
document["package"]["name"] = value("example");
let mut manifest = CargoManifest(document);
assert_eq!(manifest.package().unwrap().version(), Version::new(0, 0, 0));
manifest
.package_mut()
.unwrap()
.set_version(Version::new(0, 1, 0));
assert_eq!(manifest.package().unwrap().version(), Version::new(0, 1, 0));
manifest
.package_mut()
.unwrap()
.set_version(Version::new(0, 2, 0));
assert_eq!(manifest.package().unwrap().version(), Version::new(0, 2, 0));
}
#[test]
fn test_dependencies() {
let mut manifest = CargoManifest::new_package("example");
assert_eq!(manifest.dependencies().into_iter().count(), 0);
assert_eq!(manifest.dependencies_mut().into_iter().count(), 0);
assert!(manifest.0.get("dependencies").is_none());
manifest.add_dependency(Dependency::new("example-one").with_version(Version::new(0, 1, 0)));
assert_eq!(manifest.dependencies().into_iter().count(), 1);
assert_eq!(manifest.dependencies_mut().into_iter().count(), 1);
let one = manifest.get_dependency("example-one").unwrap();
assert_eq!(one.name(), "example-one");
assert_eq!(one.version(), Some("0.1.0"));
assert_eq!(one.path(), None);
manifest.add_dependency(Dependency::new("example-two").with_path("../example-two"));
assert_eq!(manifest.dependencies().into_iter().count(), 2);
assert_eq!(manifest.dependencies_mut().into_iter().count(), 2);
let two = manifest.get_dependency("example-two").unwrap();
assert_eq!(two.name(), "example-two");
assert_eq!(two.version(), None);
assert_eq!(two.path(), Some("../example-two"));
manifest.add_dependency(
Dependency::new("example-three")
.with_version(Version::new(0, 2, 0))
.with_path("../example-three"),
);
assert_eq!(manifest.dependencies().into_iter().count(), 3);
assert_eq!(manifest.dependencies_mut().into_iter().count(), 3);
let three = manifest.get_dependency("example-three").unwrap();
assert_eq!(three.name(), "example-three");
assert_eq!(three.version(), Some("0.2.0"));
assert_eq!(three.path(), Some("../example-three"));
}
#[test]
fn test_members() {
let mut manifest = CargoManifest::new_workspace();
manifest.add_workspace_member("packages/*");
manifest.add_workspace_member("packages/example");
manifest.add_workspace_member("examples/example");
let members = manifest.members().unwrap();
assert!(members.includes(Path::new("packages/example")));
assert!(members.includes(Path::new("examples/example")));
let expected = indoc::indoc! {r#"
[workspace]
resolver = "3"
members = ["packages/*", "examples/example"]
"#};
assert_eq!(manifest.to_string(), expected);
}
}