refactor: split models.rs into mmdb/api sub-modules with pub(crate) visibility

Split the 865-line models.rs into 3 focused files:
- models/mod.rs: conversion logic, helpers, re-exports, tests
- models/mmdb.rs: MMDB deserialization types (internal, pub(crate))
- models/api.rs: API serialization types (public) with From<MmdbProxy> impl

Mmdb types are now pub(crate) since they are internal deserialization details
that should not be part of the public API surface.
This commit is contained in:
NetworkCats
2026-03-22 17:33:40 +08:00
parent ee6ec1a28b
commit 8fba47afd1
3 changed files with 273 additions and 260 deletions
+138
View File
@@ -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<CityInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub continent: Option<ContinentInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<CountryInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<LocationInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub postal: Option<PostalInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registered_country: Option<CountryInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subdivisions: Option<Vec<SubdivisionInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub asn: Option<AsnInfo>,
pub proxy: ProxyInfo,
}
#[derive(Debug, Serialize)]
pub struct Names {
#[serde(rename = "de", skip_serializing_if = "Option::is_none")]
pub german: Option<String>,
#[serde(rename = "en", skip_serializing_if = "Option::is_none")]
pub english: Option<String>,
#[serde(rename = "es", skip_serializing_if = "Option::is_none")]
pub spanish: Option<String>,
#[serde(rename = "fr", skip_serializing_if = "Option::is_none")]
pub french: Option<String>,
#[serde(rename = "ja", skip_serializing_if = "Option::is_none")]
pub japanese: Option<String>,
#[serde(rename = "pt-BR", skip_serializing_if = "Option::is_none")]
pub brazilian_portuguese: Option<String>,
#[serde(rename = "ru", skip_serializing_if = "Option::is_none")]
pub russian: Option<String>,
#[serde(rename = "zh-CN", skip_serializing_if = "Option::is_none")]
pub simplified_chinese: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CityInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
pub struct ContinentInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
pub struct CountryInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iso_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
pub struct LocationInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub accuracy_radius: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latitude: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub longitude: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metro_code: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_zone: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct PostalInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SubdivisionInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iso_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
pub struct AsnInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub autonomous_system_number: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autonomous_system_organization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub as_domain: Option<String>,
}
#[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<MmdbProxy> 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,
}
}
}
+126
View File
@@ -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<MmdbSubdivision<'a>>,
#[serde(default)]
pub asn: MmdbAsn<'a>,
#[serde(default)]
pub proxy: MmdbProxy,
}
#[derive(Deserialize, Debug, Default)]
pub struct MmdbCity<'a> {
pub geoname_id: Option<u32>,
#[serde(borrow, default)]
pub names: MmdbNames<'a>,
}
#[derive(Deserialize, Debug, Default)]
pub struct MmdbContinent<'a> {
pub code: Option<&'a str>,
pub geoname_id: Option<u32>,
#[serde(borrow, default)]
pub names: MmdbNames<'a>,
}
#[derive(Deserialize, Debug, Default)]
pub struct MmdbCountry<'a> {
pub geoname_id: Option<u32>,
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<u16>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub metro_code: Option<u16>,
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<u32>,
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<u32>,
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()
}
}
+9 -260
View File
@@ -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<MmdbSubdivision<'a>>,
#[serde(default)]
pub asn: MmdbAsn<'a>,
#[serde(default)]
pub proxy: MmdbProxy,
}
#[derive(Deserialize, Debug, Default)]
pub struct MmdbCity<'a> {
pub geoname_id: Option<u32>,
#[serde(borrow, default)]
pub names: MmdbNames<'a>,
}
#[derive(Deserialize, Debug, Default)]
pub struct MmdbContinent<'a> {
pub code: Option<&'a str>,
pub geoname_id: Option<u32>,
#[serde(borrow, default)]
pub names: MmdbNames<'a>,
}
#[derive(Deserialize, Debug, Default)]
pub struct MmdbCountry<'a> {
pub geoname_id: Option<u32>,
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<u16>,
pub latitude: Option<f64>,
pub longitude: Option<f64>,
pub metro_code: Option<u16>,
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<u32>,
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<u32>,
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<CityInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub continent: Option<ContinentInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<CountryInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<LocationInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub postal: Option<PostalInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub registered_country: Option<CountryInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub subdivisions: Option<Vec<SubdivisionInfo>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub asn: Option<AsnInfo>,
pub proxy: ProxyInfo,
}
#[derive(Debug, Serialize)]
pub struct Names {
#[serde(rename = "de", skip_serializing_if = "Option::is_none")]
pub german: Option<String>,
#[serde(rename = "en", skip_serializing_if = "Option::is_none")]
pub english: Option<String>,
#[serde(rename = "es", skip_serializing_if = "Option::is_none")]
pub spanish: Option<String>,
#[serde(rename = "fr", skip_serializing_if = "Option::is_none")]
pub french: Option<String>,
#[serde(rename = "ja", skip_serializing_if = "Option::is_none")]
pub japanese: Option<String>,
#[serde(rename = "pt-BR", skip_serializing_if = "Option::is_none")]
pub brazilian_portuguese: Option<String>,
#[serde(rename = "ru", skip_serializing_if = "Option::is_none")]
pub russian: Option<String>,
#[serde(rename = "zh-CN", skip_serializing_if = "Option::is_none")]
pub simplified_chinese: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CityInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
pub struct ContinentInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
pub struct CountryInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iso_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
pub struct LocationInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub accuracy_radius: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latitude: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub longitude: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metro_code: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time_zone: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct PostalInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct SubdivisionInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub geoname_id: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iso_code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub names: Option<Names>,
}
#[derive(Debug, Serialize)]
pub struct AsnInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub autonomous_system_number: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub autonomous_system_organization: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub as_domain: Option<String>,
}
#[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");