mappy_client/
lib.rs

1//! # Mappy Client
2//!
3//! Client library for mappy maplet data structures with network support.
4
5pub use mappy_core::*;
6
7use anyhow::{Context, Result};
8use serde::{Deserialize, Serialize};
9use std::path::Path;
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11use tokio::net::UnixStream;
12
13/// Re-export commonly used types for convenience
14pub mod prelude {
15    pub use mappy_core::types::MapletConfig;
16    pub use mappy_core::{
17        CounterOperator, Maplet, MapletError, MapletResult, MapletStats, MaxOperator, MinOperator,
18        SetOperator,
19    };
20}
21
22/// Client configuration
23#[derive(Debug, Clone)]
24pub struct ClientConfig {
25    /// Unix socket path (default: /var/run/reynard/mappy.sock)
26    pub socket_path: Option<String>,
27    /// HTTP URL (e.g., http://localhost:8003)
28    pub http_url: Option<String>,
29}
30
31impl Default for ClientConfig {
32    fn default() -> Self {
33        Self {
34            socket_path: Some("/var/run/reynard/mappy.sock".to_string()),
35            http_url: None,
36        }
37    }
38}
39
40impl ClientConfig {
41    /// Create config from environment variables
42    pub fn from_env() -> Self {
43        let socket_path = std::env::var("MAPPY_SOCKET_PATH")
44            .ok()
45            .filter(|s| !s.is_empty());
46        let http_url = std::env::var("MAPPY_HTTP_URL")
47            .ok()
48            .filter(|s| !s.is_empty());
49
50        Self {
51            socket_path: socket_path.or_else(|| Some("/var/run/reynard/mappy.sock".to_string())),
52            http_url,
53        }
54    }
55
56    /// Use Unix socket
57    pub fn with_socket<P: AsRef<Path>>(path: P) -> Self {
58        Self {
59            socket_path: Some(path.as_ref().to_string_lossy().to_string()),
60            http_url: None,
61        }
62    }
63
64    /// Use HTTP
65    pub fn with_http(url: impl Into<String>) -> Self {
66        Self {
67            socket_path: None,
68            http_url: Some(url.into()),
69        }
70    }
71}
72
73/// Network client for Mappy server
74pub struct Client {
75    config: ClientConfig,
76}
77
78#[derive(Serialize)]
79struct SetRequest {
80    key: String,
81    value: String,
82}
83
84#[derive(Deserialize)]
85struct GetResponse {
86    key: String,
87    value: Option<String>,
88    found: bool,
89}
90
91#[derive(Deserialize)]
92struct HealthResponse {
93    status: String,
94    service: String,
95}
96
97impl Client {
98    /// Create a new client with default configuration
99    pub fn new() -> Self {
100        Self {
101            config: ClientConfig::default(),
102        }
103    }
104
105    /// Create a new client with custom configuration
106    pub fn with_config(config: ClientConfig) -> Self {
107        Self { config }
108    }
109
110    /// Create a client from environment variables
111    pub fn from_env() -> Self {
112        Self {
113            config: ClientConfig::from_env(),
114        }
115    }
116
117    /// Send a request via Unix socket
118    async fn request_unix(&self, method: &str, path: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
119        let socket_path = self
120            .config
121            .socket_path
122            .as_ref()
123            .ok_or_else(|| anyhow::anyhow!("Unix socket path not configured"))?;
124
125        let mut stream = UnixStream::connect(socket_path)
126            .await
127            .with_context(|| format!("Failed to connect to Unix socket: {}", socket_path))?;
128
129        // Build HTTP request
130        let mut request = format!("{} {} HTTP/1.1\r\n", method, path);
131        request.push_str("Host: localhost\r\n");
132        request.push_str("Content-Type: application/json\r\n");
133
134        if let Some(body) = body {
135            request.push_str(&format!("Content-Length: {}\r\n", body.len()));
136        }
137        request.push_str("\r\n");
138
139        // Send request
140        stream.write_all(request.as_bytes()).await?;
141        if let Some(body) = body {
142            stream.write_all(body).await?;
143        }
144        stream.flush().await?;
145
146        // Read response
147        let mut response = Vec::new();
148        stream.read_to_end(&mut response).await?;
149
150        // Parse HTTP response (simple parser)
151        let response_str = String::from_utf8_lossy(&response);
152        let body_start = response_str.find("\r\n\r\n").map(|i| i + 4).unwrap_or(0);
153
154        Ok(response[body_start..].to_vec())
155    }
156
157    /// Send a request via HTTP
158    async fn request_http(&self, method: &str, path: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
159        let url = self
160            .config
161            .http_url
162            .as_ref()
163            .ok_or_else(|| anyhow::anyhow!("HTTP URL not configured"))?;
164
165        let client = reqwest::Client::new();
166        let url = format!("{}{}", url.trim_end_matches('/'), path);
167
168        let response = match method {
169            "GET" => client.get(&url).send().await?,
170            "POST" => {
171                let mut req = client.post(&url);
172                if let Some(body) = body {
173                    req = req.body(body.to_vec());
174                }
175                req.send().await?
176            }
177            _ => return Err(anyhow::anyhow!("Unsupported method: {}", method)),
178        };
179
180        let bytes = response.bytes().await?;
181        Ok(bytes.to_vec())
182    }
183
184    /// Send a request (auto-detects socket vs HTTP)
185    async fn request(&self, method: &str, path: &str, body: Option<&[u8]>) -> Result<Vec<u8>> {
186        if self.config.socket_path.is_some() {
187            self.request_unix(method, path, body).await
188        } else if self.config.http_url.is_some() {
189            self.request_http(method, path, body).await
190        } else {
191            Err(anyhow::anyhow!("No connection method configured"))
192        }
193    }
194
195    /// Check server health
196    pub async fn health(&self) -> Result<HealthResponse> {
197        let response = self.request("GET", "/health", None).await?;
198        let health: HealthResponse = serde_json::from_slice(&response)?;
199        Ok(health)
200    }
201
202    /// Set a key-value pair
203    pub async fn set(&self, key: impl Into<String>, value: impl Into<String>) -> Result<()> {
204        let request = SetRequest {
205            key: key.into(),
206            value: value.into(),
207        };
208        let body = serde_json::to_vec(&request)?;
209        self.request("POST", "/set", Some(&body)).await?;
210        Ok(())
211    }
212
213    /// Get a value by key
214    pub async fn get(&self, key: impl Into<String>) -> Result<Option<String>> {
215        let key = key.into();
216        let path = format!("/get/{}", urlencoding::encode(&key));
217        let response = self.request("GET", &path, None).await?;
218        let get_response: GetResponse = serde_json::from_slice(&response)?;
219        Ok(get_response.value)
220    }
221}
222
223impl Default for Client {
224    fn default() -> Self {
225        Self::new()
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::prelude::*;
232
233    #[tokio::test]
234    async fn test_client_basic_usage() {
235        let maplet = Maplet::<String, u64, CounterOperator>::new(100, 0.01).unwrap();
236
237        maplet.insert("test".to_string(), 42).await.unwrap();
238        assert_eq!(maplet.query(&"test".to_string()).await, Some(42));
239    }
240}