#![allow(clippy::too_many_arguments)]
use crate::commands::options::{ConditionalChange, OrderBy};
use crate::error::Result;
use crate::executor::CommandExecutor;
use crate::value;
use async_trait::async_trait;
use bytes::Bytes;
use redis::{Cmd, ToRedisArgs};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeoUnit {
Meters,
Kilometers,
Miles,
Feet,
}
impl GeoUnit {
fn as_arg(&self) -> &'static str {
match self {
GeoUnit::Meters => "m",
GeoUnit::Kilometers => "km",
GeoUnit::Miles => "mi",
GeoUnit::Feet => "ft",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GeospatialData {
pub longitude: f64,
pub latitude: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum GeoSearchShape {
ByRadius {
radius: f64,
unit: GeoUnit,
},
ByBox {
width: f64,
height: f64,
unit: GeoUnit,
},
}
impl GeoSearchShape {
fn add_to(&self, cmd: &mut Cmd) {
match self {
GeoSearchShape::ByRadius { radius, unit } => {
cmd.arg("BYRADIUS").arg(radius).arg(unit.as_arg());
}
GeoSearchShape::ByBox {
width,
height,
unit,
} => {
cmd.arg("BYBOX").arg(width).arg(height).arg(unit.as_arg());
}
}
}
}
#[async_trait]
pub trait GeoCommands: CommandExecutor {
async fn geoadd<K: ToRedisArgs + Send, M: ToRedisArgs + Send + Sync>(
&self,
key: K,
members_positions: &[(M, GeospatialData)],
) -> Result<i64> {
let mut cmd = Cmd::new();
cmd.arg("GEOADD").arg(key);
for (m, pos) in members_positions {
cmd.arg(pos.longitude).arg(pos.latitude).arg(m);
}
value::to_i64(self.execute_command(cmd, None).await?)
}
async fn geodist<K: ToRedisArgs + Send, M1: ToRedisArgs + Send, M2: ToRedisArgs + Send>(
&self,
key: K,
member1: M1,
member2: M2,
unit: Option<GeoUnit>,
) -> Result<Option<f64>> {
let mut cmd = Cmd::new();
cmd.arg("GEODIST").arg(key).arg(member1).arg(member2);
if let Some(u) = unit {
cmd.arg(u.as_arg());
}
value::to_opt_f64(self.execute_command(cmd, None).await?)
}
async fn geohash<K: ToRedisArgs + Send, M: ToRedisArgs + Send + Sync>(
&self,
key: K,
members: &[M],
) -> Result<Vec<Option<Bytes>>> {
let mut cmd = Cmd::new();
cmd.arg("GEOHASH").arg(key);
for m in members {
cmd.arg(m);
}
match self.execute_command(cmd, None).await? {
redis::Value::Array(items) => items.into_iter().map(value::to_opt_bytes).collect(),
other => Ok(vec![value::to_opt_bytes(other)?]),
}
}
async fn geopos<K: ToRedisArgs + Send, M: ToRedisArgs + Send + Sync>(
&self,
key: K,
members: &[M],
) -> Result<Vec<Option<(f64, f64)>>> {
let mut cmd = Cmd::new();
cmd.arg("GEOPOS").arg(key);
for m in members {
cmd.arg(m);
}
match self.execute_command(cmd, None).await? {
redis::Value::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for it in items {
match it {
redis::Value::Nil => out.push(None),
redis::Value::Array(mut pair) if pair.len() == 2 => {
let lat = value::to_f64(pair.pop().unwrap())?;
let lon = value::to_f64(pair.pop().unwrap())?;
out.push(Some((lon, lat)));
}
_ => out.push(None),
}
}
Ok(out)
}
_ => Ok(Vec::new()),
}
}
async fn geosearch_by_radius_from_member<K: ToRedisArgs + Send, M: ToRedisArgs + Send>(
&self,
key: K,
member: M,
radius: f64,
unit: GeoUnit,
) -> Result<Vec<Bytes>> {
let mut cmd = Cmd::new();
cmd.arg("GEOSEARCH")
.arg(key)
.arg("FROMMEMBER")
.arg(member)
.arg("BYRADIUS")
.arg(radius)
.arg(unit.as_arg());
match self.execute_command(cmd, None).await? {
redis::Value::Array(items) => items.into_iter().map(value::to_bytes).collect(),
redis::Value::Nil => Ok(Vec::new()),
other => Ok(vec![value::to_bytes(other)?]),
}
}
async fn geoadd_options<K: ToRedisArgs + Send, M: ToRedisArgs + Send + Sync>(
&self,
key: K,
members_positions: &[(M, GeospatialData)],
conditional_change: Option<ConditionalChange>,
changed: bool,
) -> Result<i64> {
let mut cmd = Cmd::new();
cmd.arg("GEOADD").arg(key);
if let Some(c) = conditional_change {
c.add_to(&mut cmd);
}
if changed {
cmd.arg("CH");
}
for (m, pos) in members_positions {
cmd.arg(pos.longitude).arg(pos.latitude).arg(m);
}
value::to_i64(self.execute_command(cmd, None).await?)
}
async fn geosearch_from_member<K: ToRedisArgs + Send, M: ToRedisArgs + Send>(
&self,
key: K,
member: M,
shape: GeoSearchShape,
order: Option<OrderBy>,
count: Option<i64>,
any: bool,
) -> Result<Vec<Bytes>> {
let mut cmd = Cmd::new();
cmd.arg("GEOSEARCH").arg(key).arg("FROMMEMBER").arg(member);
shape.add_to(&mut cmd);
add_search_tail(&mut cmd, order, count, any);
collect_bytes(self.execute_command(cmd, None).await?)
}
async fn geosearch_from_coord<K: ToRedisArgs + Send>(
&self,
key: K,
coord: GeospatialData,
shape: GeoSearchShape,
order: Option<OrderBy>,
count: Option<i64>,
any: bool,
) -> Result<Vec<Bytes>> {
let mut cmd = Cmd::new();
cmd.arg("GEOSEARCH")
.arg(key)
.arg("FROMLONLAT")
.arg(coord.longitude)
.arg(coord.latitude);
shape.add_to(&mut cmd);
add_search_tail(&mut cmd, order, count, any);
collect_bytes(self.execute_command(cmd, None).await?)
}
async fn geosearchstore_from_member<
D: ToRedisArgs + Send,
S: ToRedisArgs + Send,
M: ToRedisArgs + Send,
>(
&self,
destination: D,
source: S,
member: M,
shape: GeoSearchShape,
order: Option<OrderBy>,
count: Option<i64>,
any: bool,
store_dist: bool,
) -> Result<i64> {
let mut cmd = Cmd::new();
cmd.arg("GEOSEARCHSTORE")
.arg(destination)
.arg(source)
.arg("FROMMEMBER")
.arg(member);
shape.add_to(&mut cmd);
add_search_tail(&mut cmd, order, count, any);
if store_dist {
cmd.arg("STOREDIST");
}
value::to_i64(self.execute_command(cmd, None).await?)
}
async fn geosearchstore_from_coord<D: ToRedisArgs + Send, S: ToRedisArgs + Send>(
&self,
destination: D,
source: S,
coord: GeospatialData,
shape: GeoSearchShape,
order: Option<OrderBy>,
count: Option<i64>,
any: bool,
store_dist: bool,
) -> Result<i64> {
let mut cmd = Cmd::new();
cmd.arg("GEOSEARCHSTORE")
.arg(destination)
.arg(source)
.arg("FROMLONLAT")
.arg(coord.longitude)
.arg(coord.latitude);
shape.add_to(&mut cmd);
add_search_tail(&mut cmd, order, count, any);
if store_dist {
cmd.arg("STOREDIST");
}
value::to_i64(self.execute_command(cmd, None).await?)
}
}
fn add_search_tail(cmd: &mut Cmd, order: Option<OrderBy>, count: Option<i64>, any: bool) {
if let Some(o) = order {
cmd.arg(o.as_arg());
}
if let Some(c) = count {
cmd.arg("COUNT").arg(c);
if any {
cmd.arg("ANY");
}
}
}
fn collect_bytes(v: redis::Value) -> Result<Vec<Bytes>> {
match v {
redis::Value::Array(items) => items.into_iter().map(value::to_bytes).collect(),
redis::Value::Nil => Ok(Vec::new()),
other => Ok(vec![value::to_bytes(other)?]),
}
}
impl<T: CommandExecutor + ?Sized> GeoCommands for T {}
#[cfg(test)]
mod tests {
use super::*;
fn args_of(cmd: &Cmd) -> Vec<String> {
cmd.args_iter()
.filter_map(|a| match a {
redis::Arg::Simple(bytes) => Some(String::from_utf8_lossy(bytes).into_owned()),
redis::Arg::Cursor => None,
})
.collect()
}
#[test]
fn geo_unit_args() {
assert_eq!(GeoUnit::Meters.as_arg(), "m");
assert_eq!(GeoUnit::Kilometers.as_arg(), "km");
assert_eq!(GeoUnit::Miles.as_arg(), "mi");
assert_eq!(GeoUnit::Feet.as_arg(), "ft");
}
#[test]
fn geosearch_shape_args() {
let mut cmd = Cmd::new();
GeoSearchShape::ByRadius {
radius: 5.0,
unit: GeoUnit::Kilometers,
}
.add_to(&mut cmd);
assert_eq!(args_of(&cmd), vec!["BYRADIUS", "5.0", "km"]);
let mut cmd = Cmd::new();
GeoSearchShape::ByBox {
width: 2.0,
height: 3.0,
unit: GeoUnit::Meters,
}
.add_to(&mut cmd);
assert_eq!(args_of(&cmd), vec!["BYBOX", "2.0", "3.0", "m"]);
}
#[test]
fn search_tail_args() {
let mut cmd = Cmd::new();
add_search_tail(&mut cmd, Some(OrderBy::Asc), Some(10), true);
assert_eq!(args_of(&cmd), vec!["ASC", "COUNT", "10", "ANY"]);
}
}