use std::collections::BTreeMap;
use crate::workflow::Workflow;
use crate::workflow::text_of;
use serde_yaml::{Mapping, Value as Yaml};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct JournalEvent {
pub at: String,
pub step: String,
pub detail: String,
pub ok: bool,
}
impl JournalEvent {
pub fn of(value: &Yaml) -> JournalEvent {
JournalEvent {
at: text_of(value, "at"),
step: text_of(value, "step"),
detail: text_of(value, "detail"),
ok: value.get("ok").and_then(|v| v.as_bool()).unwrap_or(false),
}
}
pub fn to_yaml(&self) -> Yaml {
let mut map = Mapping::new();
for (key, value) in [
("at", &self.at),
("step", &self.step),
("detail", &self.detail),
] {
map.insert(
Yaml::String(key.to_string()),
Yaml::String(value.to_string()),
);
}
map.insert(Yaml::String("ok".into()), Yaml::Bool(self.ok));
Yaml::Mapping(map)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RunContext {
pub root: String,
pub data: String,
pub workflows: String,
}
impl RunContext {
pub fn of(value: &Yaml) -> RunContext {
RunContext {
root: text_of(value, "root"),
data: text_of(value, "data"),
workflows: text_of(value, "workflows"),
}
}
pub fn to_yaml(&self) -> Yaml {
let mut map = Mapping::new();
for (key, value) in [
("root", &self.root),
("data", &self.data),
("workflows", &self.workflows),
] {
map.insert(
Yaml::String(key.to_string()),
Yaml::String(value.to_string()),
);
}
Yaml::Mapping(map)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Task {
pub name: String,
pub workflow_name: String,
pub start: String,
pub context: RunContext,
pub journal: Vec<JournalEvent>,
pub gates: Vec<String>,
pub products: BTreeMap<String, String>,
}
impl Task {
pub fn of(name: &str, payload: &Yaml) -> Task {
let journal = payload
.get("log")
.and_then(|v| v.as_sequence())
.map(|items| items.iter().map(JournalEvent::of).collect())
.unwrap_or_default();
let gates = payload
.get("gates")
.and_then(|v| v.as_sequence())
.map(|items| {
items
.iter()
.filter_map(|item| item.as_str().map(|text| text.to_string()))
.collect()
})
.unwrap_or_default();
let products = payload
.get("products")
.and_then(|v| v.as_mapping())
.map(|mapping| {
mapping
.iter()
.filter_map(|(key, value)| {
Some((key.as_str()?.to_string(), value.as_str()?.to_string()))
})
.collect()
})
.unwrap_or_default();
Task {
name: name.to_string(),
workflow_name: text_of(payload, "workflow"),
start: text_of(payload, "start"),
context: RunContext::of(payload),
journal,
gates,
products,
}
}
pub fn product(&self, kind: &str) -> Option<String> {
let written = self.products.get(kind)?.trim();
if written.is_empty() {
None
} else {
Some(written.to_string())
}
}
pub fn done_steps(&self, workflow: &Workflow) -> Vec<String> {
let names = workflow.step_names();
let mut verdict: Vec<(String, bool)> = Vec::new();
for event in &self.journal {
let (step, extra) = match event.step.split_once('·') {
Some((step, rest)) => (step.to_string(), Some(rest.to_string())),
None => (event.step.clone(), None),
};
if !names.contains(&step) {
continue;
}
match verdict.iter_mut().find(|(name, _)| *name == step) {
Some((_, last)) => {
if extra.is_some() {
*last = *last && event.ok;
} else {
*last = event.ok;
}
}
None => verdict.push((step, event.ok)),
}
}
verdict
.into_iter()
.filter(|(_, ok)| *ok)
.map(|(name, _)| name)
.collect()
}
pub fn next_step(&self, workflow: &Workflow) -> Option<String> {
let finished = self.done_steps(workflow);
workflow
.step_names()
.into_iter()
.find(|name| !finished.contains(name))
}
pub fn state_line(&self, workflow: &Workflow) -> String {
let names = workflow.step_names();
if names.is_empty() {
return format!(
"这条工作流没有步骤——在 workflows/{}.yaml 的 steps 里写步骤",
self.workflow_name
);
}
match self.next_step(workflow) {
Some(step) => format!("下一步:{step}"),
None => format!("{} 个步骤都走过了", names.len()),
}
}
pub fn recorded(&self, at: &str, step: &str, detail: &str, ok: bool) -> Task {
let mut task = self.clone();
task.journal.push(JournalEvent {
at: at.to_string(),
step: step.to_string(),
detail: detail.to_string(),
ok,
});
task
}
pub fn with_gates(&self, notes: &[String]) -> Task {
let mut task = self.clone();
for note in notes {
if !task.gates.contains(note) {
task.gates.push(note.clone());
}
}
task
}
pub fn to_yaml(&self) -> Yaml {
let mut map = Mapping::new();
map.insert(Yaml::String("name".into()), Yaml::String(self.name.clone()));
map.insert(
Yaml::String("start".into()),
Yaml::String(self.start.clone()),
);
map.insert(
Yaml::String("workflow".into()),
Yaml::String(self.workflow_name.clone()),
);
map.insert(
Yaml::String("log".into()),
Yaml::Sequence(self.journal.iter().map(JournalEvent::to_yaml).collect()),
);
map.insert(
Yaml::String("gates".into()),
Yaml::Sequence(
self.gates
.iter()
.map(|note| Yaml::String(note.clone()))
.collect(),
),
);
let mut products = Mapping::new();
for (key, value) in &self.products {
products.insert(Yaml::String(key.clone()), Yaml::String(value.clone()));
}
map.insert(Yaml::String("products".into()), Yaml::Mapping(products));
if let Yaml::Mapping(context) = self.context.to_yaml() {
for (key, value) in context {
map.insert(key, value);
}
}
Yaml::Mapping(map)
}
}