1use crate::client::SynapClient;
4use crate::error::Result;
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum DistanceUnit {
11 Meters,
12 Kilometers,
13 Miles,
14 Feet,
15}
16
17impl DistanceUnit {
18 fn as_str(&self) -> &'static str {
19 match self {
20 DistanceUnit::Meters => "m",
21 DistanceUnit::Kilometers => "km",
22 DistanceUnit::Miles => "mi",
23 DistanceUnit::Feet => "ft",
24 }
25 }
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Location {
31 pub lat: f64,
32 pub lon: f64,
33 pub member: String,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Coordinate {
39 pub lat: f64,
40 pub lon: f64,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct GeoradiusResult {
46 pub member: String,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub distance: Option<f64>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub coord: Option<Coordinate>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, Default)]
55pub struct GeospatialStats {
56 pub total_keys: usize,
57 pub total_locations: usize,
58 pub geoadd_count: usize,
59 pub geodist_count: usize,
60 pub georadius_count: usize,
61 pub geopos_count: usize,
62 pub geohash_count: usize,
63}
64
65#[derive(Clone)]
66pub struct GeospatialManager {
67 client: SynapClient,
68}
69
70impl GeospatialManager {
71 pub(crate) fn new(client: SynapClient) -> Self {
72 Self { client }
73 }
74
75 pub async fn geoadd(
89 &self,
90 key: &str,
91 locations: Vec<Location>,
92 nx: bool,
93 xx: bool,
94 ch: bool,
95 ) -> Result<usize> {
96 for loc in &locations {
98 if !(-90.0..=90.0).contains(&loc.lat) {
99 return Err(crate::error::SynapError::ServerError(format!(
100 "Latitude must be between -90 and 90, got: {}",
101 loc.lat
102 )));
103 }
104 if !(-180.0..=180.0).contains(&loc.lon) {
105 return Err(crate::error::SynapError::ServerError(format!(
106 "Longitude must be between -180 and 180, got: {}",
107 loc.lon
108 )));
109 }
110 }
111
112 let payload = json!({
113 "key": key,
114 "locations": locations,
115 "nx": nx,
116 "xx": xx,
117 "ch": ch,
118 });
119
120 let response = self
121 .client
122 .send_command("geospatial.geoadd", payload)
123 .await?;
124 Ok(response["added"].as_u64().unwrap_or(0) as usize)
125 }
126
127 pub async fn geodist(
140 &self,
141 key: &str,
142 member1: &str,
143 member2: &str,
144 unit: DistanceUnit,
145 ) -> Result<Option<f64>> {
146 let payload = json!({
147 "key": key,
148 "member1": member1,
149 "member2": member2,
150 "unit": unit.as_str(),
151 });
152
153 let response = self
154 .client
155 .send_command("geospatial.geodist", payload)
156 .await?;
157 let distance = response["distance"].as_f64();
158 Ok(distance)
159 }
160
161 #[allow(clippy::too_many_arguments)]
179 pub async fn georadius(
180 &self,
181 key: &str,
182 center_lat: f64,
183 center_lon: f64,
184 radius: f64,
185 unit: DistanceUnit,
186 with_dist: bool,
187 with_coord: bool,
188 count: Option<usize>,
189 sort: Option<&str>,
190 ) -> Result<Vec<GeoradiusResult>> {
191 if !(-90.0..=90.0).contains(¢er_lat) {
192 return Err(crate::error::SynapError::ServerError(format!(
193 "Latitude must be between -90 and 90, got: {}",
194 center_lat
195 )));
196 }
197 if !(-180.0..=180.0).contains(¢er_lon) {
198 return Err(crate::error::SynapError::ServerError(format!(
199 "Longitude must be between -180 and 180, got: {}",
200 center_lon
201 )));
202 }
203
204 let mut payload = json!({
205 "key": key,
206 "center_lat": center_lat,
207 "center_lon": center_lon,
208 "radius": radius,
209 "unit": unit.as_str(),
210 "with_dist": with_dist,
211 "with_coord": with_coord,
212 });
213
214 if let Some(c) = count {
215 payload["count"] = json!(c);
216 }
217 if let Some(s) = sort {
218 payload["sort"] = json!(s);
219 }
220
221 let response = self
222 .client
223 .send_command("geospatial.georadius", payload)
224 .await?;
225 let results: Vec<GeoradiusResult> =
226 serde_json::from_value(response["results"].clone()).unwrap_or_default();
227 Ok(results)
228 }
229
230 #[allow(clippy::too_many_arguments)]
247 pub async fn georadiusbymember(
248 &self,
249 key: &str,
250 member: &str,
251 radius: f64,
252 unit: DistanceUnit,
253 with_dist: bool,
254 with_coord: bool,
255 count: Option<usize>,
256 sort: Option<&str>,
257 ) -> Result<Vec<GeoradiusResult>> {
258 let mut payload = json!({
259 "key": key,
260 "member": member,
261 "radius": radius,
262 "unit": unit.as_str(),
263 "with_dist": with_dist,
264 "with_coord": with_coord,
265 });
266
267 if let Some(c) = count {
268 payload["count"] = json!(c);
269 }
270 if let Some(s) = sort {
271 payload["sort"] = json!(s);
272 }
273
274 let response = self
275 .client
276 .send_command("geospatial.georadiusbymember", payload)
277 .await?;
278 let results: Vec<GeoradiusResult> =
279 serde_json::from_value(response["results"].clone()).unwrap_or_default();
280 Ok(results)
281 }
282
283 pub async fn geopos(&self, key: &str, members: &[String]) -> Result<Vec<Option<Coordinate>>> {
294 let payload = json!({
295 "key": key,
296 "members": members,
297 });
298
299 let response = self
300 .client
301 .send_command("geospatial.geopos", payload)
302 .await?;
303 let coords: Vec<Option<Coordinate>> =
304 serde_json::from_value(response["coordinates"].clone()).unwrap_or_default();
305 Ok(coords)
306 }
307
308 #[allow(clippy::too_many_arguments)]
327 pub async fn geosearch(
328 &self,
329 key: &str,
330 from_member: Option<&str>,
331 from_lonlat: Option<(f64, f64)>,
332 by_radius: Option<(f64, DistanceUnit)>,
333 by_box: Option<(f64, f64, DistanceUnit)>,
334 with_dist: bool,
335 with_coord: bool,
336 with_hash: bool,
337 count: Option<usize>,
338 sort: Option<&str>,
339 ) -> Result<Vec<GeoradiusResult>> {
340 if from_member.is_none() && from_lonlat.is_none() {
341 return Err(crate::error::SynapError::ServerError(
342 "Either 'from_member' or 'from_lonlat' must be provided".to_string(),
343 ));
344 }
345 if by_radius.is_none() && by_box.is_none() {
346 return Err(crate::error::SynapError::ServerError(
347 "Either 'by_radius' or 'by_box' must be provided".to_string(),
348 ));
349 }
350
351 let mut payload = json!({
352 "key": key,
353 "with_dist": with_dist,
354 "with_coord": with_coord,
355 "with_hash": with_hash,
356 });
357
358 if let Some(member) = from_member {
359 payload["from_member"] = json!(member);
360 }
361 if let Some((lon, lat)) = from_lonlat {
362 payload["from_lonlat"] = json!([lon, lat]);
363 }
364 if let Some((radius, unit)) = by_radius {
365 payload["by_radius"] = json!([radius, unit.as_str()]);
366 }
367 if let Some((width, height, unit)) = by_box {
368 payload["by_box"] = json!([width, height, unit.as_str()]);
369 }
370 if let Some(c) = count {
371 payload["count"] = json!(c);
372 }
373 if let Some(s) = sort {
374 payload["sort"] = json!(s);
375 }
376
377 let response = self
378 .client
379 .send_command("geospatial.geosearch", payload)
380 .await?;
381 let results: Vec<GeoradiusResult> =
382 serde_json::from_value(response["results"].clone()).unwrap_or_default();
383 Ok(results)
384 }
385
386 pub async fn geohash(&self, key: &str, members: &[String]) -> Result<Vec<Option<String>>> {
397 let payload = json!({
398 "key": key,
399 "members": members,
400 });
401
402 let response = self
403 .client
404 .send_command("geospatial.geohash", payload)
405 .await?;
406 let geohashes: Vec<Option<String>> =
407 serde_json::from_value(response["geohashes"].clone()).unwrap_or_default();
408 Ok(geohashes)
409 }
410
411 pub async fn stats(&self) -> Result<GeospatialStats> {
413 let payload = json!({});
414 let response = self
415 .client
416 .send_command("geospatial.stats", payload)
417 .await?;
418 let stats: GeospatialStats = serde_json::from_value(response).unwrap_or_default();
419 Ok(stats)
420 }
421}