diff --git a/src/models/api.rs b/src/models/api.rs new file mode 100644 index 0000000..f821fa2 --- /dev/null +++ b/src/models/api.rs @@ -0,0 +1,138 @@ +use serde::Serialize; + +use super::mmdb::MmdbProxy; + +#[derive(Debug, Serialize)] +pub struct IpInfo { + pub ip: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub city: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub continent: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub country: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub postal: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub registered_country: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub subdivisions: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + pub asn: Option, + pub proxy: ProxyInfo, +} + +#[derive(Debug, Serialize)] +pub struct Names { + #[serde(rename = "de", skip_serializing_if = "Option::is_none")] + pub german: Option, + #[serde(rename = "en", skip_serializing_if = "Option::is_none")] + pub english: Option, + #[serde(rename = "es", skip_serializing_if = "Option::is_none")] + pub spanish: Option, + #[serde(rename = "fr", skip_serializing_if = "Option::is_none")] + pub french: Option, + #[serde(rename = "ja", skip_serializing_if = "Option::is_none")] + pub japanese: Option, + #[serde(rename = "pt-BR", skip_serializing_if = "Option::is_none")] + pub brazilian_portuguese: Option, + #[serde(rename = "ru", skip_serializing_if = "Option::is_none")] + pub russian: Option, + #[serde(rename = "zh-CN", skip_serializing_if = "Option::is_none")] + pub simplified_chinese: Option, +} + +#[derive(Debug, Serialize)] +pub struct CityInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub geoname_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub names: Option, +} + +#[derive(Debug, Serialize)] +pub struct ContinentInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub geoname_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub names: Option, +} + +#[derive(Debug, Serialize)] +pub struct CountryInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub geoname_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub iso_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub names: Option, +} + +#[derive(Debug, Serialize)] +pub struct LocationInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub accuracy_radius: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub latitude: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub longitude: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub metro_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub time_zone: Option, +} + +#[derive(Debug, Serialize)] +pub struct PostalInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub code: Option, +} + +#[derive(Debug, Serialize)] +pub struct SubdivisionInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub geoname_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub iso_code: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub names: Option, +} + +#[derive(Debug, Serialize)] +pub struct AsnInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub autonomous_system_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub autonomous_system_organization: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub as_domain: Option, +} + +#[derive(Debug, Serialize)] +pub struct ProxyInfo { + pub is_proxy: bool, + pub is_vpn: bool, + pub is_tor: bool, + pub is_hosting: bool, + pub is_cdn: bool, + pub is_school: bool, + pub is_anonymous: bool, +} + +impl From for ProxyInfo { + fn from(p: MmdbProxy) -> Self { + Self { + is_proxy: p.is_proxy, + is_vpn: p.is_vpn, + is_tor: p.is_tor, + is_hosting: p.is_hosting, + is_cdn: p.is_cdn, + is_school: p.is_school, + is_anonymous: p.is_anonymous, + } + } +} diff --git a/src/models/mmdb.rs b/src/models/mmdb.rs new file mode 100644 index 0000000..a0aac74 --- /dev/null +++ b/src/models/mmdb.rs @@ -0,0 +1,126 @@ +use serde::Deserialize; + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbRecord<'a> { + #[serde(borrow, default)] + pub city: MmdbCity<'a>, + #[serde(default)] + pub continent: MmdbContinent<'a>, + #[serde(default)] + pub country: MmdbCountry<'a>, + #[serde(default)] + pub location: MmdbLocation<'a>, + #[serde(default)] + pub postal: MmdbPostal<'a>, + #[serde(default)] + pub registered_country: MmdbCountry<'a>, + #[serde(default)] + pub subdivisions: Vec>, + #[serde(default)] + pub asn: MmdbAsn<'a>, + #[serde(default)] + pub proxy: MmdbProxy, +} + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbCity<'a> { + pub geoname_id: Option, + #[serde(borrow, default)] + pub names: MmdbNames<'a>, +} + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbContinent<'a> { + pub code: Option<&'a str>, + pub geoname_id: Option, + #[serde(borrow, default)] + pub names: MmdbNames<'a>, +} + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbCountry<'a> { + pub geoname_id: Option, + pub iso_code: Option<&'a str>, + #[serde(borrow, default)] + pub names: MmdbNames<'a>, +} + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbLocation<'a> { + pub accuracy_radius: Option, + pub latitude: Option, + pub longitude: Option, + pub metro_code: Option, + pub time_zone: Option<&'a str>, +} + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbPostal<'a> { + pub code: Option<&'a str>, +} + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbSubdivision<'a> { + pub geoname_id: Option, + pub iso_code: Option<&'a str>, + #[serde(borrow, default)] + pub names: MmdbNames<'a>, +} + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbAsn<'a> { + pub autonomous_system_number: Option, + pub autonomous_system_organization: Option<&'a str>, + pub as_domain: Option<&'a str>, +} + +#[derive(Deserialize, Debug, Default, Clone, Copy)] +pub struct MmdbProxy { + #[serde(default)] + pub is_proxy: bool, + #[serde(default)] + pub is_vpn: bool, + #[serde(default)] + pub is_tor: bool, + #[serde(default)] + pub is_hosting: bool, + #[serde(default)] + pub is_cdn: bool, + #[serde(default)] + pub is_school: bool, + #[serde(default)] + pub is_anonymous: bool, +} + +#[derive(Deserialize, Debug, Default)] +pub struct MmdbNames<'a> { + #[serde(rename = "de", default)] + pub german: Option<&'a str>, + #[serde(rename = "en", default)] + pub english: Option<&'a str>, + #[serde(rename = "es", default)] + pub spanish: Option<&'a str>, + #[serde(rename = "fr", default)] + pub french: Option<&'a str>, + #[serde(rename = "ja", default)] + pub japanese: Option<&'a str>, + #[serde(rename = "pt-BR", default)] + pub brazilian_portuguese: Option<&'a str>, + #[serde(rename = "ru", default)] + pub russian: Option<&'a str>, + #[serde(rename = "zh-CN", default)] + pub simplified_chinese: Option<&'a str>, +} + +impl MmdbNames<'_> { + pub fn is_empty(&self) -> bool { + self.english.is_none() + && self.german.is_none() + && self.spanish.is_none() + && self.french.is_none() + && self.japanese.is_none() + && self.brazilian_portuguese.is_none() + && self.russian.is_none() + && self.simplified_chinese.is_none() + } +} diff --git a/src/models.rs b/src/models/mod.rs similarity index 69% rename from src/models.rs rename to src/models/mod.rs index 8819f2b..10a057e 100644 --- a/src/models.rs +++ b/src/models/mod.rs @@ -1,257 +1,14 @@ +//! Data models for MMDB deserialization and JSON API serialization. + +pub mod api; +pub mod mmdb; + use std::fmt::Write; use std::net::IpAddr; -use serde::{Deserialize, Serialize}; - -// --- Deserialization types (from MMDB) --- - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbRecord<'a> { - #[serde(borrow, default)] - pub city: MmdbCity<'a>, - #[serde(default)] - pub continent: MmdbContinent<'a>, - #[serde(default)] - pub country: MmdbCountry<'a>, - #[serde(default)] - pub location: MmdbLocation<'a>, - #[serde(default)] - pub postal: MmdbPostal<'a>, - #[serde(default)] - pub registered_country: MmdbCountry<'a>, - #[serde(default)] - pub subdivisions: Vec>, - #[serde(default)] - pub asn: MmdbAsn<'a>, - #[serde(default)] - pub proxy: MmdbProxy, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbCity<'a> { - pub geoname_id: Option, - #[serde(borrow, default)] - pub names: MmdbNames<'a>, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbContinent<'a> { - pub code: Option<&'a str>, - pub geoname_id: Option, - #[serde(borrow, default)] - pub names: MmdbNames<'a>, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbCountry<'a> { - pub geoname_id: Option, - pub iso_code: Option<&'a str>, - #[serde(borrow, default)] - pub names: MmdbNames<'a>, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbLocation<'a> { - pub accuracy_radius: Option, - pub latitude: Option, - pub longitude: Option, - pub metro_code: Option, - pub time_zone: Option<&'a str>, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbPostal<'a> { - pub code: Option<&'a str>, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbSubdivision<'a> { - pub geoname_id: Option, - pub iso_code: Option<&'a str>, - #[serde(borrow, default)] - pub names: MmdbNames<'a>, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbAsn<'a> { - pub autonomous_system_number: Option, - pub autonomous_system_organization: Option<&'a str>, - pub as_domain: Option<&'a str>, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbProxy { - #[serde(default)] - pub is_proxy: bool, - #[serde(default)] - pub is_vpn: bool, - #[serde(default)] - pub is_tor: bool, - #[serde(default)] - pub is_hosting: bool, - #[serde(default)] - pub is_cdn: bool, - #[serde(default)] - pub is_school: bool, - #[serde(default)] - pub is_anonymous: bool, -} - -#[derive(Deserialize, Debug, Default)] -pub struct MmdbNames<'a> { - #[serde(rename = "de", default)] - pub german: Option<&'a str>, - #[serde(rename = "en", default)] - pub english: Option<&'a str>, - #[serde(rename = "es", default)] - pub spanish: Option<&'a str>, - #[serde(rename = "fr", default)] - pub french: Option<&'a str>, - #[serde(rename = "ja", default)] - pub japanese: Option<&'a str>, - #[serde(rename = "pt-BR", default)] - pub brazilian_portuguese: Option<&'a str>, - #[serde(rename = "ru", default)] - pub russian: Option<&'a str>, - #[serde(rename = "zh-CN", default)] - pub simplified_chinese: Option<&'a str>, -} - -impl MmdbNames<'_> { - fn is_empty(&self) -> bool { - self.english.is_none() - && self.german.is_none() - && self.spanish.is_none() - && self.french.is_none() - && self.japanese.is_none() - && self.brazilian_portuguese.is_none() - && self.russian.is_none() - && self.simplified_chinese.is_none() - } -} - -// --- Serialization types (API output) --- - -#[derive(Debug, Serialize)] -pub struct IpInfo { - pub ip: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub city: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub continent: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub country: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub location: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub postal: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub registered_country: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub subdivisions: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub asn: Option, - pub proxy: ProxyInfo, -} - -#[derive(Debug, Serialize)] -pub struct Names { - #[serde(rename = "de", skip_serializing_if = "Option::is_none")] - pub german: Option, - #[serde(rename = "en", skip_serializing_if = "Option::is_none")] - pub english: Option, - #[serde(rename = "es", skip_serializing_if = "Option::is_none")] - pub spanish: Option, - #[serde(rename = "fr", skip_serializing_if = "Option::is_none")] - pub french: Option, - #[serde(rename = "ja", skip_serializing_if = "Option::is_none")] - pub japanese: Option, - #[serde(rename = "pt-BR", skip_serializing_if = "Option::is_none")] - pub brazilian_portuguese: Option, - #[serde(rename = "ru", skip_serializing_if = "Option::is_none")] - pub russian: Option, - #[serde(rename = "zh-CN", skip_serializing_if = "Option::is_none")] - pub simplified_chinese: Option, -} - -#[derive(Debug, Serialize)] -pub struct CityInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub geoname_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub names: Option, -} - -#[derive(Debug, Serialize)] -pub struct ContinentInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub geoname_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub names: Option, -} - -#[derive(Debug, Serialize)] -pub struct CountryInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub geoname_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub iso_code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub names: Option, -} - -#[derive(Debug, Serialize)] -pub struct LocationInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub accuracy_radius: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub latitude: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub longitude: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub metro_code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub time_zone: Option, -} - -#[derive(Debug, Serialize)] -pub struct PostalInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub code: Option, -} - -#[derive(Debug, Serialize)] -pub struct SubdivisionInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub geoname_id: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub iso_code: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub names: Option, -} - -#[derive(Debug, Serialize)] -pub struct AsnInfo { - #[serde(skip_serializing_if = "Option::is_none")] - pub autonomous_system_number: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub autonomous_system_organization: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub as_domain: Option, -} - -#[derive(Debug, Serialize)] -pub struct ProxyInfo { - pub is_proxy: bool, - pub is_vpn: bool, - pub is_tor: bool, - pub is_hosting: bool, - pub is_cdn: bool, - pub is_school: bool, - pub is_anonymous: bool, -} +pub use api::*; +// Mmdb types are re-exported as pub(crate) since they are internal deserialization details. +pub(crate) use mmdb::*; // --- Conversion from MMDB record to API output --- @@ -390,15 +147,7 @@ pub fn from_mmdb_record(ip: IpAddr, record: &MmdbRecord<'_>) -> IpInfo { None }; - let proxy = ProxyInfo { - is_proxy: record.proxy.is_proxy, - is_vpn: record.proxy.is_vpn, - is_tor: record.proxy.is_tor, - is_hosting: record.proxy.is_hosting, - is_cdn: record.proxy.is_cdn, - is_school: record.proxy.is_school, - is_anonymous: record.proxy.is_anonymous, - }; + let proxy = ProxyInfo::from(record.proxy); let mut ip_str = String::with_capacity(45); write!(ip_str, "{ip}").expect("IP address formatting cannot fail");