1use std::collections::BTreeMap;
19use std::fmt;
20
21use serde::de::DeserializeOwned;
22use serde::ser::SerializeMap;
23use serde::{Deserialize, Deserializer, Serialize, Serializer};
24
25use crate::id::{Id, State, UTCDate};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
33pub enum Urgency {
34 VeryLow,
36 Low,
38 Normal,
40 High,
42}
43
44impl Urgency {
45 pub const fn as_str(self) -> &'static str {
47 match self {
48 Self::VeryLow => "very-low",
49 Self::Low => "low",
50 Self::Normal => "normal",
51 Self::High => "high",
52 }
53 }
54}
55
56impl Serialize for Urgency {
57 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
58 serializer.serialize_str(self.as_str())
59 }
60}
61
62impl<'de> Deserialize<'de> for Urgency {
65 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
66 const VARIANTS: &[&str] = &["very-low", "low", "normal", "high"];
67 let s = String::deserialize(deserializer)?;
68 match s.as_str() {
69 "very-low" => Ok(Self::VeryLow),
70 "low" => Ok(Self::Low),
71 "normal" => Ok(Self::Normal),
72 "high" => Ok(Self::High),
73 other => Err(serde::de::Error::unknown_variant(other, VARIANTS)),
74 }
75 }
76}
77
78const STATE_CHANGE_TYPE: &str = "StateChange";
80
81const PUSH_VERIFICATION_TYPE: &str = "PushVerification";
83
84const REDACTED: &str = "[REDACTED]";
86
87#[non_exhaustive]
101#[derive(Debug, Clone, PartialEq, Eq)]
102pub struct StateChange {
103 pub changed: BTreeMap<Id, BTreeMap<String, State>>,
108
109 pub extra: serde_json::Map<String, serde_json::Value>,
114}
115
116impl StateChange {
117 pub fn new(changed: BTreeMap<Id, BTreeMap<String, State>>) -> Self {
119 Self {
120 changed,
121 extra: serde_json::Map::new(),
122 }
123 }
124}
125
126impl Serialize for StateChange {
127 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
128 let mut map = s.serialize_map(None)?;
129 map.serialize_entry("@type", STATE_CHANGE_TYPE)?;
130 map.serialize_entry("changed", &self.changed)?;
131 for (k, v) in &self.extra {
132 map.serialize_entry(k, v)?;
133 }
134 map.end()
135 }
136}
137
138impl<'de> Deserialize<'de> for StateChange {
139 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
140 let mut extra = serde_json::Map::deserialize(d)?;
141 take_type(&mut extra, STATE_CHANGE_TYPE)?;
142 let changed = take_field(&mut extra, "changed")?;
143 Ok(StateChange { changed, extra })
144 }
145}
146
147#[non_exhaustive]
160#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct PushSubscription {
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 pub id: Option<Id>,
166 pub device_client_id: String,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
171 pub url: Option<String>,
172 #[serde(default, skip_serializing_if = "Option::is_none")]
174 pub keys: Option<PushKeys>,
175 #[serde(default, skip_serializing_if = "Option::is_none")]
178 pub verification_code: Option<String>,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub expires: Option<UTCDate>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub types: Option<Vec<String>>,
185 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
189 pub extra: serde_json::Map<String, serde_json::Value>,
190}
191
192impl fmt::Debug for PushSubscription {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 f.debug_struct("PushSubscription")
195 .field("id", &self.id)
196 .field("device_client_id", &self.device_client_id)
197 .field("url", &self.url.as_ref().map(|_| REDACTED))
198 .field("keys", &self.keys)
199 .field(
200 "verification_code",
201 &self.verification_code.as_ref().map(|_| REDACTED),
202 )
203 .field("expires", &self.expires)
204 .field("types", &self.types)
205 .field("extra", &self.extra)
206 .finish()
207 }
208}
209
210impl crate::JmapObject for PushSubscription {
213 const TYPE_NAME: &'static str = "PushSubscription";
214 type Property = ();
215}
216
217impl crate::SetObject for PushSubscription {
222 type Patch = crate::PatchObject;
223 const SERVER_SET: &'static [&'static str] = &["id"];
224 const CLIENT_SETTABLE: &'static [&'static str] = &[
225 "deviceClientId",
226 "url",
227 "keys",
228 "verificationCode",
229 "expires",
230 "types",
231 ];
232 const IMMUTABLE: &'static [&'static str] = &["deviceClientId", "url", "keys"];
233}
234
235#[non_exhaustive]
240#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
241pub struct PushKeys {
242 pub p256dh: String,
245 pub auth: String,
248 #[serde(flatten, default, skip_serializing_if = "serde_json::Map::is_empty")]
251 pub extra: serde_json::Map<String, serde_json::Value>,
252}
253
254impl fmt::Debug for PushKeys {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 f.debug_struct("PushKeys")
257 .field("p256dh", &self.p256dh)
258 .field("auth", &REDACTED)
259 .field("extra", &self.extra)
260 .finish()
261 }
262}
263
264#[non_exhaustive]
271#[derive(Clone, PartialEq, Eq)]
272pub struct PushVerification {
273 pub push_subscription_id: Id,
275 pub verification_code: String,
278 pub extra: serde_json::Map<String, serde_json::Value>,
282}
283
284impl fmt::Debug for PushVerification {
285 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286 f.debug_struct("PushVerification")
287 .field("push_subscription_id", &self.push_subscription_id)
288 .field("verification_code", &REDACTED)
289 .field("extra", &self.extra)
290 .finish()
291 }
292}
293
294impl Serialize for PushVerification {
295 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
296 let mut map = s.serialize_map(None)?;
297 map.serialize_entry("@type", PUSH_VERIFICATION_TYPE)?;
298 map.serialize_entry("pushSubscriptionId", &self.push_subscription_id)?;
299 map.serialize_entry("verificationCode", &self.verification_code)?;
300 for (k, v) in &self.extra {
301 map.serialize_entry(k, v)?;
302 }
303 map.end()
304 }
305}
306
307impl<'de> Deserialize<'de> for PushVerification {
308 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
309 let mut extra = serde_json::Map::deserialize(d)?;
310 take_type(&mut extra, PUSH_VERIFICATION_TYPE)?;
311 let push_subscription_id = take_field(&mut extra, "pushSubscriptionId")?;
312 let verification_code = take_field(&mut extra, "verificationCode")?;
313 Ok(PushVerification {
314 push_subscription_id,
315 verification_code,
316 extra,
317 })
318 }
319}
320
321fn take_type<E: serde::de::Error>(
323 map: &mut serde_json::Map<String, serde_json::Value>,
324 expected: &str,
325) -> Result<(), E> {
326 match map.remove("@type") {
327 None => Ok(()),
328 Some(serde_json::Value::String(s)) if s == expected => Ok(()),
329 Some(serde_json::Value::String(_)) => {
330 Err(E::custom(format!("@type must be \"{expected}\"")))
331 }
332 Some(other) => Err(E::custom(format!(
333 "@type must be a string, found {}",
334 crate::json_type_name(&other)
335 ))),
336 }
337}
338
339fn take_field<T: DeserializeOwned, E: serde::de::Error>(
341 map: &mut serde_json::Map<String, serde_json::Value>,
342 key: &'static str,
343) -> Result<T, E> {
344 let value = map.remove(key).ok_or_else(|| E::missing_field(key))?;
345 serde_json::from_value(value).map_err(E::custom)
346}
347
348#[cfg(test)]
349mod tests {
350 use super::*;
351
352 #[test]
357 fn push_verification_type_error_does_not_echo_value() {
358 for (raw, want) in [
359 (
360 r#"{"@type":{"note":"CANARY-kwqn"},"pushSubscriptionId":"p","verificationCode":"c"}"#,
361 "@type must be a string, found object",
362 ),
363 (
364 r#"{"@type":"CANARY-kwqn","pushSubscriptionId":"p","verificationCode":"c"}"#,
365 "@type must be \"PushVerification\"",
366 ),
367 ] {
368 let err = serde_json::from_str::<PushVerification>(raw)
369 .unwrap_err()
370 .to_string();
371 assert!(!err.contains("CANARY-kwqn"), "{err}");
372 assert!(err.contains(want), "{err}");
373 }
374 }
375
376 #[test]
379 fn push_subscription_properties_are_classified() {
380 let sub = PushSubscription {
381 id: Some(Id::from("ps1")),
382 device_client_id: "dev".into(),
383 url: Some("https://push.example.com/x".into()),
384 keys: Some(PushKeys {
385 p256dh: "p".into(),
386 auth: "a".into(),
387 extra: serde_json::Map::new(),
388 }),
389 verification_code: Some("code".into()),
390 expires: Some(UTCDate::from("2026-01-01T00:00:00Z")),
391 types: Some(vec!["Email".into()]),
392 extra: serde_json::Map::new(),
393 };
394 assert_eq!(crate::check_property_classification(&sub), Ok(()));
395 }
396 use serde_json::json;
397
398 #[test]
401 fn state_change_preserves_vendor_extras() {
402 let raw = json!({
403 "changed": {
404 "acc1": { "Email": "s42" }
405 },
406 "acmeCorpSequence": 17
407 });
408 let obj: StateChange = serde_json::from_value(raw).expect("StateChange must deserialize");
409 assert_eq!(
410 obj.extra.get("acmeCorpSequence").and_then(|v| v.as_u64()),
411 Some(17)
412 );
413
414 let v = serde_json::to_value(&obj).expect("StateChange must serialize");
416 assert_eq!(v["acmeCorpSequence"], json!(17));
417 }
418
419 #[test]
431 fn extra_equality_is_order_insensitive_under_workspace_flags() {
432 let raw_a = json!({
437 "changed": {"acc1": {"Email": "s1"}},
438 "vendorA": 1,
439 "vendorB": 2
440 });
441 let raw_b = json!({
442 "changed": {"acc1": {"Email": "s1"}},
443 "vendorB": 2,
444 "vendorA": 1
445 });
446
447 let a: StateChange = serde_json::from_value(raw_a).expect("a must deserialize");
448 let b: StateChange = serde_json::from_value(raw_b).expect("b must deserialize");
449
450 assert_eq!(
451 a, b,
452 "extra-map equality is order-insensitive under the workspace's \
453 default serde_json::Map (BTreeMap-backed); if this fails, \
454 check whether preserve_order has been enabled in the dep graph"
455 );
456 }
457
458 #[test]
461 fn urgency_wire_values_in_increasing_order() {
462 let ordered = [
463 Urgency::VeryLow,
464 Urgency::Low,
465 Urgency::Normal,
466 Urgency::High,
467 ];
468 let wire: Vec<&str> = ordered.iter().map(|u| u.as_str()).collect();
469 assert_eq!(wire, ["very-low", "low", "normal", "high"]);
470 assert!(ordered.windows(2).all(|w| w[0] < w[1]));
471 }
472
473 #[test]
477 fn urgency_serde_accepts_only_rfc8030_values() {
478 for (wire, urgency) in [
479 ("very-low", Urgency::VeryLow),
480 ("low", Urgency::Low),
481 ("normal", Urgency::Normal),
482 ("high", Urgency::High),
483 ] {
484 let parsed: Urgency = serde_json::from_value(json!(wire)).expect(wire);
485 assert_eq!(parsed, urgency);
486 assert_eq!(serde_json::to_value(urgency).expect(wire), json!(wire));
487 }
488 for bad in ["urgent", "High", "very_low", ""] {
489 assert!(
490 serde_json::from_value::<Urgency>(json!(bad)).is_err(),
491 "{bad:?}"
492 );
493 }
494 }
495}