Skip to main content

kmp_application/memory/
evidence_support_clocks.rs

1//! Accepted declaration clocks for an evidence item's `supports` associations.
2use kmp_domain::compare_temporal_instants;
3
4use crate::ApplicationError;
5
6/// Boundary data. Source `time` remains separate from the support declaration.
7#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize)]
8pub struct EvidenceSupportClocks {
9    #[serde(skip_serializing_if = "Option::is_none")]
10    pub observed_at: Option<String>,
11    #[serde(skip_serializing_if = "Option::is_none")]
12    pub ingested_at: Option<String>,
13}
14
15impl EvidenceSupportClocks {
16    pub(super) fn resolve(
17        supplied: Option<&Self>,
18        observation: Option<&str>,
19        ingestion: &str,
20    ) -> Result<Self, ApplicationError> {
21        let mut clocks = supplied.cloned().unwrap_or_default();
22        let restored = clocks.ingested_at.is_some();
23        if !restored {
24            clocks.ingested_at = Some(ingestion.to_owned());
25            clocks
26                .observed_at
27                .get_or_insert_with(|| observation.unwrap_or(ingestion).to_owned());
28        }
29        for (key, value) in [
30            ("observed_at", &clocks.observed_at),
31            ("ingested_at", &clocks.ingested_at),
32        ] {
33            if let Some(value) = value
34                && compare_temporal_instants(value, value).is_none()
35            {
36                return Err(ApplicationError::Validation(format!(
37                    "memory.evidence[].support_clocks.{key} must be a valid timestamp"
38                )));
39            }
40        }
41        if let Some(observed) = &clocks.observed_at {
42            for end in [Some(ingestion), clocks.ingested_at.as_deref()]
43                .into_iter()
44                .flatten()
45            {
46                if compare_temporal_instants(observed, end) == Some(std::cmp::Ordering::Greater) {
47                    return Err(ApplicationError::Validation(
48                        "memory.evidence[].support_clocks.observed_at cannot follow ingestion"
49                            .to_owned(),
50                    ));
51                }
52            }
53        }
54        Ok(clocks)
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn invalid_and_future_support_observations_are_refused() {
64        let ingestion = "2026-09-02T10:00:00Z";
65        for value in ["not-a-date", "2026-09-02T10:00:01Z"] {
66            assert!(EvidenceSupportClocks::resolve(None, Some(value), ingestion).is_err());
67        }
68        let reversed = EvidenceSupportClocks {
69            observed_at: Some("2026-09-01T11:00:00Z".into()),
70            ingested_at: Some("2026-09-01T10:00:00Z".into()),
71        };
72        assert!(EvidenceSupportClocks::resolve(Some(&reversed), None, ingestion).is_err());
73    }
74}