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 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 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 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 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 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 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 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 let content = std::fs::read(local_path)?;
102
103 form = form.text("path", relative_path.to_string_lossy().to_string());
105
106 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}