1use crate::error::{Error, Result};
8use crate::version::Version;
9use petgraph::graph::{DiGraph, NodeIndex};
10use petgraph::visit::Topo;
11use std::collections::HashMap;
12use std::path::PathBuf;
13
14#[derive(Debug, Clone)]
16pub struct PublishPackage {
17 pub name: String,
19 pub path: PathBuf,
21 pub version: Version,
23 pub dependencies: Vec<String>,
25}
26
27#[derive(Debug, Clone)]
29pub struct PublishPlan {
30 pub packages: Vec<PublishPackage>,
32}
33
34impl PublishPlan {
35 pub fn from_packages(packages: Vec<PublishPackage>) -> Result<Self> {
41 if packages.is_empty() {
42 return Ok(Self { packages: vec![] });
43 }
44
45 let mut graph: DiGraph<String, ()> = DiGraph::new();
47 let mut node_map: HashMap<String, NodeIndex> = HashMap::new();
48
49 for pkg in &packages {
51 let idx = graph.add_node(pkg.name.clone());
52 node_map.insert(pkg.name.clone(), idx);
53 }
54
55 for pkg in &packages {
57 let dependent_idx = node_map[&pkg.name];
58 for dep in &pkg.dependencies {
59 if let Some(&dep_idx) = node_map.get(dep) {
61 graph.add_edge(dep_idx, dependent_idx, ());
62 }
63 }
64 }
65
66 let mut topo = Topo::new(&graph);
68 let mut ordered_names = Vec::new();
69
70 while let Some(idx) = topo.next(&graph) {
71 ordered_names.push(graph[idx].clone());
72 }
73
74 if ordered_names.len() != packages.len() {
77 return Err(Error::config(
78 "Dependency cycle detected in packages to publish",
79 "Check package dependencies for circular references",
80 ));
81 }
82
83 let pkg_map: HashMap<String, PublishPackage> =
85 packages.into_iter().map(|p| (p.name.clone(), p)).collect();
86
87 let ordered_packages: Vec<PublishPackage> = ordered_names
88 .into_iter()
89 .filter_map(|name| pkg_map.get(&name).cloned())
90 .collect();
91
92 Ok(Self {
93 packages: ordered_packages,
94 })
95 }
96
97 #[must_use]
99 pub const fn len(&self) -> usize {
100 self.packages.len()
101 }
102
103 #[must_use]
105 pub const fn is_empty(&self) -> bool {
106 self.packages.is_empty()
107 }
108
109 pub fn iter(&self) -> impl Iterator<Item = &PublishPackage> {
111 self.packages.iter()
112 }
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn make_package(name: &str, deps: Vec<&str>) -> PublishPackage {
120 PublishPackage {
121 name: name.to_string(),
122 path: PathBuf::from(format!("packages/{name}")),
123 version: Version::new(1, 0, 0),
124 dependencies: deps.into_iter().map(String::from).collect(),
125 }
126 }
127
128 #[test]
129 fn test_publish_plan_empty() {
130 let plan = PublishPlan::from_packages(vec![]).unwrap();
131 assert!(plan.is_empty());
132 assert_eq!(plan.len(), 0);
133 }
134
135 #[test]
136 fn test_publish_plan_single_package() {
137 let packages = vec![make_package("pkg-a", vec![])];
138 let plan = PublishPlan::from_packages(packages).unwrap();
139
140 assert_eq!(plan.len(), 1);
141 assert_eq!(plan.packages[0].name, "pkg-a");
142 }
143
144 #[test]
145 fn test_publish_plan_linear_deps() {
146 let packages = vec![
148 make_package("pkg-c", vec!["pkg-b"]),
149 make_package("pkg-b", vec!["pkg-a"]),
150 make_package("pkg-a", vec![]),
151 ];
152
153 let plan = PublishPlan::from_packages(packages).unwrap();
154
155 assert_eq!(plan.len(), 3);
156 assert_eq!(plan.packages[0].name, "pkg-a");
158 assert_eq!(plan.packages[1].name, "pkg-b");
159 assert_eq!(plan.packages[2].name, "pkg-c");
160 }
161
162 #[test]
163 fn test_publish_plan_diamond_deps() {
164 let packages = vec![
168 make_package("pkg-d", vec!["pkg-b", "pkg-c"]),
169 make_package("pkg-b", vec!["pkg-a"]),
170 make_package("pkg-c", vec!["pkg-a"]),
171 make_package("pkg-a", vec![]),
172 ];
173
174 let plan = PublishPlan::from_packages(packages).unwrap();
175
176 assert_eq!(plan.len(), 4);
177 assert_eq!(plan.packages[0].name, "pkg-a");
179 assert_eq!(plan.packages[3].name, "pkg-d");
181 let middle: Vec<&str> = plan.packages[1..3]
183 .iter()
184 .map(|p| p.name.as_str())
185 .collect();
186 assert!(middle.contains(&"pkg-b"));
187 assert!(middle.contains(&"pkg-c"));
188 }
189
190 #[test]
191 fn test_publish_plan_external_deps() {
192 let packages = vec![
194 make_package("pkg-a", vec!["external-dep"]),
195 make_package("pkg-b", vec!["pkg-a"]),
196 ];
197
198 let plan = PublishPlan::from_packages(packages).unwrap();
199
200 assert_eq!(plan.len(), 2);
201 assert_eq!(plan.packages[0].name, "pkg-a");
202 assert_eq!(plan.packages[1].name, "pkg-b");
203 }
204
205 #[test]
206 fn test_publish_plan_iteration() {
207 let packages = vec![
208 make_package("pkg-b", vec!["pkg-a"]),
209 make_package("pkg-a", vec![]),
210 ];
211
212 let plan = PublishPlan::from_packages(packages).unwrap();
213
214 let names: Vec<&str> = plan.iter().map(|p| p.name.as_str()).collect();
215 assert_eq!(names, vec!["pkg-a", "pkg-b"]);
216 }
217}