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}