modo/geolocation/
location.rs1use axum::extract::FromRequestParts;
2use http::request::Parts;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Default, Serialize, Deserialize)]
10pub struct Location {
11 pub country_code: Option<String>,
13 pub country_name: Option<String>,
15 pub region: Option<String>,
17 pub city: Option<String>,
19 pub latitude: Option<f64>,
21 pub longitude: Option<f64>,
23 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}