use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Role {
#[serde(rename = "ROLE_USER")]
User,
#[serde(rename = "ROLE_AGENT")]
Agent,
#[serde(rename = "ROLE_UNSPECIFIED")]
Unspecified,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Part {
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub raw: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none", rename = "mediaType")]
pub media_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Message {
#[serde(rename = "messageId")]
pub message_id: String,
pub role: Role,
pub parts: Vec<Part>,
#[serde(rename = "contextId", skip_serializing_if = "Option::is_none")]
pub context_id: Option<String>,
#[serde(rename = "taskId", skip_serializing_if = "Option::is_none")]
pub task_id: Option<String>,
#[serde(rename = "referenceTaskIds", skip_serializing_if = "Option::is_none")]
pub reference_task_ids: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Vec<String>>,
}
impl Message {
pub fn user_text(text: impl Into<String>) -> Self {
Self {
message_id: uuid::Uuid::new_v4().to_string(),
role: Role::User,
parts: vec![Part {
text: Some(text.into()),
raw: None,
url: None,
data: None,
media_type: None,
filename: None,
metadata: None,
}],
context_id: None,
task_id: None,
reference_task_ids: None,
metadata: None,
extensions: None,
}
}
pub fn agent_text(text: impl Into<String>) -> Self {
let mut msg = Self::user_text(text);
msg.role = Role::Agent;
msg
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn role_serializes_to_expected_strings() {
assert_eq!(
serde_json::to_string(&Role::User).unwrap(),
r#""ROLE_USER""#
);
assert_eq!(
serde_json::to_string(&Role::Agent).unwrap(),
r#""ROLE_AGENT""#
);
assert_eq!(
serde_json::to_string(&Role::Unspecified).unwrap(),
r#""ROLE_UNSPECIFIED""#
);
}
#[test]
fn role_roundtrip() {
for role in [Role::User, Role::Agent, Role::Unspecified] {
let json = serde_json::to_string(&role).unwrap();
let back: Role = serde_json::from_str(&json).unwrap();
assert_eq!(back, role);
}
}
#[test]
fn part_text_only_omits_other_fields() {
let p = Part {
text: Some("hello".to_string()),
raw: None,
url: None,
data: None,
media_type: None,
filename: None,
metadata: None,
};
let json = serde_json::to_string(&p).unwrap();
assert!(json.contains("hello"));
assert!(!json.contains("raw"));
assert!(!json.contains("url"));
}
#[test]
fn part_roundtrip() {
let p = Part {
text: Some("content".to_string()),
raw: None,
url: Some("https://example.com".to_string()),
data: Some(serde_json::json!({"key": 1})),
media_type: Some("text/plain".to_string()),
filename: Some("file.txt".to_string()),
metadata: None,
};
let json = serde_json::to_string(&p).unwrap();
let back: Part = serde_json::from_str(&json).unwrap();
assert_eq!(back, p);
}
#[test]
fn user_text_creates_user_role_message() {
let msg = Message::user_text("hi");
assert_eq!(msg.role, Role::User);
assert_eq!(msg.parts.len(), 1);
assert_eq!(msg.parts[0].text.as_deref(), Some("hi"));
assert!(!msg.message_id.is_empty());
}
#[test]
fn agent_text_creates_agent_role_message() {
let msg = Message::agent_text("response");
assert_eq!(msg.role, Role::Agent);
assert_eq!(msg.parts[0].text.as_deref(), Some("response"));
}
#[test]
fn message_ids_are_unique() {
let a = Message::user_text("a");
let b = Message::user_text("b");
assert_ne!(a.message_id, b.message_id);
}
#[test]
fn message_roundtrip() {
let msg = Message::user_text("roundtrip test");
let json = serde_json::to_string(&msg).unwrap();
let back: Message = serde_json::from_str(&json).unwrap();
assert_eq!(back, msg);
}
#[test]
fn message_json_uses_camel_case_message_id() {
let msg = Message::user_text("x");
let json = serde_json::to_string(&msg).unwrap();
assert!(json.contains("messageId"));
assert!(!json.contains("message_id"));
}
#[test]
fn message_optional_fields_omitted_when_none() {
let msg = Message::user_text("x");
let json = serde_json::to_string(&msg).unwrap();
assert!(!json.contains("contextId"));
assert!(!json.contains("taskId"));
assert!(!json.contains("extensions"));
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Artifact {
#[serde(rename = "artifactId")]
pub artifact_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parts: Vec<Part>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Vec<String>>,
}