1use jmap_types::{Id, JmapError};
4use serde_json::Value;
5
6pub fn ser<T: serde::Serialize>(val: T) -> Result<serde_json::Value, JmapError> {
9 serde_json::to_value(val).map_err(|e| JmapError::server_fail(e.to_string()))
10}
11
12pub fn not_found_json(ids: &[Id]) -> Value {
18 if ids.is_empty() {
19 Value::Null
20 } else {
21 Value::Array(
22 ids.iter()
23 .map(|id| Value::String(id.as_ref().to_owned()))
24 .collect(),
25 )
26 }
27}
28
29pub fn extract_account_id(args: &Value) -> Result<Id, JmapError> {
31 match args.get("accountId").and_then(|v| v.as_str()) {
32 Some(s) => Ok(Id::from(s)),
33 None => Err(JmapError::invalid_arguments("accountId is required")),
34 }
35}
36
37pub fn now_utc_string() -> String {
41 use std::time::{SystemTime, UNIX_EPOCH};
42 let secs = SystemTime::now()
43 .duration_since(UNIX_EPOCH)
44 .unwrap_or_default()
49 .as_secs() as i64;
50
51 let s = secs % 60;
52 let m = (secs / 60) % 60;
53 let h = (secs / 3600) % 24;
54 let days = secs / 86400;
55 let (year, month, day) = civil_from_days(days);
56
57 format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}Z")
58}
59
60fn civil_from_days(z: i64) -> (i32, u8, u8) {
66 let z = z + 719_468;
67 let era: i64 = if z >= 0 { z } else { z - 146_096 } / 146_097;
68 let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + era * 400;
71 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let d = doy - (153 * mp + 2) / 5 + 1; let mo = if mp < 10 { mp + 3 } else { mp - 9 }; let yr = if mo <= 2 { y + 1 } else { y };
76 (yr as i32, mo as u8, d as u8)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::{civil_from_days, now_utc_string};
82
83 #[test]
86 fn civil_from_days_known_dates() {
87 let cases: &[(i64, (i32, u8, u8))] = &[
88 (0, (1970, 1, 1)), (365, (1971, 1, 1)), (10957, (2000, 1, 1)), (11016, (2000, 2, 29)), (11017, (2000, 3, 1)), (19358, (2023, 1, 1)), (19722, (2023, 12, 31)), (19782, (2024, 2, 29)), (19783, (2024, 3, 1)), ];
98
99 for &(days, expected) in cases {
100 assert_eq!(
101 civil_from_days(days),
102 expected,
103 "civil_from_days({days}) mismatch"
104 );
105 }
106 }
107
108 #[test]
109 fn now_utc_string_format() {
110 let s = now_utc_string();
111 assert_eq!(s.len(), 20, "unexpected length: {s}");
113 assert!(s.ends_with('Z'), "must end with Z: {s}");
114 assert_eq!(&s[4..5], "-", "missing year-month separator: {s}");
115 assert_eq!(&s[7..8], "-", "missing month-day separator: {s}");
116 assert_eq!(&s[10..11], "T", "missing date-time separator: {s}");
117 assert_eq!(&s[13..14], ":", "missing hour-minute separator: {s}");
118 assert_eq!(&s[16..17], ":", "missing minute-second separator: {s}");
119 assert!(
120 s.starts_with("20"),
121 "year should start with 20 in 21st century: {s}"
122 );
123 }
124}