horto_os_ui_shared/
resume.rs1use crate::error::Result;
4use crate::paths::HostPaths;
5use crate::step::Step;
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::Path;
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
13pub struct ResumeState {
14 pub steps: Vec<ResumeStepRecord>,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct ResumeStepRecord {
21 pub id: String,
23 pub step_version: u32,
25 pub completed_at: DateTime<Utc>,
27 pub ok: bool,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum StepStatus {
34 Pending,
36 Done,
38 Stale,
40 Failed,
42}
43
44pub fn load(path: &Path) -> Result<ResumeState> {
51 if !path.exists() {
52 return Ok(ResumeState::default());
53 }
54 let text = fs::read_to_string(path)?;
55 Ok(serde_json::from_str(&text)?)
56}
57
58pub fn save(path: &Path, state: &ResumeState) -> Result<()> {
65 if let Some(parent) = path.parent() {
66 fs::create_dir_all(parent)?;
67 }
68 let text = serde_json::to_string_pretty(state)?;
69 fs::write(path, text)?;
70 Ok(())
71}
72
73pub fn record_ok(paths: &HostPaths, step: &dyn Step) -> Result<()> {
79 let path = paths.resume_file();
80 let mut state = load(&path)?;
81 state.steps.retain(|r| r.id != step.id());
82 state.steps.push(ResumeStepRecord {
83 id: step.id().to_string(),
84 step_version: step.step_version(),
85 completed_at: Utc::now(),
86 ok: true,
87 });
88 save(&path, &state)
89}
90
91#[must_use]
93pub fn status_for(state: &ResumeState, step: &dyn Step) -> StepStatus {
94 match state.steps.iter().rev().find(|r| r.id == step.id()) {
95 Some(r) if r.ok && r.step_version >= step.step_version() => StepStatus::Done,
96 Some(r) if r.ok && r.step_version < step.step_version() => StepStatus::Stale,
97 Some(r) if !r.ok => StepStatus::Failed,
98 _ => StepStatus::Pending,
99 }
100}
101
102#[must_use]
104pub const fn is_stale(recorded_version: u32, binary_version: u32) -> bool {
105 binary_version > recorded_version
106}
107
108pub fn mark_failed(paths: &HostPaths, step_id: &str, step_version: u32) -> Result<()> {
116 let path = paths.resume_file();
117 let mut state = load(&path).unwrap_or_default();
118 state.steps.retain(|r| r.id != step_id);
119 state.steps.push(ResumeStepRecord {
120 id: step_id.to_string(),
121 step_version,
122 completed_at: Utc::now(),
123 ok: false,
124 });
125 save(&path, &state)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn stale_when_binary_newer() {
134 assert!(is_stale(1, 2));
135 assert!(!is_stale(2, 2));
136 assert!(!is_stale(3, 2));
137 }
138}