Skip to main content

kaish_client/
embedded.rs

1//! Embedded client for direct in-process kernel access.
2//!
3//! The `EmbeddedClient` wraps a `Kernel` instance and implements `KernelClient`,
4//! allowing direct access without network overhead. This is ideal for:
5//!
6//! - Embedding kaish in other Rust applications
7//! - Unit testing
8//! - Single-process use cases
9
10use std::path::{Path, PathBuf};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15use async_trait::async_trait;
16
17use kaish_kernel::ast::Value;
18use kaish_kernel::interpreter::ExecResult;
19use kaish_kernel::vfs::Filesystem;
20use kaish_kernel::{Kernel, KernelConfig};
21
22use crate::traits::{ClientError, ClientResult, KernelClient};
23
24/// Generate a unique blob ID.
25fn generate_blob_id() -> String {
26    static COUNTER: AtomicU64 = AtomicU64::new(0);
27
28    let timestamp = SystemTime::now()
29        .duration_since(UNIX_EPOCH)
30        .map(|d| d.as_nanos())
31        .unwrap_or(0);
32    let count = COUNTER.fetch_add(1, Ordering::SeqCst);
33
34    format!("{:x}-{:x}", timestamp, count)
35}
36
37/// A client that wraps a `Kernel` directly for in-process access.
38///
39/// # Example
40///
41/// ```ignore
42/// use kaish_client::EmbeddedClient;
43/// use kaish_kernel::{Kernel, KernelConfig};
44///
45/// let kernel = Kernel::new(KernelConfig::transient())?;
46/// let client = EmbeddedClient::new(kernel);
47///
48/// let result = client.execute("X=42").await?;
49/// assert!(result.ok());
50///
51/// let value = client.get_var("X").await?;
52/// assert_eq!(value, Some(Value::Int(42)));
53/// ```
54pub struct EmbeddedClient {
55    kernel: Arc<Kernel>,
56}
57
58impl EmbeddedClient {
59    /// Create a new embedded client wrapping the given kernel.
60    pub fn new(kernel: Kernel) -> Self {
61        Self {
62            kernel: Arc::new(kernel),
63        }
64    }
65
66    /// Create a new embedded client with a transient (non-persistent) kernel.
67    pub fn transient() -> ClientResult<Self> {
68        let kernel = Kernel::new(KernelConfig::transient())
69            .map_err(ClientError::Other)?;
70        Ok(Self::new(kernel))
71    }
72
73    /// Create a new embedded client with default configuration.
74    pub fn with_defaults() -> ClientResult<Self> {
75        let kernel = Kernel::new(KernelConfig::default())
76            .map_err(ClientError::Other)?;
77        Ok(Self::new(kernel))
78    }
79
80    /// Get a reference to the underlying kernel.
81    pub fn kernel(&self) -> &Kernel {
82        &self.kernel
83    }
84
85    /// Execute with a per-statement output callback.
86    ///
87    /// Each statement's result is passed to `on_output` as it completes.
88    /// External commands in interactive mode already stream via `Stdio::inherit()`;
89    /// this callback handles builtins and other captured output.
90    pub async fn execute_streaming(
91        &self,
92        input: &str,
93        on_output: &mut dyn FnMut(&ExecResult),
94    ) -> ClientResult<ExecResult> {
95        self.kernel
96            .execute_streaming(input, on_output)
97            .await
98            .map_err(|e| ClientError::Execution(e.to_string()))
99    }
100}
101
102#[async_trait(?Send)]
103impl KernelClient for EmbeddedClient {
104    async fn execute(&self, input: &str) -> ClientResult<ExecResult> {
105        self.kernel
106            .execute(input)
107            .await
108            .map_err(|e| ClientError::Execution(e.to_string()))
109    }
110
111    async fn get_var(&self, name: &str) -> ClientResult<Option<Value>> {
112        Ok(self.kernel.get_var(name).await)
113    }
114
115    async fn set_var(&self, name: &str, value: Value) -> ClientResult<()> {
116        self.kernel.set_var(name, value).await;
117        Ok(())
118    }
119
120    async fn list_vars(&self) -> ClientResult<Vec<(String, Value)>> {
121        Ok(self.kernel.list_vars().await)
122    }
123
124    async fn cwd(&self) -> ClientResult<String> {
125        Ok(self.kernel.cwd().await.to_string_lossy().to_string())
126    }
127
128    async fn set_cwd(&self, path: &str) -> ClientResult<()> {
129        self.kernel.set_cwd(PathBuf::from(path)).await;
130        Ok(())
131    }
132
133    async fn last_result(&self) -> ClientResult<ExecResult> {
134        Ok(self.kernel.last_result().await)
135    }
136
137    async fn reset(&self) -> ClientResult<()> {
138        self.kernel
139            .reset()
140            .await
141            .map_err(ClientError::Other)
142    }
143
144    async fn ping(&self) -> ClientResult<String> {
145        Ok("pong".to_string())
146    }
147
148    async fn shutdown(&self) -> ClientResult<()> {
149        // For embedded client, shutdown is a no-op since we don't own the kernel lifecycle
150        // The kernel will be dropped when the client is dropped
151        Ok(())
152    }
153
154    async fn read_blob(&self, id: &str) -> ClientResult<Vec<u8>> {
155        let vfs = self.kernel.vfs();
156        let path = PathBuf::from(format!("/v/blobs/{}", id));
157
158        vfs.read(&path)
159            .await
160            .map_err(ClientError::Io)
161    }
162
163    async fn write_blob(&self, content_type: &str, data: &[u8]) -> ClientResult<String> {
164        let vfs = self.kernel.vfs();
165        let id = generate_blob_id();
166        let path = PathBuf::from(format!("/v/blobs/{}", id));
167
168        // Ensure parent directory exists
169        let parent = Path::new("/v/blobs");
170        if let Err(e) = vfs.mkdir(parent).await {
171            // Ignore "already exists" errors
172            if e.kind() != std::io::ErrorKind::AlreadyExists {
173                tracing::warn!("Failed to create blob directory: {}", e);
174            }
175        }
176
177        // Store content type as metadata (could be extended later)
178        tracing::debug!("Creating blob {} with content type {}", id, content_type);
179
180        vfs.write(&path, data)
181            .await
182            .map_err(ClientError::Io)?;
183
184        Ok(id)
185    }
186
187    async fn delete_blob(&self, id: &str) -> ClientResult<bool> {
188        let vfs = self.kernel.vfs();
189        let path = PathBuf::from(format!("/v/blobs/{}", id));
190
191        match vfs.remove(&path).await {
192            Ok(()) => Ok(true),
193            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
194            Err(e) => Err(ClientError::Io(e)),
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202
203    #[tokio::test]
204    async fn test_embedded_transient() {
205        let client = EmbeddedClient::transient().expect("failed to create client");
206        let result = client.ping().await.expect("ping failed");
207        assert_eq!(result, "pong");
208    }
209
210    #[tokio::test]
211    async fn test_embedded_execute() {
212        let client = EmbeddedClient::transient().expect("failed to create client");
213        let result = client.execute("echo hello").await.expect("execute failed");
214        assert!(result.ok());
215        assert_eq!(result.out.trim(), "hello");
216    }
217
218    #[tokio::test]
219    async fn test_embedded_variables() {
220        let client = EmbeddedClient::transient().expect("failed to create client");
221
222        // Set via execute
223        client.execute("X=42").await.expect("set failed");
224        let value = client.get_var("X").await.expect("get failed");
225        assert_eq!(value, Some(Value::Int(42)));
226
227        // Set via API
228        client.set_var("Y", Value::String("hello".into())).await.expect("set_var failed");
229        let value = client.get_var("Y").await.expect("get failed");
230        assert_eq!(value, Some(Value::String("hello".into())));
231
232        // List vars
233        let vars = client.list_vars().await.expect("list failed");
234        assert!(vars.iter().any(|(n, _)| n == "X"));
235        assert!(vars.iter().any(|(n, _)| n == "Y"));
236    }
237
238    #[tokio::test]
239    async fn test_embedded_cwd() {
240        let client = EmbeddedClient::transient().expect("failed to create client");
241
242        // Transient kernel uses sandboxed mode with cwd=$HOME
243        let cwd = client.cwd().await.expect("cwd failed");
244        let home = std::env::var("HOME").unwrap_or_else(|_| "/".to_string());
245        assert_eq!(cwd, home);
246
247        client.set_cwd("/tmp").await.expect("set_cwd failed");
248        let cwd = client.cwd().await.expect("cwd failed");
249        assert_eq!(cwd, "/tmp");
250    }
251
252    #[tokio::test]
253    async fn test_embedded_reset() {
254        let client = EmbeddedClient::transient().expect("failed to create client");
255
256        client.execute("X=1").await.expect("set failed");
257        assert!(client.get_var("X").await.expect("get failed").is_some());
258
259        client.reset().await.expect("reset failed");
260        assert!(client.get_var("X").await.expect("get failed").is_none());
261    }
262
263    #[tokio::test]
264    async fn test_embedded_last_result() {
265        let client = EmbeddedClient::transient().expect("failed to create client");
266
267        client.execute("echo test").await.expect("execute failed");
268        let last = client.last_result().await.expect("last_result failed");
269        assert!(last.ok());
270        assert_eq!(last.out.trim(), "test");
271    }
272
273    #[tokio::test]
274    async fn test_embedded_blob_write_read() {
275        let client = EmbeddedClient::transient().expect("failed to create client");
276
277        let data = b"hello blob world!";
278        let id = client.write_blob("text/plain", data).await.expect("write_blob failed");
279
280        assert!(!id.is_empty(), "blob id should not be empty");
281
282        let read_data = client.read_blob(&id).await.expect("read_blob failed");
283        assert_eq!(read_data, data);
284    }
285
286    #[tokio::test]
287    async fn test_embedded_blob_delete() {
288        let client = EmbeddedClient::transient().expect("failed to create client");
289
290        let data = b"blob to delete";
291        let id = client.write_blob("application/octet-stream", data).await.expect("write_blob failed");
292
293        // Verify it exists
294        let read_data = client.read_blob(&id).await.expect("read_blob failed");
295        assert_eq!(read_data, data);
296
297        // Delete it
298        let deleted = client.delete_blob(&id).await.expect("delete_blob failed");
299        assert!(deleted, "blob should have been deleted");
300
301        // Verify it's gone
302        let result = client.read_blob(&id).await;
303        assert!(result.is_err(), "blob should not exist after deletion");
304    }
305
306    #[tokio::test]
307    async fn test_embedded_blob_delete_nonexistent() {
308        let client = EmbeddedClient::transient().expect("failed to create client");
309
310        let deleted = client.delete_blob("nonexistent-blob-id").await.expect("delete_blob failed");
311        assert!(!deleted, "deleting nonexistent blob should return false");
312    }
313
314    #[tokio::test]
315    async fn test_embedded_blob_large_data() {
316        let client = EmbeddedClient::transient().expect("failed to create client");
317
318        // Create 1MB of data
319        let data: Vec<u8> = (0..1024 * 1024).map(|i| (i % 256) as u8).collect();
320        let id = client.write_blob("application/octet-stream", &data).await.expect("write_blob failed");
321
322        let read_data = client.read_blob(&id).await.expect("read_blob failed");
323        assert_eq!(read_data.len(), data.len());
324        assert_eq!(read_data, data);
325    }
326}