1mod file_service;
7mod forwarding;
8#[cfg(feature = "gcs-artifacts")]
9mod gcs_service;
10mod in_memory;
11
12pub use file_service::FileArtifactService;
13pub use forwarding::ForwardingArtifactService;
14#[cfg(feature = "gcs-artifacts")]
15pub use gcs_service::GcsArtifactService;
16pub use in_memory::InMemoryArtifactService;
17
18use async_trait::async_trait;
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ArtifactMetadata {
24 pub name: String,
26 pub mime_type: String,
28 pub version: u32,
30 pub size: usize,
32 pub created_at: u64,
34 pub updated_at: u64,
36}
37
38#[derive(Debug, Clone)]
40pub struct Artifact {
41 pub metadata: ArtifactMetadata,
43 pub data: Vec<u8>,
45}
46
47impl Artifact {
48 pub fn new(name: impl Into<String>, mime_type: impl Into<String>, data: Vec<u8>) -> Self {
50 let now = now_secs();
51 let size = data.len();
52 Self {
53 metadata: ArtifactMetadata {
54 name: name.into(),
55 mime_type: mime_type.into(),
56 version: 1,
57 size,
58 created_at: now,
59 updated_at: now,
60 },
61 data,
62 }
63 }
64
65 pub fn json(name: impl Into<String>, value: &serde_json::Value) -> Self {
67 let data = serde_json::to_vec(value).unwrap_or_default();
68 Self::new(name, "application/json", data)
69 }
70
71 pub fn text(name: impl Into<String>, text: impl Into<String>) -> Self {
73 Self::new(name, "text/plain", text.into().into_bytes())
74 }
75}
76
77#[derive(Debug, thiserror::Error)]
79pub enum ArtifactError {
80 #[error("Artifact not found: {0}")]
82 NotFound(String),
83 #[error("Version not found: {name} v{version}")]
85 VersionNotFound {
86 name: String,
88 version: u32,
90 },
91 #[error("Storage error: {0}")]
93 Storage(String),
94}
95
96#[async_trait]
101pub trait ArtifactService: Send + Sync {
102 async fn save(
104 &self,
105 session_id: &str,
106 artifact: Artifact,
107 ) -> Result<ArtifactMetadata, ArtifactError>;
108
109 async fn load(&self, session_id: &str, name: &str) -> Result<Option<Artifact>, ArtifactError>;
111
112 async fn load_version(
114 &self,
115 session_id: &str,
116 name: &str,
117 version: u32,
118 ) -> Result<Option<Artifact>, ArtifactError>;
119
120 async fn list(&self, session_id: &str) -> Result<Vec<ArtifactMetadata>, ArtifactError>;
122
123 async fn delete(&self, session_id: &str, name: &str) -> Result<(), ArtifactError>;
125}
126
127pub(crate) fn now_secs() -> u64 {
128 std::time::SystemTime::now()
129 .duration_since(std::time::UNIX_EPOCH)
130 .unwrap_or_default()
131 .as_secs()
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn artifact_new() {
140 let a = Artifact::new("file.bin", "application/octet-stream", vec![1, 2, 3]);
141 assert_eq!(a.metadata.name, "file.bin");
142 assert_eq!(a.metadata.mime_type, "application/octet-stream");
143 assert_eq!(a.metadata.version, 1);
144 assert_eq!(a.metadata.size, 3);
145 assert_eq!(a.data, vec![1, 2, 3]);
146 }
147
148 #[test]
149 fn artifact_json() {
150 let val = serde_json::json!({"key": "value"});
151 let a = Artifact::json("config", &val);
152 assert_eq!(a.metadata.mime_type, "application/json");
153 let parsed: serde_json::Value = serde_json::from_slice(&a.data).unwrap();
154 assert_eq!(parsed["key"], "value");
155 }
156
157 #[test]
158 fn artifact_text() {
159 let a = Artifact::text("readme", "Hello, world!");
160 assert_eq!(a.metadata.mime_type, "text/plain");
161 assert_eq!(std::str::from_utf8(&a.data).unwrap(), "Hello, world!");
162 }
163
164 #[test]
165 fn artifact_service_is_object_safe() {
166 fn _assert(_: &dyn ArtifactService) {}
167 }
168
169 #[tokio::test]
170 async fn save_and_load() {
171 let svc = InMemoryArtifactService::new();
172 let artifact = Artifact::text("notes", "First version");
173 svc.save("s1", artifact).await.unwrap();
174
175 let loaded = svc.load("s1", "notes").await.unwrap();
176 assert!(loaded.is_some());
177 let loaded = loaded.unwrap();
178 assert_eq!(std::str::from_utf8(&loaded.data).unwrap(), "First version");
179 assert_eq!(loaded.metadata.version, 1);
180 }
181
182 #[tokio::test]
183 async fn versioning() {
184 let svc = InMemoryArtifactService::new();
185 svc.save("s1", Artifact::text("notes", "v1")).await.unwrap();
186 svc.save("s1", Artifact::text("notes", "v2")).await.unwrap();
187 svc.save("s1", Artifact::text("notes", "v3")).await.unwrap();
188
189 let latest = svc.load("s1", "notes").await.unwrap().unwrap();
191 assert_eq!(latest.metadata.version, 3);
192 assert_eq!(std::str::from_utf8(&latest.data).unwrap(), "v3");
193
194 let v1 = svc.load_version("s1", "notes", 1).await.unwrap().unwrap();
196 assert_eq!(std::str::from_utf8(&v1.data).unwrap(), "v1");
197
198 let v2 = svc.load_version("s1", "notes", 2).await.unwrap().unwrap();
199 assert_eq!(std::str::from_utf8(&v2.data).unwrap(), "v2");
200 }
201
202 #[tokio::test]
203 async fn load_nonexistent_returns_none() {
204 let svc = InMemoryArtifactService::new();
205 let result = svc.load("s1", "missing").await.unwrap();
206 assert!(result.is_none());
207 }
208
209 #[tokio::test]
210 async fn list_artifacts() {
211 let svc = InMemoryArtifactService::new();
212 svc.save("s1", Artifact::text("a", "data")).await.unwrap();
213 svc.save("s1", Artifact::text("b", "data")).await.unwrap();
214 svc.save("s2", Artifact::text("c", "data")).await.unwrap();
215
216 let list = svc.list("s1").await.unwrap();
217 assert_eq!(list.len(), 2);
218 }
219
220 #[tokio::test]
221 async fn delete_artifact() {
222 let svc = InMemoryArtifactService::new();
223 svc.save("s1", Artifact::text("notes", "data"))
224 .await
225 .unwrap();
226 svc.delete("s1", "notes").await.unwrap();
227 let result = svc.load("s1", "notes").await.unwrap();
228 assert!(result.is_none());
229 }
230}