use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ArtifactContent {
Text(String),
Structured(HashMap<String, Value>),
}
impl ArtifactContent {
pub fn text(content: impl Into<String>) -> Self {
Self::Text(content.into())
}
pub fn structured(data: HashMap<String, Value>) -> Self {
Self::Structured(data)
}
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(s) => Some(s),
Self::Structured(_) => None,
}
}
pub fn as_structured(&self) -> Option<&HashMap<String, Value>> {
match self {
Self::Text(_) => None,
Self::Structured(m) => Some(m),
}
}
pub fn size_bytes(&self) -> usize {
match self {
Self::Text(s) => s.len(),
Self::Structured(m) => serde_json::to_string(m).map(|s| s.len()).unwrap_or(0),
}
}
}
impl Default for ArtifactContent {
fn default() -> Self {
Self::Text(String::new())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub content: ArtifactContent,
#[serde(default)]
pub content_type: Option<String>,
#[serde(default)]
pub metadata: HashMap<String, Value>,
#[serde(default)]
pub artifact_id: Option<String>,
#[serde(default)]
pub trace_correlation_id: Option<String>,
#[serde(default)]
pub size_bytes: Option<i64>,
#[serde(default)]
pub sha256: Option<String>,
#[serde(default)]
pub storage: Option<HashMap<String, Value>>,
#[serde(default)]
pub created_at: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
}
impl Artifact {
pub fn text(content: impl Into<String>) -> Self {
Self {
content: ArtifactContent::text(content),
content_type: Some("text/plain".to_string()),
metadata: HashMap::new(),
artifact_id: None,
trace_correlation_id: None,
size_bytes: None,
sha256: None,
storage: None,
created_at: None,
name: None,
description: None,
}
}
pub fn json(data: HashMap<String, Value>) -> Self {
Self {
content: ArtifactContent::structured(data),
content_type: Some("application/json".to_string()),
metadata: HashMap::new(),
artifact_id: None,
trace_correlation_id: None,
size_bytes: None,
sha256: None,
storage: None,
created_at: None,
name: None,
description: None,
}
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.artifact_id = Some(id.into());
self
}
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
self.trace_correlation_id = Some(trace_id.into());
self
}
pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn validate_size(&self, max_size_bytes: i64) -> Result<(), String> {
let size = self
.size_bytes
.unwrap_or_else(|| self.content.size_bytes() as i64);
if size > max_size_bytes {
return Err(format!(
"Artifact size {} bytes exceeds maximum {} bytes",
size, max_size_bytes
));
}
Ok(())
}
pub fn compute_size(&mut self) {
self.size_bytes = Some(self.content.size_bytes() as i64);
}
}
impl Default for Artifact {
fn default() -> Self {
Self::text("")
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ArtifactBundle {
#[serde(default)]
pub artifacts: Vec<Artifact>,
#[serde(default)]
pub total_size_bytes: Option<i64>,
#[serde(default)]
pub metadata: HashMap<String, Value>,
}
impl ArtifactBundle {
pub fn new() -> Self {
Self::default()
}
pub fn add(&mut self, artifact: Artifact) {
self.artifacts.push(artifact);
}
pub fn compute_total_size(&mut self) -> i64 {
let total: i64 = self
.artifacts
.iter()
.map(|a| {
a.size_bytes
.unwrap_or_else(|| a.content.size_bytes() as i64)
})
.sum();
self.total_size_bytes = Some(total);
total
}
pub fn get_by_id(&self, id: &str) -> Option<&Artifact> {
self.artifacts
.iter()
.find(|a| a.artifact_id.as_deref() == Some(id))
}
pub fn get_by_name(&self, name: &str) -> Option<&Artifact> {
self.artifacts
.iter()
.find(|a| a.name.as_deref() == Some(name))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_artifact() {
let artifact = Artifact::text("Hello, world!")
.with_name("greeting")
.with_id("art-001");
assert_eq!(artifact.content.as_text(), Some("Hello, world!"));
assert_eq!(artifact.name, Some("greeting".to_string()));
assert_eq!(artifact.content_type, Some("text/plain".to_string()));
}
#[test]
fn test_json_artifact() {
let mut data = HashMap::new();
data.insert("key".to_string(), serde_json::json!("value"));
let artifact = Artifact::json(data);
assert!(artifact.content.as_structured().is_some());
assert_eq!(artifact.content_type, Some("application/json".to_string()));
}
#[test]
fn test_size_validation() {
let artifact = Artifact::text("x".repeat(1000));
assert!(artifact.validate_size(2000).is_ok());
assert!(artifact.validate_size(500).is_err());
}
#[test]
fn test_artifact_bundle() {
let mut bundle = ArtifactBundle::new();
bundle.add(Artifact::text("First").with_name("first"));
bundle.add(Artifact::text("Second").with_name("second"));
assert_eq!(bundle.artifacts.len(), 2);
assert!(bundle.get_by_name("first").is_some());
}
#[test]
fn test_serde() {
let artifact = Artifact::text("test content").with_id("test-id");
let json = serde_json::to_string(&artifact).unwrap();
let parsed: Artifact = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.content.as_text(), Some("test content"));
assert_eq!(parsed.artifact_id, Some("test-id".to_string()));
}
}