greentic_flow/
wizard_state.rs1use 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}