replaces 15 per-module copies (now_unix/now_secs) across hive-c0re and hive-ag3nt; wire_time already owns the epoch-seconds convention
79 lines
2.6 KiB
Rust
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));
|
|
}
|
|
}
|