use std::{collections::BTreeMap, path::Path, str::FromStr};
use serde::Deserialize;
use super::{plan_relative, read_json, run_relative, WorkflowStore};
use crate::workflow::{
NodeId, NodeState, PlanId, PlanManifest, RunId, RunLifecycle, RunManifest, WorkflowOutcome,
WorkflowResult,
};
const UNNAMED_WORKFLOW: &str = "(unnamed)";
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct PlanInventoryItem {
pub(crate) plan_id: PlanId,
pub(crate) created_at_unix_nanos: u64,
pub(crate) workspace_identity: String,
pub(crate) name: String,
pub(crate) step_count: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct RunInventoryItem {
pub(crate) run_id: RunId,
pub(crate) created_at_unix_nanos: u64,
pub(crate) workspace_identity: String,
pub(crate) name: String,
pub(crate) lifecycle: RunLifecycle,
pub(crate) outcome: Option<WorkflowOutcome>,
pub(crate) done_steps: usize,
pub(crate) total_steps: usize,
}
#[derive(Debug, Deserialize)]
struct InventoryGraphFile {
graph: InventoryGraphBody,
}
#[derive(Debug, Deserialize)]
struct InventoryGraphBody {
name: String,
nodes: BTreeMap<String, serde::de::IgnoredAny>,
}
#[derive(Debug, Deserialize)]
struct InventoryStateFile {
state: InventoryWorkflowState,
}
#[derive(Debug, Deserialize)]
struct InventoryWorkflowState {
lifecycle: RunLifecycle,
outcome: Option<WorkflowOutcome>,
nodes: BTreeMap<NodeId, NodeState>,
}
#[derive(Debug, Deserialize)]
struct RevisionStateFile {
state: RevisionOnly,
}
#[derive(Debug, Deserialize)]
struct RevisionOnly {
revision: u64,
}
#[derive(Debug, Deserialize)]
struct LifecycleStateFile {
state: LifecycleOnly,
}
#[derive(Debug, Deserialize)]
struct LifecycleOnly {
lifecycle: RunLifecycle,
}
fn inventory_identity(
name: String,
step_count: usize,
graph_relative: &Path,
root: &crate::workflow::secure_fs::SecureDirectory,
) -> (String, usize) {
if !name.is_empty() {
return (name, step_count);
}
match read_json::<InventoryGraphFile>(root, graph_relative) {
Ok(graph) => (graph.graph.name, graph.graph.nodes.len()),
Err(_) if step_count > 0 => (UNNAMED_WORKFLOW.to_owned(), step_count),
Err(_) => (UNNAMED_WORKFLOW.to_owned(), 0),
}
}
impl WorkflowStore {
pub(crate) fn list_plan_inventory(&self) -> WorkflowResult<Vec<PlanInventoryItem>> {
let mut plans = Vec::new();
for name in self.root.directory_names(Path::new("plans"))? {
let Ok(name) = name.into_string() else {
continue;
};
let Ok(id) = PlanId::from_str(&name) else {
continue;
};
plans.push(self.read_plan_inventory(id)?);
}
plans.sort_by_key(|plan| std::cmp::Reverse((plan.created_at_unix_nanos, plan.plan_id)));
Ok(plans)
}
pub(crate) fn list_run_inventory(&self) -> WorkflowResult<Vec<RunInventoryItem>> {
let mut runs = Vec::new();
for name in self.root.directory_names(Path::new("runs"))? {
let Ok(name) = name.into_string() else {
continue;
};
let Ok(id) = RunId::from_str(&name) else {
continue;
};
runs.push(self.read_run_inventory(id)?);
}
runs.sort_by_key(|run| std::cmp::Reverse((run.created_at_unix_nanos, run.run_id)));
Ok(runs)
}
pub(crate) fn read_plan_manifest(&self, id: PlanId) -> WorkflowResult<PlanManifest> {
read_json(&self.root, &plan_relative(id, Path::new("manifest.json")))
}
pub(crate) fn read_run_revision(&self, id: RunId) -> WorkflowResult<u64> {
let state: RevisionStateFile =
read_json(&self.root, &run_relative(id, Path::new("state.json")))?;
Ok(state.state.revision)
}
pub(crate) fn read_run_lifecycle(&self, id: RunId) -> WorkflowResult<RunLifecycle> {
let state: LifecycleStateFile =
read_json(&self.root, &run_relative(id, Path::new("state.json")))?;
Ok(state.state.lifecycle)
}
pub(crate) fn read_run_inventory(&self, id: RunId) -> WorkflowResult<RunInventoryItem> {
let manifest: RunManifest =
read_json(&self.root, &run_relative(id, Path::new("manifest.json")))?;
if manifest.run_id != id {
return Err(crate::workflow::WorkflowError::Corrupt {
path: self.layout.run_manifest(id),
reason: "run manifest ID differs from its directory ID".to_owned(),
});
}
let state: InventoryStateFile =
read_json(&self.root, &run_relative(id, Path::new("state.json")))?;
let (name, total_steps) = inventory_identity(
manifest.name,
manifest.step_count,
&run_relative(id, Path::new("graph.json")),
&self.root,
);
let done_steps = state
.state
.nodes
.values()
.filter(|node| node.terminal().is_some())
.count();
Ok(RunInventoryItem {
run_id: id,
created_at_unix_nanos: manifest.created_at_unix_nanos,
workspace_identity: manifest.workspace_identity,
name,
lifecycle: state.state.lifecycle,
outcome: state.state.outcome,
done_steps,
total_steps,
})
}
fn read_plan_inventory(&self, id: PlanId) -> WorkflowResult<PlanInventoryItem> {
let manifest: PlanManifest =
read_json(&self.root, &plan_relative(id, Path::new("manifest.json")))?;
if manifest.plan_id != id {
return Err(crate::workflow::WorkflowError::Corrupt {
path: self.layout.plan_manifest(id),
reason: "plan manifest ID differs from its directory ID".to_owned(),
});
}
let (name, step_count) = inventory_identity(
manifest.name,
manifest.step_count,
&plan_relative(id, Path::new("graph.json")),
&self.root,
);
Ok(PlanInventoryItem {
plan_id: id,
created_at_unix_nanos: manifest.created_at_unix_nanos,
workspace_identity: manifest.workspace_identity,
name,
step_count,
})
}
}