use crate::serve::storage::Failure;
use serde_json::{Value, json};
pub(super) fn parse(bytes: &[u8], store: &str) -> Result<Value, Failure> {
let body: Value = serde_json::from_slice(bytes).map_err(|_| invalid())?;
let object = body.as_object().ok_or_else(invalid)?;
let keys: [&str; 5] = ["text", "command", "config", "session", "cancel"];
let mut chosen = keys.into_iter().filter(|key| object.contains_key(*key));
let key = chosen.next().ok_or_else(invalid)?;
if chosen.next().is_some() {
return Err(invalid());
}
let value = &body[key];
let valid = match key {
"text" => value.as_str().is_some_and(|text| !text.trim().is_empty()),
"cancel" => value == true,
"command" => {
value.as_object().is_some_and(|o| o.len() == 2)
&& value["name"].as_str().is_some_and(|name| !name.is_empty())
&& value["input"].is_string()
}
"config" => value.as_object().is_some_and(|o| {
o.is_empty()
|| (o.len() == 2
&& value["option"]
.as_str()
.is_some_and(|option| !option.is_empty())
&& (value["value"].is_string() || value["value"].is_boolean()))
}),
"session" => value.as_object().is_some_and(|o| {
o.len() == 1
&& (value["list"] == true
|| value["new"] == true
|| value["open"]
.as_str()
.is_some_and(super::assistant_wire::identifier))
}),
_ => false,
};
if !valid {
return Err(invalid());
}
let mut op = json!({key:value});
if let Some(context) = body.get("context") {
if key != "text"
|| !context.as_object().is_some_and(|o| {
o.keys().all(|k| {
matches!(
k.as_str(),
"store" | "branch" | "version" | "screen" | "selection"
)
})
})
{
return Err(invalid());
}
if context
.get("store")
.is_some_and(|value| value.as_str() != Some(store))
{
return Err(Failure::new(
"assistant_context_mismatch",
"the shared context must name the store in the URL",
));
}
if let Some(branch) = context.get("branch")
&& (context["store"] != store
|| !branch.as_str().is_some_and(|branch| {
!branch.is_empty() && !branch.chars().any(char::is_control)
}))
{
return Err(Failure::new(
"assistant_context_invalid",
"name a branch within the URL's store and include that store in the context",
));
}
op["context"] = context.clone();
}
if object.keys().any(|k| {
!matches!(
k.as_str(),
"text" | "command" | "config" | "session" | "cancel" | "context" | "person"
)
}) {
return Err(invalid());
}
Ok(op)
}
fn invalid() -> Failure {
Failure::new(
"assistant_op_invalid",
"send exactly one valid text, command, config, session or cancel operation",
)
}