hyperhive/hive-sh4re/src/wire_time.rs
müde c84028ddcf refactor: single now_unix in hive_sh4re::wire_time
replaces 15 per-module copies (now_unix/now_secs) across hive-c0re and
hive-ag3nt; wire_time already owns the epoch-seconds convention
2026-07-06 21:58:32 +02:00

79 lines
2.6 KiB
Rust

//! Timestamp conventions for the wire types: fields are
//! `chrono::DateTime<Utc>` (serde serializes them as RFC 3339 UTC `Z`
//! strings, e.g. `2026-07-02T18:30:00Z`), while sqlite storage and
//! agent-facing *input* args stay unix-epoch seconds (`i64`). This
//! module owns the two conversions at those boundaries.
use chrono::{DateTime, Utc};
/// Convert unix-epoch seconds (the sqlite column / input-arg form)
/// into the wire timestamp type. Out-of-range values (never produced
/// by our clocks) clamp to the epoch rather than erroring — the db
/// read path must not fail on a weird row.
#[must_use]
pub fn from_secs(secs: i64) -> DateTime<Utc> {
DateTime::<Utc>::from_timestamp(secs, 0).unwrap_or_default()
}
/// Current unix timestamp in seconds — the single definition behind
/// every store's `created_at` / `sent_at` / … stamp (this module owns
/// the epoch-seconds convention; a dozen local copies of this fn used
/// to float around both binaries). Clamps to 0 on a pre-epoch clock.
#[must_use]
pub fn now_unix() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.ok()
.and_then(|d| i64::try_from(d.as_secs()).ok())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::from_secs;
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct Row {
at: DateTime<Utc>,
#[serde(default, skip_serializing_if = "Option::is_none")]
maybe_at: Option<DateTime<Utc>>,
}
#[test]
fn serializes_as_rfc3339_z() {
let json = serde_json::to_string(&Row {
at: from_secs(1_751_480_000),
maybe_at: None,
})
.unwrap();
assert_eq!(json, r#"{"at":"2025-07-02T18:13:20Z"}"#);
}
#[test]
fn round_trips_and_serializes_some() {
let row = Row {
at: from_secs(0),
maybe_at: Some(from_secs(1_751_480_000)),
};
let json = serde_json::to_string(&row).unwrap();
assert_eq!(
json,
r#"{"at":"1970-01-01T00:00:00Z","maybe_at":"2025-07-02T18:13:20Z"}"#
);
assert_eq!(serde_json::from_str::<Row>(&json).unwrap(), row);
}
#[test]
fn deserializes_offset_form_normalized_to_utc() {
let row: Row = serde_json::from_str(r#"{"at":"2025-07-02T20:13:20+02:00"}"#).unwrap();
assert_eq!(row.at, from_secs(1_751_480_000));
}
#[test]
fn from_secs_clamps_out_of_range_to_epoch() {
assert_eq!(from_secs(i64::MAX), from_secs(0));
}
}