use alloc::{
collections::BTreeMap,
string::{String, ToString},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub mod subscribe;
const DEFAULT_TYPE_TAG: &str = "StateChange";
fn default_type_tag() -> String {
DEFAULT_TYPE_TAG.to_string()
}
pub type JmapTypeStates = BTreeMap<String, String>;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct JmapStateChange {
#[serde(rename = "@type", default = "default_type_tag")]
pub r#type: String,
#[serde(default)]
pub changed: BTreeMap<String, JmapTypeStates>,
}
impl JmapStateChange {
pub fn parse(data: &str) -> Result<Self, JmapStateChangeParseError> {
let trimmed = data.trim();
if trimmed.is_empty() {
return Ok(Self::default());
}
let change: Self = serde_json::from_str(trimmed)?;
if change.r#type != DEFAULT_TYPE_TAG {
return Err(JmapStateChangeParseError::UnexpectedType(change.r#type));
}
Ok(change)
}
}
#[derive(Debug, Error)]
pub enum JmapStateChangeParseError {
#[error("Invalid StateChange JSON: {0}")]
InvalidJson(#[from] serde_json::Error),
#[error("Expected @type StateChange, got {0}")]
UnexpectedType(String),
}
#[derive(Clone, Copy, Debug)]
pub enum JmapCloseAfter {
No,
State,
}
impl JmapCloseAfter {
pub(super) fn as_str(self) -> &'static str {
match self {
Self::No => "no",
Self::State => "state",
}
}
}
#[cfg(test)]
mod tests {
use crate::rfc8620::event_source::*;
#[test]
fn parses_minimal_state_change() {
let json = r#"{"@type":"StateChange","changed":{"u1":{"Email":"s1"}}}"#;
let change = JmapStateChange::parse(json).unwrap();
assert_eq!(change.r#type, "StateChange");
assert_eq!(change.changed.len(), 1);
assert_eq!(change.changed["u1"]["Email"], "s1");
}
#[test]
fn parses_multi_account_multi_type() {
let json = r#"{
"@type": "StateChange",
"changed": {
"acc-a": {"Email": "e1", "Mailbox": "m1"},
"acc-b": {"Email": "e2"}
}
}"#;
let change = JmapStateChange::parse(json).unwrap();
assert_eq!(change.changed.len(), 2);
assert_eq!(change.changed["acc-a"]["Mailbox"], "m1");
assert_eq!(change.changed["acc-b"]["Email"], "e2");
}
#[test]
fn empty_data_is_keep_alive() {
let change = JmapStateChange::parse("").unwrap();
assert!(change.changed.is_empty());
let change = JmapStateChange::parse(" \n ").unwrap();
assert!(change.changed.is_empty());
}
#[test]
fn wrong_type_field_rejected() {
let json = r#"{"@type":"NotAStateChange","changed":{}}"#;
match JmapStateChange::parse(json) {
Err(JmapStateChangeParseError::UnexpectedType(t)) => assert_eq!(t, "NotAStateChange"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn invalid_json_rejected() {
match JmapStateChange::parse("{not json") {
Err(JmapStateChangeParseError::InvalidJson(_)) => {}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn missing_changed_field_defaults_to_empty() {
let json = r#"{"@type":"StateChange"}"#;
let change = JmapStateChange::parse(json).unwrap();
assert!(change.changed.is_empty());
}
}