use jmap_types::{Id, Invocation, JmapError, State};
use serde_json::{json, Value};
use crate::backend::{GetObject, JmapBackend, JmapObject, QueryObject};
use crate::helpers::{extract_account_id, not_found_json, ser};
pub async fn handle_get<O: GetObject, B: JmapBackend>(
backend: &B,
caller: &B::CallerCtx,
args: Value,
) -> Result<(Value, Vec<Invocation>), JmapError> {
let (account_id, mut args) = extract_account_id(args)?;
if !backend
.account_exists(caller, &account_id)
.await
.map_err(|e| JmapError::server_fail(e.to_string()))?
{
return Err(JmapError::account_not_found());
}
let ids: Option<Vec<Id>> = match args.remove("ids").unwrap_or(Value::Null) {
Value::Null => None,
v => Some(
serde_json::from_value(v)
.map_err(|_| JmapError::invalid_arguments("ids must be an Id array"))?,
),
};
let properties: Option<Vec<String>> = match args.remove("properties").unwrap_or(Value::Null) {
Value::Null => None,
v => Some(
serde_json::from_value(v)
.map_err(|_| JmapError::invalid_arguments("properties must be a string array"))?,
),
};
let ids_slice = ids.as_deref();
let (list, not_found) = backend
.get_objects::<O>(caller, &account_id, ids_slice, properties.as_deref())
.await
.map_err(|e| JmapError::server_fail(e.to_string()))?;
let state = backend
.get_state::<O>(caller, &account_id)
.await
.map_err(|e| JmapError::server_fail(e.to_string()))?;
let list_json: Vec<Value> = list.iter().map(ser).collect::<Result<Vec<_>, _>>()?;
Ok((
json!({
"accountId": account_id.as_ref(),
"state": state.as_ref(),
"list": list_json,
"notFound": not_found_json(¬_found),
}),
vec![],
))
}
pub async fn handle_changes<O: JmapObject, B: JmapBackend>(
backend: &B,
caller: &B::CallerCtx,
args: Value,
) -> Result<(Value, Vec<Invocation>), JmapError> {
let (account_id, args) = extract_account_id(args)?;
if !backend
.account_exists(caller, &account_id)
.await
.map_err(|e| JmapError::server_fail(e.to_string()))?
{
return Err(JmapError::account_not_found());
}
let since_state: State = match args.get("sinceState").and_then(|v| v.as_str()) {
Some(s) => State::from(s),
None => return Err(JmapError::invalid_arguments("sinceState is required")),
};
let max_changes: Option<u64> = match args.get("maxChanges") {
None | Some(Value::Null) => None,
Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
JmapError::invalid_arguments("maxChanges must be a positive integer")
})?),
};
let result = backend
.get_changes::<O>(caller, &account_id, &since_state, max_changes)
.await
.map_err(JmapError::from)?;
Ok((
json!({
"accountId": account_id.as_ref(),
"oldState": since_state.as_ref(),
"newState": result.new_state.as_ref(),
"hasMoreChanges": result.has_more_changes,
"updatedProperties": Value::Null,
"created": result.created.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
"updated": result.updated.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
"destroyed": result.destroyed.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
}),
vec![],
))
}
pub async fn handle_query<O: QueryObject, B: JmapBackend>(
backend: &B,
caller: &B::CallerCtx,
args: Value,
) -> Result<(Value, Vec<Invocation>), JmapError> {
let (account_id, mut args) = extract_account_id(args)?;
if !backend
.account_exists(caller, &account_id)
.await
.map_err(|e| JmapError::server_fail(e.to_string()))?
{
return Err(JmapError::account_not_found());
}
let calculate_total: bool = args
.get("calculateTotal")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let limit: Option<u64> = match args.get("limit") {
None | Some(Value::Null) => None,
Some(v) => match v.as_u64() {
Some(n) => Some(n),
None => {
return Err(JmapError::invalid_arguments(format!(
"limit: expected a non-negative integer, got {v}"
)))
}
},
};
let position: i64 = match args.get("position") {
None | Some(Value::Null) => 0,
Some(v) => v.as_i64().ok_or_else(|| {
JmapError::invalid_arguments(format!("position: expected an integer, got {v}"))
})?,
};
let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
Value::Null => None,
v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
};
let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
Value::Null => None,
v => Some(
serde_json::from_value(v)
.map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
),
};
let result = backend
.query_objects::<O>(
caller,
&account_id,
filter.as_ref(),
sort.as_deref(),
limit,
position,
)
.await
.map_err(|e| JmapError::server_fail(e.to_string()))?;
let mut resp = json!({
"accountId": account_id.as_ref(),
"queryState": result.query_state.as_ref(),
"canCalculateChanges": result.can_calculate_changes,
"position": result.position,
"ids": result.ids.iter().map(|id| id.as_ref()).collect::<Vec<_>>(),
});
if calculate_total {
if let Some(t) = result.total {
resp["total"] = json!(t);
}
}
Ok((resp, vec![]))
}
pub async fn handle_query_changes<O: QueryObject, B: JmapBackend>(
backend: &B,
caller: &B::CallerCtx,
args: Value,
) -> Result<(Value, Vec<Invocation>), JmapError> {
let (account_id, mut args) = extract_account_id(args)?;
if !backend
.account_exists(caller, &account_id)
.await
.map_err(|e| JmapError::server_fail(e.to_string()))?
{
return Err(JmapError::account_not_found());
}
let since_query_state: State = match args.get("sinceQueryState").and_then(|v| v.as_str()) {
Some(s) => State::from(s),
None => return Err(JmapError::invalid_arguments("sinceQueryState is required")),
};
let max_changes: Option<u64> = match args.get("maxChanges") {
None | Some(Value::Null) => None,
Some(v) => Some(v.as_u64().filter(|&n| n > 0).ok_or_else(|| {
JmapError::invalid_arguments("maxChanges must be a positive integer")
})?),
};
let up_to_id: Option<Id> = match args.get("upToId") {
None | Some(Value::Null) => None,
Some(Value::String(s)) => Some(Id::from(s.as_str())),
Some(_) => {
return Err(JmapError::invalid_arguments(
"upToId must be a string Id or null",
))
}
};
let calculate_total: bool = args
.get("calculateTotal")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let filter: Option<O::Filter> = match args.remove("filter").unwrap_or(Value::Null) {
Value::Null => None,
v => Some(serde_json::from_value(v).map_err(|_| JmapError::unsupported_filter())?),
};
let sort: Option<Vec<O::Comparator>> = match args.remove("sort").unwrap_or(Value::Null) {
Value::Null => None,
v => Some(
serde_json::from_value(v)
.map_err(|_| JmapError::invalid_arguments("sort must be an array"))?,
),
};
let result = backend
.query_changes::<O>(
caller,
&account_id,
&since_query_state,
filter.as_ref(),
sort.as_deref(),
max_changes,
up_to_id.as_ref(),
false, )
.await
.map_err(JmapError::from)?;
let removed: Vec<&str> = result.removed.iter().map(|id| id.as_ref()).collect();
let added: Vec<Value> = result
.added
.iter()
.map(|item| {
json!({
"id": item.id.as_ref(),
"index": item.index,
})
})
.collect();
let mut resp = json!({
"accountId": account_id.as_ref(),
"oldQueryState": result.old_query_state.as_ref(),
"newQueryState": result.new_query_state.as_ref(),
"removed": removed,
"added": added,
});
if calculate_total {
if let Some(t) = result.total {
resp["total"] = json!(t);
}
}
Ok((resp, vec![]))
}