1use std::collections::HashSet;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash)]
10pub struct NodeId(pub String);
11
12impl NodeId {
13 pub fn new(id: impl Into<String>) -> Self {
14 Self(id.into())
15 }
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum NodeStatus {
22 Ok,
23 Failed,
24 Running,
25 Queued,
26 Skipped,
27 Neutral,
28}
29
30impl NodeStatus {
31 pub fn glyph(self) -> &'static str {
33 match self {
34 Self::Ok => "✓",
35 Self::Failed => "✗",
36 Self::Running => "●",
37 Self::Queued => "·",
38 Self::Skipped => "–",
39 Self::Neutral => "",
40 }
41 }
42
43 #[must_use]
46 pub fn worse(self, other: Self) -> Self {
47 let rank = |s: Self| match s {
48 Self::Failed => 5,
49 Self::Running => 4,
50 Self::Queued => 3,
51 Self::Skipped => 2,
52 Self::Neutral => 1,
53 Self::Ok => 0,
54 };
55 if rank(self) >= rank(other) {
56 self
57 } else {
58 other
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub enum RankDir {
65 TopDown,
66 LeftRight,
67}
68
69#[derive(Debug, Clone)]
70pub struct Node {
71 pub id: NodeId,
72 pub label: String,
73 pub status: NodeStatus,
74 pub group: Option<String>,
78 pub foldable: Option<String>,
82}
83
84impl Node {
85 pub fn leaf(id: &str, status: NodeStatus) -> Self {
87 Self {
88 id: NodeId::new(id),
89 label: id.to_owned(),
90 status,
91 group: None,
92 foldable: None,
93 }
94 }
95
96 pub fn in_group(mut self, group: &str) -> Self {
98 self.group = Some(group.to_owned());
99 self
100 }
101
102 pub fn fold_root(mut self, group: &str) -> Self {
104 self.foldable = Some(group.to_owned());
105 self
106 }
107}
108
109#[derive(Debug, Clone)]
110pub struct Edge {
111 pub from: NodeId,
112 pub to: NodeId,
113 pub label: Option<String>,
114}
115
116#[derive(Debug, Clone)]
117pub struct Model {
118 pub rankdir: RankDir,
119 pub nodes: Vec<Node>,
120 pub edges: Vec<Edge>,
121}
122
123impl Model {
124 pub fn new(rankdir: RankDir) -> Self {
125 Self {
126 rankdir,
127 nodes: Vec::new(),
128 edges: Vec::new(),
129 }
130 }
131
132 pub fn index_of(&self, id: &NodeId) -> Option<usize> {
134 self.nodes.iter().position(|n| &n.id == id)
135 }
136
137 pub fn foldable_of(&self, id: &NodeId) -> Option<String> {
140 self.nodes
141 .iter()
142 .find(|n| &n.id == id)
143 .and_then(|n| n.foldable.clone())
144 }
145
146 #[must_use]
151 pub fn collapse(&self, collapsed: &HashSet<String>) -> Model {
152 let hidden: HashSet<&NodeId> = self
153 .nodes
154 .iter()
155 .filter(|n| n.group.as_deref().is_some_and(|g| collapsed.contains(g)))
156 .map(|n| &n.id)
157 .collect();
158
159 let mut out = Model::new(self.rankdir);
160 for node in &self.nodes {
161 if hidden.contains(&node.id) {
162 continue;
163 }
164 let mut node = node.clone();
165 if let Some(group) = node.foldable.clone() {
166 let mut worst = node.status;
167 let mut count = 0usize;
168 for member in &self.nodes {
169 if member.group.as_deref() == Some(group.as_str()) {
170 worst = worst.worse(member.status);
171 count += 1;
172 }
173 }
174 node.status = worst;
175 node.label = if collapsed.contains(&group) {
176 format!("▸ {} ({count})", node.label)
177 } else {
178 format!("▾ {}", node.label)
179 };
180 }
181 out.nodes.push(node);
182 }
183 out.edges = self
184 .edges
185 .iter()
186 .filter(|e| !hidden.contains(&e.from) && !hidden.contains(&e.to))
187 .cloned()
188 .collect();
189 out
190 }
191
192 pub fn demo() -> Self {
195 use NodeStatus::{Failed, Neutral, Ok, Queued, Running};
196 let mut model = Self::new(RankDir::TopDown);
197 model.nodes = vec![
198 Node::leaf("lint", Ok),
199 Node::leaf("typos", Ok),
200 Node::leaf("deny", Ok),
201 Node::leaf("test", Neutral).fold_root("test"),
203 Node::leaf("test ubuntu", Ok).in_group("test"),
204 Node::leaf("test macos", Ok).in_group("test"),
205 Node::leaf("test windows", Failed).in_group("test"),
206 Node::leaf("build", Running),
207 Node::leaf("publish-crates", Queued),
208 Node::leaf("publish-npm", Queued),
209 Node::leaf("publish-aur", Queued),
210 ];
211 let edge = |from: &str, to: &str| Edge {
212 from: NodeId::new(from),
213 to: NodeId::new(to),
214 label: None,
215 };
216 model.edges = vec![
217 edge("lint", "test"),
220 edge("typos", "test"),
221 edge("deny", "test"),
222 edge("test", "build"),
223 edge("build", "publish-crates"),
224 edge("build", "publish-npm"),
225 edge("build", "publish-aur"),
226 ];
227 model
228 }
229}