1use std::collections::BTreeMap;
10
11use crate::workflow::Workflow;
12use crate::workflow::text_of;
13use serde_yaml::{Mapping, Value as Yaml};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct JournalEvent {
18 pub at: String,
19 pub step: String,
20 pub detail: String,
21 pub ok: bool,
22}
23
24impl JournalEvent {
25 pub fn of(value: &Yaml) -> JournalEvent {
26 JournalEvent {
27 at: text_of(value, "at"),
28 step: text_of(value, "step"),
29 detail: text_of(value, "detail"),
30 ok: value.get("ok").and_then(|v| v.as_bool()).unwrap_or(false),
31 }
32 }
33
34 pub fn to_yaml(&self) -> Yaml {
35 let mut map = Mapping::new();
36 for (key, value) in [
37 ("at", &self.at),
38 ("step", &self.step),
39 ("detail", &self.detail),
40 ] {
41 map.insert(
42 Yaml::String(key.to_string()),
43 Yaml::String(value.to_string()),
44 );
45 }
46 map.insert(Yaml::String("ok".into()), Yaml::Bool(self.ok));
47 Yaml::Mapping(map)
48 }
49}
50
51#[derive(Debug, Clone, Default, PartialEq, Eq)]
55pub struct RunContext {
56 pub root: String,
57 pub data: String,
58 pub workflows: String,
59}
60
61impl RunContext {
62 pub fn of(value: &Yaml) -> RunContext {
63 RunContext {
64 root: text_of(value, "root"),
65 data: text_of(value, "data"),
66 workflows: text_of(value, "workflows"),
67 }
68 }
69
70 pub fn to_yaml(&self) -> Yaml {
71 let mut map = Mapping::new();
72 for (key, value) in [
73 ("root", &self.root),
74 ("data", &self.data),
75 ("workflows", &self.workflows),
76 ] {
77 map.insert(
78 Yaml::String(key.to_string()),
79 Yaml::String(value.to_string()),
80 );
81 }
82 Yaml::Mapping(map)
83 }
84}
85
86fn trim(path: &str) -> &str {
88 path.strip_suffix('/').unwrap_or(path)
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
93pub struct Task {
94 pub name: String,
95 pub workflow_name: String,
96 pub start: String,
97 pub context: RunContext,
98 pub journal: Vec<JournalEvent>,
99 pub gates: Vec<String>,
101 pub artifacts: BTreeMap<String, String>,
103}
104
105impl Task {
106 pub fn of(name: &str, payload: &Yaml) -> Task {
108 let journal = payload
109 .get("log")
110 .and_then(|v| v.as_sequence())
111 .map(|items| items.iter().map(JournalEvent::of).collect())
112 .unwrap_or_default();
113 let gates = payload
114 .get("gates")
115 .and_then(|v| v.as_sequence())
116 .map(|items| {
117 items
118 .iter()
119 .filter_map(|item| item.as_str().map(|text| text.to_string()))
120 .collect()
121 })
122 .unwrap_or_default();
123 let artifacts = payload
124 .get("artifacts")
125 .and_then(|v| v.as_mapping())
126 .map(|mapping| {
127 mapping
128 .iter()
129 .filter_map(|(key, value)| {
130 Some((key.as_str()?.to_string(), value.as_str()?.to_string()))
131 })
132 .collect()
133 })
134 .unwrap_or_default();
135 Task {
136 name: name.to_string(),
137 workflow_name: text_of(payload, "workflow"),
138 start: text_of(payload, "start"),
139 context: RunContext::of(payload),
140 journal,
141 gates,
142 artifacts,
143 }
144 }
145
146 pub fn declared(&self, kind: &str) -> Option<String> {
148 let written = self.artifacts.get(kind)?.trim();
149 if written.is_empty() {
150 None
151 } else {
152 Some(written.to_string())
153 }
154 }
155
156 pub fn artifact(&self, kind: &str, context: &RunContext) -> String {
161 let data = trim(&context.data);
162 if kind == "log" {
163 return format!("{data}/tasks/{}.yaml", self.name);
164 }
165 if let Some(written) = self.declared(kind) {
166 return if written.starts_with('/') {
167 written
168 } else {
169 format!("{}/{written}", trim(&context.root))
170 };
171 }
172 format!("{data}/artifacts/{kind}/{}.md", self.name)
173 }
174
175 pub fn done_steps(&self, workflow: &Workflow) -> Vec<String> {
180 let names = workflow.step_names();
181 let mut verdict: Vec<(String, bool)> = Vec::new();
182 for event in &self.journal {
183 let (step, extra) = match event.step.split_once('·') {
184 Some((step, rest)) => (step.to_string(), Some(rest.to_string())),
185 None => (event.step.clone(), None),
186 };
187 if !names.contains(&step) {
188 continue;
189 }
190 match verdict.iter_mut().find(|(name, _)| *name == step) {
191 Some((_, last)) => {
192 if extra.is_some() {
193 *last = *last && event.ok;
194 } else {
195 *last = event.ok;
196 }
197 }
198 None => verdict.push((step, event.ok)),
199 }
200 }
201 verdict
202 .into_iter()
203 .filter(|(_, ok)| *ok)
204 .map(|(name, _)| name)
205 .collect()
206 }
207
208 pub fn next_step(&self, workflow: &Workflow) -> Option<String> {
210 let finished = self.done_steps(workflow);
211 workflow
212 .step_names()
213 .into_iter()
214 .find(|name| !finished.contains(name))
215 }
216
217 pub fn state_line(&self, workflow: &Workflow) -> String {
219 let names = workflow.step_names();
220 if names.is_empty() {
221 return format!(
222 "这条工作流没有步骤——在 workflows/{}.yaml 的 steps 里写步骤",
223 self.workflow_name
224 );
225 }
226 match self.next_step(workflow) {
227 Some(step) => format!("下一步:{step}"),
228 None => format!("{} 个步骤都走过了", names.len()),
229 }
230 }
231
232 pub fn recorded(&self, at: &str, step: &str, detail: &str, ok: bool) -> Task {
234 let mut task = self.clone();
235 task.journal.push(JournalEvent {
236 at: at.to_string(),
237 step: step.to_string(),
238 detail: detail.to_string(),
239 ok,
240 });
241 task
242 }
243
244 pub fn with_gates(&self, notes: &[String]) -> Task {
246 let mut task = self.clone();
247 for note in notes {
248 if !task.gates.contains(note) {
249 task.gates.push(note.clone());
250 }
251 }
252 task
253 }
254
255 pub fn to_yaml(&self) -> Yaml {
257 let mut map = Mapping::new();
258 map.insert(Yaml::String("name".into()), Yaml::String(self.name.clone()));
259 map.insert(
260 Yaml::String("start".into()),
261 Yaml::String(self.start.clone()),
262 );
263 map.insert(
264 Yaml::String("workflow".into()),
265 Yaml::String(self.workflow_name.clone()),
266 );
267 map.insert(
268 Yaml::String("log".into()),
269 Yaml::Sequence(self.journal.iter().map(JournalEvent::to_yaml).collect()),
270 );
271 map.insert(
272 Yaml::String("gates".into()),
273 Yaml::Sequence(
274 self.gates
275 .iter()
276 .map(|note| Yaml::String(note.clone()))
277 .collect(),
278 ),
279 );
280 let mut artifacts = Mapping::new();
281 for (key, value) in &self.artifacts {
282 artifacts.insert(Yaml::String(key.clone()), Yaml::String(value.clone()));
283 }
284 map.insert(Yaml::String("artifacts".into()), Yaml::Mapping(artifacts));
285 if let Yaml::Mapping(context) = self.context.to_yaml() {
286 for (key, value) in context {
287 map.insert(key, value);
288 }
289 }
290 Yaml::Mapping(map)
291 }
292}