use serde::{Deserialize, Serialize};
use super::media::{AudioMediaType, DocumentMediaType, ImageMediaType, VideoMediaType};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum UserContent {
Text(String),
Parts(Vec<UserContentPart>),
}
impl UserContent {
#[must_use]
pub fn text(s: impl Into<String>) -> Self {
Self::Text(s.into())
}
#[must_use]
pub fn parts(parts: Vec<UserContentPart>) -> Self {
Self::Parts(parts)
}
#[must_use]
pub fn is_text(&self) -> bool {
matches!(self, Self::Text(_))
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s),
_ => None,
}
}
#[must_use]
pub fn to_parts(&self) -> Vec<UserContentPart> {
match self {
Self::Text(s) => vec![UserContentPart::Text { text: s.clone() }],
Self::Parts(parts) => parts.clone(),
}
}
}
impl Default for UserContent {
fn default() -> Self {
Self::Text(String::new())
}
}
impl From<String> for UserContent {
fn from(s: String) -> Self {
Self::Text(s)
}
}
impl From<&str> for UserContent {
fn from(s: &str) -> Self {
Self::Text(s.to_string())
}
}
impl From<Vec<UserContentPart>> for UserContent {
fn from(parts: Vec<UserContentPart>) -> Self {
Self::Parts(parts)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum UserContentPart {
Text {
text: String,
},
Image {
#[serde(flatten)]
image: ImageContent,
},
Audio {
#[serde(flatten)]
audio: AudioContent,
},
Video {
#[serde(flatten)]
video: VideoContent,
},
Document {
#[serde(flatten)]
document: DocumentContent,
},
File {
#[serde(flatten)]
file: FileContent,
},
}
impl UserContentPart {
#[must_use]
pub fn text(s: impl Into<String>) -> Self {
Self::Text { text: s.into() }
}
#[must_use]
pub fn image_url(url: impl Into<String>) -> Self {
Self::Image {
image: ImageContent::url(url),
}
}
#[must_use]
pub fn image_binary(data: Vec<u8>, media_type: ImageMediaType) -> Self {
Self::Image {
image: ImageContent::binary(data, media_type),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ImageContent {
Url(ImageUrl),
Binary(BinaryImage),
}
impl ImageContent {
#[must_use]
pub fn url(url: impl Into<String>) -> Self {
Self::Url(ImageUrl::new(url))
}
#[must_use]
pub fn binary(data: Vec<u8>, media_type: ImageMediaType) -> Self {
Self::Binary(BinaryImage::new(data, media_type))
}
#[must_use]
pub fn media_type(&self) -> Option<ImageMediaType> {
match self {
Self::Url(u) => u.media_type,
Self::Binary(b) => Some(b.media_type),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ImageUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<ImageMediaType>,
#[serde(default)]
pub force_download: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor_metadata: Option<serde_json::Value>,
}
impl ImageUrl {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
media_type: None,
force_download: false,
vendor_metadata: None,
}
}
#[must_use]
pub fn with_media_type(mut self, media_type: ImageMediaType) -> Self {
self.media_type = Some(media_type);
self
}
#[must_use]
pub fn with_force_download(mut self, force: bool) -> Self {
self.force_download = force;
self
}
#[must_use]
pub fn with_vendor_metadata(mut self, metadata: serde_json::Value) -> Self {
self.vendor_metadata = Some(metadata);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BinaryImage {
#[serde(with = "base64_serde")]
pub data: Vec<u8>,
pub media_type: ImageMediaType,
}
impl BinaryImage {
#[must_use]
pub fn new(data: Vec<u8>, media_type: ImageMediaType) -> Self {
Self { data, media_type }
}
#[must_use]
pub fn to_base64(&self) -> String {
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &self.data)
}
#[must_use]
pub fn to_data_url(&self) -> String {
format!(
"data:{};base64,{}",
self.media_type.mime_type(),
self.to_base64()
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AudioContent {
Url(AudioUrl),
Binary(BinaryAudio),
}
impl AudioContent {
#[must_use]
pub fn url(url: impl Into<String>) -> Self {
Self::Url(AudioUrl::new(url))
}
#[must_use]
pub fn binary(data: Vec<u8>, media_type: AudioMediaType) -> Self {
Self::Binary(BinaryAudio::new(data, media_type))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AudioUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<AudioMediaType>,
#[serde(default)]
pub force_download: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor_metadata: Option<serde_json::Value>,
}
impl AudioUrl {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
media_type: None,
force_download: false,
vendor_metadata: None,
}
}
#[must_use]
pub fn with_media_type(mut self, media_type: AudioMediaType) -> Self {
self.media_type = Some(media_type);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BinaryAudio {
#[serde(with = "base64_serde")]
pub data: Vec<u8>,
pub media_type: AudioMediaType,
}
impl BinaryAudio {
#[must_use]
pub fn new(data: Vec<u8>, media_type: AudioMediaType) -> Self {
Self { data, media_type }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VideoContent {
Url(VideoUrl),
Binary(BinaryVideo),
}
impl VideoContent {
#[must_use]
pub fn url(url: impl Into<String>) -> Self {
Self::Url(VideoUrl::new(url))
}
#[must_use]
pub fn binary(data: Vec<u8>, media_type: VideoMediaType) -> Self {
Self::Binary(BinaryVideo::new(data, media_type))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VideoUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<VideoMediaType>,
#[serde(default)]
pub force_download: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor_metadata: Option<serde_json::Value>,
}
impl VideoUrl {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
media_type: None,
force_download: false,
vendor_metadata: None,
}
}
#[must_use]
pub fn with_media_type(mut self, media_type: VideoMediaType) -> Self {
self.media_type = Some(media_type);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BinaryVideo {
#[serde(with = "base64_serde")]
pub data: Vec<u8>,
pub media_type: VideoMediaType,
}
impl BinaryVideo {
#[must_use]
pub fn new(data: Vec<u8>, media_type: VideoMediaType) -> Self {
Self { data, media_type }
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum DocumentContent {
Url(DocumentUrl),
Binary(BinaryDocument),
}
impl DocumentContent {
#[must_use]
pub fn url(url: impl Into<String>) -> Self {
Self::Url(DocumentUrl::new(url))
}
#[must_use]
pub fn binary(data: Vec<u8>, media_type: DocumentMediaType) -> Self {
Self::Binary(BinaryDocument::new(data, media_type))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DocumentUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_type: Option<DocumentMediaType>,
#[serde(default)]
pub force_download: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor_metadata: Option<serde_json::Value>,
}
impl DocumentUrl {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
media_type: None,
force_download: false,
vendor_metadata: None,
}
}
#[must_use]
pub fn with_media_type(mut self, media_type: DocumentMediaType) -> Self {
self.media_type = Some(media_type);
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BinaryDocument {
#[serde(with = "base64_serde")]
pub data: Vec<u8>,
pub media_type: DocumentMediaType,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
}
impl BinaryDocument {
#[must_use]
pub fn new(data: Vec<u8>, media_type: DocumentMediaType) -> Self {
Self {
data,
media_type,
filename: None,
}
}
#[must_use]
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum FileContent {
Url(FileUrl),
Binary(BinaryFile),
}
impl FileContent {
#[must_use]
pub fn url(url: impl Into<String>) -> Self {
Self::Url(FileUrl::new(url))
}
#[must_use]
pub fn binary(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self::Binary(BinaryFile::new(data, mime_type))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct FileUrl {
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
#[serde(default)]
pub force_download: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub vendor_metadata: Option<serde_json::Value>,
}
impl FileUrl {
#[must_use]
pub fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
mime_type: None,
force_download: false,
vendor_metadata: None,
}
}
#[must_use]
pub fn with_mime_type(mut self, mime_type: impl Into<String>) -> Self {
self.mime_type = Some(mime_type.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BinaryFile {
#[serde(with = "base64_serde")]
pub data: Vec<u8>,
pub mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filename: Option<String>,
}
impl BinaryFile {
#[must_use]
pub fn new(data: Vec<u8>, mime_type: impl Into<String>) -> Self {
Self {
data,
mime_type: mime_type.into(),
filename: None,
}
}
#[must_use]
pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
self.filename = Some(filename.into());
self
}
}
mod base64_serde {
use base64::{engine::general_purpose::STANDARD, Engine};
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S>(data: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&STANDARD.encode(data))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
STANDARD.decode(s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_content_text() {
let content = UserContent::text("Hello, world!");
assert!(content.is_text());
assert_eq!(content.as_text(), Some("Hello, world!"));
}
#[test]
fn test_user_content_from_string() {
let content: UserContent = "Hello".into();
assert!(content.is_text());
}
#[test]
fn test_image_url() {
let img =
ImageUrl::new("https://example.com/image.png").with_media_type(ImageMediaType::Png);
assert_eq!(img.url, "https://example.com/image.png");
assert_eq!(img.media_type, Some(ImageMediaType::Png));
}
#[test]
fn test_binary_image_to_data_url() {
let img = BinaryImage::new(vec![1, 2, 3, 4], ImageMediaType::Png);
let data_url = img.to_data_url();
assert!(data_url.starts_with("data:image/png;base64,"));
}
#[test]
fn test_serde_roundtrip() {
let content = UserContent::parts(vec![
UserContentPart::text("Hello"),
UserContentPart::image_url("https://example.com/img.jpg"),
]);
let json = serde_json::to_string(&content).unwrap();
let parsed: UserContent = serde_json::from_str(&json).unwrap();
assert_eq!(content, parsed);
}
}