use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::version::SemanticVersion;
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CrateFlags {
pub is_lib: bool,
pub has_bin: bool,
pub publish: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateEntity {
pub name: String,
pub path: PathBuf,
pub version: SemanticVersion,
pub published: bool,
pub dependencies: Vec<Dependency>,
#[serde(flatten)]
pub flags: CrateFlags,
pub features: Vec<String>,
}
impl CrateEntity {
#[must_use]
pub const fn new(
name: String,
path: PathBuf,
version: SemanticVersion,
published: bool,
) -> Self {
Self {
name,
path,
version,
published,
dependencies: Vec::new(),
flags: CrateFlags {
is_lib: false,
has_bin: false,
publish: true,
},
features: Vec::new(),
}
}
#[must_use]
pub const fn is_lib(&self) -> bool {
self.flags.is_lib
}
#[must_use]
pub const fn has_bin(&self) -> bool {
self.flags.has_bin
}
#[must_use]
pub const fn should_publish(&self) -> bool {
self.flags.publish
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn version(&self) -> &SemanticVersion {
&self.version
}
#[must_use]
pub const fn path(&self) -> &PathBuf {
&self.path
}
#[must_use]
pub fn depends_on(&self, crate_name: &str) -> bool {
self.dependencies.iter().any(|d| d.name == crate_name)
}
#[must_use]
pub fn workspace_dependencies(&self) -> Vec<&Dependency> {
self.dependencies
.iter()
.filter(|d| d.is_workspace())
.collect()
}
#[must_use]
pub fn external_dependencies(&self) -> Vec<&Dependency> {
self.dependencies
.iter()
.filter(|d| !d.is_workspace())
.collect()
}
}
impl PartialEq for CrateEntity {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for CrateEntity {}
impl std::hash::Hash for CrateEntity {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum DependencyKind {
#[default]
Runtime,
Dev,
Build,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DependencyFlags {
pub is_workspace: bool,
pub kind: DependencyKind,
pub is_optional: bool,
}
impl DependencyFlags {
#[must_use]
pub const fn new() -> Self {
Self {
is_workspace: false,
kind: DependencyKind::Runtime,
is_optional: false,
}
}
#[must_use]
pub fn is_dev(&self) -> bool {
self.kind == DependencyKind::Dev
}
#[must_use]
pub fn is_build(&self) -> bool {
self.kind == DependencyKind::Build
}
#[must_use]
pub const fn with_dev(mut self) -> Self {
self.kind = DependencyKind::Dev;
self
}
#[must_use]
pub const fn with_build(mut self) -> Self {
self.kind = DependencyKind::Build;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub version_req: Option<String>,
#[serde(flatten)]
pub flags: DependencyFlags,
pub features: Vec<String>,
pub registry: Option<String>,
}
impl Dependency {
#[must_use]
pub const fn new(name: String) -> Self {
Self {
name,
version_req: None,
flags: DependencyFlags::new(),
features: Vec::new(),
registry: None,
}
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn is_workspace(&self) -> bool {
self.flags.is_workspace
}
#[must_use]
pub fn is_dev(&self) -> bool {
self.flags.is_dev()
}
#[must_use]
pub fn is_build(&self) -> bool {
self.flags.is_build()
}
#[must_use]
pub const fn is_optional(&self) -> bool {
self.flags.is_optional
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Workspace {
pub root: PathBuf,
pub crates: Vec<CrateEntity>,
pub workspace_version: Option<SemanticVersion>,
}
impl Workspace {
#[must_use]
pub const fn new(root: PathBuf) -> Self {
Self {
root,
crates: Vec::new(),
workspace_version: None,
}
}
pub fn add_crate(&mut self, krate: CrateEntity) {
if !self.crates.contains(&krate) {
self.crates.push(krate);
}
}
#[must_use]
pub fn get_crate(&self, name: &str) -> Option<&CrateEntity> {
self.crates.iter().find(|c| c.name() == name)
}
pub fn get_crate_mut(&mut self, name: &str) -> Option<&mut CrateEntity> {
self.crates.iter_mut().find(|c| c.name() == name)
}
#[must_use]
pub fn lib_crates(&self) -> Vec<&CrateEntity> {
self.crates.iter().filter(|c| c.is_lib()).collect()
}
#[must_use]
pub fn publishable_crates(&self) -> Vec<&CrateEntity> {
self.crates.iter().filter(|c| c.should_publish()).collect()
}
#[must_use]
pub fn unpublished_crates(&self) -> Vec<&CrateEntity> {
self.crates
.iter()
.filter(|c| !c.published && c.should_publish())
.collect()
}
pub fn publish_order(&self) -> Result<Vec<&CrateEntity>, WorkspaceCycleError> {
use petgraph::algo::toposort;
use petgraph::graph::DiGraph;
let mut graph = DiGraph::new();
let mut indices = std::collections::HashMap::new();
for krate in &self.crates {
let idx = graph.add_node(krate.name.clone());
indices.insert(krate.name.clone(), idx);
}
for krate in &self.crates {
if let Some(&from_idx) = indices.get(&krate.name) {
for dep in krate.workspace_dependencies() {
if let Some(&to_idx) = indices.get(&dep.name) {
graph.add_edge(from_idx, to_idx, ());
}
}
}
}
toposort(&graph, None).map_or(Err(WorkspaceCycleError), |order| {
let mut crates = Vec::new();
for idx in order {
let name = &graph[idx];
if let Some(krate) = self.get_crate(name) {
crates.push(krate);
}
}
Ok(crates)
})
}
#[must_use]
pub fn crates_to_publish(&self) -> Vec<&CrateEntity> {
self.publishable_crates()
.into_iter()
.filter(|c| !c.published)
.collect()
}
}
#[derive(Debug, Clone, thiserror::Error)]
#[error("Dependency cycle detected in workspace")]
pub struct WorkspaceCycleError;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workspace_add_crate() {
let mut workspace = Workspace::new(PathBuf::from("/test"));
let krate = CrateEntity::new(
"test-crate".to_string(),
PathBuf::from("/test/test-crate"),
SemanticVersion::parse("1.0.0").unwrap(),
false,
);
workspace.add_crate(krate);
assert_eq!(workspace.crates.len(), 1);
assert!(workspace.get_crate("test-crate").is_some());
}
#[test]
fn test_crate_depends_on() {
let mut krate = CrateEntity::new(
"test-crate".to_string(),
PathBuf::from("/test"),
SemanticVersion::parse("1.0.0").unwrap(),
false,
);
krate.dependencies.push(Dependency::new("dep1".to_string()));
assert!(krate.depends_on("dep1"));
assert!(!krate.depends_on("dep2"));
}
}