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