actix_web_location/
domain.rs1#[cfg(feature = "maxmind")]
2use maxminddb::geoip2::City;
3#[cfg(feature = "serde")]
4use serde::Serialize;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8#[cfg_attr(feature = "serde", derive(Serialize))]
9pub struct Location {
10 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
12 pub country: Option<String>,
13
14 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
17 pub region: Option<String>,
18
19 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
21 pub city: Option<String>,
22
23 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
27 pub dma: Option<u16>,
28
29 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 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 .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}