Skip to main content

wist_shared/
primitives.rs

1//! 跨域共用的原始类型(模型标量 DateTime/Secret 的单一真源)。
2
3pub 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    /// `days` 天后的时刻(如注册 Token 有效期)。
17    ///
18    /// `days` 越界(大到 `Duration` 或加法溢出)时退化为"现在":与下面 `checked_add_signed`
19    /// 的兜底一致,按已过期处理,而不是 panic。
20    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    /// 取底层 chrono 值(如 sqlx TIMESTAMPTZ 绑定)。
32    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        // 不带时区的朴素时间不是合法 RFC 3339。
66        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        // 旧实现 `Duration::days` 越界会 panic;现在应当退化而不是崩溃。
79        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}