1#![allow(clippy::too_many_arguments)]
5
6use crate::GlideError;
7use crate::ValkeyResult;
8use crate::cmd::Cmd;
9use crate::commands::options::{ExistenceCheck, OrderBy};
10use crate::executor::CommandExecutor;
11use crate::value::FromValkeyValue;
12use crate::value::ValkeyValue;
13use crate::value::to_glide_error;
14use crate::write::ToSingleValkeyArg;
15use crate::write::ToValkeyArgs;
16use crate::write::ValkeyWrite;
17use async_trait::async_trait;
18use bytes::Bytes;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum GeoUnit {
25 Meters,
27 Kilometers,
29 Miles,
31 Feet,
33}
34
35impl ToValkeyArgs for GeoUnit {
36 fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
37 out.write_arg(match self {
38 GeoUnit::Meters => b"m".as_slice(),
39 GeoUnit::Kilometers => b"km".as_slice(),
40 GeoUnit::Miles => b"mi".as_slice(),
41 GeoUnit::Feet => b"ft".as_slice(),
42 });
43 }
44}
45
46impl ToSingleValkeyArg for GeoUnit {}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct GeoCoord<T> {
53 pub longitude: T,
55 pub latitude: T,
57}
58
59impl<T> GeoCoord<T> {
60 pub fn lon_lat(longitude: T, latitude: T) -> Self {
62 Self {
63 longitude,
64 latitude,
65 }
66 }
67}
68
69impl<T: ToValkeyArgs> ToValkeyArgs for GeoCoord<T> {
70 fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
71 self.longitude.write_valkey_args(out);
72 self.latitude.write_valkey_args(out);
73 }
74}
75
76impl<T: FromValkeyValue> FromValkeyValue for GeoCoord<T> {
77 fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
78 match value {
79 ValkeyValue::Array(items) if items.len() == 2 => {
80 let mut items = items.into_iter();
81 let (Some(longitude), Some(latitude)) = (items.next(), items.next()) else {
82 unreachable!("checked length");
83 };
84 Ok(Self {
85 longitude: T::from_owned_valkey_value(longitude)?,
86 latitude: T::from_owned_valkey_value(latitude)?,
87 })
88 }
89 other => Err(to_glide_error(other, "Expected a pair of numbers.")),
90 }
91 }
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
98pub enum GeoSearchShape {
99 ByRadius {
101 radius: f64,
103 unit: GeoUnit,
105 },
106 ByBox {
108 width: f64,
110 height: f64,
112 unit: GeoUnit,
114 },
115}
116
117impl ToValkeyArgs for GeoSearchShape {
118 fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
119 match self {
120 GeoSearchShape::ByRadius { radius, unit } => {
121 out.write_arg(b"BYRADIUS");
122 radius.write_valkey_args(out);
123 unit.write_valkey_args(out);
124 }
125 GeoSearchShape::ByBox {
126 width,
127 height,
128 unit,
129 } => {
130 out.write_arg(b"BYBOX");
131 width.write_valkey_args(out);
132 height.write_valkey_args(out);
133 unit.write_valkey_args(out);
134 }
135 }
136 }
137}
138
139#[async_trait]
141pub trait GeoCommands: CommandExecutor {
142 async fn geosearch_by_radius_from_member<K: ToValkeyArgs + Send, M: ToValkeyArgs + Send>(
144 &self,
145 key: K,
146 member: M,
147 radius: f64,
148 unit: GeoUnit,
149 ) -> ValkeyResult<Vec<Bytes>> {
150 let mut cmd = Cmd::new();
151 cmd.arg("GEOSEARCH")
152 .arg(key)
153 .arg("FROMMEMBER")
154 .arg(member)
155 .arg("BYRADIUS")
156 .arg(radius)
157 .arg(unit);
158 match self.execute_command(cmd, None).await? {
159 ValkeyValue::Array(items) => items
160 .into_iter()
161 .map(Bytes::from_owned_valkey_value)
162 .collect(),
163 ValkeyValue::Nil => Ok(Vec::new()),
164 other => Ok(vec![Bytes::from_owned_valkey_value(other)?]),
165 }
166 }
167
168 async fn geoadd_options<K: ToValkeyArgs + Send, M: ToValkeyArgs + Send + Sync>(
171 &self,
172 key: K,
173 members: M,
174 existence_check: Option<ExistenceCheck>,
175 changed: bool,
176 ) -> ValkeyResult<i64> {
177 let mut cmd = Cmd::new();
178 cmd.arg("GEOADD").arg(key);
179 if let Some(c) = existence_check {
180 cmd.arg(c);
181 }
182 if changed {
183 cmd.arg("CH");
184 }
185 cmd.arg(members);
186 i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
187 }
188
189 async fn geosearch_from_member<K: ToValkeyArgs + Send, M: ToValkeyArgs + Send>(
192 &self,
193 key: K,
194 member: M,
195 shape: GeoSearchShape,
196 options: GeoSearchOptions,
197 ) -> ValkeyResult<Vec<GeoSearchResult>> {
198 let mut cmd = Cmd::new();
199 cmd.arg("GEOSEARCH")
200 .arg(key)
201 .arg("FROMMEMBER")
202 .arg(member)
203 .arg(shape)
204 .arg(options);
205 Vec::from_owned_valkey_value(self.execute_command(cmd, None).await?)
206 }
207
208 async fn geosearch_from_coord<K: ToValkeyArgs + Send>(
211 &self,
212 key: K,
213 coord: GeoCoord<f64>,
214 shape: GeoSearchShape,
215 options: GeoSearchOptions,
216 ) -> ValkeyResult<Vec<GeoSearchResult>> {
217 let mut cmd = Cmd::new();
218 cmd.arg("GEOSEARCH")
219 .arg(key)
220 .arg("FROMLONLAT")
221 .arg(coord.longitude)
222 .arg(coord.latitude)
223 .arg(shape)
224 .arg(options);
225 Vec::from_owned_valkey_value(self.execute_command(cmd, None).await?)
226 }
227
228 async fn geosearchstore_from_member<
231 D: ToValkeyArgs + Send,
232 S: ToValkeyArgs + Send,
233 M: ToValkeyArgs + Send,
234 >(
235 &self,
236 destination: D,
237 source: S,
238 member: M,
239 shape: GeoSearchShape,
240 options: GeoSearchStoreOptions,
241 ) -> ValkeyResult<i64> {
242 let mut cmd = Cmd::new();
243 cmd.arg("GEOSEARCHSTORE")
244 .arg(destination)
245 .arg(source)
246 .arg("FROMMEMBER")
247 .arg(member)
248 .arg(shape)
249 .arg(options);
250 i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
251 }
252
253 async fn geosearchstore_from_coord<D: ToValkeyArgs + Send, S: ToValkeyArgs + Send>(
256 &self,
257 destination: D,
258 source: S,
259 coord: GeoCoord<f64>,
260 shape: GeoSearchShape,
261 options: GeoSearchStoreOptions,
262 ) -> ValkeyResult<i64> {
263 let mut cmd = Cmd::new();
264 cmd.arg("GEOSEARCHSTORE")
265 .arg(destination)
266 .arg(source)
267 .arg("FROMLONLAT")
268 .arg(coord.longitude)
269 .arg(coord.latitude)
270 .arg(shape)
271 .arg(options);
272 i64::from_owned_valkey_value(self.execute_command(cmd, None).await?)
273 }
274}
275
276#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
278pub struct GeoSearchOptions {
279 pub order: Option<OrderBy>,
281 pub count: Option<i64>,
283 pub any: bool,
285 pub with_position: bool,
287 pub with_distance: bool,
289 pub with_hash: bool,
291}
292
293impl ToValkeyArgs for GeoSearchOptions {
294 fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
295 write_order_count_any(out, self.order, self.count, self.any);
296 if self.with_position {
297 out.write_arg(b"WITHCOORD");
298 }
299 if self.with_distance {
300 out.write_arg(b"WITHDIST");
301 }
302 if self.with_hash {
303 out.write_arg(b"WITHHASH");
304 }
305 }
306}
307
308#[derive(Debug, Clone, PartialEq)]
310pub struct GeoSearchResult {
311 pub member: Bytes,
313 pub position: Option<GeoCoord<f64>>,
315 pub distance: Option<f64>,
317 pub hash: Option<i64>,
319}
320
321impl FromValkeyValue for GeoSearchResult {
322 fn from_owned_valkey_value(value: ValkeyValue) -> ValkeyResult<Self> {
323 if let ValkeyValue::BulkString(member) = value {
325 return Ok(Self {
326 member,
327 position: None,
328 distance: None,
329 hash: None,
330 });
331 }
332
333 let ValkeyValue::Array(items) = value else {
334 return Err(to_glide_error(value, "Unexpected GEOSEARCH result."));
335 };
336
337 if items.len() != 2 {
339 return Err(to_glide_error(
340 ValkeyValue::Array(items),
341 "Unexpected GEOSEARCH result.",
342 ));
343 }
344 let mut items = items.into_iter();
345 let (Some(member), Some(ValkeyValue::Array(extras))) = (items.next(), items.next()) else {
346 return Err(GlideError::Request("Unexpected GEOSEARCH result.".into()));
347 };
348
349 let mut result = Self {
350 member: Bytes::from_owned_valkey_value(member)?,
351 position: None,
352 distance: None,
353 hash: None,
354 };
355
356 for extra in extras {
361 match extra {
362 ValkeyValue::Int(hash) => result.hash = Some(hash),
363 ValkeyValue::Array(_) => {
364 result.position = Some(GeoCoord::from_owned_valkey_value(extra)?)
365 }
366 distance => result.distance = Some(f64::from_owned_valkey_value(distance)?),
367 }
368 }
369
370 Ok(result)
371 }
372}
373
374#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
376pub struct GeoSearchStoreOptions {
377 pub order: Option<OrderBy>,
379 pub count: Option<i64>,
381 pub any: bool,
383 pub store_dist: bool,
385}
386
387impl ToValkeyArgs for GeoSearchStoreOptions {
388 fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
389 write_order_count_any(out, self.order, self.count, self.any);
390 if self.store_dist {
391 out.write_arg(b"STOREDIST");
392 }
393 }
394}
395
396fn write_order_count_any<W: ?Sized + ValkeyWrite>(
398 out: &mut W,
399 order: Option<OrderBy>,
400 count: Option<i64>,
401 any: bool,
402) {
403 if let Some(order) = order {
404 out.write_arg(order.as_arg().as_bytes());
405 }
406 if let Some(count) = count {
407 out.write_arg(b"COUNT");
408 out.write_arg_fmt(count);
409 }
410 if any {
411 out.write_arg(b"ANY");
412 }
413}
414
415impl<T: CommandExecutor + ?Sized> GeoCommands for T {}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420 use crate::test_utils::assert_args;
421 use crate::test_utils::assert_args_empty;
422
423 #[test]
424 fn unit_args() {
425 assert_args(GeoUnit::Meters, &["m"]);
426 assert_args(GeoUnit::Kilometers, &["km"]);
427 assert_args(GeoUnit::Miles, &["mi"]);
428 assert_args(GeoUnit::Feet, &["ft"]);
429 }
430
431 #[test]
432 fn coord_args() {
433 assert_args(GeoCoord::lon_lat(13.5, 38.5), &["13.5", "38.5"]);
434 }
435
436 #[test]
437 fn geosearch_shape_args() {
438 assert_args(
439 GeoSearchShape::ByRadius {
440 radius: 5.0,
441 unit: GeoUnit::Kilometers,
442 },
443 &["BYRADIUS", "5.0", "km"],
444 );
445 assert_args(
446 GeoSearchShape::ByBox {
447 width: 2.0,
448 height: 3.0,
449 unit: GeoUnit::Meters,
450 },
451 &["BYBOX", "2.0", "3.0", "m"],
452 );
453 }
454
455 #[test]
456 fn geosearch_options_args() {
457 assert_args_empty(GeoSearchOptions::default());
458 assert_args(
459 GeoSearchOptions {
460 order: Some(OrderBy::Asc),
461 count: Some(10),
462 any: true,
463 ..Default::default()
464 },
465 &["ASC", "COUNT", "10", "ANY"],
466 );
467 assert_args(
468 GeoSearchOptions {
469 order: Some(OrderBy::Desc),
470 count: Some(5),
471 with_position: true,
472 with_distance: true,
473 with_hash: true,
474 ..Default::default()
475 },
476 &["DESC", "COUNT", "5", "WITHCOORD", "WITHDIST", "WITHHASH"],
477 );
478 }
479
480 #[test]
481 fn geosearchstore_options_args() {
482 assert_args_empty(GeoSearchStoreOptions::default());
483 assert_args(
484 GeoSearchStoreOptions {
485 order: Some(OrderBy::Desc),
486 count: Some(3),
487 any: true,
488 store_dist: true,
489 },
490 &["DESC", "COUNT", "3", "ANY", "STOREDIST"],
491 );
492 }
493
494 #[test]
495 fn coord_decoding() {
496 let pos = ValkeyValue::Array(vec![
497 ValkeyValue::BulkString(b"13.5".to_vec().into()),
498 ValkeyValue::BulkString(b"38.5".to_vec().into()),
499 ]);
500 assert_eq!(
501 GeoCoord::<f64>::from_owned_valkey_value(pos.clone()).unwrap(),
502 GeoCoord::lon_lat(13.5, 38.5)
503 );
504 assert_eq!(
505 GeoCoord::<String>::from_owned_valkey_value(pos).unwrap(),
506 GeoCoord::lon_lat("13.5".to_string(), "38.5".to_string())
507 );
508 assert!(
509 GeoCoord::<f64>::from_owned_valkey_value(ValkeyValue::Array(vec![ValkeyValue::Int(1)]))
510 .is_err()
511 );
512 }
513
514 #[test]
515 fn geosearch_result_decoding() {
516 let bulk = |s: &str| ValkeyValue::BulkString(s.as_bytes().to_vec().into());
517 let member = |name: &str| GeoSearchResult {
518 member: Bytes::from(name.to_string()),
519 position: None,
520 distance: None,
521 hash: None,
522 };
523
524 assert_eq!(
525 GeoSearchResult::from_owned_valkey_value(bulk("Palermo")).unwrap(),
526 member("Palermo")
527 );
528
529 let all = ValkeyValue::Array(vec![
530 bulk("Palermo"),
531 ValkeyValue::Array(vec![
532 ValkeyValue::Double(190.4424),
533 ValkeyValue::Int(3479099956230698),
534 ValkeyValue::Array(vec![ValkeyValue::Double(13.5), ValkeyValue::Double(38.5)]),
535 ]),
536 ]);
537 assert_eq!(
538 GeoSearchResult::from_owned_valkey_value(all).unwrap(),
539 GeoSearchResult {
540 position: Some(GeoCoord::lon_lat(13.5, 38.5)),
541 distance: Some(190.4424),
542 hash: Some(3479099956230698),
543 ..member("Palermo")
544 }
545 );
546
547 let hash_only = ValkeyValue::Array(vec![
548 bulk("Catania"),
549 ValkeyValue::Array(vec![ValkeyValue::Int(42)]),
550 ]);
551 assert_eq!(
552 GeoSearchResult::from_owned_valkey_value(hash_only).unwrap(),
553 GeoSearchResult {
554 hash: Some(42),
555 ..member("Catania")
556 }
557 );
558
559 let strings = ValkeyValue::Array(vec![
560 bulk("Palermo"),
561 ValkeyValue::Array(vec![
562 bulk("190.4424"),
563 ValkeyValue::Array(vec![bulk("13.5"), bulk("38.5")]),
564 ]),
565 ]);
566 assert_eq!(
567 GeoSearchResult::from_owned_valkey_value(strings).unwrap(),
568 GeoSearchResult {
569 position: Some(GeoCoord::lon_lat(13.5, 38.5)),
570 distance: Some(190.4424),
571 ..member("Palermo")
572 }
573 );
574
575 let position_only = ValkeyValue::Array(vec![
576 bulk("Catania"),
577 ValkeyValue::Array(vec![ValkeyValue::Array(vec![
578 ValkeyValue::Double(15.0),
579 ValkeyValue::Double(37.5),
580 ])]),
581 ]);
582 assert_eq!(
583 GeoSearchResult::from_owned_valkey_value(position_only).unwrap(),
584 GeoSearchResult {
585 position: Some(GeoCoord::lon_lat(15.0, 37.5)),
586 ..member("Catania")
587 }
588 );
589
590 assert!(GeoSearchResult::from_owned_valkey_value(ValkeyValue::Int(1)).is_err());
591 assert!(GeoSearchResult::from_owned_valkey_value(ValkeyValue::Array(vec![])).is_err());
592 assert!(
593 GeoSearchResult::from_owned_valkey_value(ValkeyValue::Array(vec![
594 bulk("Palermo"),
595 ValkeyValue::Array(vec![]),
596 ValkeyValue::Int(1),
597 ]))
598 .is_err()
599 );
600 assert!(
601 GeoSearchResult::from_owned_valkey_value(ValkeyValue::Array(vec![
602 bulk("Palermo"),
603 ValkeyValue::Int(1),
604 ]))
605 .is_err()
606 );
607 }
608}