synth_ai_core/data/
artifacts.rs1use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum ArtifactContent {
13 Text(String),
15 Structured(HashMap<String, Value>),
17}
18
19impl ArtifactContent {
20 pub fn text(content: impl Into<String>) -> Self {
22 Self::Text(content.into())
23 }
24
25 pub fn structured(data: HashMap<String, Value>) -> Self {
27 Self::Structured(data)
28 }
29
30 pub fn as_text(&self) -> Option<&str> {
32 match self {
33 Self::Text(s) => Some(s),
34 Self::Structured(_) => None,
35 }
36 }
37
38 pub fn as_structured(&self) -> Option<&HashMap<String, Value>> {
40 match self {
41 Self::Text(_) => None,
42 Self::Structured(m) => Some(m),
43 }
44 }
45
46 pub fn size_bytes(&self) -> usize {
48 match self {
49 Self::Text(s) => s.len(),
50 Self::Structured(m) => serde_json::to_string(m).map(|s| s.len()).unwrap_or(0),
51 }
52 }
53}
54
55impl Default for ArtifactContent {
56 fn default() -> Self {
57 Self::Text(String::new())
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct Artifact {
64 pub content: ArtifactContent,
66 #[serde(default)]
68 pub content_type: Option<String>,
69 #[serde(default)]
71 pub metadata: HashMap<String, Value>,
72 #[serde(default)]
74 pub artifact_id: Option<String>,
75 #[serde(default)]
77 pub trace_correlation_id: Option<String>,
78 #[serde(default)]
80 pub size_bytes: Option<i64>,
81 #[serde(default)]
83 pub sha256: Option<String>,
84 #[serde(default)]
86 pub storage: Option<HashMap<String, Value>>,
87 #[serde(default)]
89 pub created_at: Option<String>,
90 #[serde(default)]
92 pub name: Option<String>,
93 #[serde(default)]
95 pub description: Option<String>,
96}
97
98impl Artifact {
99 pub fn text(content: impl Into<String>) -> Self {
101 Self {
102 content: ArtifactContent::text(content),
103 content_type: Some("text/plain".to_string()),
104 metadata: HashMap::new(),
105 artifact_id: None,
106 trace_correlation_id: None,
107 size_bytes: None,
108 sha256: None,
109 storage: None,
110 created_at: None,
111 name: None,
112 description: None,
113 }
114 }
115
116 pub fn json(data: HashMap<String, Value>) -> Self {
118 Self {
119 content: ArtifactContent::structured(data),
120 content_type: Some("application/json".to_string()),
121 metadata: HashMap::new(),
122 artifact_id: None,
123 trace_correlation_id: None,
124 size_bytes: None,
125 sha256: None,
126 storage: None,
127 created_at: None,
128 name: None,
129 description: None,
130 }
131 }
132
133 pub fn with_id(mut self, id: impl Into<String>) -> Self {
135 self.artifact_id = Some(id.into());
136 self
137 }
138
139 pub fn with_name(mut self, name: impl Into<String>) -> Self {
141 self.name = Some(name.into());
142 self
143 }
144
145 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
147 self.trace_correlation_id = Some(trace_id.into());
148 self
149 }
150
151 pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
153 self.metadata.insert(key.into(), value);
154 self
155 }
156
157 pub fn validate_size(&self, max_size_bytes: i64) -> Result<(), String> {
159 let size = self.size_bytes.unwrap_or_else(|| self.content.size_bytes() as i64);
160 if size > max_size_bytes {
161 return Err(format!(
162 "Artifact size {} bytes exceeds maximum {} bytes",
163 size, max_size_bytes
164 ));
165 }
166 Ok(())
167 }
168
169 pub fn compute_size(&mut self) {
171 self.size_bytes = Some(self.content.size_bytes() as i64);
172 }
173}
174
175impl Default for Artifact {
176 fn default() -> Self {
177 Self::text("")
178 }
179}
180
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
183pub struct ArtifactBundle {
184 #[serde(default)]
186 pub artifacts: Vec<Artifact>,
187 #[serde(default)]
189 pub total_size_bytes: Option<i64>,
190 #[serde(default)]
192 pub metadata: HashMap<String, Value>,
193}
194
195impl ArtifactBundle {
196 pub fn new() -> Self {
198 Self::default()
199 }
200
201 pub fn add(&mut self, artifact: Artifact) {
203 self.artifacts.push(artifact);
204 }
205
206 pub fn compute_total_size(&mut self) -> i64 {
208 let total: i64 = self.artifacts.iter().map(|a| {
209 a.size_bytes.unwrap_or_else(|| a.content.size_bytes() as i64)
210 }).sum();
211 self.total_size_bytes = Some(total);
212 total
213 }
214
215 pub fn get_by_id(&self, id: &str) -> Option<&Artifact> {
217 self.artifacts.iter().find(|a| a.artifact_id.as_deref() == Some(id))
218 }
219
220 pub fn get_by_name(&self, name: &str) -> Option<&Artifact> {
222 self.artifacts.iter().find(|a| a.name.as_deref() == Some(name))
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_text_artifact() {
232 let artifact = Artifact::text("Hello, world!")
233 .with_name("greeting")
234 .with_id("art-001");
235
236 assert_eq!(artifact.content.as_text(), Some("Hello, world!"));
237 assert_eq!(artifact.name, Some("greeting".to_string()));
238 assert_eq!(artifact.content_type, Some("text/plain".to_string()));
239 }
240
241 #[test]
242 fn test_json_artifact() {
243 let mut data = HashMap::new();
244 data.insert("key".to_string(), serde_json::json!("value"));
245
246 let artifact = Artifact::json(data);
247
248 assert!(artifact.content.as_structured().is_some());
249 assert_eq!(artifact.content_type, Some("application/json".to_string()));
250 }
251
252 #[test]
253 fn test_size_validation() {
254 let artifact = Artifact::text("x".repeat(1000));
255
256 assert!(artifact.validate_size(2000).is_ok());
257 assert!(artifact.validate_size(500).is_err());
258 }
259
260 #[test]
261 fn test_artifact_bundle() {
262 let mut bundle = ArtifactBundle::new();
263 bundle.add(Artifact::text("First").with_name("first"));
264 bundle.add(Artifact::text("Second").with_name("second"));
265
266 assert_eq!(bundle.artifacts.len(), 2);
267 assert!(bundle.get_by_name("first").is_some());
268 }
269
270 #[test]
271 fn test_serde() {
272 let artifact = Artifact::text("test content")
273 .with_id("test-id");
274
275 let json = serde_json::to_string(&artifact).unwrap();
276 let parsed: Artifact = serde_json::from_str(&json).unwrap();
277
278 assert_eq!(parsed.content.as_text(), Some("test content"));
279 assert_eq!(parsed.artifact_id, Some("test-id".to_string()));
280 }
281}