use serde_json::{Value, json};
use std::sync::atomic::AtomicBool;
use super::loaded::{self, Entry};
use super::{Site, render};
use crate::registry::roster::ClientRow;
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.";
pub fn schema() -> Value {
json!({
"type": "object",
"properties": {
"op": {"type": "string", "enum": ["list", "get", "load"],
"description": "which operation to perform"},
"client": {"type": "string",
"description": "client identity; required for get and load"},
"tools": {"type": "array", "items": {"type": "string"},
"description": "advertised tool names to load; required for load"}
},
"required": ["op"]
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Op {
List,
Get(String),
Load(String, 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)?)),
other => Err(format!(
"unknown op {other:?}; expected \"list\", \"get\" or \"load\""
)),
}
}
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)
}
pub fn answer(site: &Site, input: &Value, stop: &AtomicBool) -> Result<String, String> {
let op = parse(input)?;
let observed = site.observed();
let rows = super::ask::roster(&site.state_root, &site.workspace, site.budget, stop)?;
match op {
Op::List => Ok(render::list(&site.workspace, &observed, &rows)),
Op::Get(client) => Ok(render::get(
&site.workspace,
&observed,
&row_of(&rows, &client)?,
)),
Op::Load(client, names) => load(site, &rows, &client, &names, &observed),
}
}
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()
)
})
}
fn load(
site: &Site,
rows: &[ClientRow],
client: &str,
names: &[String],
observed: &str,
) -> Result<String, String> {
let row = row_of(rows, client)?;
let mut entries = Vec::new();
for name in names {
let tool = row
.tools
.iter()
.find(|t| &t.name == name)
.ok_or_else(|| format!("client {client:?} advertises no tool {name:?}"))?;
let entry = Entry {
client: client.to_owned(),
tool: tool.clone(),
};
let presented = entry.presented();
if !loaded::callable(&presented) {
return Err(format!("{presented:?} is not a usable tool name"));
}
entries.push(entry);
}
let all = loaded::add(&site.state_root, &site.workspace, &site.agent, &entries)
.map_err(|e| format!("recording the load: {e}"))?;
Ok(render::load(observed, &entries, all.len()))
}
#[cfg(test)]
mod tests;