kmp_application/memory/
evidence_support_clocks.rs1use kmp_domain::compare_temporal_instants;
3
4use crate::ApplicationError;
5
6#[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}