wist_shared/
primitives.rs1pub type Int = i64;
4pub type Bool = bool;
5pub type Float = f64;
6
7#[derive(
8 Debug, Clone, PartialEq, Eq, PartialOrd, Ord, ::serde::Serialize, ::serde::Deserialize,
9)]
10pub struct DateTime(chrono::DateTime<chrono::Utc>);
11
12impl DateTime {
13 pub fn now() -> Self {
14 Self(chrono::Utc::now())
15 }
16 pub fn in_days(days: i64) -> Self {
21 let now = chrono::Utc::now();
22 let shifted =
23 chrono::Duration::try_days(days).and_then(|delta| now.checked_add_signed(delta));
24 Self(shifted.unwrap_or(now))
25 }
26 pub fn from_rfc3339(value: &str) -> Option<Self> {
27 chrono::DateTime::parse_from_rfc3339(value)
28 .ok()
29 .map(|v| Self(v.with_timezone(&chrono::Utc)))
30 }
31 pub fn to_chrono(&self) -> chrono::DateTime<chrono::Utc> {
33 self.0
34 }
35 pub fn seconds_until(&self, later: &Self) -> i64 {
36 later.0.signed_duration_since(self.0).num_seconds().max(0)
37 }
38}
39
40#[derive(Clone, ::serde::Serialize, ::serde::Deserialize)]
41pub struct Secret(String);
42
43impl std::fmt::Debug for Secret {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.write_str("Secret(***)")
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::*;
52
53 #[test]
54 fn date_time_round_trips_through_serde_json() {
55 let value = DateTime::from_rfc3339("2026-09-23T20:24:24Z").expect("parse");
56 let json = serde_json::to_string(&value).expect("serialize");
57 let back: DateTime = serde_json::from_str(&json).expect("deserialize");
58 assert_eq!(value, back);
59 assert_eq!(back.to_chrono().to_rfc3339(), "2026-09-23T20:24:24+00:00");
60 }
61
62 #[test]
63 fn from_rfc3339_rejects_garbage() {
64 assert!(DateTime::from_rfc3339("not a timestamp").is_none());
65 assert!(DateTime::from_rfc3339("2026-09-23 20:24:24").is_none());
67 }
68
69 #[test]
70 fn in_days_moves_forward_and_backward() {
71 let now = DateTime::now();
72 assert!(DateTime::in_days(1).to_chrono() > now.to_chrono());
73 assert!(DateTime::in_days(-1).to_chrono() < now.to_chrono());
74 }
75
76 #[test]
77 fn in_days_does_not_panic_on_absurd_inputs() {
78 for days in [i64::MAX, i64::MIN, i64::MAX / 2] {
80 let _ = DateTime::in_days(days);
81 }
82 }
83
84 #[test]
85 fn seconds_until_clamps_negative_differences_to_zero() {
86 let base = DateTime::from_rfc3339("2026-09-23T20:00:00Z").expect("base");
87 let later = DateTime::from_rfc3339("2026-09-23T20:00:30Z").expect("later");
88 assert_eq!(base.seconds_until(&later), 30);
89 assert_eq!(later.seconds_until(&base), 0, "负差被夹到 0");
90 assert_eq!(base.seconds_until(&base), 0);
91 }
92
93 #[test]
94 fn secret_debug_redacts_the_value() {
95 let secret: Secret = serde_json::from_str("\"super-secret-token\"").expect("secret");
96 let debug = format!("{secret:?}");
97 assert_eq!(debug, "Secret(***)");
98 assert!(!debug.contains("super-secret-token"));
99 }
100
101 #[test]
102 fn secret_round_trips_through_serde_json() {
103 let secret: Secret = serde_json::from_str("\"s3cr3t\"").expect("secret");
104 assert_eq!(
105 serde_json::to_string(&secret).expect("serialize"),
106 "\"s3cr3t\""
107 );
108 }
109}