use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ToolArguments(String);
impl ToolArguments {
pub fn from_json(json: impl Into<String>) -> Self {
Self(json.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for ToolArguments {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ToolCall {
pub name: String,
pub arguments: ToolArguments,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChatMessage {
pub role: String,
pub content: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tool_calls: Vec<ToolCall>,
}
impl ChatMessage {
pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
Self {
role: role.into(),
content: content.into(),
tool_calls: Vec::new(),
}
}
pub fn user(content: impl Into<String>) -> Self {
Self::new("user", content)
}
pub fn assistant(content: impl Into<String>) -> Self {
Self::new("assistant", content)
}
pub fn system(content: impl Into<String>) -> Self {
Self::new("system", content)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn user_message_has_user_role() {
let m = ChatMessage::user("hello");
assert_eq!(m.role, "user");
assert_eq!(m.content, "hello");
assert!(m.tool_calls.is_empty());
}
#[test]
fn serde_roundtrip_preserves_simple_message() {
let m = ChatMessage::system("be helpful");
let json = serde_json::to_string(&m).unwrap();
let back: ChatMessage = serde_json::from_str(&json).unwrap();
assert_eq!(m, back);
}
#[test]
fn serde_omits_empty_tool_calls() {
let m = ChatMessage::assistant("ok");
let json = serde_json::to_string(&m).unwrap();
assert!(!json.contains("tool_calls"));
}
#[test]
fn serde_preserves_tool_calls_when_present() {
let m = ChatMessage {
role: "assistant".to_string(),
content: "running it".to_string(),
tool_calls: vec![ToolCall {
name: "bash".to_string(),
arguments: ToolArguments::from_json(r#"{"cmd":"ls"}"#),
}],
};
let json = serde_json::to_string(&m).unwrap();
let back: ChatMessage = serde_json::from_str(&json).unwrap();
assert_eq!(m, back);
assert_eq!(back.tool_calls.len(), 1);
}
#[test]
fn serde_default_fills_missing_tool_calls() {
let json = r#"{"role":"user","content":"hi"}"#;
let back: ChatMessage = serde_json::from_str(json).unwrap();
assert_eq!(back.role, "user");
assert!(back.tool_calls.is_empty());
}
#[test]
fn tool_arguments_round_trip_through_serde() {
let args = ToolArguments::from_json(r#"{"k":"v","n":3}"#);
let json = serde_json::to_string(&args).unwrap();
let back: ToolArguments = serde_json::from_str(&json).unwrap();
assert_eq!(args, back);
assert_eq!(args.as_str(), r#"{"k":"v","n":3}"#);
assert_eq!(args.into_string(), r#"{"k":"v","n":3}"#);
}
#[test]
fn tool_arguments_display_renders_raw_payload() {
let args = ToolArguments::from_json(r#"{"x":1}"#);
assert_eq!(args.to_string(), r#"{"x":1}"#);
}
}