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
160 .size_bytes
161 .unwrap_or_else(|| self.content.size_bytes() as i64);
162 if size > max_size_bytes {
163 return Err(format!(
164 "Artifact size {} bytes exceeds maximum {} bytes",
165 size, max_size_bytes
166 ));
167 }
168 Ok(())
169 }
170
171 pub fn compute_size(&mut self) {
173 self.size_bytes = Some(self.content.size_bytes() as i64);
174 }
175}
176
177impl Default for Artifact {
178 fn default() -> Self {
179 Self::text("")
180 }
181}
182
183#[derive(Debug, Clone, Default, Serialize, Deserialize)]
185pub struct ArtifactBundle {
186 #[serde(default)]
188 pub artifacts: Vec<Artifact>,
189 #[serde(default)]
191 pub total_size_bytes: Option<i64>,
192 #[serde(default)]
194 pub metadata: HashMap<String, Value>,
195}
196
197impl ArtifactBundle {
198 pub fn new() -> Self {
200 Self::default()
201 }
202
203 pub fn add(&mut self, artifact: Artifact) {
205 self.artifacts.push(artifact);
206 }
207
208 pub fn compute_total_size(&mut self) -> i64 {
210 let total: i64 = self
211 .artifacts
212 .iter()
213 .map(|a| {
214 a.size_bytes
215 .unwrap_or_else(|| a.content.size_bytes() as i64)
216 })
217 .sum();
218 self.total_size_bytes = Some(total);
219 total
220 }
221
222 pub fn get_by_id(&self, id: &str) -> Option<&Artifact> {
224 self.artifacts
225 .iter()
226 .find(|a| a.artifact_id.as_deref() == Some(id))
227 }
228
229 pub fn get_by_name(&self, name: &str) -> Option<&Artifact> {
231 self.artifacts
232 .iter()
233 .find(|a| a.name.as_deref() == Some(name))
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn test_text_artifact() {
243 let artifact = Artifact::text("Hello, world!")
244 .with_name("greeting")
245 .with_id("art-001");
246
247 assert_eq!(artifact.content.as_text(), Some("Hello, world!"));
248 assert_eq!(artifact.name, Some("greeting".to_string()));
249 assert_eq!(artifact.content_type, Some("text/plain".to_string()));
250 }
251
252 #[test]
253 fn test_json_artifact() {
254 let mut data = HashMap::new();
255 data.insert("key".to_string(), serde_json::json!("value"));
256
257 let artifact = Artifact::json(data);
258
259 assert!(artifact.content.as_structured().is_some());
260 assert_eq!(artifact.content_type, Some("application/json".to_string()));
261 }
262
263 #[test]
264 fn test_size_validation() {
265 let artifact = Artifact::text("x".repeat(1000));
266
267 assert!(artifact.validate_size(2000).is_ok());
268 assert!(artifact.validate_size(500).is_err());
269 }
270
271 #[test]
272 fn test_artifact_bundle() {
273 let mut bundle = ArtifactBundle::new();
274 bundle.add(Artifact::text("First").with_name("first"));
275 bundle.add(Artifact::text("Second").with_name("second"));
276
277 assert_eq!(bundle.artifacts.len(), 2);
278 assert!(bundle.get_by_name("first").is_some());
279 }
280
281 #[test]
282 fn test_serde() {
283 let artifact = Artifact::text("test content").with_id("test-id");
284
285 let json = serde_json::to_string(&artifact).unwrap();
286 let parsed: Artifact = serde_json::from_str(&json).unwrap();
287
288 assert_eq!(parsed.content.as_text(), Some("test content"));
289 assert_eq!(parsed.artifact_id, Some("test-id".to_string()));
290 }
291}