1use serde::{Deserialize, Serialize};
2
3use crate::A2AError;
4use crate::types::JsonObject;
5
6#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
8pub enum Role {
9 #[default]
10 #[serde(rename = "ROLE_UNSPECIFIED")]
11 Unspecified,
13 #[serde(rename = "ROLE_USER")]
14 User,
16 #[serde(rename = "ROLE_AGENT")]
17 Agent,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct Part {
25 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub text: Option<String>,
28 #[serde(
29 default,
30 skip_serializing_if = "Option::is_none",
31 with = "crate::types::base64_bytes::option"
32 )]
33 pub raw: Option<Vec<u8>>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub url: Option<String>,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub data: Option<serde_json::Value>,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub metadata: Option<JsonObject>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
45 pub filename: Option<String>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub media_type: Option<String>,
50}
51
52impl Part {
53 pub fn content_count(&self) -> usize {
55 usize::from(self.text.is_some())
56 + usize::from(self.raw.is_some())
57 + usize::from(self.url.is_some())
58 + usize::from(self.data.is_some())
59 }
60
61 pub fn has_single_content(&self) -> bool {
63 self.content_count() == 1
64 }
65
66 pub fn validate(&self) -> Result<(), A2AError> {
68 match self.content_count() {
69 1 => Ok(()),
70 0 => Err(A2AError::InvalidRequest(
71 "part must contain exactly one of text, raw, url, or data".to_owned(),
72 )),
73 _ => Err(A2AError::InvalidRequest(
74 "part cannot contain more than one of text, raw, url, or data".to_owned(),
75 )),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct Message {
84 pub message_id: String,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub context_id: Option<String>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub task_id: Option<String>,
92 pub role: Role,
94 #[serde(default, skip_serializing_if = "Vec::is_empty")]
95 pub parts: Vec<Part>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub metadata: Option<JsonObject>,
100 #[serde(default, skip_serializing_if = "Vec::is_empty")]
101 pub extensions: Vec<String>,
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
104 pub reference_task_ids: Vec<String>,
106}
107
108impl Message {
109 pub fn validate(&self) -> Result<(), A2AError> {
111 if self.parts.is_empty() {
112 return Err(A2AError::InvalidRequest(
113 "message must contain at least one part".to_owned(),
114 ));
115 }
116
117 for part in &self.parts {
118 part.validate()?;
119 }
120
121 Ok(())
122 }
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct Artifact {
129 pub artifact_id: String,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub name: Option<String>,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub description: Option<String>,
137 #[serde(default, skip_serializing_if = "Vec::is_empty")]
138 pub parts: Vec<Part>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub metadata: Option<JsonObject>,
143 #[serde(default, skip_serializing_if = "Vec::is_empty")]
144 pub extensions: Vec<String>,
146}
147
148impl Artifact {
149 pub fn validate(&self) -> Result<(), A2AError> {
151 if self.parts.is_empty() {
152 return Err(A2AError::InvalidRequest(
153 "artifact must contain at least one part".to_owned(),
154 ));
155 }
156
157 for part in &self.parts {
158 part.validate()?;
159 }
160
161 Ok(())
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::{Artifact, Message, Part, Role};
168
169 #[test]
170 fn part_reports_single_content_field() {
171 let part = Part {
172 text: Some("hello".to_owned()),
173 raw: None,
174 url: None,
175 data: None,
176 metadata: None,
177 filename: None,
178 media_type: None,
179 };
180
181 assert_eq!(part.content_count(), 1);
182 assert!(part.has_single_content());
183 }
184
185 #[test]
186 fn part_raw_serializes_as_base64() {
187 let part = Part {
188 text: None,
189 raw: Some(vec![104, 105]),
190 url: None,
191 data: None,
192 metadata: None,
193 filename: None,
194 media_type: None,
195 };
196
197 let json = serde_json::to_string(&part).expect("part should serialize");
198 assert_eq!(json, r#"{"raw":"aGk="}"#);
199 }
200
201 #[test]
202 fn part_validate_rejects_multiple_content_fields() {
203 let part = Part {
204 text: Some("hello".to_owned()),
205 raw: Some(vec![104, 105]),
206 url: None,
207 data: None,
208 metadata: None,
209 filename: None,
210 media_type: None,
211 };
212
213 let error = part.validate().expect_err("part should be invalid");
214 assert!(
215 error
216 .to_string()
217 .contains("part cannot contain more than one")
218 );
219 }
220
221 #[test]
222 fn message_and_artifact_round_trip_serialization() {
223 let message = Message {
224 message_id: "msg-1".to_owned(),
225 context_id: Some("ctx-1".to_owned()),
226 task_id: Some("task-1".to_owned()),
227 role: Role::User,
228 parts: vec![Part {
229 text: Some("hello".to_owned()),
230 raw: None,
231 url: None,
232 data: None,
233 metadata: None,
234 filename: None,
235 media_type: None,
236 }],
237 metadata: None,
238 extensions: vec!["trace".to_owned()],
239 reference_task_ids: vec!["task-0".to_owned()],
240 };
241 let artifact = Artifact {
242 artifact_id: "artifact-1".to_owned(),
243 name: Some("transcript".to_owned()),
244 description: Some("conversation log".to_owned()),
245 parts: vec![Part {
246 text: Some("hello".to_owned()),
247 raw: None,
248 url: None,
249 data: None,
250 metadata: None,
251 filename: None,
252 media_type: None,
253 }],
254 metadata: None,
255 extensions: vec!["indexed".to_owned()],
256 };
257
258 let message_json = serde_json::to_string(&message).expect("message should serialize");
259 let artifact_json = serde_json::to_string(&artifact).expect("artifact should serialize");
260
261 let message_round_trip: Message =
262 serde_json::from_str(&message_json).expect("message should deserialize");
263 let artifact_round_trip: Artifact =
264 serde_json::from_str(&artifact_json).expect("artifact should deserialize");
265
266 assert_eq!(message_round_trip.message_id, "msg-1");
267 assert_eq!(artifact_round_trip.artifact_id, "artifact-1");
268 assert_eq!(artifact_round_trip.parts.len(), 1);
269 }
270
271 #[test]
272 fn message_validate_rejects_empty_parts() {
273 let message = Message {
274 message_id: "msg-1".to_owned(),
275 context_id: None,
276 task_id: None,
277 role: Role::User,
278 parts: Vec::new(),
279 metadata: None,
280 extensions: Vec::new(),
281 reference_task_ids: Vec::new(),
282 };
283
284 let error = message.validate().expect_err("message should be invalid");
285 assert!(
286 error
287 .to_string()
288 .contains("message must contain at least one part")
289 );
290 }
291
292 #[test]
293 fn artifact_validate_rejects_empty_parts() {
294 let artifact = Artifact {
295 artifact_id: "artifact-1".to_owned(),
296 name: None,
297 description: None,
298 parts: Vec::new(),
299 metadata: None,
300 extensions: Vec::new(),
301 };
302
303 let error = artifact.validate().expect_err("artifact should be invalid");
304 assert!(
305 error
306 .to_string()
307 .contains("artifact must contain at least one part")
308 );
309 }
310}