#![deny(missing_docs)]
pub mod types;
#[cfg(feature = "db")]
mod db;
#[cfg(feature = "build-db")]
pub mod builder;
pub use types::{
haversine_km, location_from_village, AdminLevel, DataInfo, LocateMethod, Location,
LookupResult, PrefixResult, Village,
};
#[cfg(feature = "db")]
pub use db::{Database, Error};
pub const fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
#[cfg(feature = "db")]
pub fn data_info() -> DataInfo {
db::cached_data_info().clone()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_db() {
let db = Database::open().expect("should open embedded database");
let count = db.village_count().expect("should count villages");
assert!(count > 80000, "expected >80k villages, got {count}");
}
#[test]
fn test_data_info() {
let info = data_info();
if info.village_count > 0 {
assert!(info.village_count > 80000);
}
if info.build_date > 0 {
assert!(!info.source.is_empty());
assert!(!info.decree.is_empty());
assert!(
!info.decree.contains("unknown"),
"decree should be from DB, not 'unknown': {}",
info.decree
);
}
}
#[test]
fn test_data_info_via_database() {
let db = Database::open().unwrap();
let info = db.data_info();
assert_eq!(info, data_info());
}
#[test]
fn test_version() {
assert_eq!(version(), "0.5.0");
}
#[test]
fn test_nearest_jakarta() {
let db = Database::open().unwrap();
let results = db.find_nearest(-6.1647, 106.8453, 1).unwrap();
assert_eq!(results.len(), 1);
let v = &results[0];
assert!(
v.dist_km.unwrap() < 5.0,
"should be within 5km of Jakarta center"
);
assert_eq!(
v.city, "Kota Administrasi Jakarta Pusat",
"expected Jakarta Pusat, got {}",
v.city
);
}
#[test]
fn test_nearest_papua() {
let db = Database::open().unwrap();
let results = db.find_nearest(-2.5, 140.0, 1).unwrap();
assert!(!results.is_empty());
assert!(results[0].province.contains("Papua"));
}
#[test]
fn test_search() {
let db = Database::open().unwrap();
let results = db.find_by_name("kemayoran", 5).unwrap();
assert!(!results.is_empty(), "should find Kemayoran");
assert!(results
.iter()
.any(|v| v.name.to_lowercase().contains("kemayoran")));
}
#[test]
fn test_search_qualified() {
let db = Database::open().unwrap();
let results = db.find_by_name("kemayoran jakarta", 5).unwrap();
assert!(!results.is_empty(), "should find Kemayoran Jakarta");
assert!(results.iter().all(|v| v.city.contains("Jakarta")));
}
#[test]
fn test_unique_found() {
let db = Database::open().unwrap();
let result = db.find_by_name_unique("abadijaya").unwrap();
assert!(
matches!(result, LookupResult::Found(_)),
"expected Found, got {:?}",
result
);
if let LookupResult::Found(v) = result {
assert_eq!(v.name, "Abadijaya");
}
}
#[test]
fn test_unique_ambiguous() {
let db = Database::open().unwrap();
let result = db.find_by_name_unique("sukamaju").unwrap();
assert!(
matches!(result, LookupResult::Ambiguous(_)),
"sukamaju should be ambiguous, got {:?}",
result
);
if let LookupResult::Ambiguous(results) = result {
assert!(results.len() > 1, "should have multiple matches");
}
}
#[test]
fn test_find_by_code() {
let db = Database::open().unwrap();
let v = db.find_by_code("31.71.03.1001").unwrap();
assert!(v.is_some(), "31.71.03.1001 should exist");
let v = v.unwrap();
assert_eq!(v.name, "Kemayoran");
assert_eq!(v.district, "Kemayoran");
assert_eq!(v.city, "Kota Administrasi Jakarta Pusat");
assert_eq!(v.province, "Provinsi Daerah Khusus Ibukota Jakarta");
}
#[test]
fn test_find_by_code_not_found() {
let db = Database::open().unwrap();
let v = db.find_by_code("99.99.99.9999").unwrap();
assert!(v.is_none());
}
#[test]
fn test_find_by_code_prefix_kecamatan() {
let db = Database::open().unwrap();
let result = db.find_by_code_prefix("31.71.03", 100, 0).unwrap();
assert!(
!result.villages.is_empty(),
"should find villages in kecamatan 31.71.03"
);
assert!(result
.villages
.iter()
.all(|v| v.code.starts_with("31.71.03")));
assert!(result.villages.iter().all(|v| v.district == "Kemayoran"));
assert_eq!(result.total, result.villages.len());
assert!(!result.has_more);
}
#[test]
fn test_find_by_code_prefix_kabupaten() {
let db = Database::open().unwrap();
let result = db.find_by_code_prefix("31.71", 500, 0).unwrap();
assert!(
!result.villages.is_empty(),
"should find villages in kabupaten 31.71"
);
assert!(result.villages.iter().all(|v| v.code.starts_with("31.71")));
assert!(result.total > 0);
assert_eq!(result.has_more, result.villages.len() < result.total);
}
#[test]
fn test_find_by_code_prefix_not_found() {
let db = Database::open().unwrap();
let result = db.find_by_code_prefix("99.99.99", 100, 0).unwrap();
assert!(result.villages.is_empty());
assert_eq!(result.total, 0);
assert!(!result.has_more);
}
#[test]
fn test_unique_not_found() {
let db = Database::open().unwrap();
let result = db.find_by_name_unique("zzzznonexistent").unwrap();
assert!(
matches!(result, LookupResult::NotFound),
"should be not found, got {:?}",
result
);
}
#[test]
fn test_locate_jakarta() {
let db = Database::open().unwrap();
let loc = db
.locate(-6.1647, 106.8453)
.unwrap()
.expect("should locate Jakarta");
assert_eq!(loc.province.code, "31");
assert!(loc.city.name.contains("Jakarta"));
assert!(loc.district.name.len() > 0);
assert!(loc.village.len() > 0);
assert!(loc.village_code.contains('.'));
assert!(loc.dist_km < 5.0);
assert_eq!(loc.method, LocateMethod::Nearest);
}
#[test]
fn test_locate_display() {
let db = Database::open().unwrap();
let loc = db
.locate(-6.1647, 106.8453)
.unwrap()
.expect("should locate Jakarta");
let s = format!("{loc}");
assert!(s.contains(&loc.province.code));
assert!(s.contains(&loc.village));
}
#[test]
fn test_admin_level_display() {
let level = AdminLevel {
code: "31.71".into(),
name: "Jakarta".into(),
};
assert_eq!(format!("{level}"), "31.71 Jakarta");
}
#[test]
fn test_locate_method_display() {
assert_eq!(format!("{}", LocateMethod::Nearest), "nearest");
assert_eq!(format!("{}", LocateMethod::Contained), "contained");
}
#[test]
fn test_haversine_km() {
let d = haversine_km(-6.1647, 106.8453, -6.1647, 106.8453);
assert!(d.abs() < 0.001, "same point should be 0 km, got {d}");
let d = haversine_km(-6.1647, 106.8453, -6.2, 106.8);
assert!(d > 0.0 && d < 50.0, "nearby point should be close, got {d}");
}
#[test]
fn test_location_from_village() {
let v = Village {
code: "31.71.03.1001".into(),
name: "Kemayoran".into(),
district: "Kemayoran".into(),
city: "Jakarta Pusat".into(),
province: "DKI Jakarta".into(),
lat: -6.1647,
lon: 106.8453,
dist_km: None,
};
let loc = location_from_village(&v, 1.5).expect("should parse valid code");
assert_eq!(loc.province.code, "31");
assert_eq!(loc.city.code, "31.71");
assert_eq!(loc.district.code, "31.71.03");
assert_eq!(loc.village_code, "31.71.03.1001");
assert_eq!(loc.dist_km, 1.5);
assert_eq!(loc.method, LocateMethod::Nearest);
}
#[test]
fn test_location_from_village_bad_code() {
let v = Village {
code: "invalid".into(),
name: "Test".into(),
district: "Test".into(),
city: "Test".into(),
province: "Test".into(),
lat: 0.0,
lon: 0.0,
dist_km: None,
};
assert!(location_from_village(&v, 0.0).is_none());
}
#[test]
fn test_error_display() {
let db = Database::open().unwrap();
let result = db.find_by_code("31.71.03.1001");
assert!(result.is_ok());
}
#[test]
fn test_database_is_send_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<Database>();
assert_sync::<Database>();
}
}