use serde_json::{Map, Value};
use super::Reply;
use super::board::decode::board;
use super::config_answer::ConfigAnswer as A;
use super::queue::queue_row_of;
use super::rows::decode::{
conv_row, join_row, lineage_row, provider_row, role_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;
fn config(answer: A) -> Reply {
Reply::Config(answer)
}
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),
super::queue::ACKNOWLEDGED => super::queue::acknowledged_of(o),
"trail-cleared" => Ok(Reply::TrailCleared),
"applied" => Ok(Reply::Applied),
"advertised" => bool_of(o, "wrote").map(|wrote| Reply::Advertised { wrote }),
super::encode::ENROLLED => enrolled(o),
"routed" => routed(o),
super::encode::LOGIN => crate::login::wire::view_of(o).map(Reply::Login),
"marks" => str_of(o, "branch").map(|branch| Reply::Marks { branch }),
"config" => super::config_view::config_of(o).map(|v| config(A::File(v))),
_ => 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")?;
let said = str_of(o, "scope")?;
Ok(Reply::Answered(crate::boundary::control::Answered {
tool_use: str_of(o, "tool_use")?,
tool: str_of(o, "tool")?,
answer: crate::control::judge::Answer {
ruling: crate::control::judge::Ruling::of(&word)
.ok_or_else(|| format!("unknown verdict {word:?}"))?,
scope: crate::control::judge::Scope::of(&said)
.ok_or_else(|| format!("unknown scope {said:?}"))?,
},
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, super::op_row::decode).map(Reply::Ops),
"help" => help(o),
"search" => search(o),
"providers" => rows_of(o, provider_row).map(|r| config(A::Providers(r))),
"roles" => rows_of(o, role_row).map(|r| config(A::Roles(r))),
"lineages" => rows_of(o, lineage_row).map(|r| config(A::Lineages(r))),
"models" => strings_of(o, "rows").map(|r| config(A::Models(r))),
crate::proposals::wire::KIND => {
crate::proposals::wire::view_of(o).map(|v| config(A::Proposals(v)))
}
"clients" => rows_of(o, client_row).map(Reply::Clients),
"doctor" => rows_of(o, doctor_row).map(Reply::Doctor),
"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")?)?,
last_seen: crate::boundary::codec::fields::opt(
o,
"last_seen",
crate::boundary::codec::fields::i64_of,
)?,
})
}
fn doctor_row(v: &Value) -> Result<crate::doctor::Row, String> {
let o = v.as_object().ok_or("doctor row: not an object")?;
Ok(crate::doctor::Row {
check: str_of(o, "check")?,
ok: bool_of(o, "ok")?,
fact: str_of(o, "fact")?,
remedy: crate::boundary::codec::fields::opt(o, "remedy", str_of)?,
})
}
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")?,
}))
}