Skip to main content

ravenrs_app/model/
store.rs

1//! Where a layer's difference is kept.
2
3use std::cell::RefCell;
4
5use super::layers::Diff;
6use crate::ModelError;
7
8/// Keeps one layer's difference.
9pub trait ModelStore {
10    /// What it holds; `None` when nothing was kept yet.
11    fn load(&self) -> Result<Option<Diff>, ModelError>;
12    /// Keeps this.
13    fn save(&self, difference: &Diff) -> Result<(), ModelError>;
14}
15
16/// A difference that came inside the program, as a module's does. It cannot change.
17#[derive(Clone, Copy, Debug)]
18#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
19pub struct EmbeddedStore(pub &'static str);
20
21impl ModelStore for EmbeddedStore {
22    fn load(&self) -> Result<Option<Diff>, ModelError> {
23        Diff::parse(self.0).map(Some)
24    }
25
26    fn save(&self, _difference: &Diff) -> Result<(), ModelError> {
27        Err(ModelError("a difference inside the program cannot be saved".into()))
28    }
29}
30
31/// A difference that lasts as long as the program, where nothing else is at hand.
32#[derive(Debug, Default)]
33#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
34pub struct MemoryModelStore(RefCell<Option<Diff>>);
35
36impl ModelStore for MemoryModelStore {
37    fn load(&self) -> Result<Option<Diff>, ModelError> {
38        Ok(self.0.borrow().clone())
39    }
40
41    fn save(&self, difference: &Diff) -> Result<(), ModelError> {
42        *self.0.borrow_mut() = Some(difference.clone());
43        Ok(())
44    }
45}
46
47/// The person's own settings kept in a [`Storage`](ravenrs_core::Storage) under a name: a file on
48/// the desktop, the browser's IndexedDB on the web, beside the records.
49#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
50pub struct StoredModel {
51    storage: std::rc::Rc<dyn ravenrs_core::Storage>,
52    name: String,
53}
54
55impl std::fmt::Debug for StoredModel {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        f.debug_struct("StoredModel").field("name", &self.name).finish_non_exhaustive()
58    }
59}
60
61#[cfg_attr(feature = "live", ravenrs_live::expose)]
62impl StoredModel {
63    /// Settings kept in `storage` under `name`.
64    pub fn new(storage: impl ravenrs_core::Storage + 'static, name: impl Into<String>) -> Self {
65        Self { storage: std::rc::Rc::new(storage), name: name.into() }
66    }
67}
68
69impl ModelStore for StoredModel {
70    fn load(&self) -> Result<Option<Diff>, ModelError> {
71        let failed = |e: String| ModelError(format!("{}: {e}", self.name));
72        let Some(bytes) = self.storage.read(&self.name).map_err(failed)? else { return Ok(None) };
73        let text = String::from_utf8(bytes).map_err(|e| failed(e.to_string()))?;
74        Diff::parse(&text).map(Some).map_err(|e| failed(e.to_string()))
75    }
76
77    fn save(&self, difference: &Diff) -> Result<(), ModelError> {
78        self.storage
79            .write(&self.name, difference.to_json().as_bytes())
80            .map_err(|e| ModelError(format!("{}: {e}", self.name)))
81    }
82}
83
84/// A difference in a file. A failure while saving leaves the old file whole.
85#[cfg(not(target_arch = "wasm32"))]
86#[derive(Clone, Debug)]
87#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
88pub struct FileStore(pub std::path::PathBuf);
89
90#[cfg(not(target_arch = "wasm32"))]
91impl ModelStore for FileStore {
92    fn load(&self) -> Result<Option<Diff>, ModelError> {
93        match std::fs::read_to_string(&self.0) {
94            Ok(text) => {
95                Diff::parse(&text).map(Some).map_err(|e| ModelError(format!("{}: {e}", self.0.display())))
96            }
97            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
98            Err(e) => Err(ModelError(format!("{}: {e}", self.0.display()))),
99        }
100    }
101
102    fn save(&self, difference: &Diff) -> Result<(), ModelError> {
103        let failed = |e: std::io::Error| ModelError(format!("{}: {e}", self.0.display()));
104        if let Some(folder) = self.0.parent() {
105            std::fs::create_dir_all(folder).map_err(failed)?;
106        }
107        let draft = self.0.with_extension("tmp");
108        std::fs::write(&draft, difference.to_json()).map_err(failed)?;
109        std::fs::rename(&draft, &self.0).map_err(failed)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116    use crate::{ModelLayer, ModelLayers, ModelNode};
117
118    fn node() -> ModelNode {
119        ModelNode::new("Application").child(ModelNode::new("Views"))
120    }
121
122    #[test]
123    fn embedded_differences_load_and_refuse_saving() {
124        let store = EmbeddedStore(r#"{ "Views": { "Caption": "x" } }"#);
125        let mut layers = ModelLayers::new(node());
126        let module = ModelLayer::Module("m".into());
127        layers.load(&module, &store).expect("loads");
128        assert_eq!(layers.merged().root.at(&["Views"]).and_then(|n| n.text("Caption")), Some("x"));
129        assert!(layers.save(&module, &store).is_err());
130    }
131
132    #[test]
133    fn a_memory_store_gives_back_what_it_was_given() {
134        let store = MemoryModelStore::default();
135        let mut layers = ModelLayers::new(node());
136        layers.load(&ModelLayer::User, &store).expect("nothing is fine");
137        layers.set(&ModelLayer::User, &["Views"], "Caption", "x".into());
138        layers.save(&ModelLayer::User, &store).expect("saves");
139        let mut again = ModelLayers::new(node());
140        again.load(&ModelLayer::User, &store).expect("loads");
141        assert_eq!(again, layers);
142    }
143
144    #[cfg(not(target_arch = "wasm32"))]
145    #[test]
146    fn a_file_store_keeps_it_on_disk() {
147        let folder = std::env::temp_dir().join(format!("ravenrs-app-{}", std::process::id()));
148        let store = FileStore(folder.join("deep").join("user.json"));
149        assert_eq!(store.load(), Ok(None));
150        let mut layers = ModelLayers::new(node());
151        layers.set(&ModelLayer::User, &["Views"], "Caption", "x".into());
152        layers.save(&ModelLayer::User, &store).expect("saves");
153        assert_eq!(store.load().expect("loads").as_ref(), layers.difference(&ModelLayer::User));
154        std::fs::write(&store.0, "{ broken").expect("writes");
155        assert!(store.load().is_err_and(|e| e.0.contains("user.json")), "says which file");
156        std::fs::remove_dir_all(folder).expect("cleans up");
157    }
158}