Skip to main content

modo/geolocation/
location.rs

1use axum::extract::FromRequestParts;
2use http::request::Parts;
3use serde::{Deserialize, Serialize};
4
5/// Geolocation data resolved from a client IP address.
6///
7/// All fields are `Option` — an IP not found in the database
8/// (private, loopback, etc.) produces a `Location` with all `None` fields.
9#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct Location {
11    /// ISO 3166-1 alpha-2 country code, e.g. "US"
12    pub country_code: Option<String>,
13    /// English country name, e.g. "United States"
14    pub country_name: Option<String>,
15    /// First subdivision name (English), e.g. "California"
16    pub region: Option<String>,
17    /// City name (English), e.g. "San Francisco"
18    pub city: Option<String>,
19    /// Latitude
20    pub latitude: Option<f64>,
21    /// Longitude
22    pub longitude: Option<f64>,
23    /// IANA timezone, e.g. "America/Los_Angeles"
24    pub timezone: Option<String>,
25}
26
27impl<S: Send + Sync> FromRequestParts<S> for Location {
28    type Rejection = std::convert::Infallible;
29
30    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
31        Ok(parts
32            .extensions
33            .get::<Location>()
34            .cloned()
35            .unwrap_or_default())
36    }
37}
38
39#[cfg(test)]
40mod tests {
41    use super::*;
42
43    #[test]
44    fn default_location_has_all_none() {
45        let loc = Location::default();
46        assert!(loc.country_code.is_none());
47        assert!(loc.country_name.is_none());
48        assert!(loc.region.is_none());
49        assert!(loc.city.is_none());
50        assert!(loc.latitude.is_none());
51        assert!(loc.longitude.is_none());
52        assert!(loc.timezone.is_none());
53    }
54
55    #[tokio::test]
56    async fn extractor_returns_default_when_missing() {
57        let req = http::Request::builder().body(()).unwrap();
58        let (mut parts, _) = req.into_parts();
59        let loc = Location::from_request_parts(&mut parts, &()).await.unwrap();
60        assert!(loc.country_code.is_none());
61    }
62
63    #[tokio::test]
64    async fn extractor_returns_location_from_extensions() {
65        let mut req = http::Request::builder().body(()).unwrap();
66        req.extensions_mut().insert(Location {
67            country_code: Some("US".to_string()),
68            ..Default::default()
69        });
70        let (mut parts, _) = req.into_parts();
71        let loc = Location::from_request_parts(&mut parts, &()).await.unwrap();
72        assert_eq!(loc.country_code.as_deref(), Some("US"));
73    }
74}