use jmap_types::{Id, JmapError};
use serde_json::{Map, Value};
pub fn ser<T: serde::Serialize>(val: T) -> Result<serde_json::Value, JmapError> {
serde_json::to_value(val).map_err(|e| JmapError::server_fail(e.to_string()))
}
pub fn not_found_json(ids: &[Id]) -> Value {
Value::Array(
ids.iter()
.map(|id| Value::String(id.as_ref().to_owned()))
.collect(),
)
}
pub fn extract_account_id(args: Value) -> Result<(Id, Map<String, Value>), JmapError> {
let Value::Object(args) = args else {
return Err(JmapError::invalid_arguments(
"arguments must be an object containing accountId",
));
};
match args.get("accountId").and_then(|v| v.as_str()) {
Some(s) => {
let id = Id::from(s);
Ok((id, args))
}
None => Err(JmapError::invalid_arguments("accountId is required")),
}
}
pub fn now_utc_string() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now();
let (secs, millis): (i64, u32) = match now.duration_since(UNIX_EPOCH) {
Ok(d) => (d.as_secs() as i64, d.subsec_millis()),
Err(e) => {
let d = e.duration();
(-(d.as_secs() as i64), d.subsec_millis())
}
};
let s = secs.rem_euclid(60);
let m = (secs / 60).rem_euclid(60);
let h = (secs / 3600).rem_euclid(24);
let days = secs.div_euclid(86400);
let (year, month, day) = civil_from_days(days);
format!("{year:04}-{month:02}-{day:02}T{h:02}:{m:02}:{s:02}.{millis:03}Z")
}
fn civil_from_days(z: i64) -> (i32, u8, u8) {
let z = z + 719_468;
let era: i64 = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; let y = yoe + era * 400;
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 };
(yr as i32, mo as u8, d as u8)
}
pub const MAX_MERGE_PATCH_DEPTH: usize = 32;
pub fn json_merge_patch(target: &mut Value, patch: Value) {
json_merge_patch_inner(target, patch, 0);
}
fn json_merge_patch_inner(target: &mut Value, patch: Value, depth: usize) {
if depth > MAX_MERGE_PATCH_DEPTH {
return;
}
match patch {
Value::Object(patch_map) => {
if !target.is_object() {
*target = Value::Object(Map::new());
}
let target_map = target
.as_object_mut()
.expect("target was just set to Value::Object above");
for (key, patch_val) in patch_map {
if patch_val.is_null() {
target_map.remove(&key);
} else {
let entry = target_map.entry(key).or_insert(Value::Null);
json_merge_patch_inner(entry, patch_val, depth + 1);
}
}
}
other => *target = other,
}
}
#[cfg(test)]
mod tests {
use super::{civil_from_days, json_merge_patch, now_utc_string};
#[test]
fn civil_from_days_known_dates() {
let cases: &[(i64, (i32, u8, u8))] = &[
(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)), ];
for &(days, expected) in cases {
assert_eq!(
civil_from_days(days),
expected,
"civil_from_days({days}) mismatch"
);
}
}
#[test]
fn now_utc_string_format() {
let s = now_utc_string();
assert_eq!(s.len(), 24, "unexpected length: {s}");
assert!(s.ends_with('Z'), "must end with Z: {s}");
assert_eq!(&s[4..5], "-", "missing year-month separator: {s}");
assert_eq!(&s[7..8], "-", "missing month-day separator: {s}");
assert_eq!(&s[10..11], "T", "missing date-time separator: {s}");
assert_eq!(&s[13..14], ":", "missing hour-minute separator: {s}");
assert_eq!(&s[16..17], ":", "missing minute-second separator: {s}");
assert_eq!(&s[19..20], ".", "missing decimal point before millis: {s}");
assert!(
s[20..23].chars().all(|c| c.is_ascii_digit()),
"milliseconds must be 3 digits: {s}"
);
assert!(
s.starts_with("20"),
"year should start with 20 in 21st century: {s}"
);
}
#[test]
fn json_merge_patch_does_not_stack_overflow() {
const DEPTH: usize = 1000;
let mut target = serde_json::json!({});
for _ in 0..DEPTH {
target = serde_json::json!({ "a": target });
}
let mut patch = serde_json::json!({});
for _ in 0..DEPTH {
patch = serde_json::json!({ "a": patch });
}
json_merge_patch(&mut target, patch);
assert!(
target.is_object(),
"after a deeply-nested merge patch, target must remain a JSON object; got {target:?}"
);
}
#[test]
fn json_merge_patch_shallow_applies_normally() {
let mut target = serde_json::json!({ "a": 1, "b": { "c": 2 } });
let patch = serde_json::json!({ "b": { "c": 99, "d": 7 }, "e": null });
json_merge_patch(&mut target, patch);
assert_eq!(
target,
serde_json::json!({ "a": 1, "b": { "c": 99, "d": 7 } }),
"RFC 7396 merge semantics broken at shallow depth"
);
}
#[test]
fn json_merge_patch_adds_nested_object_to_absent_field() {
let mut target = serde_json::json!({ "a": 1 });
let patch = serde_json::json!({ "b": { "c": 2 } });
json_merge_patch(&mut target, patch);
assert_eq!(
target,
serde_json::json!({ "a": 1, "b": { "c": 2 } }),
"patch must add the nested object at the previously-absent field"
);
}
}