Skip to main content

rs_adk/artifacts/
mod.rs

1//! Artifact service — versioned binary/JSON artifact storage.
2//!
3//! Mirrors ADK-JS's `BaseArtifactService`. Provides a trait for storing
4//! and retrieving versioned artifacts with an in-memory default.
5
6mod 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/// Metadata for a stored artifact.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ArtifactMetadata {
24    /// Artifact name/key.
25    pub name: String,
26    /// MIME type (e.g., "application/json", "image/png").
27    pub mime_type: String,
28    /// Current version number (1-based).
29    pub version: u32,
30    /// Size in bytes.
31    pub size: usize,
32    /// When created (Unix timestamp seconds).
33    pub created_at: u64,
34    /// When last updated (Unix timestamp seconds).
35    pub updated_at: u64,
36}
37
38/// A versioned artifact with data and metadata.
39#[derive(Debug, Clone)]
40pub struct Artifact {
41    /// Artifact metadata.
42    pub metadata: ArtifactMetadata,
43    /// The artifact data.
44    pub data: Vec<u8>,
45}
46
47impl Artifact {
48    /// Create a new artifact.
49    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    /// Create a JSON artifact.
66    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    /// Create a text artifact.
72    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/// Errors from artifact service operations.
78#[derive(Debug, thiserror::Error)]
79pub enum ArtifactError {
80    /// The requested artifact was not found.
81    #[error("Artifact not found: {0}")]
82    NotFound(String),
83    /// The requested version of the artifact was not found.
84    #[error("Version not found: {name} v{version}")]
85    VersionNotFound {
86        /// Artifact name.
87        name: String,
88        /// Requested version number.
89        version: u32,
90    },
91    /// A storage backend error.
92    #[error("Storage error: {0}")]
93    Storage(String),
94}
95
96/// Trait for artifact persistence — CRUD with versioning.
97///
98/// Artifacts are scoped by session ID and identified by name.
99/// Each update creates a new version.
100#[async_trait]
101pub trait ArtifactService: Send + Sync {
102    /// Save an artifact, creating a new version if it already exists.
103    async fn save(
104        &self,
105        session_id: &str,
106        artifact: Artifact,
107    ) -> Result<ArtifactMetadata, ArtifactError>;
108
109    /// Load the latest version of an artifact.
110    async fn load(&self, session_id: &str, name: &str) -> Result<Option<Artifact>, ArtifactError>;
111
112    /// Load a specific version of an artifact.
113    async fn load_version(
114        &self,
115        session_id: &str,
116        name: &str,
117        version: u32,
118    ) -> Result<Option<Artifact>, ArtifactError>;
119
120    /// List all artifact metadata for a session.
121    async fn list(&self, session_id: &str) -> Result<Vec<ArtifactMetadata>, ArtifactError>;
122
123    /// Delete all versions of an artifact.
124    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        // Latest should be v3
190        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        // Load specific version
195        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}