1use crate::error::GeoResult;
41use lat_long::{Coordinate, Latitude, Longitude};
42use rfham_core::{CountryCode, error::CoreError};
43use serde::{Deserialize, Serialize};
44use serde_with::{DeserializeFromStr, SerializeDisplay};
45use std::{
46 fmt::{Debug, Display},
47 net::IpAddr,
48 str::FromStr,
49};
50use uom::si::f64::Length;
51
52pub trait Provider {
62 fn lookup(&self, address: &IpAddr) -> GeoResult<Option<IpGeoData>>;
65
66 fn license(&self) -> ProviderDataLicense;
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)]
72pub enum ProviderDataLicense {
73 Public,
75 ServiceLicensed,
77 ClientLicensed,
79}
80
81#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
83pub struct IpGeoData {
84 ip_address: IpAddr,
85 location: Location,
86 hostname: Option<String>,
87 locale: Option<Locale>,
88 asn: Option<Asn>,
89}
90
91#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
92pub struct Location {
93 continent: Code<ContinentCode>,
94 country: Code<CountryCode>,
95 location: Option<GeoLocation>,
96 region: Option<String>,
97 city: Option<String>,
98 district: Option<String>,
99 postal_code: Option<String>,
100}
101
102#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
103pub struct GeoLocation {
104 coordinate: Coordinate,
105 accuracy: Option<Length>,
106}
107
108#[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)]
109pub struct Locale {
110 timezone: Option<String>,
111 currency: Option<Code<CurrencyCode>>,
112 language: Option<Code<LanguageCode>>,
113}
114
115#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
116pub struct Asn {
117 number: u64,
118 name: String,
119 organization: String,
120}
121
122#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
123pub struct Code<T>
124where
125 T: Clone + Debug + Display + PartialEq + Eq,
126{
127 code: T,
128 label: String,
129}
130
131#[derive(Clone, Copy, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
132pub enum ContinentCode {
133 AF,
135 NA,
137 OC,
139 AN,
141 AS,
143 EU,
145 SA,
147}
148
149#[derive(Clone, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
151pub struct CurrencyCode(String);
152
153#[derive(Clone, Debug, PartialEq, Eq, DeserializeFromStr, SerializeDisplay)]
155pub struct LanguageCode(String);
156
157impl IpGeoData {
174 pub const fn new(ip_address: IpAddr, location: Location) -> Self {
175 Self {
176 ip_address,
177 location,
178 hostname: None,
179 locale: None,
180 asn: None,
181 }
182 }
183
184 pub fn with_hostname<S: Into<String>>(mut self, hostname: S) -> Self {
185 self.hostname = Some(hostname.into());
186 self
187 }
188
189 pub fn with_locale(mut self, locale: Locale) -> Self {
190 self.locale = Some(locale);
191 self
192 }
193
194 pub fn with_asn(mut self, asn: Asn) -> Self {
195 self.asn = Some(asn);
196 self
197 }
198
199 pub const fn ip_address(&self) -> &IpAddr {
200 &self.ip_address
201 }
202
203 pub const fn location(&self) -> &Location {
204 &self.location
205 }
206
207 pub const fn locale(&self) -> Option<&Locale> {
208 self.locale.as_ref()
209 }
210
211 pub const fn asn(&self) -> Option<&Asn> {
212 self.asn.as_ref()
213 }
214}
215
216impl Location {
219 pub const fn new(continent: Code<ContinentCode>, country: Code<CountryCode>) -> Self {
220 Self {
221 continent,
222 country,
223 location: None,
224 region: None,
225 city: None,
226 district: None,
227 postal_code: None,
228 }
229 }
230 pub fn with_location(mut self, location: GeoLocation) -> Self {
231 self.location = Some(location);
232 self
233 }
234
235 pub fn with_region<S: Into<String>>(mut self, region: S) -> Self {
236 self.region = Some(region.into());
237 self
238 }
239
240 pub fn with_city<S: Into<String>>(mut self, city: S) -> Self {
241 self.city = Some(city.into());
242 self
243 }
244
245 pub fn with_district<S: Into<String>>(mut self, district: S) -> Self {
246 self.district = Some(district.into());
247 self
248 }
249
250 pub fn with_postal_code<S: Into<String>>(mut self, postal_code: S) -> Self {
251 self.postal_code = Some(postal_code.into());
252 self
253 }
254
255 pub const fn continent(&self) -> &Code<ContinentCode> {
256 &self.continent
257 }
258
259 pub const fn country(&self) -> &Code<CountryCode> {
260 &self.country
261 }
262
263 pub const fn location(&self) -> Option<&GeoLocation> {
264 self.location.as_ref()
265 }
266
267 pub const fn region(&self) -> Option<&String> {
268 self.region.as_ref()
269 }
270
271 pub const fn city(&self) -> Option<&String> {
272 self.city.as_ref()
273 }
274
275 pub const fn district(&self) -> Option<&String> {
276 self.district.as_ref()
277 }
278
279 pub const fn postal_code(&self) -> Option<&String> {
280 self.postal_code.as_ref()
281 }
282}
283
284impl From<Coordinate> for GeoLocation {
287 fn from(value: Coordinate) -> Self {
288 Self::new(value)
289 }
290}
291
292impl GeoLocation {
293 pub const fn new(coordinate: Coordinate) -> Self {
294 Self {
295 coordinate,
296 accuracy: None,
297 }
298 }
299
300 pub fn with_accuracy(mut self, accuracy: Length) -> Self {
301 self.accuracy = Some(accuracy);
302 self
303 }
304
305 pub const fn coordinate(&self) -> Coordinate {
306 self.coordinate
307 }
308
309 pub const fn longitude(&self) -> Longitude {
310 self.coordinate.longitude()
311 }
312
313 pub const fn latitude(&self) -> Latitude {
314 self.coordinate.latitude()
315 }
316
317 pub const fn accuracy(&self) -> Option<&Length> {
318 self.accuracy.as_ref()
319 }
320}
321
322impl Locale {
325 pub fn with_timezone(mut self, timezone: String) -> Self {
326 self.timezone = Some(timezone);
327 self
328 }
329
330 pub fn with_currency(mut self, currency: Code<CurrencyCode>) -> Self {
331 self.currency = Some(currency);
332 self
333 }
334
335 pub fn with_language(mut self, language: Code<LanguageCode>) -> Self {
336 self.language = Some(language);
337 self
338 }
339
340 pub const fn timezone(&self) -> Option<&String> {
341 self.timezone.as_ref()
342 }
343
344 pub const fn currency(&self) -> Option<&Code<CurrencyCode>> {
345 self.currency.as_ref()
346 }
347
348 pub const fn language(&self) -> Option<&Code<LanguageCode>> {
349 self.language.as_ref()
350 }
351}
352
353impl Asn {
356 pub const fn new(number: u64, name: String, organization: String) -> Self {
357 Self {
358 number,
359 name,
360 organization,
361 }
362 }
363
364 pub const fn number(&self) -> u64 {
365 self.number
366 }
367
368 pub const fn name(&self) -> &String {
369 &self.name
370 }
371
372 pub const fn organization(&self) -> &String {
373 &self.organization
374 }
375}
376
377impl<T> Display for Code<T>
382where
383 T: Clone + Debug + Display + PartialEq + Eq,
384{
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 write!(
387 f,
388 "{}",
389 if f.alternate() {
390 format!("{}: {}", self.code, self.label)
391 } else {
392 self.label.to_string()
393 }
394 )
395 }
396}
397
398impl<T> Code<T>
399where
400 T: Clone + Debug + Display + PartialEq + Eq,
401{
402 pub fn new<S: Into<String>>(code: T, label: S) -> Self {
403 Self {
404 code,
405 label: label.into(),
406 }
407 }
408
409 pub const fn code(&self) -> &T {
410 &self.code
411 }
412
413 pub const fn label(&self) -> &String {
414 &self.label
415 }
416}
417impl Display for ContinentCode {
420 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
421 write!(
422 f,
423 "{}",
424 match self {
425 Self::AF => "AF",
426 Self::AN => "AN",
427 Self::AS => "AS",
428 Self::EU => "EU",
429 Self::NA => "NA",
430 Self::OC => "OC",
431 Self::SA => "SA",
432 }
433 )
434 }
435}
436
437impl FromStr for ContinentCode {
438 type Err = CoreError;
439
440 fn from_str(s: &str) -> Result<Self, Self::Err> {
441 match s {
442 "AF" => Ok(Self::AF),
443 "AN" => Ok(Self::AN),
444 "AS" => Ok(Self::AS),
445 "EU" => Ok(Self::EU),
446 "NA" => Ok(Self::NA),
447 "OC" => Ok(Self::OC),
448 "SA" => Ok(Self::SA),
449 _ => Err(CoreError::InvalidValueFromStr(
450 s.to_string(),
451 "ContinentCode",
452 )),
453 }
454 }
455}
456
457impl ContinentCode {
458 pub fn name(&self) -> &str {
459 match self {
460 Self::AF => "Africa",
461 Self::AN => "Antarctica",
462 Self::AS => "Asia",
463 Self::EU => "Europe",
464 Self::NA => "North America",
465 Self::OC => "Oceania",
466 Self::SA => "South America",
467 }
468 }
469}
470
471impl Display for CurrencyCode {
474 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
475 write!(f, "{}", self.0)
476 }
477}
478
479impl FromStr for CurrencyCode {
480 type Err = CoreError;
481
482 fn from_str(s: &str) -> Result<Self, Self::Err> {
483 if Self::is_valid(s) {
484 Ok(Self(s.to_string()))
485 } else {
486 Err(CoreError::InvalidValueFromStr(
487 s.to_string(),
488 "CurrencyCode",
489 ))
490 }
491 }
492}
493
494impl CurrencyCode {
495 pub fn is_valid(s: &str) -> bool {
496 s.len() == 3 && s.chars().all(|c| c.is_ascii_uppercase())
497 }
498}
499
500impl Display for LanguageCode {
503 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
504 write!(f, "{}", self.0)
505 }
506}
507
508impl FromStr for LanguageCode {
509 type Err = CoreError;
510
511 fn from_str(s: &str) -> Result<Self, Self::Err> {
512 if Self::is_valid(s) {
513 Ok(Self(s.to_string()))
514 } else {
515 Err(CoreError::InvalidValueFromStr(
516 s.to_string(),
517 "LanguageCode",
518 ))
519 }
520 }
521}
522
523impl LanguageCode {
524 pub fn is_valid(s: &str) -> bool {
525 s.len() == 2 && s.chars().all(|c| c.is_ascii_lowercase())
526 }
527}
528
529pub mod providers;
542
543#[cfg(test)]
544mod tests {
545 use super::*;
546 use lat_long::{Latitude, Longitude};
547 use pretty_assertions::assert_eq;
548 use serde_json::to_string_pretty;
549
550 #[test]
551 fn test_serialize_roundtrip() {
552 let data = IpGeoData::new(
553 IpAddr::from_str("23.64.167.34").unwrap(),
554 Location {
555 continent: Code {
556 code: ContinentCode::NA,
557 label: "North America".to_string(),
558 },
559 country: Code {
560 code: "US".parse().unwrap(),
561 label: "United States".to_string(),
562 },
563 location: Some(GeoLocation {
564 coordinate: Coordinate::new(
565 Latitude::from_str("32.814").unwrap(),
566 Longitude::from_str("-96.9489").unwrap(),
567 ),
568 accuracy: None,
569 }),
570 region: Some("Texas".to_string()),
571 city: Some("Irving".to_string()),
572 district: None,
573 postal_code: None,
574 },
575 )
576 .with_locale(Locale {
577 timezone: Some("America/Chicago".to_string()),
578 currency: Some(Code {
579 code: CurrencyCode::from_str("USD").unwrap(),
580 label: "United States Dollar".to_string(),
581 }),
582 language: Some(Code {
583 code: LanguageCode::from_str("en").unwrap(),
584 label: "English".to_string(),
585 }),
586 });
587
588 let json = to_string_pretty(&data).unwrap();
589 assert!(json.contains("23.64.167.34"));
590 assert!(json.contains("Texas"));
591
592 let deserialized: IpGeoData = serde_json::from_str(&json).unwrap();
593 assert_eq!(data, deserialized);
594 }
595
596 #[test]
597 fn test_continent_code_roundtrip() {
598 for (s, code) in [
599 ("AF", ContinentCode::AF),
600 ("AN", ContinentCode::AN),
601 ("AS", ContinentCode::AS),
602 ("EU", ContinentCode::EU),
603 ("NA", ContinentCode::NA),
604 ("OC", ContinentCode::OC),
605 ("SA", ContinentCode::SA),
606 ] {
607 assert_eq!(code.to_string(), s);
608 assert_eq!(ContinentCode::from_str(s).unwrap(), code);
609 }
610 }
611
612 #[test]
613 fn test_continent_code_name() {
614 assert_eq!(ContinentCode::NA.name(), "North America");
615 assert_eq!(ContinentCode::EU.name(), "Europe");
616 }
617
618 #[test]
619 fn test_continent_code_invalid() {
620 assert!(ContinentCode::from_str("XX").is_err());
621 }
622
623 #[test]
624 fn test_currency_code_valid() {
625 assert!(CurrencyCode::from_str("USD").is_ok());
626 assert!(CurrencyCode::from_str("EUR").is_ok());
627 assert!(CurrencyCode::from_str("JPY").is_ok());
628 }
629
630 #[test]
631 fn test_currency_code_invalid() {
632 assert!(CurrencyCode::from_str("us").is_err()); assert!(CurrencyCode::from_str("USDD").is_err()); assert!(CurrencyCode::from_str("US").is_err()); }
636
637 #[test]
638 fn test_language_code_valid() {
639 assert!(LanguageCode::from_str("en").is_ok());
640 assert!(LanguageCode::from_str("ja").is_ok());
641 }
642
643 #[test]
644 fn test_language_code_invalid() {
645 assert!(LanguageCode::from_str("EN").is_err()); assert!(LanguageCode::from_str("eng").is_err()); }
648
649 #[test]
650 fn test_code_display() {
651 let c = Code::new(ContinentCode::EU, "Europe");
652 assert_eq!(c.to_string(), "Europe");
653 assert_eq!(format!("{c:#}"), "EU: Europe");
654 }
655
656 #[test]
657 fn test_ip_geo_data_accessors() {
658 let location = Location::new(
659 Code::new(ContinentCode::NA, "North America"),
660 Code::new("US".parse::<rfham_core::CountryCode>().unwrap(), "United States"),
661 );
662 let data = IpGeoData::new("203.0.113.1".parse::<IpAddr>().unwrap(), location);
663 assert_eq!(data.ip_address().to_string(), "203.0.113.1");
664 assert_eq!(data.location().continent().code(), &ContinentCode::NA);
665 assert!(data.locale().is_none());
666 assert!(data.asn().is_none());
667 }
668}