agsim 0.1.0

A discrete event simulation library.
Documentation
use std::collections::{BTreeMap, HashSet};
use std::fmt;
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, Debug)]
#[derive(Clone)]
pub struct StateChangeEvent {
    #[serde(rename = "Time")]
    pub time: DateTime<Utc>,
    #[serde(rename = "Field")]
    pub field: String,
    #[serde(rename = "NewValue")]
    pub new_value: String,
    #[serde(rename = "OldValue")]
    pub old_value: String,
}

pub trait State: Sized + Clone + Default {
    fn update_field(&mut self, field: &str, value: &str);
    fn get_field(&self, field: &str) -> String;
    fn get_field_names() -> &'static [&'static str];
}

#[derive(Debug, Clone)]
pub struct TimelineEntry<S> {
    pub timestamp: DateTime<Utc>,
    pub state: S,
    pub events: Vec<String>,
}

impl<S: fmt::Display> fmt::Display for TimelineEntry<S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let event_str = if self.events.is_empty() {
            "Initial State".to_string()
        } else {
            format!("Events: {}", self.events.join(", "))
        };
        write!(
            f,
            "[{}] State -> [ {} ] *({})*",
            self.timestamp.format("%Y-%m-%d %H:%M:%S"),
            self.state,
            event_str
        )
    }
}

pub struct Timeline<S> {
    pub entries: Vec<TimelineEntry<S>>,
}

impl<S: State> Timeline<S> {

    pub fn generate(events: &[StateChangeEvent]) -> Option<Self> {
        if events.is_empty() {
            return None;
        }

        let mut sorted_events = events.to_vec();
        sorted_events.sort_by_key(|e| e.time);

        let mut current_state = S::default();
        let mut seen_fields = HashSet::new();
        for event in &sorted_events {
            if seen_fields.insert(event.field.clone()) {
                current_state.update_field(&event.field, &event.old_value);
            }
        }

        let mut events_by_time = BTreeMap::<DateTime<Utc>, Vec<&StateChangeEvent>>::new();
        for event in &sorted_events {
            events_by_time.entry(event.time).or_default().push(event);
        }

        let mut entries = Vec::new();

        entries.push(TimelineEntry {
            timestamp: sorted_events[0].time - Duration::seconds(1),
            state: current_state.clone(),
            events: Vec::new(),
        });

        for (timestamp, event_group) in events_by_time {
            let mut state_for_this_entry = current_state.clone();
            let mut changed_fields = Vec::new();

            for event in event_group {
                state_for_this_entry.update_field(&event.field, &event.new_value);
                changed_fields.push(event.field.clone());
            }

            entries.push(TimelineEntry {
                timestamp,
                state: state_for_this_entry.clone(),
                events: changed_fields,
            });

            current_state = state_for_this_entry;
        }

        Some(Timeline { entries })
    }
}

impl<S: fmt::Display> fmt::Display for Timeline<S> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for entry in &self.entries {
            writeln!(f, "{}", entry)?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use state_macros::{State, StateDisplay};
    use super::*;

    #[derive(Clone, Default, Debug, State, StateDisplay)]
    struct TestState {
        property1: String,
        property2: String,
    }

    #[test]
    fn test_timeline() {
        let entries: &[StateChangeEvent] = &[
            StateChangeEvent {
                time: Utc::now(),
                field: "property1".to_string(),
                new_value: "1".to_string(),
                old_value: "0".to_string(),
            },
            StateChangeEvent {
                time: Utc::now() + Duration::seconds(1),
                field: "property1".to_string(),
                new_value: "0".to_string(),
                old_value: "1".to_string(),
            },
            StateChangeEvent {
                time: Utc::now(),
                field: "property2".to_string(),
                new_value: "1".to_string(),
                old_value: "0".to_string(),
            },
        ];

        let timeline = Timeline::<TestState>::generate(&*entries).unwrap();
        println!("{}", timeline)
    }
}