1pub 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
13pub 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#[derive(Debug, Clone)]
24pub struct ClientConfig {
25 pub socket_path: Option<String>,
27 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 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 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 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
73pub 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 pub fn new() -> Self {
100 Self {
101 config: ClientConfig::default(),
102 }
103 }
104
105 pub fn with_config(config: ClientConfig) -> Self {
107 Self { config }
108 }
109
110 pub fn from_env() -> Self {
112 Self {
113 config: ClientConfig::from_env(),
114 }
115 }
116
117 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 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 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 let mut response = Vec::new();
148 stream.read_to_end(&mut response).await?;
149
150 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 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 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 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 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 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}