Skip to main content

kmp_domain/value_objects/
temporal_cursor.rs

1use crate::DomainError;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum TemporalDirection {
5    Goto,
6    Near,
7    Rewind,
8    Forward,
9}
10
11/// Clock used to project a temporal read.
12///
13/// `Default` preserves the original precedence (occurred, validity start,
14/// observed, ingested). Explicit variants never substitute a different clock
15/// when the requested clock is absent.
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
17pub enum TemporalAxis {
18    #[default]
19    Default,
20    Occurred,
21    Observed,
22    Ingested,
23    Validity,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub enum TemporalCursor {
28    Ref(String),
29    Time(String),
30    Sequence(u32),
31}
32
33impl TemporalCursor {
34    pub fn ref_id(value: impl Into<String>) -> Result<Self, DomainError> {
35        let value = normalize_required(value.into(), "temporal cursor ref")?;
36        Ok(Self::Ref(value))
37    }
38
39    pub fn time(value: impl Into<String>) -> Result<Self, DomainError> {
40        let value = normalize_required(value.into(), "temporal cursor time")?;
41        Ok(Self::Time(value))
42    }
43
44    pub fn sequence(value: u32) -> Result<Self, DomainError> {
45        if value == 0 {
46            return Err(DomainError::InvalidState(
47                "temporal cursor sequence must be greater than zero".to_string(),
48            ));
49        }
50        Ok(Self::Sequence(value))
51    }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct TemporalWindow {
56    before_entries: usize,
57    after_entries: usize,
58}
59
60impl TemporalWindow {
61    pub fn new(before_entries: usize, after_entries: usize) -> Self {
62        Self {
63            before_entries,
64            after_entries,
65        }
66    }
67
68    pub fn before_entries(&self) -> usize {
69        self.before_entries
70    }
71
72    pub fn after_entries(&self) -> usize {
73        self.after_entries
74    }
75}
76
77impl Default for TemporalWindow {
78    fn default() -> Self {
79        Self {
80            before_entries: 2,
81            after_entries: 2,
82        }
83    }
84}
85
86fn normalize_required(value: String, field: &'static str) -> Result<String, DomainError> {
87    let value = value.trim();
88    if value.is_empty() {
89        Err(DomainError::EmptyValue(field))
90    } else {
91        Ok(value.to_string())
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use crate::{DomainError, TemporalAxis, TemporalCursor};
98
99    #[test]
100    fn cursor_requires_non_empty_values() {
101        assert_eq!(
102            TemporalCursor::ref_id(" ").expect_err("empty ref must fail"),
103            DomainError::EmptyValue("temporal cursor ref")
104        );
105        assert!(TemporalCursor::sequence(1).is_ok());
106        assert!(TemporalCursor::sequence(0).is_err());
107    }
108
109    #[test]
110    fn temporal_axis_defaults_to_the_compatible_precedence() {
111        assert_eq!(TemporalAxis::default(), TemporalAxis::Default);
112    }
113}