actix_web_location/
domain.rs

1#[cfg(feature = "maxmind")]
2use maxminddb::geoip2::City;
3#[cfg(feature = "serde")]
4use serde::Serialize;
5
6/// The location information that providers must produce.
7#[derive(Debug, Clone, PartialEq, Eq)]
8#[cfg_attr(feature = "serde", derive(Serialize))]
9pub struct Location {
10    /// Country in ISO 3166-1 alpha-2 format, such as "MX" for Mexico or "IT" for Italy.
11    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
12    pub country: Option<String>,
13
14    /// Region/region (e.g. a US state) in ISO 3166-2 format, such as "QC"
15    /// for Quebec (with country = "CA") or "TX" for Texas (with country = "US").
16    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
17    pub region: Option<String>,
18
19    /// City, listed by name such as "Portland" or "Berlin".
20    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
21    pub city: Option<String>,
22
23    /// The Designated Market Area code, as defined by [Nielsen]. Only defined in the US.
24    ///
25    /// [Nielsen]: https://www.nielsen.com/us/en/contact-us/intl-campaigns/dma-maps/
26    #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
27    pub dma: Option<u16>,
28
29    /// The name of the provider that produced this recommendation.
30    pub provider: String,
31}
32
33macro_rules! location_field {
34    ($field: ident, $type: ty) => {
35        location_field!(
36            $field,
37            $type,
38            concat!(
39                "Get an owned copy of the ",
40                stringify!($field),
41                ", or the default if the field is None"
42            )
43        );
44    };
45
46    ($field: ident, $type: ty, $doc: expr) => {
47        #[doc = $doc]
48        pub fn $field(&self) -> $type {
49            self.$field.clone().unwrap_or_default()
50        }
51    };
52}
53
54impl Location {
55    /// Create a builder for a [`Location`] that can be assembled incrementally.
56    pub fn build() -> LocationBuilder {
57        LocationBuilder::default()
58    }
59
60    location_field!(country, String);
61    location_field!(region, String);
62    location_field!(city, String);
63    location_field!(dma, u16);
64}
65
66#[derive(Default)]
67pub struct LocationBuilder {
68    country: Option<String>,
69    region: Option<String>,
70    city: Option<String>,
71    dma: Option<u16>,
72    provider: Option<String>,
73}
74
75macro_rules! builder_field {
76    ($field: ident, $type: ty) => {
77        pub fn $field<O: Into<Option<$type>>>(mut self, $field: O) -> Self {
78            self.$field = $field.into();
79            self
80        }
81    };
82}
83
84impl LocationBuilder {
85    builder_field!(country, String);
86    builder_field!(region, String);
87    builder_field!(city, String);
88    builder_field!(dma, u16);
89    builder_field!(provider, String);
90
91    pub fn finish(self) -> Result<Location, ()> {
92        Ok(Location {
93            country: self.country,
94            region: self.region,
95            city: self.city,
96            dma: self.dma,
97            provider: self.provider.ok_or(())?,
98        })
99    }
100}
101
102#[cfg(feature = "maxmind")]
103impl<'a> From<(City<'a>, &str)> for LocationBuilder {
104    fn from((val, preferred_language): (City<'a>, &str)) -> Self {
105        Location::build()
106            .country(
107                val.country
108                    .and_then(|country| country.iso_code)
109                    .map(String::from),
110            )
111            .region(
112                val.subdivisions
113                    // Subdivisions are listed in least-specific order. In the US, this might mean that subdivisions is state and then county. We want only the first.
114                    .and_then(|subdivisions| {
115                        subdivisions
116                            .get(0)
117                            .and_then(|subdivision| subdivision.iso_code)
118                    })
119                    .map(ToString::to_string),
120            )
121            .city(
122                val.city
123                    .and_then(|city| city.names)
124                    .and_then(|names| names.get(preferred_language).map(|name| name.to_string()))
125                    .map(|name| (*name).to_string()),
126            )
127            .dma(val.location.and_then(|location| location.metro_code))
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::Location;
134
135    #[test]
136    fn builder_works() {
137        let location = Location::build()
138            .country("US".to_string())
139            .region("OR".to_string())
140            .city("Portland".to_string())
141            .dma(810)
142            .provider("test".to_string())
143            .finish()
144            .unwrap();
145
146        assert_eq!(
147            location,
148            Location {
149                country: Some("US".to_string()),
150                region: Some("OR".to_string()),
151                city: Some("Portland".to_string()),
152                dma: Some(810),
153                provider: "test".to_string()
154            }
155        );
156    }
157
158    #[test]
159    fn methods_get_values() {
160        let location = Location::build()
161            .country("US".to_string())
162            .region("CA".to_string())
163            .city("Sunnyvale".to_string())
164            .dma(807)
165            .provider("test".to_string())
166            .finish()
167            .unwrap();
168
169        assert_eq!(location.country(), "US");
170        assert_eq!(location.region(), "CA");
171        assert_eq!(location.city(), "Sunnyvale");
172        assert_eq!(location.dma(), 807);
173    }
174
175    #[test]
176    fn methods_get_defaults() {
177        let location = Location::build()
178            .provider("test".to_string())
179            .finish()
180            .unwrap();
181
182        assert_eq!(location.country(), "");
183        assert_eq!(location.region(), "");
184        assert_eq!(location.city(), "");
185        assert_eq!(location.dma(), 0);
186    }
187
188    #[cfg(maxmind)]
189    #[actix_rt::test]
190    async fn known_ip() {
191        use maxminddb::geoip2::model::City;
192
193        use crate::providers::tests::maxmind::{MMDB_LOC, TEST_ADDR_1};
194
195        let mmdb = maxminddb::Reader::open_readfile(path)
196            .map_err(|e| Error::Setup(anyhow!("{}", e)))
197            .expect("could not create mmdb");
198        let db_value = mmdb.lookup::<City>(TEST_ADDR_1);
199        let location: Location = (db_value, "en").into();
200
201        assert_eq!(
202            location,
203            Location::build()
204                .country("US".to_string())
205                .region("WA".to_string())
206                .city("Milton".to_string())
207                .dma(819)
208                .provider("maxmind".to_string())
209                .finish()
210                .expect("bug when creating location")
211        );
212    }
213}