lib_efo/epoch/epoch_traits.rs
1//! epoch_traits.rs
2//!
3//! All traits used by the `lib_efo` library.
4//! These traits are marker traits that describe behaviour
5//! common to many of the library's element types.
6#![warn(clippy::missing_docs_in_private_items)]
7
8use cli_table::{Style, Table, WithTitle};
9use serde::Serialize;
10use std::{fmt::Display, fs::File, io::Write};
11use uuid::Uuid;
12
13use crate::{epoch::SaveMode, error::EpochError, items_table};
14
15/// Elements that can be compared with another instance to produce a diff.
16pub trait Diffable {
17 /// Compare self with another struct of the same type and return a new instance containing the difference
18 fn delta(&self, other: &Self) -> Self;
19}
20
21/// Elements that can be applied to the current world state.
22pub trait Applyable {}
23
24/// Elements that can advance to a next state over time.
25///
26/// Implementors usually provide a `.next()` method that returns the
27/// element's state after a time step (e.g., fading or diminishing).
28pub trait Nextable {}
29
30/// Elements that can be rendered into a pretty, human-readable form.
31pub trait Prettiable: Display {
32 /// Shows many items as a table.
33 fn as_table(list: Vec<&Self>) -> std::result::Result<cli_table::TableDisplay, EpochError>
34 where
35 for<'a> Vec<&'a Self>: Table + WithTitle,
36 {
37 items_table!(list, "Converting to table".to_string(), Uuid::nil())
38 }
39
40 /// Displays the struct as a log entry by printing the output of
41 /// `as_log()` to stdout.
42 fn show_as_log(&self) {
43 println!("{}", self.as_log())
44 }
45
46 /// Converts the struct into a pretty log string.
47 fn as_log(&self) -> String {
48 format!("{}", self)
49 }
50}
51
52/// Trait representing a serialisable / writable config
53pub trait ConfigWritable: Serialize {
54 /// The full path indicating where to save the config
55 fn save_path(&self) -> String;
56
57 /// The folder where to save the file to save the config
58 fn save_folder(&self) -> String;
59
60 /// Serialises the config into a pretty JSON string.
61 ///
62 /// # Arguments
63 /// * `action` - A description of the action for error reporting.
64 ///
65 /// # Returns
66 /// * `Ok(String)` containing the pretty-printed JSON.
67 /// * `Err(EpochError::Serde)` if serialisation fails.
68 fn as_json(&self, action: String) -> Result<String, EpochError> {
69 serde_json::to_string_pretty(self).map_err(|e| EpochError::Serde {
70 source: e,
71 file_name: action,
72 version: format!("Current Version:{}", env!("EFO_SHORT_VERSION")),
73 })
74 }
75
76 /// Saves the config into its nominal path.
77 ///
78 /// Existing subdirectories are not created.
79 /// If `do_not_erase` is `SaveMode::Erase`, an existing file is overwritten.
80 /// If `do_not_erase` is `SaveMode::DoNotErase`, an error is returned when the
81 /// file already exists.
82 ///
83 /// # Arguments
84 /// * `action_description` - A human-readable description used in error messages.
85 /// * `do_not_erase` - Determines whether to overwrite an existing file.
86 ///
87 /// # Returns
88 /// * `Ok(())` on success.
89 /// * `Err(EpochError::ConfigWrite)` on any serialization error.
90 /// * `Err(EpochError::ConfigCreation)` on any I/O error.
91 fn save_with_context(
92 &self,
93 // Very unsure about this versus a macro
94 action_description: &str,
95 do_not_erase: &SaveMode,
96 // advice: Option<String>,
97 ) -> Result<(), EpochError> {
98 let mut f = match *do_not_erase {
99 SaveMode::Erase => File::create(self.save_path()),
100 SaveMode::DoNotErase => File::create_new(self.save_path()),
101 }
102 .map_err(|e| EpochError::ConfigCreation(self.save_path(), e))?;
103 write!(f, "{}", self.as_json(action_description.to_owned())?)
104 .map_err(EpochError::ConfigWrite)?;
105 Ok(())
106 }
107}
108
109#[allow(clippy::unwrap_used)]
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use derive_more::Display;
114 use insta::assert_snapshot;
115 use tempfile::tempdir;
116
117 /// A tiny helper struct that implements every trait you defined.
118 #[derive(Debug, Clone, PartialEq, Serialize, Table, Display)]
119 #[display("{id}:{value}")]
120 struct DummyItem {
121 id: Uuid,
122 value: i32,
123 #[serde(skip)]
124 #[table(skip)]
125 save: String,
126 }
127
128 impl DummyItem {
129 fn new(value: i32) -> Self {
130 Self {
131 id: Uuid::nil(),
132 value,
133 save: tempdir()
134 .expect("Tempdir")
135 .path()
136 .join("dummy.json")
137 .to_string_lossy()
138 .into(),
139 }
140 }
141 }
142
143 impl Diffable for DummyItem {
144 fn delta(&self, other: &Self) -> Self {
145 Self {
146 id: self.id, // keep the same id
147 value: self.value - other.value,
148 save: self.save.clone(),
149 }
150 }
151 }
152
153 // impl Applyable for DummyItem {}
154 // impl Nextable for DummyItem {}
155 impl Prettiable for DummyItem {}
156
157 /* ------------------- ConfigWritable ------------------- */
158 impl ConfigWritable for DummyItem {
159 fn save_path(&self) -> String {
160 self.save.clone()
161 }
162
163 fn save_folder(&self) -> String {
164 String::new()
165 }
166 }
167
168 #[test]
169 fn test_prettiable_as_table() {
170 let item = DummyItem::new(42);
171 let table = DummyItem::as_table(vec![&item]).expect("Dummytable");
172 assert_snapshot!(format!("{}", table));
173 }
174
175 #[test]
176 fn test_prettiable_show_as_log() {
177 DummyItem::new(7).show_as_log();
178 }
179
180 #[test]
181 fn test_config_writable_as_json() {
182 let item = DummyItem::new(123);
183 let json = item.as_json("test action".into()).unwrap();
184 assert_snapshot!(json);
185 }
186
187 // fn dummy_with_tempfile(value: i32) -> (DummyItem, std::path::PathBuf) {
188 // let dummy = DummyItem::new(value);
189 // (dummy.clone(), dummy.save.clone().into())
190 // }
191 // #[test]
192 // fn test_config_writable_save_with_context_erase() {
193 // let (item, path) = dummy_with_tempfile(10);
194 // // Erase: we expect a file to be created (or overwritten)
195 // item.save_with_context("Erase test", &SaveMode::DoNotErase)
196 // .unwrap();
197
198 // // Verify file contents
199 // let mut file = File::open(&path).unwrap();
200 // let mut contents = String::new();
201 // file.read_to_string(&mut contents).unwrap();
202
203 // assert_snapshot!(contents);
204 // }
205
206 // #[test]
207 // fn test_config_writable_save_with_context_donot_erase() {
208 // let (item, _path) = dummy_with_tempfile(20);
209
210 // // First write - must succeed
211 // item.save_with_context("DoNotErase test 1", &SaveMode::DoNotErase)
212 // .unwrap();
213
214 // // Second write - should fail with `ConfigCreation` because the file
215 // // already exists.
216 // let err = item
217 // .save_with_context("DoNotErase test 2", &SaveMode::DoNotErase)
218 // .unwrap_err();
219
220 // assert_snapshot!(format!("{}", err));
221 // }
222
223 #[test]
224 fn test_diffable_delta() {
225 let a = DummyItem::new(30);
226 let b = DummyItem::new(15);
227 let diff = a.delta(&b);
228 assert_eq!(diff.value, 15);
229 assert_eq!(diff.id, a.id); // same id
230 }
231}