use serde::{Deserialize, Serialize};
use super::ACTION_RING_CAP;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionRecord {
pub at: String,
pub action: String,
pub detail: Option<String>,
}
impl ActionRecord {
pub fn new(action: impl Into<String>) -> Self {
Self {
at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(),
action: action.into(),
detail: None,
}
}
pub fn with_detail(action: impl Into<String>, detail: impl Into<String>) -> Self {
Self {
at: chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(),
action: action.into(),
detail: Some(detail.into()),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ActionRing {
pub entries: std::collections::VecDeque<ActionRecord>,
}
impl ActionRing {
pub fn push(&mut self, record: ActionRecord) {
if self.entries.len() >= ACTION_RING_CAP {
self.entries.pop_front();
}
self.entries.push_back(record);
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.entries.len()
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ring_starts_empty() {
let r = ActionRing::default();
assert!(r.is_empty());
assert_eq!(r.len(), 0);
}
#[test]
fn ring_caps_at_action_ring_cap() {
let mut r = ActionRing::default();
for i in 0..ACTION_RING_CAP + 25 {
r.push(ActionRecord::new(format!("a{i}")));
}
assert_eq!(r.len(), ACTION_RING_CAP);
assert_eq!(
r.entries.front().unwrap().action,
format!("a{}", 25)
);
assert_eq!(
r.entries.back().unwrap().action,
format!("a{}", ACTION_RING_CAP + 25 - 1)
);
}
#[test]
fn with_detail_preserves_action_and_detail() {
let r = ActionRecord::with_detail("save", "para=01-opening");
assert_eq!(r.action, "save");
assert_eq!(r.detail.as_deref(), Some("para=01-opening"));
}
#[test]
fn at_field_has_iso8601_z_shape() {
let r = ActionRecord::new("test");
assert_eq!(r.at.len(), 20);
assert!(r.at.ends_with('Z'));
assert!(r.at.contains('T'));
}
}