use serde_json::{Value, json};
use std::sync::atomic::AtomicBool;
use super::{Site, render};
use crate::registry::roster::ClientRow;
mod edit;
pub const NAME: &str = "clients";
pub const DESCRIPTION: &str = "Registered client machines of this workspace \
and the tools they advertise. op=list: every client and which are connected \
right now. op=get with client=<name>: one client's detail and its advertised \
tools. op=load with client=<name> and tools=[<name>,…]: make those tools \
callable, by their prefixed names, from the next step on. op=unload with \
client=<name>, and tools=[…] or no tools at all: stop declaring them — the \
whole of that client's loaded tools when tools is omitted.";
pub fn schema() -> Value {
json!({
"type": "object",
"properties": {
"op": {"type": "string", "enum": ["list", "get", "load", "unload"],
"description": "which operation to perform"},
"client": {"type": "string",
"description": "client identity; required for get, load and unload"},
"tools": {"type": "array", "items": {"type": "string"},
"description": "advertised tool names; required for load, and \
optional for unload where omitting it means \
that client's whole loaded set"}
},
"required": ["op"]
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Op {
List,
Get(String),
Load(String, Vec<String>),
Unload(String, Option<Vec<String>>),
}
pub fn parse(input: &Value) -> Result<Op, String> {
let o = input.as_object().ok_or("input is not a JSON object")?;
let op = o
.get("op")
.and_then(Value::as_str)
.ok_or("missing string field \"op\"")?;
match op {
"list" => Ok(Op::List),
"get" => Ok(Op::Get(client_of(input)?)),
"load" => Ok(Op::Load(client_of(input)?, tools_of(input)?)),
"unload" => Ok(Op::Unload(client_of(input)?, tools_opt(input)?)),
other => Err(format!(
"unknown op {other:?}; expected \"list\", \"get\", \"load\" or \"unload\""
)),
}
}
fn client_of(input: &Value) -> Result<String, String> {
input
.get("client")
.and_then(Value::as_str)
.filter(|c| !c.is_empty())
.map(str::to_owned)
.ok_or_else(|| "missing non-empty string field \"client\"".to_owned())
}
fn tools_of(input: &Value) -> Result<Vec<String>, String> {
let rows = input
.get("tools")
.and_then(Value::as_array)
.ok_or("missing array field \"tools\"")?;
let names: Vec<String> = rows
.iter()
.filter_map(Value::as_str)
.map(str::to_owned)
.collect();
if names.len() != rows.len() || names.is_empty() {
return Err("field \"tools\" must be a non-empty array of names".to_owned());
}
Ok(names)
}
fn tools_opt(input: &Value) -> Result<Option<Vec<String>>, String> {
match input.get("tools") {
None | Some(Value::Null) => Ok(None),
Some(_) => tools_of(input).map(Some),
}
}
pub fn answer(site: &Site, input: &Value, stop: &AtomicBool) -> Result<String, String> {
let op = parse(input)?;
let observed = site.observed();
match op {
Op::List => Ok(render::list(
&site.workspace,
&observed,
&asked(site, stop)?,
)),
Op::Get(client) => Ok(render::get(
&site.workspace,
&observed,
&row_of(&asked(site, stop)?, &client)?,
)),
Op::Load(client, names) => {
edit::load(site, &asked(site, stop)?, &client, &names, &observed)
}
Op::Unload(client, names) => edit::unload(site, &client, names.as_deref(), &observed),
}
}
fn asked(site: &Site, stop: &AtomicBool) -> Result<Vec<ClientRow>, String> {
super::ask::roster(&site.state_root, &site.workspace, site.budget, stop)
}
fn row_of(rows: &[ClientRow], client: &str) -> Result<ClientRow, String> {
rows.iter()
.find(|r| r.client == client)
.cloned()
.ok_or_else(|| {
format!(
"no client {client:?} is registered in this workspace; \
op=list shows the {} there are",
rows.len()
)
})
}
#[cfg(test)]
mod tests;