Skip to main content

determa_state/
store.rs

1//! Store backends (SPEC §8/§13.1). A store persists registered definition YAML
2//! sources, instance snapshots, the virtual clock, and the processing mode. The
3//! three standard backends — `file`, `mem`, `sqlite` — are behaviorally identical.
4
5use crate::runtime::{Mode, Snapshot};
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::fs;
9use std::path::PathBuf;
10
11#[derive(Debug, Clone, Serialize, Deserialize, Default)]
12pub struct StoreData {
13    #[serde(default)]
14    pub defs: Vec<String>,
15    #[serde(default)]
16    pub instances: Vec<Snapshot>,
17    #[serde(default)]
18    pub clock: u64,
19    #[serde(default = "default_mode")]
20    pub mode: String,
21}
22
23fn default_mode() -> String {
24    "auto".to_string()
25}
26
27impl StoreData {
28    pub fn mode(&self) -> Mode {
29        if self.mode == "manual" {
30            Mode::Manual
31        } else {
32            Mode::Auto
33        }
34    }
35}
36
37/// A parsed `--store` spec.
38#[derive(Debug, Clone)]
39pub enum StoreSpec {
40    File(PathBuf),
41    Mem,
42    Sqlite(PathBuf),
43}
44
45impl StoreSpec {
46    pub fn parse(spec: &str) -> StoreSpec {
47        if let Some(rest) = spec.strip_prefix("file:") {
48            StoreSpec::File(PathBuf::from(rest))
49        } else if spec == "mem:" || spec == "mem" {
50            StoreSpec::Mem
51        } else if let Some(rest) = spec.strip_prefix("sqlite:") {
52            StoreSpec::Sqlite(PathBuf::from(rest))
53        } else {
54            StoreSpec::File(PathBuf::from(spec))
55        }
56    }
57}
58
59pub trait Store {
60    fn load(&self) -> StoreData;
61    fn save(&self, data: &StoreData);
62}
63
64// ---- file backend (single JSON document in a directory) ----
65
66pub struct FileStore {
67    dir: PathBuf,
68}
69
70impl FileStore {
71    pub fn new(dir: PathBuf) -> Self {
72        FileStore { dir }
73    }
74    fn path(&self) -> PathBuf {
75        self.dir.join("store.json")
76    }
77}
78
79impl Store for FileStore {
80    fn load(&self) -> StoreData {
81        match fs::read(self.path()) {
82            Ok(bytes) => serde_json::from_slice(&bytes).unwrap_or_default(),
83            Err(_) => StoreData::default(),
84        }
85    }
86    fn save(&self, data: &StoreData) {
87        let _ = fs::create_dir_all(&self.dir);
88        let bytes = serde_json::to_vec_pretty(data).expect("serialize store");
89        let _ = fs::write(self.path(), bytes);
90    }
91}
92
93// ---- in-memory backend (ephemeral; lives only within one process) ----
94
95use std::cell::RefCell;
96use std::rc::Rc;
97
98pub struct MemStore {
99    inner: Rc<RefCell<StoreData>>,
100}
101
102impl MemStore {
103    pub fn new() -> Self {
104        MemStore {
105            inner: Rc::new(RefCell::new(StoreData::default())),
106        }
107    }
108    pub fn handle(&self) -> Rc<RefCell<StoreData>> {
109        self.inner.clone()
110    }
111}
112
113impl Default for MemStore {
114    fn default() -> Self {
115        Self::new()
116    }
117}
118
119impl Store for MemStore {
120    fn load(&self) -> StoreData {
121        self.inner.borrow().clone()
122    }
123    fn save(&self, data: &StoreData) {
124        *self.inner.borrow_mut() = data.clone();
125    }
126}
127
128// ---- sqlite backend (single-row JSON blob) ----
129
130pub struct SqliteStore {
131    path: PathBuf,
132}
133
134impl SqliteStore {
135    pub fn new(path: PathBuf) -> Self {
136        SqliteStore { path }
137    }
138    fn conn(&self) -> rusqlite::Result<rusqlite::Connection> {
139        if let Some(parent) = self.path.parent() {
140            let _ = std::fs::create_dir_all(parent);
141        }
142        let conn = rusqlite::Connection::open(&self.path)?;
143        conn.execute_batch(
144            "CREATE TABLE IF NOT EXISTS determa_store (id INTEGER PRIMARY KEY, data TEXT NOT NULL);",
145        )?;
146        Ok(conn)
147    }
148}
149
150impl Store for SqliteStore {
151    fn load(&self) -> StoreData {
152        let conn = match self.conn() {
153            Ok(c) => c,
154            Err(_) => return StoreData::default(),
155        };
156        let row: Option<String> = conn
157            .query_row(
158                "SELECT data FROM determa_store WHERE id = 0",
159                [],
160                |r| r.get(0),
161            )
162            .ok();
163        match row {
164            Some(s) => serde_json::from_str(&s).unwrap_or_default(),
165            None => StoreData::default(),
166        }
167    }
168    fn save(&self, data: &StoreData) {
169        if let Ok(conn) = self.conn() {
170            let s = serde_json::to_string(data).unwrap_or_default();
171            let _ = conn.execute(
172                "INSERT INTO determa_store (id, data) VALUES (0, ?1) \
173                 ON CONFLICT(id) DO UPDATE SET data = excluded.data",
174                rusqlite::params![s],
175            );
176        }
177    }
178}
179
180/// Open the appropriate backend for a spec.
181pub fn open(spec: &StoreSpec) -> Box<dyn Store> {
182    match spec {
183        StoreSpec::File(dir) => Box::new(FileStore::new(dir.clone())),
184        StoreSpec::Mem => Box::new(MemStore::new()),
185        StoreSpec::Sqlite(path) => Box::new(SqliteStore::new(path.clone())),
186    }
187}
188
189/// Map a `BTreeMap<String,Value>` snapshot map back into typed data (unused hook).
190pub fn _inflate(_m: BTreeMap<String, crate::Value>) {}