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)]
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 #[serde(alias = "mediaType")]
171 pub media_type: Option<String>,
172}
173
174#[allow(clippy::too_many_lines)]
179impl<'de> serde::Deserialize<'de> for Part {
180 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
181 where
182 D: serde::Deserializer<'de>,
183 {
184 use serde::de::{self, MapAccess, Visitor};
185
186 #[derive(Debug)]
188 enum Field {
189 Text,
190 Raw,
191 Url,
192 Data,
193 Metadata,
194 Filename,
195 MediaType,
196 Unknown,
197 }
198
199 impl<'de> serde::Deserialize<'de> for Field {
200 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
201 where
202 D: serde::Deserializer<'de>,
203 {
204 struct FieldVisitor;
205 impl serde::de::Visitor<'_> for FieldVisitor {
206 type Value = Field;
207 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 f.write_str("a Part field name")
209 }
210 fn visit_str<E: de::Error>(self, v: &str) -> Result<Field, E> {
211 Ok(match v {
212 "text" => Field::Text,
213 "raw" => Field::Raw,
214 "url" => Field::Url,
215 "data" => Field::Data,
216 "metadata" => Field::Metadata,
217 "filename" => Field::Filename,
218 "mediaType" | "media_type" => Field::MediaType,
219 _ => Field::Unknown,
220 })
221 }
222 }
223 deserializer.deserialize_identifier(FieldVisitor)
224 }
225 }
226
227 struct PartVisitor;
228
229 impl<'de> Visitor<'de> for PartVisitor {
230 type Value = Part;
231
232 fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233 f.write_str("a Part object with text, raw, url, or data content")
234 }
235
236 fn visit_map<A>(self, mut map: A) -> Result<Part, A::Error>
237 where
238 A: MapAccess<'de>,
239 {
240 let mut text: Option<String> = None;
241 let mut raw: Option<String> = None;
242 let mut url: Option<String> = None;
243 let mut data: Option<serde_json::Value> = None;
244 let mut metadata: Option<serde_json::Value> = None;
245 let mut filename: Option<String> = None;
246 let mut media_type: Option<String> = None;
247
248 while let Some(key) = map.next_key::<Field>()? {
249 match key {
250 Field::Text => {
251 if text.is_some() {
252 return Err(de::Error::duplicate_field("text"));
253 }
254 text = Some(map.next_value()?);
255 }
256 Field::Raw => {
257 if raw.is_some() {
258 return Err(de::Error::duplicate_field("raw"));
259 }
260 raw = Some(map.next_value()?);
261 }
262 Field::Url => {
263 if url.is_some() {
264 return Err(de::Error::duplicate_field("url"));
265 }
266 url = Some(map.next_value()?);
267 }
268 Field::Data => {
269 if data.is_some() {
270 return Err(de::Error::duplicate_field("data"));
271 }
272 data = Some(map.next_value()?);
273 }
274 Field::Metadata => {
275 if metadata.is_some() {
276 return Err(de::Error::duplicate_field("metadata"));
277 }
278 metadata = Some(map.next_value()?);
279 }
280 Field::Filename => {
281 if filename.is_some() {
282 return Err(de::Error::duplicate_field("filename"));
283 }
284 filename = Some(map.next_value()?);
285 }
286 Field::MediaType => {
287 if media_type.is_some() {
288 return Err(de::Error::duplicate_field("mediaType"));
289 }
290 media_type = Some(map.next_value()?);
291 }
292 Field::Unknown => {
293 let _ = map.next_value::<de::IgnoredAny>()?;
295 }
296 }
297 }
298
299 let content = if let Some(t) = text {
302 PartContent::Text(t)
303 } else if let Some(r) = raw {
304 PartContent::Raw(r)
305 } else if let Some(u) = url {
306 PartContent::Url(u)
307 } else if let Some(d) = data {
308 PartContent::Data(d)
309 } else {
310 return Err(de::Error::custom(
311 "Part must contain one of: text, raw, url, data",
312 ));
313 };
314
315 Ok(Part {
316 content,
317 metadata,
318 filename,
319 media_type,
320 })
321 }
322 }
323
324 deserializer.deserialize_map(PartVisitor)
325 }
326}
327
328impl Part {
329 #[must_use]
331 pub fn text(text: impl Into<String>) -> Self {
332 Self {
333 content: PartContent::Text(text.into()),
334 metadata: None,
335 filename: None,
336 media_type: None,
337 }
338 }
339
340 #[must_use]
342 pub fn raw(raw: impl Into<String>) -> Self {
343 Self {
344 content: PartContent::Raw(raw.into()),
345 metadata: None,
346 filename: None,
347 media_type: None,
348 }
349 }
350
351 #[must_use]
353 pub fn url(url: impl Into<String>) -> Self {
354 Self {
355 content: PartContent::Url(url.into()),
356 metadata: None,
357 filename: None,
358 media_type: None,
359 }
360 }
361
362 #[must_use]
364 pub const fn data(data: serde_json::Value) -> Self {
365 Self {
366 content: PartContent::Data(data),
367 metadata: None,
368 filename: None,
369 media_type: None,
370 }
371 }
372
373 #[must_use]
375 pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
376 self.filename = Some(filename.into());
377 self
378 }
379
380 #[must_use]
382 pub fn with_media_type(mut self, media_type: impl Into<String>) -> Self {
383 self.media_type = Some(media_type.into());
384 self
385 }
386
387 #[must_use]
389 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
390 self.metadata = Some(metadata);
391 self
392 }
393
394 #[must_use]
396 pub fn text_content(&self) -> Option<&str> {
397 match &self.content {
398 PartContent::Text(text) => Some(text),
399 _ => None,
400 }
401 }
402
403 #[must_use]
409 pub fn file_bytes(bytes: impl Into<String>) -> Self {
410 Self::raw(bytes)
411 }
412
413 #[must_use]
417 pub fn file_uri(uri: impl Into<String>) -> Self {
418 Self::url(uri)
419 }
420
421 #[must_use]
425 pub fn file(file: FileContent) -> Self {
426 let mut part = if let Some(bytes) = file.bytes {
427 Self::raw(bytes)
428 } else if let Some(uri) = file.uri {
429 Self::url(uri)
430 } else {
431 Self::raw("")
433 };
434 part.filename = file.name;
435 part.media_type = file.mime_type;
436 part
437 }
438}
439
440#[non_exhaustive]
450#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
451#[serde(rename_all = "camelCase")]
452pub enum PartContent {
453 Text(String),
455 Raw(String),
457 Url(String),
459 Data(serde_json::Value),
461}
462
463#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
470#[serde(rename_all = "camelCase")]
471pub struct FileContent {
472 #[serde(skip_serializing_if = "Option::is_none")]
474 pub name: Option<String>,
475
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub mime_type: Option<String>,
479
480 #[serde(skip_serializing_if = "Option::is_none")]
482 pub bytes: Option<String>,
483
484 #[serde(skip_serializing_if = "Option::is_none")]
486 pub uri: Option<String>,
487}
488
489impl FileContent {
490 #[must_use]
492 pub fn from_bytes(bytes: impl Into<String>) -> Self {
493 Self {
494 name: None,
495 mime_type: None,
496 bytes: Some(bytes.into()),
497 uri: None,
498 }
499 }
500
501 #[must_use]
503 pub fn from_uri(uri: impl Into<String>) -> Self {
504 Self {
505 name: None,
506 mime_type: None,
507 bytes: None,
508 uri: Some(uri.into()),
509 }
510 }
511
512 #[must_use]
514 pub fn with_name(mut self, name: impl Into<String>) -> Self {
515 self.name = Some(name.into());
516 self
517 }
518
519 #[must_use]
521 pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
522 self.mime_type = Some(mime_type.into());
523 self
524 }
525
526 pub const fn validate(&self) -> Result<(), &'static str> {
532 if self.bytes.is_none() && self.uri.is_none() {
533 Err("FileContent must have at least one of 'bytes' or 'uri' set")
534 } else {
535 Ok(())
536 }
537 }
538}
539
540#[cfg(test)]
543mod tests {
544 use super::*;
545
546 fn make_message() -> Message {
547 Message {
548 id: MessageId::new("msg-1"),
549 role: MessageRole::User,
550 parts: vec![Part::text("Hello")],
551 task_id: None,
552 context_id: None,
553 reference_task_ids: None,
554 extensions: None,
555 metadata: None,
556 }
557 }
558
559 #[test]
560 fn message_roundtrip() {
561 let msg = make_message();
562 let json = serde_json::to_string(&msg).expect("serialize");
563 assert!(json.contains("\"messageId\":\"msg-1\""));
564 assert!(json.contains("\"role\":\"ROLE_USER\""));
565
566 let back: Message = serde_json::from_str(&json).expect("deserialize");
567 assert_eq!(back.id, MessageId::new("msg-1"));
568 assert_eq!(back.role, MessageRole::User);
569 }
570
571 #[test]
572 fn role_serializes_as_proto_names() {
573 assert_eq!(
574 serde_json::to_string(&MessageRole::User).unwrap(),
575 "\"ROLE_USER\""
576 );
577 assert_eq!(
578 serde_json::to_string(&MessageRole::Agent).unwrap(),
579 "\"ROLE_AGENT\""
580 );
581 assert_eq!(
582 serde_json::to_string(&MessageRole::Unspecified).unwrap(),
583 "\"ROLE_UNSPECIFIED\""
584 );
585 }
586
587 #[test]
588 fn role_accepts_legacy_lowercase() {
589 let back: MessageRole = serde_json::from_str("\"user\"").unwrap();
590 assert_eq!(back, MessageRole::User);
591 let back: MessageRole = serde_json::from_str("\"agent\"").unwrap();
592 assert_eq!(back, MessageRole::Agent);
593 }
594
595 #[test]
596 fn text_part_v1_format() {
597 let part = Part::text("hello world");
598 let json = serde_json::to_string(&part).expect("serialize");
599 assert!(
600 json.contains("\"text\":\"hello world\""),
601 "should have text field: {json}"
602 );
603 assert!(
605 !json.contains("\"type\""),
606 "v1.0 should not have type field: {json}"
607 );
608 let back: Part = serde_json::from_str(&json).expect("deserialize");
609 assert!(matches!(back.content, PartContent::Text(ref t) if t == "hello world"));
610 }
611
612 #[test]
613 fn raw_part_v1_format() {
614 let part = Part::raw("aGVsbG8=")
615 .with_filename("test.png")
616 .with_media_type("image/png");
617 let json = serde_json::to_string(&part).expect("serialize");
618 assert!(json.contains("\"raw\":\"aGVsbG8=\""));
619 assert!(json.contains("\"filename\":\"test.png\""));
620 assert!(json.contains("\"mediaType\":\"image/png\""));
621 assert!(!json.contains("\"type\""));
622 let back: Part = serde_json::from_str(&json).expect("deserialize");
623 assert!(matches!(back.content, PartContent::Raw(ref r) if r == "aGVsbG8="));
624 assert_eq!(back.filename.as_deref(), Some("test.png"));
625 assert_eq!(back.media_type.as_deref(), Some("image/png"));
626 }
627
628 #[test]
629 fn url_part_v1_format() {
630 let part = Part::url("https://example.com/file.pdf")
631 .with_filename("file.pdf")
632 .with_media_type("application/pdf");
633 let json = serde_json::to_string(&part).expect("serialize");
634 assert!(json.contains("\"url\":\"https://example.com/file.pdf\""));
635 assert!(json.contains("\"filename\":\"file.pdf\""));
636 assert!(!json.contains("\"type\""));
637 let back: Part = serde_json::from_str(&json).expect("deserialize");
638 assert!(
639 matches!(back.content, PartContent::Url(ref u) if u == "https://example.com/file.pdf")
640 );
641 }
642
643 #[test]
644 fn data_part_v1_format() {
645 let part = Part::data(serde_json::json!({"key": "value"}));
646 let json = serde_json::to_string(&part).expect("serialize");
647 assert!(json.contains("\"data\""));
648 assert!(!json.contains("\"type\""));
649 let back: Part = serde_json::from_str(&json).expect("deserialize");
650 match &back.content {
651 PartContent::Data(data) => assert_eq!(data["key"], "value"),
652 _ => panic!("expected Data variant"),
653 }
654 }
655
656 #[test]
657 fn none_fields_omitted() {
658 let msg = make_message();
659 let json = serde_json::to_string(&msg).expect("serialize");
660 assert!(
661 !json.contains("\"taskId\""),
662 "taskId should be omitted: {json}"
663 );
664 assert!(
665 !json.contains("\"metadata\""),
666 "metadata should be omitted: {json}"
667 );
668 }
669
670 #[test]
671 fn message_role_display_trait() {
672 assert_eq!(MessageRole::User.to_string(), "ROLE_USER");
673 assert_eq!(MessageRole::Agent.to_string(), "ROLE_AGENT");
674 assert_eq!(MessageRole::Unspecified.to_string(), "ROLE_UNSPECIFIED");
675 }
676
677 #[test]
678 fn message_with_reference_task_ids() {
679 use crate::task::TaskId;
680
681 let msg = Message {
682 id: MessageId::new("msg-ref"),
683 role: MessageRole::User,
684 parts: vec![Part::text("check these tasks")],
685 task_id: None,
686 context_id: None,
687 reference_task_ids: Some(vec![TaskId::new("task-100"), TaskId::new("task-200")]),
688 extensions: None,
689 metadata: None,
690 };
691
692 let json = serde_json::to_string(&msg).expect("serialize");
693 assert!(json.contains("\"referenceTaskIds\""));
694 assert!(json.contains("\"task-100\""));
695
696 let back: Message = serde_json::from_str(&json).expect("deserialize");
697 let refs = back
698 .reference_task_ids
699 .expect("should have reference_task_ids");
700 assert_eq!(refs.len(), 2);
701 }
702
703 #[test]
704 fn backward_compat_file_bytes_constructor() {
705 let part = Part::file_bytes("aGVsbG8=");
706 assert!(matches!(part.content, PartContent::Raw(_)));
707 }
708
709 #[test]
710 fn backward_compat_file_uri_constructor() {
711 let part = Part::file_uri("https://example.com/file.pdf");
712 assert!(matches!(part.content, PartContent::Url(_)));
713 }
714
715 #[test]
716 fn backward_compat_file_constructor() {
717 let fc = FileContent::from_bytes("aGVsbG8=")
718 .with_name("test.png")
719 .with_mime_type("image/png");
720 let part = Part::file(fc);
721 assert!(matches!(part.content, PartContent::Raw(ref r) if r == "aGVsbG8="));
722 assert_eq!(part.filename.as_deref(), Some("test.png"));
723 assert_eq!(part.media_type.as_deref(), Some("image/png"));
724 }
725
726 #[test]
729 fn message_id_display() {
730 let id = MessageId::new("msg-42");
731 assert_eq!(id.to_string(), "msg-42");
732 }
733
734 #[test]
735 fn message_id_as_ref() {
736 let id = MessageId::new("ref-test");
737 assert_eq!(id.as_ref(), "ref-test");
738 }
739
740 #[test]
741 fn message_id_from_impls() {
742 let from_str: MessageId = "str-id".into();
743 assert_eq!(from_str, MessageId::new("str-id"));
744
745 let from_string: MessageId = String::from("string-id").into();
746 assert_eq!(from_string, MessageId::new("string-id"));
747 }
748
749 #[test]
750 fn part_text_has_no_metadata() {
751 let p = Part::text("hi");
752 assert!(p.metadata.is_none());
753 assert!(p.filename.is_none());
754 assert!(p.media_type.is_none());
755 }
756}