use serde_json::{Map, Value};
use super::Reply;
use super::board::decode::board;
use super::queue::queue_row_of;
use super::rows::decode::{conv_row, join_row, lineage_row, op_row, provider_row, rows_of, ws_row};
use super::search::hit_of;
use crate::boundary::codec::fields::{bool_of, list_of, opt_str_of, opt_val, str_of, strings_of};
use crate::boundary::codec::prepared_from_value;
use crate::registry::mailbox::{capture_of, invocation_of};
mod inspector;
pub fn decode(v: &Value) -> Result<Result<Reply, String>, String> {
let o = v.as_object().ok_or("reply: not a JSON object")?;
let Some(kind) = o.get("kind") else {
return refusal_of(o).map(Err);
};
let kind = kind.as_str().ok_or("reply: non-string field \"kind\"")?;
receipt(kind, o)
.or_else(|| listing(kind, o))
.or_else(|| inspector::decode(kind, o))
.unwrap_or_else(|| Err(format!("unknown reply kind {kind:?}")))
.map(Ok)
}
fn refusal_of(o: &Map<String, Value>) -> Result<String, String> {
if bool_of(o, "ok")? {
return Err("reply: an answer with no kind".to_owned());
}
str_of(o, "error")
}
fn receipt(kind: &str, o: &Map<String, Value>) -> Option<Result<Reply, String>> {
Some(match kind {
"outcome" => outcome(o),
"prepared" => prepared(o),
"fanned" => fanned(o),
"retired" => bool_of(o, "discarded").map(|discarded| Reply::Retired { discarded }),
"delivered" => delivered(o),
"started" => str_of(o, "conversation").map(|conversation| Reply::Started { conversation }),
"deleted" => Ok(Reply::Deleted),
"armed" => bool_of(o, "armed").map(|armed| Reply::Armed { armed }),
"flagged" => Ok(Reply::Flagged),
"answered" => answered(o),
"floored" => bool_of(o, "standing").map(|standing| Reply::Floored { standing }),
"nudged" => Ok(Reply::Nudged),
"acked" => Ok(Reply::Acked),
"trail-cleared" => Ok(Reply::TrailCleared),
"applied" => Ok(Reply::Applied),
"advertised" => Ok(Reply::Advertised),
super::encode::ENROLLED => enrolled(o),
"routed" => routed(o),
"marks" => str_of(o, "branch").map(|branch| Reply::Marks { branch }),
"config" => str_of(o, "text").map(|text| Reply::Config { text }),
_ => return None,
})
}
fn prepared(o: &Map<String, Value>) -> Result<Reply, String> {
let body = o.get("prepared").ok_or("prepared: missing prepared")?;
prepared_from_value(body).map(Reply::Prepared)
}
fn fanned(o: &Map<String, Value>) -> Result<Reply, String> {
list_of(o, "rows", prepared_from_value).map(Reply::Fanned)
}
fn delivered(o: &Map<String, Value>) -> Result<Reply, String> {
Ok(Reply::Delivered(crate::fan::Delivery {
target: str_of(o, "target")?,
base: str_of(o, "base")?,
source: opt_str_of(o, "source")?,
commit: opt_str_of(o, "commit")?,
}))
}
fn outcome(o: &Map<String, Value>) -> Result<Reply, String> {
let exit = o
.get("exit")
.and_then(Value::as_i64)
.and_then(|n| i32::try_from(n).ok())
.ok_or("outcome: missing or out-of-range field \"exit\"")?;
Ok(Reply::Outcome(crate::actions::verbs::Outcome {
exit,
stdout: str_of(o, "stdout")?,
stderr: str_of(o, "stderr")?,
}))
}
fn enrolled(o: &Map<String, Value>) -> Result<Reply, String> {
Ok(Reply::Enrolled(crate::registry::enroll::Enrolled {
grade: crate::boundary::codec::grade_of(&str_of(o, "grade")?)?,
name: str_of(o, "name")?,
address: str_of(o, "address")?,
ca: str_of(o, "ca")?,
cert: str_of(o, "cert")?,
key: str_of(o, "key")?,
}))
}
fn answered(o: &Map<String, Value>) -> Result<Reply, String> {
let word = str_of(o, "verdict")?;
Ok(Reply::Answered {
tool_use: str_of(o, "tool_use")?,
tool: str_of(o, "tool")?,
ruling: crate::control::judge::Ruling::of(&word)
.ok_or_else(|| format!("unknown verdict {word:?}"))?,
advanced: bool_of(o, "advanced")?,
})
}
fn listing(kind: &str, o: &Map<String, Value>) -> Option<Result<Reply, String>> {
Some(match kind {
"workspaces" => workspaces(o),
"workspace-balls" => rows_of(o, super::balls::bound_ball_of).map(Reply::WorkspaceBalls),
"conversations" => rows_of(o, conv_row).map(Reply::Conversations),
"balls" => rows_of(o, join_row).map(Reply::Balls),
"board" => board(o).map(Reply::Board),
"attention" => rows_of(o, queue_row_of).map(Reply::Attention),
"ops" => rows_of(o, op_row).map(Reply::Ops),
"help" => help(o),
"search" => search(o),
"providers" => rows_of(o, provider_row).map(Reply::Providers),
"lineages" => rows_of(o, lineage_row).map(Reply::Lineages),
"models" => strings_of(o, "rows").map(Reply::Models),
"clients" => rows_of(o, client_row).map(Reply::Clients),
"invocations" => rows_of(o, invocation_of).map(Reply::Invocations),
_ => return None,
})
}
fn workspaces(o: &Map<String, Value>) -> Result<Reply, String> {
Ok(Reply::Workspaces(crate::boundary::reply::Workspaces {
rows: rows_of(o, ws_row)?,
stale: opt_str_of(o, "stale")?,
growth: opt_str_of(o, "growth")?,
}))
}
fn routed(o: &Map<String, Value>) -> Result<Reply, String> {
Ok(Reply::Routed {
invocation: str_of(o, "invocation")?,
capture: opt_val(o, "capture", capture_of)?,
})
}
fn client_row(v: &Value) -> Result<crate::registry::roster::ClientRow, String> {
let o = v.as_object().ok_or("client row: not an object")?;
Ok(crate::registry::roster::ClientRow {
client: str_of(o, "client")?,
present: bool_of(o, "present")?,
tools: crate::registry::tools::decode(o.get("tools").ok_or("client row: missing tools")?)?,
})
}
fn help(o: &Map<String, Value>) -> Result<Reply, String> {
let table = crate::boundary::help::table();
rows_of(o, |v| {
let verb = str_of(v.as_object().ok_or("help row: not an object")?, "verb")?;
table
.iter()
.find(|row| row.verb == verb)
.copied()
.ok_or_else(|| format!("help: unknown verb {verb:?}"))
})
.map(Reply::Help)
}
fn search(o: &Map<String, Value>) -> Result<Reply, String> {
Ok(Reply::Search(crate::search::Found {
needle: str_of(o, "needle")?,
hits: list_of(o, "rows", hit_of)?,
unreadable: strings_of(o, "unreadable")?,
}))
}