1use serde::{Deserialize, Serialize};
20
21use crate::task::{ContextId, TaskId};
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub struct MessageId(pub String);
31
32impl MessageId {
33 #[must_use]
35 pub fn new(s: impl Into<String>) -> Self {
36 Self(s.into())
37 }
38}
39
40impl std::fmt::Display for MessageId {
41 #[inline]
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.write_str(&self.0)
44 }
45}
46
47impl From<String> for MessageId {
48 fn from(s: String) -> Self {
49 Self(s)
50 }
51}
52
53impl From<&str> for MessageId {
54 fn from(s: &str) -> Self {
55 Self(s.to_owned())
56 }
57}
58
59impl AsRef<str> for MessageId {
60 fn as_ref(&self) -> &str {
61 &self.0
62 }
63}
64
65#[non_exhaustive]
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
70pub enum MessageRole {
71 #[serde(rename = "ROLE_UNSPECIFIED", alias = "unspecified")]
73 Unspecified,
74 #[serde(rename = "ROLE_USER", alias = "user")]
76 User,
77 #[serde(rename = "ROLE_AGENT", alias = "agent")]
79 Agent,
80}
81
82impl std::fmt::Display for MessageRole {
83 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84 let s = match self {
85 Self::Unspecified => "ROLE_UNSPECIFIED",
86 Self::User => "ROLE_USER",
87 Self::Agent => "ROLE_AGENT",
88 };
89 f.write_str(s)
90 }
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct Message {
105 #[serde(rename = "messageId")]
107 pub id: MessageId,
108
109 pub role: MessageRole,
111
112 pub parts: Vec<Part>,
117
118 #[serde(skip_serializing_if = "Option::is_none")]
120 pub task_id: Option<TaskId>,
121
122 #[serde(skip_serializing_if = "Option::is_none")]
124 pub context_id: Option<ContextId>,
125
126 #[serde(skip_serializing_if = "Option::is_none")]
128 pub reference_task_ids: Option<Vec<TaskId>>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub extensions: Option<Vec<String>>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub metadata: Option<serde_json::Value>,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct Part {
150 #[serde(flatten)]
152 pub content: PartContent,
153
154 #[serde(skip_serializing_if = "Option::is_none")]
156 pub metadata: Option<serde_json::Value>,
157}
158
159impl Part {
160 #[must_use]
162 pub fn text(text: impl Into<String>) -> Self {
163 Self {
164 content: PartContent::Text { text: text.into() },
165 metadata: None,
166 }
167 }
168
169 #[must_use]
171 pub fn file_bytes(bytes: impl Into<String>) -> Self {
172 Self {
173 content: PartContent::File {
174 file: FileContent {
175 name: None,
176 mime_type: None,
177 bytes: Some(bytes.into()),
178 uri: None,
179 },
180 },
181 metadata: None,
182 }
183 }
184
185 #[must_use]
187 pub fn file_uri(uri: impl Into<String>) -> Self {
188 Self {
189 content: PartContent::File {
190 file: FileContent {
191 name: None,
192 mime_type: None,
193 bytes: None,
194 uri: Some(uri.into()),
195 },
196 },
197 metadata: None,
198 }
199 }
200
201 #[must_use]
203 pub const fn file(file: FileContent) -> Self {
204 Self {
205 content: PartContent::File { file },
206 metadata: None,
207 }
208 }
209
210 #[must_use]
212 pub const fn data(data: serde_json::Value) -> Self {
213 Self {
214 content: PartContent::Data { data },
215 metadata: None,
216 }
217 }
218
219 #[must_use]
226 pub fn raw(raw: impl Into<String>) -> Self {
227 Self::file_bytes(raw)
228 }
229
230 #[must_use]
235 pub fn url(url: impl Into<String>) -> Self {
236 Self::file_uri(url)
237 }
238}
239
240#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct FileContent {
249 #[serde(skip_serializing_if = "Option::is_none")]
251 pub name: Option<String>,
252
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub mime_type: Option<String>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub bytes: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub uri: Option<String>,
264}
265
266impl FileContent {
267 #[must_use]
269 pub fn from_bytes(bytes: impl Into<String>) -> Self {
270 Self {
271 name: None,
272 mime_type: None,
273 bytes: Some(bytes.into()),
274 uri: None,
275 }
276 }
277
278 #[must_use]
280 pub fn from_uri(uri: impl Into<String>) -> Self {
281 Self {
282 name: None,
283 mime_type: None,
284 bytes: None,
285 uri: Some(uri.into()),
286 }
287 }
288
289 #[must_use]
291 pub fn with_name(mut self, name: impl Into<String>) -> Self {
292 self.name = Some(name.into());
293 self
294 }
295
296 #[must_use]
298 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
299 self.mime_type = Some(mime_type.into());
300 self
301 }
302
303 pub const fn validate(&self) -> Result<(), &'static str> {
311 if self.bytes.is_none() && self.uri.is_none() {
312 Err("FileContent must have at least one of 'bytes' or 'uri' set")
313 } else {
314 Ok(())
315 }
316 }
317}
318
319#[non_exhaustive]
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
329#[serde(tag = "type")]
330pub enum PartContent {
331 #[serde(rename = "text")]
333 Text {
334 text: String,
336 },
337 #[serde(rename = "file")]
339 File {
340 file: FileContent,
342 },
343 #[serde(rename = "data")]
345 Data {
346 data: serde_json::Value,
348 },
349}
350
351#[cfg(test)]
354mod tests {
355 use super::*;
356
357 fn make_message() -> Message {
358 Message {
359 id: MessageId::new("msg-1"),
360 role: MessageRole::User,
361 parts: vec![Part::text("Hello")],
362 task_id: None,
363 context_id: None,
364 reference_task_ids: None,
365 extensions: None,
366 metadata: None,
367 }
368 }
369
370 #[test]
371 fn message_roundtrip() {
372 let msg = make_message();
373 let json = serde_json::to_string(&msg).expect("serialize");
374 assert!(json.contains("\"messageId\":\"msg-1\""));
375 assert!(json.contains("\"role\":\"ROLE_USER\""));
376
377 let back: Message = serde_json::from_str(&json).expect("deserialize");
378 assert_eq!(back.id, MessageId::new("msg-1"));
379 assert_eq!(back.role, MessageRole::User);
380 }
381
382 #[test]
383 fn text_part_has_type_discriminator() {
384 let part = Part::text("hello world");
385 let json = serde_json::to_string(&part).expect("serialize");
386 assert!(
387 json.contains("\"type\":\"text\""),
388 "should have type discriminator: {json}"
389 );
390 assert!(json.contains("\"text\":\"hello world\""));
391 let back: Part = serde_json::from_str(&json).expect("deserialize");
392 assert!(matches!(back.content, PartContent::Text { ref text } if text == "hello world"));
393 }
394
395 #[test]
396 fn file_bytes_part_roundtrip() {
397 let part = Part::file(
398 FileContent::from_bytes("aGVsbG8=")
399 .with_name("test.png")
400 .with_mime_type("image/png"),
401 );
402 let json = serde_json::to_string(&part).expect("serialize");
403 assert!(
404 json.contains("\"type\":\"file\""),
405 "should have type discriminator: {json}"
406 );
407 assert!(json.contains("\"file\""));
408 assert!(json.contains("\"name\":\"test.png\""));
409 assert!(json.contains("\"mimeType\":\"image/png\""));
410 let back: Part = serde_json::from_str(&json).expect("deserialize");
411 match back.content {
412 PartContent::File { file } => {
413 assert_eq!(file.name.as_deref(), Some("test.png"));
414 assert_eq!(file.mime_type.as_deref(), Some("image/png"));
415 assert_eq!(file.bytes.as_deref(), Some("aGVsbG8="));
416 }
417 _ => panic!("expected File variant"),
418 }
419 }
420
421 #[test]
422 fn file_uri_part_roundtrip() {
423 let part = Part::file_uri("https://example.com/file.pdf");
424 let json = serde_json::to_string(&part).expect("serialize");
425 assert!(json.contains("\"type\":\"file\""));
426 assert!(json.contains("\"uri\":\"https://example.com/file.pdf\""));
427 let back: Part = serde_json::from_str(&json).expect("deserialize");
428 match back.content {
429 PartContent::File { file } => {
430 assert_eq!(file.uri.as_deref(), Some("https://example.com/file.pdf"));
431 }
432 _ => panic!("expected File variant"),
433 }
434 }
435
436 #[test]
437 fn data_part_has_type_discriminator() {
438 let part = Part::data(serde_json::json!({"key": "value"}));
439 let json = serde_json::to_string(&part).expect("serialize");
440 assert!(
441 json.contains("\"type\":\"data\""),
442 "should have type discriminator: {json}"
443 );
444 assert!(json.contains("\"data\""));
445 let back: Part = serde_json::from_str(&json).expect("deserialize");
446 match &back.content {
447 PartContent::Data { data } => assert_eq!(data["key"], "value"),
448 _ => panic!("expected Data variant"),
449 }
450 }
451
452 #[test]
453 fn none_fields_omitted() {
454 let msg = make_message();
455 let json = serde_json::to_string(&msg).expect("serialize");
456 assert!(
457 !json.contains("\"taskId\""),
458 "taskId should be omitted: {json}"
459 );
460 assert!(
461 !json.contains("\"metadata\""),
462 "metadata should be omitted: {json}"
463 );
464 }
465
466 #[test]
467 fn wire_format_role_unspecified_roundtrip() {
468 let json = serde_json::to_string(&MessageRole::Unspecified).unwrap();
469 assert_eq!(json, "\"ROLE_UNSPECIFIED\"");
470
471 let back: MessageRole = serde_json::from_str("\"ROLE_UNSPECIFIED\"").unwrap();
472 assert_eq!(back, MessageRole::Unspecified);
473 }
474
475 #[test]
476 fn message_role_display_trait() {
477 assert_eq!(MessageRole::User.to_string(), "ROLE_USER");
478 assert_eq!(MessageRole::Agent.to_string(), "ROLE_AGENT");
479 assert_eq!(MessageRole::Unspecified.to_string(), "ROLE_UNSPECIFIED");
480 }
481
482 #[test]
483 fn mixed_part_message_roundtrip() {
484 let msg = Message {
485 id: MessageId::new("msg-mixed"),
486 role: MessageRole::Agent,
487 parts: vec![
488 Part::text("Here is the result"),
489 Part::file_bytes("aGVsbG8="),
490 Part::file_uri("https://example.com/output.pdf"),
491 ],
492 task_id: None,
493 context_id: None,
494 reference_task_ids: None,
495 extensions: None,
496 metadata: None,
497 };
498
499 let json = serde_json::to_string(&msg).expect("serialize mixed-part message");
500 assert!(json.contains("\"text\":\"Here is the result\""));
501 assert!(json.contains("\"type\":\"file\""));
502
503 let back: Message = serde_json::from_str(&json).expect("deserialize mixed-part message");
504 assert_eq!(back.parts.len(), 3);
505 assert!(
506 matches!(&back.parts[0].content, PartContent::Text { text } if text == "Here is the result")
507 );
508 assert!(matches!(&back.parts[1].content, PartContent::File { .. }));
509 assert!(matches!(&back.parts[2].content, PartContent::File { .. }));
510 }
511
512 #[test]
513 fn message_with_reference_task_ids() {
514 use crate::task::TaskId;
515
516 let msg = Message {
517 id: MessageId::new("msg-ref"),
518 role: MessageRole::User,
519 parts: vec![Part::text("check these tasks")],
520 task_id: None,
521 context_id: None,
522 reference_task_ids: Some(vec![TaskId::new("task-100"), TaskId::new("task-200")]),
523 extensions: None,
524 metadata: None,
525 };
526
527 let json = serde_json::to_string(&msg).expect("serialize");
528 assert!(
529 json.contains("\"referenceTaskIds\""),
530 "referenceTaskIds should be present: {json}"
531 );
532 assert!(json.contains("\"task-100\""));
533 assert!(json.contains("\"task-200\""));
534
535 let back: Message = serde_json::from_str(&json).expect("deserialize");
536 let refs = back
537 .reference_task_ids
538 .expect("should have reference_task_ids");
539 assert_eq!(refs.len(), 2);
540 assert_eq!(refs[0], TaskId::new("task-100"));
541 assert_eq!(refs[1], TaskId::new("task-200"));
542 }
543
544 #[test]
545 fn backward_compat_raw_constructor() {
546 let part = Part::raw("aGVsbG8=");
547 let json = serde_json::to_string(&part).expect("serialize");
548 assert!(json.contains("\"type\":\"file\""));
549 assert!(json.contains("\"bytes\":\"aGVsbG8=\""));
550 }
551
552 #[test]
553 fn backward_compat_url_constructor() {
554 let part = Part::url("https://example.com/file.pdf");
555 let json = serde_json::to_string(&part).expect("serialize");
556 assert!(json.contains("\"type\":\"file\""));
557 assert!(json.contains("\"uri\":\"https://example.com/file.pdf\""));
558 }
559
560 #[test]
563 fn file_content_from_bytes_sets_bytes_only() {
564 let fc = FileContent::from_bytes("base64data");
565 assert_eq!(fc.bytes.as_deref(), Some("base64data"));
566 assert!(fc.uri.is_none());
567 assert!(fc.name.is_none());
568 assert!(fc.mime_type.is_none());
569 }
570
571 #[test]
572 fn file_content_from_uri_sets_uri_only() {
573 let fc = FileContent::from_uri("https://example.com/f.txt");
574 assert_eq!(fc.uri.as_deref(), Some("https://example.com/f.txt"));
575 assert!(fc.bytes.is_none());
576 assert!(fc.name.is_none());
577 assert!(fc.mime_type.is_none());
578 }
579
580 #[test]
581 fn file_content_with_name_sets_name() {
582 let fc = FileContent::from_bytes("data").with_name("report.pdf");
583 assert_eq!(fc.name.as_deref(), Some("report.pdf"));
584 assert_eq!(fc.bytes.as_deref(), Some("data"));
586 }
587
588 #[test]
589 fn file_content_with_mime_type_sets_mime_type() {
590 let fc = FileContent::from_bytes("data").with_mime_type("application/pdf");
591 assert_eq!(fc.mime_type.as_deref(), Some("application/pdf"));
592 assert_eq!(fc.bytes.as_deref(), Some("data"));
593 }
594
595 #[test]
596 fn file_content_builder_chaining() {
597 let fc = FileContent::from_uri("https://example.com/img.png")
598 .with_name("img.png")
599 .with_mime_type("image/png");
600 assert_eq!(fc.uri.as_deref(), Some("https://example.com/img.png"));
601 assert_eq!(fc.name.as_deref(), Some("img.png"));
602 assert_eq!(fc.mime_type.as_deref(), Some("image/png"));
603 assert!(fc.bytes.is_none());
604 }
605
606 #[test]
609 fn message_id_display() {
610 let id = MessageId::new("msg-42");
611 assert_eq!(id.to_string(), "msg-42");
612 }
613
614 #[test]
615 fn message_id_as_ref() {
616 let id = MessageId::new("ref-test");
617 assert_eq!(id.as_ref(), "ref-test");
618 }
619
620 #[test]
621 fn message_id_from_impls() {
622 let from_str: MessageId = "str-id".into();
623 assert_eq!(from_str, MessageId::new("str-id"));
624
625 let from_string: MessageId = String::from("string-id").into();
626 assert_eq!(from_string, MessageId::new("string-id"));
627 }
628
629 #[test]
632 fn part_text_has_no_metadata() {
633 let p = Part::text("hi");
634 assert!(p.metadata.is_none());
635 assert!(matches!(p.content, PartContent::Text { text } if text == "hi"));
636 }
637
638 #[test]
639 fn part_file_bytes_sets_bytes_field() {
640 let p = Part::file_bytes("b64");
641 match &p.content {
642 PartContent::File { file } => {
643 assert_eq!(file.bytes.as_deref(), Some("b64"));
644 assert!(file.uri.is_none());
645 assert!(file.name.is_none());
646 assert!(file.mime_type.is_none());
647 }
648 _ => panic!("expected File variant"),
649 }
650 assert!(p.metadata.is_none());
651 }
652
653 #[test]
654 fn part_file_uri_sets_uri_field() {
655 let p = Part::file_uri("https://a.b/c");
656 match &p.content {
657 PartContent::File { file } => {
658 assert_eq!(file.uri.as_deref(), Some("https://a.b/c"));
659 assert!(file.bytes.is_none());
660 }
661 _ => panic!("expected File variant"),
662 }
663 }
664
665 #[test]
666 fn part_data_carries_value() {
667 let val = serde_json::json!({"key": 123});
668 let p = Part::data(val.clone());
669 match &p.content {
670 PartContent::Data { data } => assert_eq!(data, &val),
671 _ => panic!("expected Data variant"),
672 }
673 assert!(p.metadata.is_none());
674 }
675
676 #[test]
679 fn file_content_validate_ok_with_bytes() {
680 let fc = FileContent::from_bytes("data");
681 assert!(fc.validate().is_ok());
682 }
683
684 #[test]
685 fn file_content_validate_ok_with_uri() {
686 let fc = FileContent::from_uri("https://example.com/f.txt");
687 assert!(fc.validate().is_ok());
688 }
689
690 #[test]
691 fn file_content_validate_ok_with_both() {
692 let fc = FileContent {
693 name: None,
694 mime_type: None,
695 bytes: Some("data".into()),
696 uri: Some("https://example.com/f.txt".into()),
697 };
698 assert!(fc.validate().is_ok());
699 }
700
701 #[test]
702 fn file_content_validate_err_with_neither() {
703 let fc = FileContent {
704 name: Some("empty.txt".into()),
705 mime_type: Some("text/plain".into()),
706 bytes: None,
707 uri: None,
708 };
709 let err = fc.validate().unwrap_err();
710 assert!(err.contains("bytes"));
711 assert!(err.contains("uri"));
712 }
713
714 #[test]
715 fn part_file_constructor_preserves_all_fields() {
716 let fc = FileContent {
717 name: Some("n".into()),
718 mime_type: Some("m".into()),
719 bytes: Some("b".into()),
720 uri: Some("u".into()),
721 };
722 let p = Part::file(fc);
723 match &p.content {
724 PartContent::File { file } => {
725 assert_eq!(file.name.as_deref(), Some("n"));
726 assert_eq!(file.mime_type.as_deref(), Some("m"));
727 assert_eq!(file.bytes.as_deref(), Some("b"));
728 assert_eq!(file.uri.as_deref(), Some("u"));
729 }
730 _ => panic!("expected File variant"),
731 }
732 }
733}