Skip to main content

greentic_flow/
wizard_state.rs

1use crate::error::{FlowError, FlowErrorLocation, Result};
2use greentic_types::cbor::canonical;
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct WizardState {
10    pub flow_id: String,
11    pub locale: String,
12    pub steps: Vec<WizardStepState>,
13    pub last_updated: u64,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct WizardStepState {
18    pub node_id: String,
19    pub mode: String,
20    pub updated_at: u64,
21}
22
23pub fn wizard_state_path(flow_path: &Path, flow_id: &str) -> PathBuf {
24    let base = flow_path.parent().unwrap_or_else(|| Path::new("."));
25    base.join(".greentic/cache/flow_wizard")
26        .join(format!("{flow_id}.cbor"))
27}
28
29pub fn load_wizard_state(flow_path: &Path, flow_id: &str) -> Result<Option<WizardState>> {
30    let path = wizard_state_path(flow_path, flow_id);
31    if !path.exists() {
32        return Ok(None);
33    }
34    let bytes = fs::read(&path).map_err(|err| FlowError::Internal {
35        message: format!("read wizard state: {err}"),
36        location: FlowErrorLocation::new(None, None, None),
37    })?;
38    let state: WizardState = canonical::from_cbor(&bytes).map_err(|err| FlowError::Internal {
39        message: format!("decode wizard state: {err}"),
40        location: FlowErrorLocation::new(None, None, None),
41    })?;
42    Ok(Some(state))
43}
44
45pub fn update_wizard_state(
46    flow_path: &Path,
47    flow_id: &str,
48    node_id: &str,
49    mode: &str,
50    locale: &str,
51) -> Result<()> {
52    let mut state = load_wizard_state(flow_path, flow_id)?.unwrap_or_else(|| WizardState {
53        flow_id: flow_id.to_string(),
54        locale: locale.to_string(),
55        steps: Vec::new(),
56        last_updated: 0,
57    });
58    let now = now_epoch_secs();
59    state.locale = locale.to_string();
60    state.last_updated = now;
61    if let Some(step) = state.steps.iter_mut().find(|s| s.node_id == node_id) {
62        step.mode = mode.to_string();
63        step.updated_at = now;
64    } else {
65        state.steps.push(WizardStepState {
66            node_id: node_id.to_string(),
67            mode: mode.to_string(),
68            updated_at: now,
69        });
70    }
71    write_wizard_state(flow_path, &state)
72}
73
74pub fn remove_wizard_step(flow_path: &Path, flow_id: &str, node_id: &str) -> Result<()> {
75    let Some(mut state) = load_wizard_state(flow_path, flow_id)? else {
76        return Ok(());
77    };
78    let now = now_epoch_secs();
79    state.last_updated = now;
80    state.steps.retain(|step| step.node_id != node_id);
81    write_wizard_state(flow_path, &state)
82}
83
84fn write_wizard_state(flow_path: &Path, state: &WizardState) -> Result<()> {
85    let path = wizard_state_path(flow_path, &state.flow_id);
86    if let Some(parent) = path.parent() {
87        fs::create_dir_all(parent).map_err(|err| FlowError::Internal {
88            message: format!("create wizard state directory: {err}"),
89            location: FlowErrorLocation::new(None, None, None),
90        })?;
91    }
92    let bytes = canonical::to_canonical_cbor(state).map_err(|err| FlowError::Internal {
93        message: format!("encode wizard state: {err}"),
94        location: FlowErrorLocation::new(None, None, None),
95    })?;
96    fs::write(&path, bytes).map_err(|err| FlowError::Internal {
97        message: format!("write wizard state: {err}"),
98        location: FlowErrorLocation::new(None, None, None),
99    })?;
100    Ok(())
101}
102
103fn now_epoch_secs() -> u64 {
104    SystemTime::now()
105        .duration_since(UNIX_EPOCH)
106        .map(|d| d.as_secs())
107        .unwrap_or(0)
108}