1use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize, de::Deserializer, ser::Serializer};
7use serde_json::Value;
8
9pub const ERROR_INFO_TYPE: &str = "type.googleapis.com/google.rpc.ErrorInfo";
10pub const BAD_REQUEST_TYPE: &str = "type.googleapis.com/google.rpc.BadRequest";
11pub const STRUCT_TYPE: &str = "type.googleapis.com/google.protobuf.Struct";
12pub const PROTOCOL_DOMAIN: &str = "a2a-protocol.org";
13
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct FieldViolation {
17 pub field: String,
18 pub description: String,
19}
20
21#[derive(Debug, Clone, PartialEq)]
27pub struct TypedDetail {
28 pub type_url: String,
29 pub value: HashMap<String, Value>,
30}
31
32impl TypedDetail {
33 pub fn new(type_url: impl Into<String>, value: HashMap<String, Value>) -> Self {
34 Self {
35 type_url: type_url.into(),
36 value,
37 }
38 }
39
40 pub fn error_info(
42 reason: impl Into<String>,
43 domain: impl Into<String>,
44 metadata: Option<HashMap<String, String>>,
45 ) -> Self {
46 let mut value = HashMap::new();
47 value.insert("reason".to_string(), Value::String(reason.into()));
48 value.insert("domain".to_string(), Value::String(domain.into()));
49 if let Some(meta) = metadata {
50 let meta_obj: serde_json::Map<String, Value> = meta
51 .into_iter()
52 .map(|(k, v)| (k, Value::String(v)))
53 .collect();
54 value.insert("metadata".to_string(), Value::Object(meta_obj));
55 }
56 Self {
57 type_url: ERROR_INFO_TYPE.to_string(),
58 value,
59 }
60 }
61
62 pub fn bad_request(field_violations: Vec<FieldViolation>) -> Self {
64 let violations: Vec<Value> = field_violations
65 .into_iter()
66 .map(|fv| serde_json::to_value(fv).unwrap_or_default())
67 .collect();
68 let mut value = HashMap::new();
69 value.insert("fieldViolations".to_string(), Value::Array(violations));
70 Self {
71 type_url: BAD_REQUEST_TYPE.to_string(),
72 value,
73 }
74 }
75
76 pub fn from_struct(fields: HashMap<String, Value>) -> Self {
78 Self {
79 type_url: STRUCT_TYPE.to_string(),
80 value: fields,
81 }
82 }
83}
84
85impl Serialize for TypedDetail {
86 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
87 use serde::ser::SerializeMap;
88 let mut map = serializer.serialize_map(Some(self.value.len() + 1))?;
89 map.serialize_entry("@type", &self.type_url)?;
90 for (k, v) in &self.value {
91 map.serialize_entry(k, v)?;
92 }
93 map.end()
94 }
95}
96
97impl<'de> Deserialize<'de> for TypedDetail {
98 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
99 let mut map: HashMap<String, Value> = HashMap::deserialize(deserializer)?;
100 let type_url = map
101 .remove("@type")
102 .and_then(|v| v.as_str().map(String::from))
103 .unwrap_or_else(|| STRUCT_TYPE.to_string());
104 Ok(Self {
105 type_url,
106 value: map,
107 })
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
116 fn test_serialize_typed_detail() {
117 let detail = TypedDetail::error_info("TASK_NOT_FOUND", PROTOCOL_DOMAIN, None);
118 let json = serde_json::to_value(&detail).unwrap();
119 assert_eq!(json["@type"], ERROR_INFO_TYPE);
120 assert_eq!(json["reason"], "TASK_NOT_FOUND");
121 assert_eq!(json["domain"], PROTOCOL_DOMAIN);
122 }
123
124 #[test]
125 fn test_deserialize_typed_detail() {
126 let json = serde_json::json!({
127 "@type": ERROR_INFO_TYPE,
128 "reason": "TASK_NOT_FOUND",
129 "domain": PROTOCOL_DOMAIN,
130 "metadata": {"taskId": "t1"}
131 });
132 let detail: TypedDetail = serde_json::from_value(json).unwrap();
133 assert_eq!(detail.type_url, ERROR_INFO_TYPE);
134 assert_eq!(detail.value["reason"], "TASK_NOT_FOUND");
135 assert_eq!(detail.value["domain"], PROTOCOL_DOMAIN);
136 assert_eq!(detail.value["metadata"]["taskId"], "t1");
137 }
138
139 #[test]
140 fn test_deserialize_without_type_defaults_to_struct() {
141 let json = serde_json::json!({"resource": "task"});
142 let detail: TypedDetail = serde_json::from_value(json).unwrap();
143 assert_eq!(detail.type_url, STRUCT_TYPE);
144 assert_eq!(detail.value["resource"], "task");
145 }
146
147 #[test]
148 fn test_round_trip() {
149 let meta = HashMap::from([("taskId".to_string(), "t1".to_string())]);
150 let detail = TypedDetail::error_info("TASK_NOT_FOUND", PROTOCOL_DOMAIN, Some(meta));
151 let serialized = serde_json::to_value(&detail).unwrap();
152 let deserialized: TypedDetail = serde_json::from_value(serialized).unwrap();
153 assert_eq!(detail, deserialized);
154 }
155
156 #[test]
157 fn test_bad_request() {
158 let violations = vec![
159 FieldViolation {
160 field: "message.parts".into(),
161 description: "At least one part is required".into(),
162 },
163 FieldViolation {
164 field: "message.role".into(),
165 description: "Role must be 'user' or 'agent'".into(),
166 },
167 ];
168 let detail = TypedDetail::bad_request(violations);
169 assert_eq!(detail.type_url, BAD_REQUEST_TYPE);
170
171 let json = serde_json::to_value(&detail).unwrap();
172 assert_eq!(json["@type"], BAD_REQUEST_TYPE);
173 let fv = json["fieldViolations"].as_array().unwrap();
174 assert_eq!(fv.len(), 2);
175 assert_eq!(fv[0]["field"], "message.parts");
176 assert_eq!(fv[0]["description"], "At least one part is required");
177 assert_eq!(fv[1]["field"], "message.role");
178 }
179
180 #[test]
181 fn test_bad_request_round_trip() {
182 let violations = vec![FieldViolation {
183 field: "task.id".into(),
184 description: "Must not be empty".into(),
185 }];
186 let detail = TypedDetail::bad_request(violations);
187 let serialized = serde_json::to_value(&detail).unwrap();
188 let deserialized: TypedDetail = serde_json::from_value(serialized).unwrap();
189 assert_eq!(detail, deserialized);
190 }
191
192 #[test]
193 fn test_from_struct() {
194 let mut fields = HashMap::new();
195 fields.insert("key".to_string(), Value::String("val".to_string()));
196 let detail = TypedDetail::from_struct(fields.clone());
197 assert_eq!(detail.type_url, STRUCT_TYPE);
198 assert_eq!(detail.value, fields);
199 }
200}