use std::collections::BTreeMap;
use std::fmt;
use serde::de::DeserializeOwned;
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::id::{Id, State, UTCDate};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Urgency {
VeryLow,
Low,
Normal,
High,
}
impl Urgency {
pub const fn as_str(self) -> &'static str {
match self {
Self::VeryLow => "very-low",
Self::Low => "low",
Self::Normal => "normal",
Self::High => "high",
}
}
}
impl Serialize for Urgency {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for Urgency {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
const VARIANTS: &[&str] = &["very-low", "low", "normal", "high"];
let s = String::deserialize(deserializer)?;
match s.as_str() {
"very-low" => Ok(Self::VeryLow),
"low" => Ok(Self::Low),
"normal" => Ok(Self::Normal),
"high" => Ok(Self::High),
other => Err(serde::de::Error::unknown_variant(other, VARIANTS)),
}
}
}
const STATE_CHANGE_TYPE: &str = "StateChange";
const PUSH_VERIFICATION_TYPE: &str = "PushVerification";
const REDACTED: &str = "[REDACTED]";
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateChange {
pub changed: BTreeMap<Id, BTreeMap<String, State>>,
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl StateChange {
pub fn new(changed: BTreeMap<Id, BTreeMap<String, State>>) -> Self {
Self {
changed,
extra: serde_json::Map::new(),
}
}
}
impl Serialize for StateChange {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut map = s.serialize_map(None)?;
map.serialize_entry("@type", STATE_CHANGE_TYPE)?;
map.serialize_entry("changed", &self.changed)?;
for (k, v) in &self.extra {
map.serialize_entry(k, v)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for StateChange {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let mut extra = serde_json::Map::deserialize(d)?;
take_type(&mut extra, STATE_CHANGE_TYPE)?;
let changed = take_field(&mut extra, "changed")?;
Ok(StateChange { changed, extra })
}
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PushSubscription {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Id>,
pub device_client_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub keys: Option<PushKeys>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verification_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires: Option<UTCDate>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub types: Option<Vec<String>>,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl fmt::Debug for PushSubscription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PushSubscription")
.field("id", &self.id)
.field("device_client_id", &self.device_client_id)
.field("url", &self.url.as_ref().map(|_| REDACTED))
.field("keys", &self.keys)
.field(
"verification_code",
&self.verification_code.as_ref().map(|_| REDACTED),
)
.field("expires", &self.expires)
.field("types", &self.types)
.field("extra", &self.extra)
.finish()
}
}
impl crate::JmapObject for PushSubscription {
const TYPE_NAME: &'static str = "PushSubscription";
type Property = ();
}
impl crate::SetObject for PushSubscription {
type Patch = crate::PatchObject;
const SERVER_SET: &'static [&'static str] = &["id"];
const CLIENT_SETTABLE: &'static [&'static str] = &[
"deviceClientId",
"url",
"keys",
"verificationCode",
"expires",
"types",
];
const IMMUTABLE: &'static [&'static str] = &["deviceClientId", "url", "keys"];
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PushKeys {
pub p256dh: String,
pub auth: String,
#[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl fmt::Debug for PushKeys {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PushKeys")
.field("p256dh", &self.p256dh)
.field("auth", &REDACTED)
.field("extra", &self.extra)
.finish()
}
}
#[non_exhaustive]
#[derive(Clone, PartialEq, Eq)]
pub struct PushVerification {
pub push_subscription_id: Id,
pub verification_code: String,
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl fmt::Debug for PushVerification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PushVerification")
.field("push_subscription_id", &self.push_subscription_id)
.field("verification_code", &REDACTED)
.field("extra", &self.extra)
.finish()
}
}
impl Serialize for PushVerification {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
let mut map = s.serialize_map(None)?;
map.serialize_entry("@type", PUSH_VERIFICATION_TYPE)?;
map.serialize_entry("pushSubscriptionId", &self.push_subscription_id)?;
map.serialize_entry("verificationCode", &self.verification_code)?;
for (k, v) in &self.extra {
map.serialize_entry(k, v)?;
}
map.end()
}
}
impl<'de> Deserialize<'de> for PushVerification {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let mut extra = serde_json::Map::deserialize(d)?;
take_type(&mut extra, PUSH_VERIFICATION_TYPE)?;
let push_subscription_id = take_field(&mut extra, "pushSubscriptionId")?;
let verification_code = take_field(&mut extra, "verificationCode")?;
Ok(PushVerification {
push_subscription_id,
verification_code,
extra,
})
}
}
fn take_type<E: serde::de::Error>(
map: &mut serde_json::Map<String, serde_json::Value>,
expected: &str,
) -> Result<(), E> {
match map.remove("@type") {
None => Ok(()),
Some(serde_json::Value::String(s)) if s == expected => Ok(()),
Some(serde_json::Value::String(_)) => {
Err(E::custom(format!("@type must be \"{expected}\"")))
}
Some(other) => Err(E::custom(format!(
"@type must be a string, found {}",
crate::json_type_name(&other)
))),
}
}
fn take_field<T: DeserializeOwned, E: serde::de::Error>(
map: &mut serde_json::Map<String, serde_json::Value>,
key: &'static str,
) -> Result<T, E> {
let value = map.remove(key).ok_or_else(|| E::missing_field(key))?;
serde_json::from_value(value).map_err(E::custom)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn push_verification_type_error_does_not_echo_value() {
for (raw, want) in [
(
r#"{"@type":{"note":"CANARY-kwqn"},"pushSubscriptionId":"p","verificationCode":"c"}"#,
"@type must be a string, found object",
),
(
r#"{"@type":"CANARY-kwqn","pushSubscriptionId":"p","verificationCode":"c"}"#,
"@type must be \"PushVerification\"",
),
] {
let err = serde_json::from_str::<PushVerification>(raw)
.unwrap_err()
.to_string();
assert!(!err.contains("CANARY-kwqn"), "{err}");
assert!(err.contains(want), "{err}");
}
}
#[test]
fn push_subscription_properties_are_classified() {
let sub = PushSubscription {
id: Some(Id::from("ps1")),
device_client_id: "dev".into(),
url: Some("https://push.example.com/x".into()),
keys: Some(PushKeys {
p256dh: "p".into(),
auth: "a".into(),
extra: serde_json::Map::new(),
}),
verification_code: Some("code".into()),
expires: Some(UTCDate::from("2026-01-01T00:00:00Z")),
types: Some(vec!["Email".into()]),
extra: serde_json::Map::new(),
};
assert_eq!(crate::check_property_classification(&sub), Ok(()));
}
use serde_json::json;
#[test]
fn state_change_preserves_vendor_extras() {
let raw = json!({
"changed": {
"acc1": { "Email": "s42" }
},
"acmeCorpSequence": 17
});
let obj: StateChange = serde_json::from_value(raw).expect("StateChange must deserialize");
assert_eq!(
obj.extra.get("acmeCorpSequence").and_then(|v| v.as_u64()),
Some(17)
);
let v = serde_json::to_value(&obj).expect("StateChange must serialize");
assert_eq!(v["acmeCorpSequence"], json!(17));
}
#[test]
fn extra_equality_is_order_insensitive_under_workspace_flags() {
let raw_a = json!({
"changed": {"acc1": {"Email": "s1"}},
"vendorA": 1,
"vendorB": 2
});
let raw_b = json!({
"changed": {"acc1": {"Email": "s1"}},
"vendorB": 2,
"vendorA": 1
});
let a: StateChange = serde_json::from_value(raw_a).expect("a must deserialize");
let b: StateChange = serde_json::from_value(raw_b).expect("b must deserialize");
assert_eq!(
a, b,
"extra-map equality is order-insensitive under the workspace's \
default serde_json::Map (BTreeMap-backed); if this fails, \
check whether preserve_order has been enabled in the dep graph"
);
}
#[test]
fn urgency_wire_values_in_increasing_order() {
let ordered = [
Urgency::VeryLow,
Urgency::Low,
Urgency::Normal,
Urgency::High,
];
let wire: Vec<&str> = ordered.iter().map(|u| u.as_str()).collect();
assert_eq!(wire, ["very-low", "low", "normal", "high"]);
assert!(ordered.windows(2).all(|w| w[0] < w[1]));
}
#[test]
fn urgency_serde_accepts_only_rfc8030_values() {
for (wire, urgency) in [
("very-low", Urgency::VeryLow),
("low", Urgency::Low),
("normal", Urgency::Normal),
("high", Urgency::High),
] {
let parsed: Urgency = serde_json::from_value(json!(wire)).expect(wire);
assert_eq!(parsed, urgency);
assert_eq!(serde_json::to_value(urgency).expect(wire), json!(wire));
}
for bad in ["urgent", "High", "very_low", ""] {
assert!(
serde_json::from_value::<Urgency>(json!(bad)).is_err(),
"{bad:?}"
);
}
}
}