1use serde::{Deserialize, Serialize};
21
22use crate::task::{ContextId, TaskId};
23
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
31pub struct MessageId(pub String);
32
33impl MessageId {
34 #[must_use]
36 pub fn new(s: impl Into<String>) -> Self {
37 Self(s.into())
38 }
39}
40
41impl std::fmt::Display for MessageId {
42 #[inline]
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.write_str(&self.0)
45 }
46}
47
48impl From<String> for MessageId {
49 fn from(s: String) -> Self {
50 Self(s)
51 }
52}
53
54impl From<&str> for MessageId {
55 fn from(s: &str) -> Self {
56 Self(s.to_owned())
57 }
58}
59
60impl AsRef<str> for MessageId {
61 fn as_ref(&self) -> &str {
62 &self.0
63 }
64}
65
66#[non_exhaustive]
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub enum MessageRole {
75 #[serde(rename = "ROLE_UNSPECIFIED", alias = "unspecified")]
77 Unspecified,
78 #[serde(rename = "ROLE_USER", alias = "user")]
80 User,
81 #[serde(rename = "ROLE_AGENT", alias = "agent")]
83 Agent,
84}
85
86impl std::fmt::Display for MessageRole {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 let s = match self {
89 Self::Unspecified => "ROLE_UNSPECIFIED",
90 Self::User => "ROLE_USER",
91 Self::Agent => "ROLE_AGENT",
92 };
93 f.write_str(s)
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(rename_all = "camelCase")]
102pub struct Message {
103 #[serde(rename = "messageId")]
105 pub id: MessageId,
106
107 pub role: MessageRole,
109
110 pub parts: Vec<Part>,
115
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub task_id: Option<TaskId>,
119
120 #[serde(skip_serializing_if = "Option::is_none")]
122 pub context_id: Option<ContextId>,
123
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub reference_task_ids: Option<Vec<TaskId>>,
127
128 #[serde(skip_serializing_if = "Option::is_none")]
130 pub extensions: Option<Vec<String>>,
131
132 #[serde(skip_serializing_if = "Option::is_none")]
134 pub metadata: Option<serde_json::Value>,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(rename_all = "camelCase")]
155pub struct Part {
156 #[serde(flatten)]
158 pub content: PartContent,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
162 pub metadata: Option<serde_json::Value>,
163
164 #[serde(skip_serializing_if = "Option::is_none")]
166 pub filename: Option<String>,
167
168 #[serde(skip_serializing_if = "Option::is_none")]
170 pub media_type: Option<String>,
171}
172
173impl Part {
174 #[must_use]
176 pub fn text(text: impl Into<String>) -> Self {
177 Self {
178 content: PartContent::Text(text.into()),
179 metadata: None,
180 filename: None,
181 media_type: None,
182 }
183 }
184
185 #[must_use]
187 pub fn raw(raw: impl Into<String>) -> Self {
188 Self {
189 content: PartContent::Raw(raw.into()),
190 metadata: None,
191 filename: None,
192 media_type: None,
193 }
194 }
195
196 #[must_use]
198 pub fn url(url: impl Into<String>) -> Self {
199 Self {
200 content: PartContent::Url(url.into()),
201 metadata: None,
202 filename: None,
203 media_type: None,
204 }
205 }
206
207 #[must_use]
209 pub const fn data(data: serde_json::Value) -> Self {
210 Self {
211 content: PartContent::Data(data),
212 metadata: None,
213 filename: None,
214 media_type: None,
215 }
216 }
217
218 #[must_use]
220 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
221 self.filename = Some(filename.into());
222 self
223 }
224
225 #[must_use]
227 pub fn with_media_type(mut self, media_type: impl Into<String>) -> Self {
228 self.media_type = Some(media_type.into());
229 self
230 }
231
232 #[must_use]
234 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
235 self.metadata = Some(metadata);
236 self
237 }
238
239 #[must_use]
241 pub fn text_content(&self) -> Option<&str> {
242 match &self.content {
243 PartContent::Text(text) => Some(text),
244 _ => None,
245 }
246 }
247
248 #[must_use]
254 pub fn file_bytes(bytes: impl Into<String>) -> Self {
255 Self::raw(bytes)
256 }
257
258 #[must_use]
262 pub fn file_uri(uri: impl Into<String>) -> Self {
263 Self::url(uri)
264 }
265
266 #[must_use]
270 pub fn file(file: FileContent) -> Self {
271 let mut part = if let Some(bytes) = file.bytes {
272 Self::raw(bytes)
273 } else if let Some(uri) = file.uri {
274 Self::url(uri)
275 } else {
276 Self::raw("")
278 };
279 part.filename = file.name;
280 part.media_type = file.mime_type;
281 part
282 }
283}
284
285#[non_exhaustive]
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297pub enum PartContent {
298 Text(String),
300 Raw(String),
302 Url(String),
304 Data(serde_json::Value),
306}
307
308#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub struct FileContent {
317 #[serde(skip_serializing_if = "Option::is_none")]
319 pub name: Option<String>,
320
321 #[serde(skip_serializing_if = "Option::is_none")]
323 pub mime_type: Option<String>,
324
325 #[serde(skip_serializing_if = "Option::is_none")]
327 pub bytes: Option<String>,
328
329 #[serde(skip_serializing_if = "Option::is_none")]
331 pub uri: Option<String>,
332}
333
334impl FileContent {
335 #[must_use]
337 pub fn from_bytes(bytes: impl Into<String>) -> Self {
338 Self {
339 name: None,
340 mime_type: None,
341 bytes: Some(bytes.into()),
342 uri: None,
343 }
344 }
345
346 #[must_use]
348 pub fn from_uri(uri: impl Into<String>) -> Self {
349 Self {
350 name: None,
351 mime_type: None,
352 bytes: None,
353 uri: Some(uri.into()),
354 }
355 }
356
357 #[must_use]
359 pub fn with_name(mut self, name: impl Into<String>) -> Self {
360 self.name = Some(name.into());
361 self
362 }
363
364 #[must_use]
366 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
367 self.mime_type = Some(mime_type.into());
368 self
369 }
370
371 pub const fn validate(&self) -> Result<(), &'static str> {
377 if self.bytes.is_none() && self.uri.is_none() {
378 Err("FileContent must have at least one of 'bytes' or 'uri' set")
379 } else {
380 Ok(())
381 }
382 }
383}
384
385#[cfg(test)]
388mod tests {
389 use super::*;
390
391 fn make_message() -> Message {
392 Message {
393 id: MessageId::new("msg-1"),
394 role: MessageRole::User,
395 parts: vec![Part::text("Hello")],
396 task_id: None,
397 context_id: None,
398 reference_task_ids: None,
399 extensions: None,
400 metadata: None,
401 }
402 }
403
404 #[test]
405 fn message_roundtrip() {
406 let msg = make_message();
407 let json = serde_json::to_string(&msg).expect("serialize");
408 assert!(json.contains("\"messageId\":\"msg-1\""));
409 assert!(json.contains("\"role\":\"ROLE_USER\""));
410
411 let back: Message = serde_json::from_str(&json).expect("deserialize");
412 assert_eq!(back.id, MessageId::new("msg-1"));
413 assert_eq!(back.role, MessageRole::User);
414 }
415
416 #[test]
417 fn role_serializes_as_proto_names() {
418 assert_eq!(
419 serde_json::to_string(&MessageRole::User).unwrap(),
420 "\"ROLE_USER\""
421 );
422 assert_eq!(
423 serde_json::to_string(&MessageRole::Agent).unwrap(),
424 "\"ROLE_AGENT\""
425 );
426 assert_eq!(
427 serde_json::to_string(&MessageRole::Unspecified).unwrap(),
428 "\"ROLE_UNSPECIFIED\""
429 );
430 }
431
432 #[test]
433 fn role_accepts_legacy_lowercase() {
434 let back: MessageRole = serde_json::from_str("\"user\"").unwrap();
435 assert_eq!(back, MessageRole::User);
436 let back: MessageRole = serde_json::from_str("\"agent\"").unwrap();
437 assert_eq!(back, MessageRole::Agent);
438 }
439
440 #[test]
441 fn text_part_v1_format() {
442 let part = Part::text("hello world");
443 let json = serde_json::to_string(&part).expect("serialize");
444 assert!(
445 json.contains("\"text\":\"hello world\""),
446 "should have text field: {json}"
447 );
448 assert!(
450 !json.contains("\"type\""),
451 "v1.0 should not have type field: {json}"
452 );
453 let back: Part = serde_json::from_str(&json).expect("deserialize");
454 assert!(matches!(back.content, PartContent::Text(ref t) if t == "hello world"));
455 }
456
457 #[test]
458 fn raw_part_v1_format() {
459 let part = Part::raw("aGVsbG8=")
460 .with_filename("test.png")
461 .with_media_type("image/png");
462 let json = serde_json::to_string(&part).expect("serialize");
463 assert!(json.contains("\"raw\":\"aGVsbG8=\""));
464 assert!(json.contains("\"filename\":\"test.png\""));
465 assert!(json.contains("\"mediaType\":\"image/png\""));
466 assert!(!json.contains("\"type\""));
467 let back: Part = serde_json::from_str(&json).expect("deserialize");
468 assert!(matches!(back.content, PartContent::Raw(ref r) if r == "aGVsbG8="));
469 assert_eq!(back.filename.as_deref(), Some("test.png"));
470 assert_eq!(back.media_type.as_deref(), Some("image/png"));
471 }
472
473 #[test]
474 fn url_part_v1_format() {
475 let part = Part::url("https://example.com/file.pdf")
476 .with_filename("file.pdf")
477 .with_media_type("application/pdf");
478 let json = serde_json::to_string(&part).expect("serialize");
479 assert!(json.contains("\"url\":\"https://example.com/file.pdf\""));
480 assert!(json.contains("\"filename\":\"file.pdf\""));
481 assert!(!json.contains("\"type\""));
482 let back: Part = serde_json::from_str(&json).expect("deserialize");
483 assert!(
484 matches!(back.content, PartContent::Url(ref u) if u == "https://example.com/file.pdf")
485 );
486 }
487
488 #[test]
489 fn data_part_v1_format() {
490 let part = Part::data(serde_json::json!({"key": "value"}));
491 let json = serde_json::to_string(&part).expect("serialize");
492 assert!(json.contains("\"data\""));
493 assert!(!json.contains("\"type\""));
494 let back: Part = serde_json::from_str(&json).expect("deserialize");
495 match &back.content {
496 PartContent::Data(data) => assert_eq!(data["key"], "value"),
497 _ => panic!("expected Data variant"),
498 }
499 }
500
501 #[test]
502 fn none_fields_omitted() {
503 let msg = make_message();
504 let json = serde_json::to_string(&msg).expect("serialize");
505 assert!(
506 !json.contains("\"taskId\""),
507 "taskId should be omitted: {json}"
508 );
509 assert!(
510 !json.contains("\"metadata\""),
511 "metadata should be omitted: {json}"
512 );
513 }
514
515 #[test]
516 fn message_role_display_trait() {
517 assert_eq!(MessageRole::User.to_string(), "ROLE_USER");
518 assert_eq!(MessageRole::Agent.to_string(), "ROLE_AGENT");
519 assert_eq!(MessageRole::Unspecified.to_string(), "ROLE_UNSPECIFIED");
520 }
521
522 #[test]
523 fn message_with_reference_task_ids() {
524 use crate::task::TaskId;
525
526 let msg = Message {
527 id: MessageId::new("msg-ref"),
528 role: MessageRole::User,
529 parts: vec![Part::text("check these tasks")],
530 task_id: None,
531 context_id: None,
532 reference_task_ids: Some(vec![TaskId::new("task-100"), TaskId::new("task-200")]),
533 extensions: None,
534 metadata: None,
535 };
536
537 let json = serde_json::to_string(&msg).expect("serialize");
538 assert!(json.contains("\"referenceTaskIds\""));
539 assert!(json.contains("\"task-100\""));
540
541 let back: Message = serde_json::from_str(&json).expect("deserialize");
542 let refs = back
543 .reference_task_ids
544 .expect("should have reference_task_ids");
545 assert_eq!(refs.len(), 2);
546 }
547
548 #[test]
549 fn backward_compat_file_bytes_constructor() {
550 let part = Part::file_bytes("aGVsbG8=");
551 assert!(matches!(part.content, PartContent::Raw(_)));
552 }
553
554 #[test]
555 fn backward_compat_file_uri_constructor() {
556 let part = Part::file_uri("https://example.com/file.pdf");
557 assert!(matches!(part.content, PartContent::Url(_)));
558 }
559
560 #[test]
561 fn backward_compat_file_constructor() {
562 let fc = FileContent::from_bytes("aGVsbG8=")
563 .with_name("test.png")
564 .with_mime_type("image/png");
565 let part = Part::file(fc);
566 assert!(matches!(part.content, PartContent::Raw(ref r) if r == "aGVsbG8="));
567 assert_eq!(part.filename.as_deref(), Some("test.png"));
568 assert_eq!(part.media_type.as_deref(), Some("image/png"));
569 }
570
571 #[test]
574 fn message_id_display() {
575 let id = MessageId::new("msg-42");
576 assert_eq!(id.to_string(), "msg-42");
577 }
578
579 #[test]
580 fn message_id_as_ref() {
581 let id = MessageId::new("ref-test");
582 assert_eq!(id.as_ref(), "ref-test");
583 }
584
585 #[test]
586 fn message_id_from_impls() {
587 let from_str: MessageId = "str-id".into();
588 assert_eq!(from_str, MessageId::new("str-id"));
589
590 let from_string: MessageId = String::from("string-id").into();
591 assert_eq!(from_string, MessageId::new("string-id"));
592 }
593
594 #[test]
595 fn part_text_has_no_metadata() {
596 let p = Part::text("hi");
597 assert!(p.metadata.is_none());
598 assert!(p.filename.is_none());
599 assert!(p.media_type.is_none());
600 }
601}