acdp_primitives/time.rs
1//! Time helpers shared by the producer builder and search-params formatter.
2//!
3//! ACDP timestamps in publish requests are part of the JCS-canonicalized
4//! body (RFC-ACDP-0001 §5.3). Producers MUST emit canonical millisecond
5//! precision so the resulting `content_hash` is reproducible across
6//! implementations. `chrono::DateTime<Utc>` defaults to nanosecond
7//! precision; this module centralizes the truncation and the canonical
8//! string format used everywhere on the producer path.
9
10use chrono::{DateTime, Utc};
11
12/// Truncate to millisecond precision per RFC-ACDP-0001 §5.3.
13///
14/// Returns the input unchanged if `timestamp_millis()` cannot round-trip
15/// (extremely far-future timestamps).
16pub fn trunc_ms(dt: DateTime<Utc>) -> DateTime<Utc> {
17 DateTime::from_timestamp_millis(dt.timestamp_millis()).unwrap_or(dt)
18}
19
20/// Format as the canonical RFC 3339 string with explicit `Z` suffix
21/// and millisecond precision, e.g. `2026-04-16T10:30:15.123Z`.
22pub fn fmt_rfc3339_ms(dt: DateTime<Utc>) -> String {
23 dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()
24}
25
26#[cfg(test)]
27mod tests {
28 use super::*;
29
30 #[test]
31 fn ms_truncation_drops_sub_ms() {
32 let dt = DateTime::from_timestamp_nanos(1_700_000_000_123_456_789);
33 let truncated = trunc_ms(dt);
34 assert_eq!(truncated.timestamp_subsec_nanos() % 1_000_000, 0);
35 }
36
37 #[test]
38 fn fmt_emits_canonical_form() {
39 let dt = DateTime::from_timestamp_millis(1_700_000_000_123).unwrap();
40 let s = fmt_rfc3339_ms(dt);
41 assert!(s.ends_with("Z"), "got {s}");
42 assert!(s.contains(".123"), "got {s}");
43 }
44}