ravenrs_app/model/
store.rs1use std::cell::RefCell;
4
5use super::layers::Diff;
6use crate::ModelError;
7
8pub trait ModelStore {
10 fn load(&self) -> Result<Option<Diff>, ModelError>;
12 fn save(&self, difference: &Diff) -> Result<(), ModelError>;
14}
15
16#[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#[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#[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 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#[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}