Skip to main content

configvault_sdk/
client.rs

1use std::collections::HashMap;
2use std::time::Duration;
3
4use reqwest::header::{HeaderMap, HeaderValue};
5
6use crate::errors::{handle_error_response, ConfigVaultError};
7use crate::models::{ConfigListResponse, ConfigResponse, HealthResponse, SyncResponse};
8use crate::watcher::ConfigWatcher;
9
10/// Async HTTP client for the ConfigVault API.
11pub struct ConfigVaultClient {
12    base_url: String,
13    api_key: String,
14    http: reqwest::Client,
15    timeout: Duration,
16}
17
18impl ConfigVaultClient {
19    /// Create a new client with the default 30-second timeout.
20    pub fn new(base_url: &str, api_key: &str) -> Self {
21        Self::with_timeout(base_url, api_key, Duration::from_secs(30))
22    }
23
24    /// Create a new client with a custom timeout.
25    pub fn with_timeout(base_url: &str, api_key: &str, timeout: Duration) -> Self {
26        let mut headers = HeaderMap::new();
27        headers.insert(
28            "X-Api-Key",
29            HeaderValue::from_str(api_key).expect("API key must be a valid header value"),
30        );
31
32        let http = reqwest::Client::builder()
33            .default_headers(headers)
34            .timeout(timeout)
35            .use_rustls_tls()
36            .build()
37            .expect("Failed to build HTTP client");
38
39        Self {
40            base_url: base_url.trim_end_matches('/').to_string(),
41            api_key: api_key.to_string(),
42            http,
43            timeout,
44        }
45    }
46
47    /// Get a configuration value by key.
48    ///
49    /// Sends `GET /config/{key}` and returns the value string.
50    pub async fn get(&self, key: &str) -> Result<String, ConfigVaultError> {
51        let url = format!("{}/config/{}", self.base_url, key);
52        let response = self.http.get(&url).send().await?;
53
54        if !response.status().is_success() {
55            return Err(handle_error_response(
56                response.status().as_u16(),
57                Some(key),
58            ));
59        }
60
61        let data: ConfigResponse = response.json().await?;
62        Ok(data.value)
63    }
64
65    /// Check whether a configuration key exists.
66    ///
67    /// Sends `HEAD /config/{key}` and returns `true` on 200, `false` on 404.
68    pub async fn exists(&self, key: &str) -> Result<bool, ConfigVaultError> {
69        let url = format!("{}/config/{}", self.base_url, key);
70        let response = self.http.head(&url).send().await?;
71
72        let status = response.status().as_u16();
73        match status {
74            200 => Ok(true),
75            404 => Ok(false),
76            401 | 503 => Err(handle_error_response(status, None)),
77            _ => Err(handle_error_response(status, None)),
78        }
79    }
80
81    /// List all configurations under a namespace prefix.
82    ///
83    /// Sends `GET /config?prefix={namespace}` and returns a map of key → value.
84    pub async fn list(&self, namespace: &str) -> Result<HashMap<String, String>, ConfigVaultError> {
85        let url = reqwest::Url::parse_with_params(
86            &format!("{}/config", self.base_url),
87            &[("prefix", namespace)],
88        )
89        .map_err(|e| ConfigVaultError::Unexpected {
90            status: 0,
91            message: format!("Invalid URL: {e}"),
92        })?;
93        let response = self.http.get(url).send().await?;
94
95        if !response.status().is_success() {
96            return Err(handle_error_response(
97                response.status().as_u16(),
98                None,
99            ));
100        }
101
102        let data: ConfigListResponse = response.json().await?;
103        Ok(data.configs)
104    }
105
106    /// Trigger an immediate sync with the upstream Vaultwarden server.
107    ///
108    /// Sends `POST /sync` and instructs ConfigVault to pull the latest data
109    /// from Vaultwarden. After a successful sync the next read of any
110    /// configuration key will return the up-to-date value.
111    pub async fn sync(&self) -> Result<SyncResponse, ConfigVaultError> {
112        let url = format!("{}/sync", self.base_url);
113        let response = self.http.post(&url).send().await?;
114
115        if !response.status().is_success() {
116            return Err(handle_error_response(response.status().as_u16(), None));
117        }
118
119        let data: SyncResponse = response.json().await?;
120        Ok(data)
121    }
122
123    /// Check the health of the ConfigVault service.
124    ///
125    /// Sends `GET /health` and returns the health response.
126    pub async fn health(&self) -> Result<HealthResponse, ConfigVaultError> {
127        let url = format!("{}/health", self.base_url);
128        let response = self.http.get(&url).send().await?;
129
130        let data: HealthResponse = response.json().await?;
131        Ok(data)
132    }
133
134    /// Create a `ConfigWatcher` for SSE-based change notifications.
135    ///
136    /// The watcher connects to `GET /events?filter={filter}`.
137    pub fn watch(&self, filter: Option<&str>) -> ConfigWatcher {
138        ConfigWatcher::new(
139            &self.base_url,
140            &self.api_key,
141            filter,
142            self.timeout,
143        )
144    }
145}