pub trait JmapObject:
serde::Serialize + for<'de> serde::Deserialize<'de> + Send + Sync + 'static
{
const TYPE_NAME: &'static str;
type Property: Send + Sync + 'static;
}
pub trait GetObject: JmapObject {}
pub trait SetObject: JmapObject {
type Patch: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
const SERVER_SET: &'static [&'static str];
const CLIENT_SETTABLE: &'static [&'static str];
const IMMUTABLE: &'static [&'static str];
}
pub fn check_property_classification<T: SetObject>(obj: &T) -> Result<(), String> {
let value = serde_json::to_value(obj).map_err(|e| format!("serialize: {e}"))?;
let serde_json::Value::Object(map) = value else {
return Err(format!("{} does not serialize to an object", T::TYPE_NAME));
};
let mut problems = Vec::new();
for key in map.keys() {
let k = key.as_str();
match (T::SERVER_SET.contains(&k), T::CLIENT_SETTABLE.contains(&k)) {
(false, false) => problems.push(format!("{}.{k} is unclassified", T::TYPE_NAME)),
(true, true) => problems.push(format!("{}.{k} is classified twice", T::TYPE_NAME)),
_ => {}
}
}
for name in T::IMMUTABLE {
if !T::CLIENT_SETTABLE.contains(name) {
problems.push(format!(
"{}.{name} is IMMUTABLE but not CLIENT_SETTABLE",
T::TYPE_NAME
));
}
}
if problems.is_empty() {
Ok(())
} else {
Err(problems.join("\n"))
}
}
pub trait QueryObject: JmapObject {
type Filter: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
type Comparator: serde::Serialize + serde::de::DeserializeOwned + Send + Sync + 'static;
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(serde::Serialize, serde::Deserialize)]
struct Probe {
id: String,
name: String,
email: String,
}
impl JmapObject for Probe {
const TYPE_NAME: &'static str = "Probe";
type Property = ();
}
impl SetObject for Probe {
type Patch = serde_json::Value;
const SERVER_SET: &'static [&'static str] = &["id"];
const CLIENT_SETTABLE: &'static [&'static str] = &["name", "email"];
const IMMUTABLE: &'static [&'static str] = &["email"];
}
#[derive(serde::Serialize, serde::Deserialize)]
struct BadProbe {
id: String,
name: String,
email: String,
}
impl JmapObject for BadProbe {
const TYPE_NAME: &'static str = "BadProbe";
type Property = ();
}
impl SetObject for BadProbe {
type Patch = serde_json::Value;
const SERVER_SET: &'static [&'static str] = &["id", "name"];
const CLIENT_SETTABLE: &'static [&'static str] = &["name"];
const IMMUTABLE: &'static [&'static str] = &["id"];
}
fn probe<T: SetObject>(make: fn(String, String, String) -> T) -> Result<(), String> {
check_property_classification(&make("i".into(), "n".into(), "e".into()))
}
#[test]
fn classification_accepts_a_partition() {
assert_eq!(probe(|id, name, email| Probe { id, name, email }), Ok(()));
}
#[test]
fn classification_reports_every_problem() {
let err = probe(|id, name, email| BadProbe { id, name, email }).unwrap_err();
let mut lines: Vec<&str> = err.lines().collect();
lines.sort_unstable();
assert_eq!(
lines,
[
"BadProbe.email is unclassified",
"BadProbe.id is IMMUTABLE but not CLIENT_SETTABLE",
"BadProbe.name is classified twice",
]
);
}
}