use std::path::PathBuf;
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ToolReport {
pub id: String,
pub title: Option<String>,
pub kind: Option<String>,
pub status: Option<String>,
pub raw_input: Option<serde_json::Value>,
pub locations: Option<Vec<PathBuf>>,
}
impl ToolReport {
#[must_use]
pub fn from_wire(id: String, fields: &serde_json::Value) -> Self {
let text = |k: &str| fields.get(k).and_then(|v| v.as_str()).map(str::to_owned);
Self {
id,
title: text("title"),
kind: text("kind"),
status: text("status"),
raw_input: fields.get("rawInput").filter(|v| !v.is_null()).cloned(),
locations: fields
.get("locations")
.and_then(|v| v.as_array())
.map(|ls| {
ls.iter()
.filter_map(|l| l.get("path").and_then(|p| p.as_str()))
.map(PathBuf::from)
.collect()
}),
}
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ToolRequest {
pub id: String,
pub title: Option<String>,
pub kind: Option<String>,
pub raw_input: Option<serde_json::Value>,
pub locations: Vec<PathBuf>,
pub status: Option<String>,
}
impl ToolRequest {
pub fn merge(&mut self, r: &ToolReport) {
if let Some(t) = &r.title {
self.title = Some(t.chars().take(200).collect());
}
if let Some(k) = &r.kind {
self.kind = Some(k.clone());
}
if let Some(i) = &r.raw_input {
self.raw_input = Some(i.clone());
}
if let Some(l) = &r.locations {
self.locations.clone_from(l);
}
if let Some(s) = &r.status {
self.status = Some(s.clone());
}
}
#[must_use]
pub fn shown_title(&self) -> String {
match self.title.as_deref() {
Some(t) if !t.trim().is_empty() => t.to_owned(),
_ => format!("untitled call {}", self.id),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn wire(v: serde_json::Value) -> ToolReport {
ToolReport::from_wire("c1".into(), &v)
}
#[test]
fn a_present_field_replaces_and_an_absent_or_null_one_keeps() {
let mut call = ToolRequest {
id: "c1".into(),
..ToolRequest::default()
};
call.merge(&wire(json!({
"title": "Run a command", "kind": "execute",
"rawInput": {"command": "ls"}, "locations": [{"path": "/a"}]
})));
call.merge(&wire(json!({"rawInput": {"command": "rm -rf build"}})));
assert_eq!(call.title.as_deref(), Some("Run a command"));
assert_eq!(call.raw_input, Some(json!({"command": "rm -rf build"})));
call.merge(&wire(
json!({"title": null, "kind": null, "rawInput": null, "locations": null}),
));
assert_eq!(call.kind.as_deref(), Some("execute"), "null keeps");
assert_eq!(call.locations, vec![PathBuf::from("/a")]);
call.merge(&wire(json!({"locations": []})));
assert!(call.locations.is_empty(), "an empty list is said");
assert_eq!(
ToolRequest {
id: "t9".into(),
..ToolRequest::default()
}
.shown_title(),
"untitled call t9"
);
}
}