1use serde::{Deserialize, Serialize};
17
18use crate::task::{ContextId, TaskId};
19
20#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
27pub struct MessageId(pub String);
28
29impl MessageId {
30 #[must_use]
32 pub fn new(s: impl Into<String>) -> Self {
33 Self(s.into())
34 }
35}
36
37impl std::fmt::Display for MessageId {
38 #[inline]
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.write_str(&self.0)
41 }
42}
43
44impl From<String> for MessageId {
45 fn from(s: String) -> Self {
46 Self(s)
47 }
48}
49
50impl From<&str> for MessageId {
51 fn from(s: &str) -> Self {
52 Self(s.to_owned())
53 }
54}
55
56impl AsRef<str> for MessageId {
57 fn as_ref(&self) -> &str {
58 &self.0
59 }
60}
61
62#[non_exhaustive]
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub enum MessageRole {
68 #[serde(rename = "ROLE_UNSPECIFIED")]
70 Unspecified,
71 #[serde(rename = "ROLE_USER")]
73 User,
74 #[serde(rename = "ROLE_AGENT")]
76 Agent,
77}
78
79impl std::fmt::Display for MessageRole {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 let s = match self {
82 Self::Unspecified => "ROLE_UNSPECIFIED",
83 Self::User => "ROLE_USER",
84 Self::Agent => "ROLE_AGENT",
85 };
86 f.write_str(s)
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(rename_all = "camelCase")]
101pub struct Message {
102 #[serde(rename = "messageId")]
104 pub id: MessageId,
105
106 pub role: MessageRole,
108
109 pub parts: Vec<Part>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub task_id: Option<TaskId>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub context_id: Option<ContextId>,
122
123 #[serde(skip_serializing_if = "Option::is_none")]
125 pub reference_task_ids: Option<Vec<TaskId>>,
126
127 #[serde(skip_serializing_if = "Option::is_none")]
129 pub extensions: Option<Vec<String>>,
130
131 #[serde(skip_serializing_if = "Option::is_none")]
133 pub metadata: Option<serde_json::Value>,
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct Part {
146 #[serde(flatten)]
148 pub content: PartContent,
149
150 #[serde(skip_serializing_if = "Option::is_none")]
152 pub metadata: Option<serde_json::Value>,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub filename: Option<String>,
157
158 #[serde(skip_serializing_if = "Option::is_none")]
160 pub media_type: Option<String>,
161}
162
163impl Part {
164 #[must_use]
166 pub fn text(text: impl Into<String>) -> Self {
167 Self {
168 content: PartContent::Text { text: text.into() },
169 metadata: None,
170 filename: None,
171 media_type: None,
172 }
173 }
174
175 #[must_use]
177 pub fn raw(raw: impl Into<String>) -> Self {
178 Self {
179 content: PartContent::Raw { raw: raw.into() },
180 metadata: None,
181 filename: None,
182 media_type: None,
183 }
184 }
185
186 #[must_use]
188 pub fn url(url: impl Into<String>) -> Self {
189 Self {
190 content: PartContent::Url { url: url.into() },
191 metadata: None,
192 filename: None,
193 media_type: None,
194 }
195 }
196
197 #[must_use]
199 pub const fn data(data: serde_json::Value) -> Self {
200 Self {
201 content: PartContent::Data { data },
202 metadata: None,
203 filename: None,
204 media_type: None,
205 }
206 }
207}
208
209#[non_exhaustive]
216#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
217#[serde(untagged)]
218pub enum PartContent {
219 Text {
221 text: String,
223 },
224 Raw {
226 raw: String,
228 },
229 Url {
231 url: String,
233 },
234 Data {
236 data: serde_json::Value,
238 },
239}
240
241#[cfg(test)]
244mod tests {
245 use super::*;
246
247 fn make_message() -> Message {
248 Message {
249 id: MessageId::new("msg-1"),
250 role: MessageRole::User,
251 parts: vec![Part::text("Hello")],
252 task_id: None,
253 context_id: None,
254 reference_task_ids: None,
255 extensions: None,
256 metadata: None,
257 }
258 }
259
260 #[test]
261 fn message_roundtrip() {
262 let msg = make_message();
263 let json = serde_json::to_string(&msg).expect("serialize");
264 assert!(json.contains("\"messageId\":\"msg-1\""));
265 assert!(json.contains("\"role\":\"ROLE_USER\""));
266
267 let back: Message = serde_json::from_str(&json).expect("deserialize");
268 assert_eq!(back.id, MessageId::new("msg-1"));
269 assert_eq!(back.role, MessageRole::User);
270 }
271
272 #[test]
273 fn text_part_roundtrip() {
274 let part = Part::text("hello world");
275 let json = serde_json::to_string(&part).expect("serialize");
276 assert!(!json.contains("\"kind\""), "v1.0 should not have kind tag");
277 assert!(json.contains("\"text\":\"hello world\""));
278 let back: Part = serde_json::from_str(&json).expect("deserialize");
279 assert!(matches!(back.content, PartContent::Text { .. }));
280 }
281
282 #[test]
283 fn raw_part_roundtrip() {
284 let mut part = Part::raw("aGVsbG8=");
285 part.filename = Some("test.png".into());
286 part.media_type = Some("image/png".into());
287 let json = serde_json::to_string(&part).expect("serialize");
288 assert!(json.contains("\"raw\""));
289 assert!(json.contains("\"filename\""));
290 assert!(json.contains("\"mediaType\""));
291 let back: Part = serde_json::from_str(&json).expect("deserialize");
292 assert!(matches!(back.content, PartContent::Raw { .. }));
293 assert_eq!(back.filename.as_deref(), Some("test.png"));
294 }
295
296 #[test]
297 fn url_part_roundtrip() {
298 let part = Part::url("https://example.com/file.pdf");
299 let json = serde_json::to_string(&part).expect("serialize");
300 assert!(json.contains("\"url\""));
301 let back: Part = serde_json::from_str(&json).expect("deserialize");
302 assert!(matches!(back.content, PartContent::Url { .. }));
303 }
304
305 #[test]
306 fn data_part_roundtrip() {
307 let part = Part::data(serde_json::json!({"key": "value"}));
308 let json = serde_json::to_string(&part).expect("serialize");
309 assert!(!json.contains("\"kind\""), "v1.0 should not have kind tag");
310 assert!(json.contains("\"data\""));
311 let back: Part = serde_json::from_str(&json).expect("deserialize");
312 assert!(matches!(back.content, PartContent::Data { .. }));
313 }
314
315 #[test]
316 fn none_fields_omitted() {
317 let msg = make_message();
318 let json = serde_json::to_string(&msg).expect("serialize");
319 assert!(
320 !json.contains("\"taskId\""),
321 "taskId should be omitted: {json}"
322 );
323 assert!(
324 !json.contains("\"metadata\""),
325 "metadata should be omitted: {json}"
326 );
327 }
328
329 #[test]
330 fn wire_format_role_unspecified_roundtrip() {
331 let json = serde_json::to_string(&MessageRole::Unspecified).unwrap();
332 assert_eq!(json, "\"ROLE_UNSPECIFIED\"");
333
334 let back: MessageRole = serde_json::from_str("\"ROLE_UNSPECIFIED\"").unwrap();
335 assert_eq!(back, MessageRole::Unspecified);
336 }
337
338 #[test]
339 fn message_role_display_trait() {
340 assert_eq!(MessageRole::User.to_string(), "ROLE_USER");
341 assert_eq!(MessageRole::Agent.to_string(), "ROLE_AGENT");
342 assert_eq!(MessageRole::Unspecified.to_string(), "ROLE_UNSPECIFIED");
343 }
344
345 #[test]
346 fn mixed_part_message_roundtrip() {
347 let msg = Message {
348 id: MessageId::new("msg-mixed"),
349 role: MessageRole::Agent,
350 parts: vec![
351 Part::text("Here is the result"),
352 Part::raw("aGVsbG8="),
353 Part::url("https://example.com/output.pdf"),
354 ],
355 task_id: None,
356 context_id: None,
357 reference_task_ids: None,
358 extensions: None,
359 metadata: None,
360 };
361
362 let json = serde_json::to_string(&msg).expect("serialize mixed-part message");
363 assert!(json.contains("\"text\":\"Here is the result\""));
364 assert!(json.contains("\"raw\":\"aGVsbG8=\""));
365 assert!(json.contains("\"url\":\"https://example.com/output.pdf\""));
366
367 let back: Message = serde_json::from_str(&json).expect("deserialize mixed-part message");
368 assert_eq!(back.parts.len(), 3);
369 assert!(
370 matches!(&back.parts[0].content, PartContent::Text { text } if text == "Here is the result")
371 );
372 assert!(matches!(&back.parts[1].content, PartContent::Raw { raw } if raw == "aGVsbG8="));
373 assert!(
374 matches!(&back.parts[2].content, PartContent::Url { url } if url == "https://example.com/output.pdf")
375 );
376 }
377
378 #[test]
379 fn message_with_reference_task_ids() {
380 use crate::task::TaskId;
381
382 let msg = Message {
383 id: MessageId::new("msg-ref"),
384 role: MessageRole::User,
385 parts: vec![Part::text("check these tasks")],
386 task_id: None,
387 context_id: None,
388 reference_task_ids: Some(vec![TaskId::new("task-100"), TaskId::new("task-200")]),
389 extensions: None,
390 metadata: None,
391 };
392
393 let json = serde_json::to_string(&msg).expect("serialize");
394 assert!(
395 json.contains("\"referenceTaskIds\""),
396 "referenceTaskIds should be present: {json}"
397 );
398 assert!(json.contains("\"task-100\""));
399 assert!(json.contains("\"task-200\""));
400
401 let back: Message = serde_json::from_str(&json).expect("deserialize");
402 let refs = back
403 .reference_task_ids
404 .expect("should have reference_task_ids");
405 assert_eq!(refs.len(), 2);
406 assert_eq!(refs[0], TaskId::new("task-100"));
407 assert_eq!(refs[1], TaskId::new("task-200"));
408 }
409}