Skip to main content

capsula_client/
lib.rs

1use capsula_api_types::{UploadResponse, VaultExistsResponse, VaultInfo, VaultsResponse};
2use reqwest::blocking::multipart;
3use serde_json::Value as JsonValue;
4use std::path::Path;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8pub enum ClientError {
9    #[error("HTTP request failed: {0}")]
10    Request(#[from] reqwest::Error),
11
12    #[error("IO error: {0}")]
13    Io(#[from] std::io::Error),
14
15    #[error("JSON serialization error: {0}")]
16    Json(#[from] serde_json::Error),
17
18    #[error("Server returned error: {0}")]
19    ServerError(String),
20}
21
22pub type Result<T> = std::result::Result<T, ClientError>;
23
24#[derive(Debug, Clone)]
25pub struct CapsulaClient {
26    base_url: String,
27    client: reqwest::blocking::Client,
28}
29
30impl CapsulaClient {
31    /// Create a new Capsula client
32    pub fn new(base_url: impl Into<String>) -> Self {
33        Self {
34            base_url: base_url.into(),
35            client: reqwest::blocking::Client::new(),
36        }
37    }
38
39    /// List all vaults on the server
40    pub fn list_vaults(&self) -> Result<Vec<VaultInfo>> {
41        let url = format!("{}/api/v1/vaults", self.base_url);
42        let response = self.client.get(&url).send()?;
43
44        if !response.status().is_success() {
45            return Err(ClientError::ServerError(format!(
46                "Failed to list vaults: {}",
47                response.status()
48            )));
49        }
50
51        let vaults_response: VaultsResponse = response.json()?;
52        Ok(vaults_response.vaults)
53    }
54
55    /// Check if a vault exists on the server
56    pub fn vault_exists(&self, vault_name: &str) -> Result<Option<VaultInfo>> {
57        let url = format!("{}/api/v1/vaults/{}", self.base_url, vault_name);
58        let response = self.client.get(&url).send()?;
59
60        if !response.status().is_success() {
61            return Err(ClientError::ServerError(format!(
62                "Failed to check vault: {}",
63                response.status()
64            )));
65        }
66
67        let vault_response: VaultExistsResponse = response.json()?;
68        Ok(vault_response.vault)
69    }
70
71    /// Upload a run's data and files to the server
72    pub fn upload_run(
73        &self,
74        run_id: &str,
75        files: &[(impl AsRef<Path>, impl AsRef<Path>)],
76        pre_run_hooks: Option<Vec<JsonValue>>,
77        post_run_hooks: Option<Vec<JsonValue>>,
78    ) -> Result<UploadResponse> {
79        let url = format!("{}/api/v1/upload", self.base_url);
80
81        let mut form = multipart::Form::new().text("run_id", run_id.to_string());
82
83        // Add pre-run hooks if provided
84        if let Some(hooks) = pre_run_hooks {
85            let hooks_json = serde_json::to_string(&hooks)?;
86            form = form.text("pre_run", hooks_json);
87        }
88
89        // Add post-run hooks if provided
90        if let Some(hooks) = post_run_hooks {
91            let hooks_json = serde_json::to_string(&hooks)?;
92            form = form.text("post_run", hooks_json);
93        }
94
95        // Add files
96        for (local_path, relative_path) in files {
97            let local_path = local_path.as_ref();
98            let relative_path = relative_path.as_ref();
99
100            // Read file content
101            let content = std::fs::read(local_path)?;
102
103            // Add path field
104            form = form.text("path", relative_path.to_string_lossy().to_string());
105
106            // Add file part
107            let file_name = local_path
108                .file_name()
109                .and_then(|n| n.to_str())
110                .unwrap_or("file");
111
112            let part = multipart::Part::bytes(content).file_name(file_name.to_string());
113
114            form = form.part("file", part);
115        }
116
117        let response = self.client.post(&url).multipart(form).send()?;
118
119        if !response.status().is_success() {
120            return Err(ClientError::ServerError(format!(
121                "Upload failed: {}",
122                response.status()
123            )));
124        }
125
126        let upload_response: UploadResponse = response.json()?;
127        Ok(upload_response)
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_client_creation() {
137        let client = CapsulaClient::new("http://localhost:8500");
138        assert_eq!(client.base_url, "http://localhost:8500");
139    }
140}