1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8pub enum Role {
9 #[serde(rename = "ROLE_USER")]
11 User,
12 #[serde(rename = "ROLE_AGENT")]
14 Agent,
15 #[serde(rename = "ROLE_UNSPECIFIED")]
17 Unspecified,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
24pub struct Part {
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub text: Option<String>,
28 #[serde(skip_serializing_if = "Option::is_none")]
30 pub raw: Option<String>,
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub url: Option<String>,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub data: Option<serde_json::Value>,
37 #[serde(skip_serializing_if = "Option::is_none", rename = "mediaType")]
39 pub media_type: Option<String>,
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub filename: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
45 pub metadata: Option<HashMap<String, serde_json::Value>>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50pub struct Message {
51 #[serde(rename = "messageId")]
53 pub message_id: String,
54 pub role: Role,
56 pub parts: Vec<Part>,
58 #[serde(rename = "contextId", skip_serializing_if = "Option::is_none")]
60 pub context_id: Option<String>,
61 #[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
63 pub task_id: Option<String>,
64 #[serde(rename = "referenceTaskIds", skip_serializing_if = "Option::is_none")]
66 pub reference_task_ids: Option<Vec<String>>,
67 #[serde(skip_serializing_if = "Option::is_none")]
69 pub metadata: Option<HashMap<String, serde_json::Value>>,
70 #[serde(skip_serializing_if = "Option::is_none")]
72 pub extensions: Option<Vec<String>>,
73}
74
75impl Message {
76 pub fn user_text(text: impl Into<String>) -> Self {
78 Self {
79 message_id: uuid::Uuid::new_v4().to_string(),
80 role: Role::User,
81 parts: vec![Part {
82 text: Some(text.into()),
83 raw: None,
84 url: None,
85 data: None,
86 media_type: None,
87 filename: None,
88 metadata: None,
89 }],
90 context_id: None,
91 task_id: None,
92 reference_task_ids: None,
93 metadata: None,
94 extensions: None,
95 }
96 }
97
98 pub fn agent_text(text: impl Into<String>) -> Self {
100 let mut msg = Self::user_text(text);
101 msg.role = Role::Agent;
102 msg
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
113 fn role_serializes_to_expected_strings() {
114 assert_eq!(
115 serde_json::to_string(&Role::User).unwrap(),
116 r#""ROLE_USER""#
117 );
118 assert_eq!(
119 serde_json::to_string(&Role::Agent).unwrap(),
120 r#""ROLE_AGENT""#
121 );
122 assert_eq!(
123 serde_json::to_string(&Role::Unspecified).unwrap(),
124 r#""ROLE_UNSPECIFIED""#
125 );
126 }
127
128 #[test]
129 fn role_roundtrip() {
130 for role in [Role::User, Role::Agent, Role::Unspecified] {
131 let json = serde_json::to_string(&role).unwrap();
132 let back: Role = serde_json::from_str(&json).unwrap();
133 assert_eq!(back, role);
134 }
135 }
136
137 #[test]
140 fn part_text_only_omits_other_fields() {
141 let p = Part {
142 text: Some("hello".to_string()),
143 raw: None,
144 url: None,
145 data: None,
146 media_type: None,
147 filename: None,
148 metadata: None,
149 };
150 let json = serde_json::to_string(&p).unwrap();
151 assert!(json.contains("hello"));
152 assert!(!json.contains("raw"));
153 assert!(!json.contains("url"));
154 }
155
156 #[test]
157 fn part_roundtrip() {
158 let p = Part {
159 text: Some("content".to_string()),
160 raw: None,
161 url: Some("https://example.com".to_string()),
162 data: Some(serde_json::json!({"key": 1})),
163 media_type: Some("text/plain".to_string()),
164 filename: Some("file.txt".to_string()),
165 metadata: None,
166 };
167 let json = serde_json::to_string(&p).unwrap();
168 let back: Part = serde_json::from_str(&json).unwrap();
169 assert_eq!(back, p);
170 }
171
172 #[test]
175 fn user_text_creates_user_role_message() {
176 let msg = Message::user_text("hi");
177 assert_eq!(msg.role, Role::User);
178 assert_eq!(msg.parts.len(), 1);
179 assert_eq!(msg.parts[0].text.as_deref(), Some("hi"));
180 assert!(!msg.message_id.is_empty());
181 }
182
183 #[test]
184 fn agent_text_creates_agent_role_message() {
185 let msg = Message::agent_text("response");
186 assert_eq!(msg.role, Role::Agent);
187 assert_eq!(msg.parts[0].text.as_deref(), Some("response"));
188 }
189
190 #[test]
191 fn message_ids_are_unique() {
192 let a = Message::user_text("a");
193 let b = Message::user_text("b");
194 assert_ne!(a.message_id, b.message_id);
195 }
196
197 #[test]
198 fn message_roundtrip() {
199 let msg = Message::user_text("roundtrip test");
200 let json = serde_json::to_string(&msg).unwrap();
201 let back: Message = serde_json::from_str(&json).unwrap();
202 assert_eq!(back, msg);
203 }
204
205 #[test]
206 fn message_json_uses_camel_case_message_id() {
207 let msg = Message::user_text("x");
208 let json = serde_json::to_string(&msg).unwrap();
209 assert!(json.contains("messageId"));
210 assert!(!json.contains("message_id"));
211 }
212
213 #[test]
214 fn message_optional_fields_omitted_when_none() {
215 let msg = Message::user_text("x");
216 let json = serde_json::to_string(&msg).unwrap();
217 assert!(!json.contains("contextId"));
218 assert!(!json.contains("taskId"));
219 assert!(!json.contains("extensions"));
220 }
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
225pub struct Artifact {
226 #[serde(rename = "artifactId")]
228 pub artifact_id: String,
229 #[serde(skip_serializing_if = "Option::is_none")]
231 pub name: Option<String>,
232 #[serde(skip_serializing_if = "Option::is_none")]
234 pub description: Option<String>,
235 pub parts: Vec<Part>,
237 #[serde(skip_serializing_if = "Option::is_none")]
239 pub metadata: Option<HashMap<String, serde_json::Value>>,
240 #[serde(skip_serializing_if = "Option::is_none")]
242 pub extensions: Option<Vec<String>>,
243}