arena_core/engine/
inspector.rs

1use std::{fs::OpenOptions, io::Seek};
2
3use serde::{Deserialize, Serialize};
4
5/// Trait allowing custom behavior to be defined for logging and inspecting values.
6pub trait Inspector<V> {
7    /// Log a value to state.
8    fn log(&mut self, value: V);
9
10    /// Inspect a value at a given time step.
11    fn inspect(&self, step: usize) -> Option<V>;
12
13    /// Save the inspector state.
14    fn save(&self);
15}
16
17/// Type that allows for logging indexed values to files on disc.
18#[derive(Serialize, Deserialize, Debug, Clone)]
19pub struct LogMessage {
20    /// Index of the log message.
21    pub id: usize,
22
23    /// Key of the log message.
24    pub name: String,
25
26    /// Data of the log message.
27    pub data: String,
28}
29
30impl LogMessage {
31    /// Public constructor function for a new [`LogMessage`].
32    pub fn new(name: String, data: String) -> Self {
33        Self { id: 0, name, data }
34    }
35}
36
37#[derive(Debug)]
38/// Custom implementation of an [`Inspector`] for logging values to a file (CSV or JSON).
39pub struct Logger {
40    values: Vec<LogMessage>,
41    counter: usize,
42    file_path: String,
43    format: LogFormat,
44}
45
46#[derive(Debug)]
47/// Enum to specify the logging format.
48enum LogFormat {
49    Csv,
50    Json,
51}
52
53impl Logger {
54    /// Public constructor function for a new [`Logger`] for CSV format.
55    pub fn new_csv(file_path: String) -> Self {
56        Self {
57            values: Vec::new(),
58            counter: 0,
59            file_path,
60            format: LogFormat::Csv,
61        }
62    }
63
64    /// Public constructor function for a new [`Logger`] for JSON format.
65    pub fn new_json(file_path: String) -> Self {
66        Self {
67            values: Vec::new(),
68            counter: 0,
69            file_path,
70            format: LogFormat::Json,
71        }
72    }
73
74    /// Append a log message to the appropriate file format.
75    fn append_to_file(&self, record: &LogMessage) -> Result<(), Box<dyn std::error::Error>> {
76        let mut file = OpenOptions::new()
77            .append(true)
78            .create(true)
79            .open(&self.file_path)?;
80
81        match self.format {
82            LogFormat::Csv => {
83                let mut writer = csv::Writer::from_writer(file);
84                writer.serialize((record.id, &record.name, &record.data))?;
85                writer.flush()?;
86            }
87            LogFormat::Json => {
88                let mut records: Vec<LogMessage> = if file.metadata()?.len() > 0 {
89                    serde_json::from_reader(&file)?
90                } else {
91                    Vec::new()
92                };
93                records.push(record.clone());
94                file.set_len(0)?;
95                file.seek(std::io::SeekFrom::Start(0))?;
96                serde_json::to_writer_pretty(file, &records)?;
97            }
98        }
99        Ok(())
100    }
101}
102
103impl Inspector<LogMessage> for Logger {
104    fn log(&mut self, mut value: LogMessage) {
105        value.id = self.counter;
106        self.counter += 1;
107        self.values.push(value.clone());
108
109        if let Err(e) = self.append_to_file(&value) {
110            eprintln!("Failed to append to file: {}", e);
111        }
112    }
113
114    fn inspect(&self, step: usize) -> Option<LogMessage> {
115        self.values.get(step).cloned()
116    }
117
118    fn save(&self) {}
119}
120
121/// No-op implementation of an [`Inspector`] for custom use cases.
122pub struct EmptyInspector;
123
124impl Inspector<f64> for EmptyInspector {
125    fn inspect(&self, _step: usize) -> Option<f64> {
126        None
127    }
128    fn log(&mut self, _value: f64) {}
129    fn save(&self) {}
130}