#![deny(missing_docs)]
mod db;
#[cfg(feature = "build-db")]
#[allow(missing_docs)]
pub mod pipeline;
pub use db::{
by_code, by_code_prefix, nearest, open_embedded, search, search_unique, LookupResult, Village,
};
#[derive(Debug, Clone)]
pub struct DataInfo {
pub source: &'static str,
pub decree: &'static str,
pub village_count: u32,
pub build_date: u64,
}
pub const fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub fn data_info() -> DataInfo {
DataInfo {
source: env!("WILAYAH_DATA_SOURCE"),
decree: env!("WILAYAH_DATA_DECREE"),
village_count: env!("WILAYAH_VILLAGE_COUNT").parse().unwrap_or(0),
build_date: env!("WILAYAH_BUILD_DATE").parse().unwrap_or(0),
}
}
pub fn open() -> rusqlite::Result<rusqlite::Connection> {
db::open_embedded()
}
pub fn find_nearest(
conn: &rusqlite::Connection,
lat: f64,
lon: f64,
limit: usize,
) -> rusqlite::Result<Vec<Village>> {
db::nearest(conn, lat, lon, limit)
}
pub fn find_by_name(
conn: &rusqlite::Connection,
query: &str,
limit: usize,
) -> rusqlite::Result<Vec<Village>> {
db::search(conn, query, limit)
}
pub fn find_by_name_unique(
conn: &rusqlite::Connection,
query: &str,
) -> rusqlite::Result<LookupResult> {
db::search_unique(conn, query)
}
pub fn find_by_code(conn: &rusqlite::Connection, code: &str) -> rusqlite::Result<Option<Village>> {
db::by_code(conn, code)
}
pub fn find_by_code_prefix(
conn: &rusqlite::Connection,
prefix: &str,
limit: usize,
) -> rusqlite::Result<Vec<Village>> {
db::by_code_prefix(conn, prefix, limit)
}
pub fn village_count(conn: &rusqlite::Connection) -> rusqlite::Result<i64> {
conn.query_row("SELECT COUNT(*) FROM locations", [], |row| row.get(0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_open_db() {
let conn = open().expect("should open embedded database");
let count = village_count(&conn).expect("should count villages");
assert!(count > 80000, "expected >80k villages, got {count}");
}
#[test]
fn test_data_info() {
let info = data_info();
assert!(info.village_count > 80000);
assert!(info.build_date > 0);
assert!(!info.source.is_empty());
assert!(!info.decree.is_empty());
}
#[test]
fn test_version() {
assert_eq!(version(), "0.3.0");
}
#[test]
fn test_nearest_jakarta() {
let conn = open().unwrap();
let results = find_nearest(&conn, -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 conn = open().unwrap();
let results = find_nearest(&conn, -2.5, 140.0, 1).unwrap();
assert!(!results.is_empty());
assert!(results[0].province.contains("Papua"));
}
#[test]
fn test_search() {
let conn = open().unwrap();
let results = find_by_name(&conn, "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 conn = open().unwrap();
let results = find_by_name(&conn, "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 conn = open().unwrap();
let result = find_by_name_unique(&conn, "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 conn = open().unwrap();
let result = find_by_name_unique(&conn, "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 conn = open().unwrap();
let v = find_by_code(&conn, "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 conn = open().unwrap();
let v = find_by_code(&conn, "99.99.99.9999").unwrap();
assert!(v.is_none());
}
#[test]
fn test_find_by_code_prefix_kecamatan() {
let conn = open().unwrap();
let villages = find_by_code_prefix(&conn, "31.71.03", 100).unwrap();
assert!(
!villages.is_empty(),
"should find villages in kecamatan 31.71.03"
);
assert!(villages.iter().all(|v| v.code.starts_with("31.71.03")));
assert!(villages.iter().all(|v| v.district == "Kemayoran"));
}
#[test]
fn test_find_by_code_prefix_kabupaten() {
let conn = open().unwrap();
let villages = find_by_code_prefix(&conn, "31.71", 500).unwrap();
assert!(
!villages.is_empty(),
"should find villages in kabupaten 31.71"
);
assert!(villages.iter().all(|v| v.code.starts_with("31.71")));
}
#[test]
fn test_find_by_code_prefix_not_found() {
let conn = open().unwrap();
let villages = find_by_code_prefix(&conn, "99.99.99", 100).unwrap();
assert!(villages.is_empty());
}
#[test]
fn test_unique_not_found() {
let conn = open().unwrap();
let result = find_by_name_unique(&conn, "zzzznonexistent").unwrap();
assert!(
matches!(result, LookupResult::NotFound),
"should be not found, got {:?}",
result
);
}
}