redis/
geo.rs

1//! Defines types to use with the geospatial commands.
2
3use super::{ErrorKind, RedisResult};
4use crate::types::{FromRedisValue, RedisWrite, ToRedisArgs, Value};
5
6macro_rules! invalid_type_error {
7    ($v:expr, $det:expr) => {{
8        fail!((
9            ErrorKind::TypeError,
10            "Response was of incompatible type",
11            format!("{:?} (response was {:?})", $det, $v)
12        ));
13    }};
14}
15
16/// Units used by [`geo_dist`][1] and [`geo_radius`][2].
17///
18/// [1]: ../trait.Commands.html#method.geo_dist
19/// [2]: ../trait.Commands.html#method.geo_radius
20pub enum Unit {
21    /// Represents meters.
22    Meters,
23    /// Represents kilometers.
24    Kilometers,
25    /// Represents miles.
26    Miles,
27    /// Represents feed.
28    Feet,
29}
30
31impl ToRedisArgs for Unit {
32    fn write_redis_args<W>(&self, out: &mut W)
33    where
34        W: ?Sized + RedisWrite,
35    {
36        let unit = match *self {
37            Unit::Meters => "m",
38            Unit::Kilometers => "km",
39            Unit::Miles => "mi",
40            Unit::Feet => "ft",
41        };
42        out.write_arg(unit.as_bytes());
43    }
44}
45
46/// A coordinate (longitude, latitude).
47///
48/// Can be used with [`geo_pos`][1] to parse response from Redis.
49///
50/// [1]: ../trait.Commands.html#method.geo_pos
51///
52/// `T` is the type of the every value.
53///
54/// * You may want to use either `f64` or `f32` if you want to perform mathematical operations.
55/// * To keep the raw value from Redis, use `String`.
56#[allow(clippy::derive_partial_eq_without_eq)] // allow f32/f64 here, which don't implement Eq
57#[derive(Debug, PartialEq)]
58pub struct Coord<T> {
59    /// Longitude
60    pub longitude: T,
61    /// Latitude
62    pub latitude: T,
63}
64
65impl<T> Coord<T> {
66    /// Create a new Coord with the (longitude, latitude)
67    pub fn lon_lat(longitude: T, latitude: T) -> Coord<T> {
68        Coord {
69            longitude,
70            latitude,
71        }
72    }
73}
74
75impl<T: FromRedisValue> FromRedisValue for Coord<T> {
76    fn from_redis_value(v: &Value) -> RedisResult<Self> {
77        let values: Vec<T> = FromRedisValue::from_redis_value(v)?;
78        let mut values = values.into_iter();
79        let (longitude, latitude) = match (values.next(), values.next(), values.next()) {
80            (Some(longitude), Some(latitude), None) => (longitude, latitude),
81            _ => invalid_type_error!(v, "Expect a pair of numbers"),
82        };
83        Ok(Coord {
84            longitude,
85            latitude,
86        })
87    }
88}
89
90impl<T: ToRedisArgs> ToRedisArgs for Coord<T> {
91    fn write_redis_args<W>(&self, out: &mut W)
92    where
93        W: ?Sized + RedisWrite,
94    {
95        ToRedisArgs::write_redis_args(&self.longitude, out);
96        ToRedisArgs::write_redis_args(&self.latitude, out);
97    }
98
99    fn num_of_args(&self) -> usize {
100        2
101    }
102}
103
104/// Options to sort results from [GEORADIUS][1] and [GEORADIUSBYMEMBER][2] commands
105///
106/// [1]: https://redis.io/commands/georadius
107/// [2]: https://redis.io/commands/georadiusbymember
108#[derive(Default)]
109pub enum RadiusOrder {
110    /// Don't sort the results
111    #[default]
112    Unsorted,
113
114    /// Sort returned items from the nearest to the farthest, relative to the center.
115    Asc,
116
117    /// Sort returned items from the farthest to the nearest, relative to the center.
118    Desc,
119}
120
121/// Options for the [GEORADIUS][1] and [GEORADIUSBYMEMBER][2] commands
122///
123/// [1]: https://redis.io/commands/georadius
124/// [2]: https://redis.io/commands/georadiusbymember
125///
126/// # Example
127///
128/// ```rust,no_run
129/// use redis::{Commands, RedisResult};
130/// use redis::geo::{RadiusSearchResult, RadiusOptions, RadiusOrder, Unit};
131/// fn nearest_in_radius(
132///     con: &mut redis::Connection,
133///     key: &str,
134///     longitude: f64,
135///     latitude: f64,
136///     meters: f64,
137///     limit: usize,
138/// ) -> RedisResult<Vec<RadiusSearchResult>> {
139///     let opts = RadiusOptions::default()
140///         .order(RadiusOrder::Asc)
141///         .limit(limit);
142///     con.geo_radius(key, longitude, latitude, meters, Unit::Meters, opts)
143/// }
144/// ```
145#[derive(Default)]
146pub struct RadiusOptions {
147    with_coord: bool,
148    with_dist: bool,
149    count: Option<usize>,
150    order: RadiusOrder,
151    store: Option<Vec<Vec<u8>>>,
152    store_dist: Option<Vec<Vec<u8>>>,
153}
154
155impl RadiusOptions {
156    /// Limit the results to the first N matching items.
157    pub fn limit(mut self, n: usize) -> Self {
158        self.count = Some(n);
159        self
160    }
161
162    /// Return the distance of the returned items from the specified center.
163    /// The distance is returned in the same unit as the unit specified as the
164    /// radius argument of the command.
165    pub fn with_dist(mut self) -> Self {
166        self.with_dist = true;
167        self
168    }
169
170    /// Return the `longitude, latitude` coordinates of the matching items.
171    pub fn with_coord(mut self) -> Self {
172        self.with_coord = true;
173        self
174    }
175
176    /// Sort the returned items
177    pub fn order(mut self, o: RadiusOrder) -> Self {
178        self.order = o;
179        self
180    }
181
182    /// Store the results in a sorted set at `key`, instead of returning them.
183    ///
184    /// This feature can't be used with any `with_*` method.
185    pub fn store<K: ToRedisArgs>(mut self, key: K) -> Self {
186        self.store = Some(ToRedisArgs::to_redis_args(&key));
187        self
188    }
189
190    /// Store the results in a sorted set at `key`, with the distance from the
191    /// center as its score. This feature can't be used with any `with_*` method.
192    pub fn store_dist<K: ToRedisArgs>(mut self, key: K) -> Self {
193        self.store_dist = Some(ToRedisArgs::to_redis_args(&key));
194        self
195    }
196}
197
198impl ToRedisArgs for RadiusOptions {
199    fn write_redis_args<W>(&self, out: &mut W)
200    where
201        W: ?Sized + RedisWrite,
202    {
203        if self.with_coord {
204            out.write_arg(b"WITHCOORD");
205        }
206
207        if self.with_dist {
208            out.write_arg(b"WITHDIST");
209        }
210
211        if let Some(n) = self.count {
212            out.write_arg(b"COUNT");
213            out.write_arg_fmt(n);
214        }
215
216        match self.order {
217            RadiusOrder::Asc => out.write_arg(b"ASC"),
218            RadiusOrder::Desc => out.write_arg(b"DESC"),
219            _ => (),
220        };
221
222        if let Some(ref store) = self.store {
223            out.write_arg(b"STORE");
224            for i in store {
225                out.write_arg(i);
226            }
227        }
228
229        if let Some(ref store_dist) = self.store_dist {
230            out.write_arg(b"STOREDIST");
231            for i in store_dist {
232                out.write_arg(i);
233            }
234        }
235    }
236
237    fn num_of_args(&self) -> usize {
238        let mut n: usize = 0;
239        if self.with_coord {
240            n += 1;
241        }
242        if self.with_dist {
243            n += 1;
244        }
245        if self.count.is_some() {
246            n += 2;
247        }
248        match self.order {
249            RadiusOrder::Asc => n += 1,
250            RadiusOrder::Desc => n += 1,
251            _ => {}
252        };
253        if self.store.is_some() {
254            n += 1 + self.store.as_ref().unwrap().len();
255        }
256        if self.store_dist.is_some() {
257            n += 1 + self.store_dist.as_ref().unwrap().len();
258        }
259        n
260    }
261}
262
263/// Contain an item returned by [`geo_radius`][1] and [`geo_radius_by_member`][2].
264///
265/// [1]: ../trait.Commands.html#method.geo_radius
266/// [2]: ../trait.Commands.html#method.geo_radius_by_member
267pub struct RadiusSearchResult {
268    /// The name that was found.
269    pub name: String,
270    /// The coordinate if available.
271    pub coord: Option<Coord<f64>>,
272    /// The distance if available.
273    pub dist: Option<f64>,
274}
275
276impl FromRedisValue for RadiusSearchResult {
277    fn from_redis_value(v: &Value) -> RedisResult<Self> {
278        // If we receive only the member name, it will be a plain string
279        if let Ok(name) = FromRedisValue::from_redis_value(v) {
280            return Ok(RadiusSearchResult {
281                name,
282                coord: None,
283                dist: None,
284            });
285        }
286
287        // Try to parse the result from multitple values
288        if let Value::Array(ref items) = *v {
289            if let Some(result) = RadiusSearchResult::parse_multi_values(items) {
290                return Ok(result);
291            }
292        }
293
294        invalid_type_error!(v, "Response type not RadiusSearchResult compatible.");
295    }
296}
297
298impl RadiusSearchResult {
299    fn parse_multi_values(items: &[Value]) -> Option<Self> {
300        let mut iter = items.iter();
301
302        // First item is always the member name
303        let name: String = match iter.next().map(FromRedisValue::from_redis_value) {
304            Some(Ok(n)) => n,
305            _ => return None,
306        };
307
308        let mut next = iter.next();
309
310        // Next element, if present, will be the distance.
311        let dist = match next.map(FromRedisValue::from_redis_value) {
312            Some(Ok(c)) => {
313                next = iter.next();
314                Some(c)
315            }
316            _ => None,
317        };
318
319        // Finally, if present, the last item will be the coordinates
320
321        let coord = match next.map(FromRedisValue::from_redis_value) {
322            Some(Ok(c)) => Some(c),
323            _ => None,
324        };
325
326        Some(RadiusSearchResult { name, coord, dist })
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::{Coord, RadiusOptions, RadiusOrder};
333    use crate::types::ToRedisArgs;
334    use std::str;
335
336    macro_rules! assert_args {
337        ($value:expr, $($args:expr),+) => {
338            let args = $value.to_redis_args();
339            let strings: Vec<_> = args.iter()
340                                      .map(|a| str::from_utf8(a.as_ref()).unwrap())
341                                      .collect();
342            assert_eq!(strings, vec![$($args),+]);
343        }
344    }
345
346    #[test]
347    fn test_coord_to_args() {
348        let member = ("Palermo", Coord::lon_lat("13.361389", "38.115556"));
349        assert_args!(&member, "Palermo", "13.361389", "38.115556");
350    }
351
352    #[test]
353    fn test_radius_options() {
354        // Without options, should not generate any argument
355        let empty = RadiusOptions::default();
356        assert_eq!(ToRedisArgs::to_redis_args(&empty).len(), 0);
357
358        // Some combinations with WITH* options
359        let opts = RadiusOptions::default;
360
361        assert_args!(opts().with_coord().with_dist(), "WITHCOORD", "WITHDIST");
362
363        assert_args!(opts().limit(50), "COUNT", "50");
364
365        assert_args!(opts().limit(50).store("x"), "COUNT", "50", "STORE", "x");
366
367        assert_args!(
368            opts().limit(100).store_dist("y"),
369            "COUNT",
370            "100",
371            "STOREDIST",
372            "y"
373        );
374
375        assert_args!(
376            opts().order(RadiusOrder::Asc).limit(10).with_dist(),
377            "WITHDIST",
378            "COUNT",
379            "10",
380            "ASC"
381        );
382    }
383}