use serde_json::Value;
pub fn find_by_name(state: &Value, collection: &str, name: &str) -> Option<Value> {
state
.get(collection)
.and_then(Value::as_array)?
.iter()
.find(|item| item.get("name").and_then(Value::as_str) == Some(name))
.cloned()
}
pub fn find_by_field(state: &Value, collection: &str, field: &str, value: &str) -> Option<Value> {
state
.get(collection)
.and_then(Value::as_array)?
.iter()
.find(|item| item.get(field).and_then(Value::as_str) == Some(value))
.cloned()
}
pub fn find_matching_template(state: &Value, uri: &str) -> Option<Value> {
state
.get("resource_templates")
.and_then(Value::as_array)?
.iter()
.find(|t| {
t.get("uriTemplate")
.and_then(Value::as_str)
.is_some_and(|tmpl| matches_uri_template(tmpl, uri))
})
.cloned()
}
pub fn matches_uri_template(template: &str, uri: &str) -> bool {
let mut literals = Vec::new();
let mut rest = template;
while let Some(start) = rest.find('{') {
literals.push(&rest[..start]);
let Some(end) = rest[start..].find('}') else {
return false; };
rest = &rest[start + end + 1..];
}
literals.push(rest);
let last_nonempty = literals.iter().rposition(|l| !l.is_empty());
let mut pos = 0;
for (i, literal) in literals.iter().enumerate() {
if literal.is_empty() {
if i > 0 && i < literals.len() - 1 {
if pos >= uri.len() {
return false;
}
pos += uri[pos..].chars().next().map_or(1, char::len_utf8);
}
continue;
}
let skip = if i > 0 {
uri.get(pos..)
.and_then(|s| s.chars().next())
.map_or(1, char::len_utf8)
} else {
0
};
if pos + skip > uri.len() {
return false;
}
let search = &uri[pos + skip..];
let found = if Some(i) == last_nonempty && i > 0 {
search.rfind(literal)
} else {
search.find(literal)
};
let Some(found) = found else {
return false;
};
if i == 0 && found != 0 {
return false;
}
pos += skip + found + literal.len();
}
let last_literal = literals.last().unwrap_or(&"");
if last_literal.is_empty() {
if literals.len() > 1 {
return pos < uri.len();
}
}
pos == uri.len()
}
pub fn find_a2a_skill(state: &Value, skill_id: &str) -> Option<Value> {
find_by_field(state, "skills", "id", skill_id)
.or_else(|| {
state
.get("agent_card")
.and_then(|ac| ac.get("skills"))
.and_then(Value::as_array)
.and_then(|arr| {
arr.iter()
.find(|s| s.get("id").and_then(Value::as_str) == Some(skill_id))
})
.cloned()
})
.or_else(|| find_by_field(state, "skills", "name", skill_id))
.or_else(|| {
state
.get("agent_card")
.and_then(|ac| ac.get("skills"))
.and_then(Value::as_array)
.and_then(|arr| {
arr.iter()
.find(|s| s.get("name").and_then(Value::as_str) == Some(skill_id))
})
.cloned()
})
}
pub fn a2a_skill_array(state: &Value) -> Option<&Vec<Value>> {
crate::engine::a2a::skill_array(state)
}
pub fn a2a_skill_name(skill: &Value) -> Option<&str> {
crate::engine::a2a::skill_name(skill)
}
#[must_use]
pub fn build_a2a_response_content(state: &Value) -> Option<A2aResponseContent> {
let mut parts = Vec::new();
if let Some(task_parts) = state
.pointer("/task/message/parts")
.and_then(Value::as_array)
{
for part in task_parts {
if part.get("type").and_then(Value::as_str) == Some("text")
&& let Some(text) = part.get("text").and_then(Value::as_str)
{
parts.push(text.to_string());
}
}
}
let artifact_sources = [state.pointer("/task/artifacts"), state.get("artifacts")];
for source in artifact_sources.into_iter().flatten() {
if let Some(artifacts) = source.as_array() {
for artifact in artifacts {
if let Some(content) = artifact.get("content").and_then(Value::as_str) {
parts.push(content.to_string());
}
}
}
}
if parts.is_empty()
&& let Some(responses) = state.get("task_responses").and_then(Value::as_array)
&& let Some(first) = responses.first()
{
if let Some(msg_parts) = first
.pointer("/content/message/parts")
.and_then(Value::as_array)
{
for part in msg_parts {
if part.get("type").and_then(Value::as_str) == Some("text")
&& let Some(text) = part.get("text").and_then(Value::as_str)
{
parts.push(text.to_string());
}
}
}
if let Some(artifacts) = first
.pointer("/content/artifacts")
.and_then(Value::as_array)
{
for artifact in artifacts {
if let Some(content) = artifact.get("content").and_then(Value::as_str) {
parts.push(content.to_string());
}
}
}
}
if parts.is_empty() {
return None;
}
let task_status = state
.pointer("/task/status")
.and_then(Value::as_str)
.unwrap_or("completed")
.to_string();
let mut text = String::new();
if task_status == "input-required" {
text.push_str("[Agent requires additional input]\n");
}
text.push_str(&parts.join("\n"));
Some(A2aResponseContent {
text,
status: task_status,
})
}
pub struct A2aResponseContent {
pub text: String,
pub status: String,
}
pub fn strip_internal_fields(value: &Value, fields: &[&str]) -> Value {
let Some(obj) = value.as_object() else {
return value.clone();
};
let mut cleaned = obj.clone();
for field in fields {
cleaned.remove(*field);
}
Value::Object(cleaned)
}
pub fn u64_to_usize(v: u64) -> usize {
usize::try_from(v).unwrap_or(usize::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
#[test]
fn regression_fuzz_multibyte_uri_no_panic() {
let template = "{Z}4{}}4{}\x01";
let uri = "\u{0182}4{}2=e";
let _ = matches_uri_template(template, uri);
}
#[test]
fn multibyte_chars_in_template_and_uri() {
let _ = matches_uri_template("{v}é{w}", "Ƃéx");
let _ = matches_uri_template("préfixe{v}suffixe", "préfixeXsuffixe");
let _ = matches_uri_template("{v}", "日本語");
let _ = matches_uri_template("{a}{b}{c}", "αβγ");
}
#[test]
fn find_by_name_found() {
let state = serde_json::json!({
"tools": [
{"name": "search", "desc": "web search"},
{"name": "calc", "desc": "calculator"}
]
});
let result = find_by_name(&state, "tools", "calc").unwrap();
assert_eq!(result["desc"], "calculator");
}
#[test]
fn find_by_name_not_found() {
let state = serde_json::json!({"tools": [{"name": "search"}]});
assert!(find_by_name(&state, "tools", "missing").is_none());
}
#[test]
fn find_by_name_missing_collection() {
let state = serde_json::json!({});
assert!(find_by_name(&state, "tools", "any").is_none());
}
#[test]
fn find_by_field_custom_field() {
let state = serde_json::json!({
"skills": [
{"id": "s1", "name": "translate"},
{"id": "s2", "name": "summarize"}
]
});
let result = find_by_field(&state, "skills", "id", "s2").unwrap();
assert_eq!(result["name"], "summarize");
}
#[test]
fn find_a2a_skill_by_id_top_level() {
let state = serde_json::json!({
"skills": [{"id": "translate", "name": "Translate Text"}]
});
let skill = find_a2a_skill(&state, "translate").unwrap();
assert_eq!(skill["name"], "Translate Text");
}
#[test]
fn find_a2a_skill_by_id_in_agent_card() {
let state = serde_json::json!({
"agent_card": {
"skills": [{"id": "analyze", "name": "Data Analysis"}]
}
});
let skill = find_a2a_skill(&state, "analyze").unwrap();
assert_eq!(skill["name"], "Data Analysis");
}
#[test]
fn find_a2a_skill_falls_back_to_name() {
let state = serde_json::json!({
"skills": [{"name": "process-data"}]
});
let skill = find_a2a_skill(&state, "process-data").unwrap();
assert_eq!(skill["name"], "process-data");
}
#[test]
fn find_a2a_skill_name_fallback_in_agent_card() {
let state = serde_json::json!({
"agent_card": {
"skills": [{"name": "deep-search"}]
}
});
let skill = find_a2a_skill(&state, "deep-search").unwrap();
assert_eq!(skill["name"], "deep-search");
}
#[test]
fn find_a2a_skill_not_found() {
let state = serde_json::json!({
"skills": [{"id": "a", "name": "b"}]
});
assert!(find_a2a_skill(&state, "nonexistent").is_none());
}
#[test]
fn find_a2a_skill_empty_state() {
assert!(find_a2a_skill(&serde_json::json!({}), "any").is_none());
}
#[test]
fn build_a2a_response_from_task_parts() {
let state = serde_json::json!({
"task": {
"status": "completed",
"message": {
"parts": [
{"type": "text", "text": "Analysis complete."},
{"type": "text", "text": "Revenue is $1M."}
]
}
}
});
let resp = build_a2a_response_content(&state).unwrap();
assert_eq!(resp.status, "completed");
assert!(resp.text.contains("Analysis complete."));
assert!(resp.text.contains("Revenue is $1M."));
}
#[test]
fn build_a2a_response_from_artifacts() {
let state = serde_json::json!({
"task": {
"status": "completed",
"artifacts": [{"content": "CSV data here"}]
}
});
let resp = build_a2a_response_content(&state).unwrap();
assert!(resp.text.contains("CSV data here"));
}
#[test]
fn build_a2a_response_input_required() {
let state = serde_json::json!({
"task": {
"status": "input-required",
"message": {
"parts": [{"type": "text", "text": "What file?"}]
}
}
});
let resp = build_a2a_response_content(&state).unwrap();
assert_eq!(resp.status, "input-required");
assert!(resp.text.starts_with("[Agent requires additional input]"));
}
#[test]
fn build_a2a_response_empty_returns_none() {
let state = serde_json::json!({"task": {"status": "completed"}});
assert!(build_a2a_response_content(&state).is_none());
}
#[test]
fn build_a2a_response_top_level_artifacts() {
let state = serde_json::json!({
"artifacts": [{"content": "top-level artifact"}]
});
let resp = build_a2a_response_content(&state).unwrap();
assert!(resp.text.contains("top-level artifact"));
}
#[test]
fn strip_internal_fields_removes_specified() {
let value = serde_json::json!({"name": "calc", "responses": [], "_internal": true});
let stripped = strip_internal_fields(&value, &["responses", "_internal"]);
assert!(stripped.get("name").is_some());
assert!(stripped.get("responses").is_none());
assert!(stripped.get("_internal").is_none());
}
#[test]
fn strip_internal_fields_non_object_passthrough() {
let value = serde_json::json!("just a string");
let stripped = strip_internal_fields(&value, &["anything"]);
assert_eq!(stripped, serde_json::json!("just a string"));
}
#[test]
fn u64_to_usize_normal() {
assert_eq!(u64_to_usize(42), 42);
assert_eq!(u64_to_usize(0), 0);
}
#[test]
fn find_matching_template_found() {
let state = serde_json::json!({
"resource_templates": [
{"uriTemplate": "file:///{path}", "name": "file"}
]
});
let result = find_matching_template(&state, "file:///etc/passwd").unwrap();
assert_eq!(result["name"], "file");
}
#[test]
fn find_matching_template_no_match() {
let state = serde_json::json!({
"resource_templates": [
{"uriTemplate": "file:///{path}", "name": "file"}
]
});
assert!(find_matching_template(&state, "http://example.com").is_none());
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(256))]
#[test]
fn prop_literal_self_match(literal in "[a-zA-Z0-9/_.-]{1,40}") {
prop_assert!(matches_uri_template(&literal, &literal),
"literal template '{}' should match itself", literal);
}
#[test]
fn prop_variable_substitution(
prefix in "[a-z]{1,5}",
value in "[a-z0-9]{1,5}",
suffix in "[a-z]{1,5}",
) {
let template = format!("{prefix}{{var}}{suffix}");
let uri = format!("{prefix}{value}{suffix}");
prop_assert!(matches_uri_template(&template, &uri));
}
#[test]
fn prop_empty_variable_rejected(
prefix in "[a-z]{1,5}",
suffix in "[a-z]{1,5}",
) {
let template = format!("{prefix}{{x}}{suffix}");
let uri = format!("{prefix}{suffix}");
prop_assert!(!matches_uri_template(&template, &uri),
"template '{}' should NOT match '{}' (empty variable)", template, uri);
}
#[test]
fn prop_no_panic_on_arbitrary(
template in ".*",
uri in ".*",
) {
let _ = matches_uri_template(&template, &uri);
}
}
#[test]
fn build_a2a_response_content_from_task_responses() {
let state = serde_json::json!({
"task_responses": [{
"content": {
"id": "task-1",
"status": "completed",
"message": {
"role": "agent",
"parts": [
{"type": "text", "text": "Dataset cleaned."}
]
},
"artifacts": [
{"name": "output.csv", "content": "a,b,c\n1,2,3"}
]
}
}]
});
let result = build_a2a_response_content(&state).unwrap();
assert!(result.text.contains("Dataset cleaned."));
assert!(result.text.contains("a,b,c"));
assert_eq!(result.status, "completed");
}
#[test]
fn build_a2a_response_content_prefers_task_over_task_responses() {
let state = serde_json::json!({
"task": {
"message": {
"parts": [{"type": "text", "text": "From task."}]
},
"status": "completed"
},
"task_responses": [{
"content": {
"message": {
"parts": [{"type": "text", "text": "From task_responses."}]
}
}
}]
});
let result = build_a2a_response_content(&state).unwrap();
assert!(result.text.contains("From task."));
assert!(!result.text.contains("From task_responses."));
}
#[test]
fn build_a2a_response_content_empty_state_returns_none() {
let state = serde_json::json!({});
assert!(build_a2a_response_content(&state).is_none());
}
}