geocoder.rs (15365B)
1 use radroots_geocoder::{ 2 Geocoder, GeocoderCountryListResult, GeocoderError, GeocoderPoint, GeocoderReverseOptions, 3 }; 4 use rusqlite::Connection; 5 use std::fs; 6 use std::path::Path; 7 use tempfile::NamedTempFile; 8 9 #[test] 10 fn reverse_returns_nearest_match_by_default() { 11 let geocoder = open_fixture_geocoder(); 12 13 let results = geocoder 14 .reverse( 15 GeocoderPoint { 16 lat: 37.7749, 17 lng: -122.4194, 18 }, 19 None, 20 ) 21 .expect("reverse query"); 22 23 assert_eq!(results.len(), 1); 24 assert_eq!(results[0].id, 1); 25 assert_eq!(results[0].name, "San Francisco"); 26 assert_eq!(results[0].country_id, "US"); 27 assert_eq!(results[0].admin1_id, Some(6)); 28 assert_eq!(results[0].admin1_name.as_deref(), Some("California")); 29 } 30 31 #[test] 32 fn reverse_respects_limit_and_returns_sorted_matches() { 33 let geocoder = open_fixture_geocoder(); 34 35 let results = geocoder 36 .reverse( 37 GeocoderPoint { 38 lat: 37.7749, 39 lng: -122.4194, 40 }, 41 Some(GeocoderReverseOptions { 42 limit: 2, 43 degree_offset: 10.0, 44 }), 45 ) 46 .expect("reverse query"); 47 48 assert_eq!(results.len(), 2); 49 assert_eq!(results[0].id, 1); 50 assert_eq!(results[1].id, 2); 51 } 52 53 #[test] 54 fn reverse_orders_high_latitude_results_by_scaled_longitude_distance() { 55 let geocoder = open_high_latitude_geocoder(); 56 57 let results = geocoder 58 .reverse( 59 GeocoderPoint { 60 lat: 75.0, 61 lng: 0.0, 62 }, 63 Some(GeocoderReverseOptions { 64 limit: 2, 65 degree_offset: 1.0, 66 }), 67 ) 68 .expect("reverse query"); 69 70 assert_eq!(results.len(), 2); 71 assert_eq!(results[0].id, 1); 72 assert_eq!(results[0].name, "Polar East"); 73 assert_eq!(results[1].id, 2); 74 assert_eq!(results[1].name, "Polar North"); 75 } 76 77 #[test] 78 fn open_bytes_supports_reverse_queries() { 79 let path = build_fixture_database(); 80 let bytes = fs::read(&path).expect("read fixture database bytes"); 81 let geocoder = Geocoder::open_bytes(&bytes).expect("open byte-backed geocoder"); 82 83 let results = geocoder 84 .reverse( 85 GeocoderPoint { 86 lat: 34.0522, 87 lng: -118.2437, 88 }, 89 None, 90 ) 91 .expect("reverse query"); 92 93 assert_eq!(results.len(), 1); 94 assert_eq!(results[0].id, 2); 95 assert_eq!(results[0].name, "Los Angeles"); 96 } 97 98 #[test] 99 fn open_path_supports_string_and_path_ref_inputs() { 100 let path = build_fixture_database(); 101 let path_str = path.to_str().expect("utf-8 fixture path"); 102 103 let geocoder_from_str = Geocoder::open_path(path_str).expect("open geocoder from string path"); 104 let string_results = geocoder_from_str 105 .country("US") 106 .expect("country query from string-path geocoder"); 107 assert_eq!(string_results.len(), 3); 108 109 let geocoder_from_path = 110 Geocoder::open_path(Path::new(path_str)).expect("open geocoder from path ref"); 111 let path_results = geocoder_from_path 112 .country("US") 113 .expect("country query from path-ref geocoder"); 114 assert_eq!(path_results.len(), 3); 115 } 116 117 #[test] 118 fn open_path_supports_pathbuf_inputs() { 119 let temp_path = build_fixture_database(); 120 let path = temp_path.to_path_buf(); 121 122 let geocoder_from_pathbuf = 123 Geocoder::open_path(path.clone()).expect("open geocoder from pathbuf"); 124 let pathbuf_results = geocoder_from_pathbuf 125 .country("US") 126 .expect("country query from pathbuf geocoder"); 127 assert_eq!(pathbuf_results.len(), 3); 128 129 let geocoder_from_pathbuf_ref = 130 Geocoder::open_path(&path).expect("open geocoder from pathbuf ref"); 131 let pathbuf_ref_results = geocoder_from_pathbuf_ref 132 .country("US") 133 .expect("country query from pathbuf-ref geocoder"); 134 assert_eq!(pathbuf_ref_results.len(), 3); 135 } 136 137 #[test] 138 fn country_returns_all_rows_for_requested_country() { 139 let geocoder = open_fixture_geocoder(); 140 141 let results = geocoder.country("US").expect("country query"); 142 143 assert_eq!(results.len(), 3); 144 assert!(results.iter().all(|result| result.country_id == "US")); 145 } 146 147 #[test] 148 fn country_list_returns_average_centers() { 149 let geocoder = open_fixture_geocoder(); 150 151 let results = geocoder.country_list().expect("country list query"); 152 153 assert_eq!(results.len(), 2); 154 assert_eq!( 155 results[0], 156 GeocoderCountryListResult { 157 country_id: "BR".to_owned(), 158 country: Some("Brazil".to_owned()), 159 lat: -23.5505, 160 lng: -46.6333, 161 } 162 ); 163 assert_eq!(results[1].country_id, "US"); 164 assert_eq!(results[1].country.as_deref(), Some("United States")); 165 assert!(approx_eq( 166 results[1].lat, 167 (37.7749 + 34.0522 + 40.7128) / 3.0 168 )); 169 assert!(approx_eq( 170 results[1].lng, 171 (-122.4194 + -118.2437 + -74.0060) / 3.0 172 )); 173 } 174 175 #[test] 176 fn country_center_returns_average_for_country() { 177 let geocoder = open_fixture_geocoder(); 178 179 let result = geocoder.country_center("US").expect("country center query"); 180 181 assert!(approx_eq(result.lat, (37.7749 + 34.0522 + 40.7128) / 3.0)); 182 assert!(approx_eq( 183 result.lng, 184 (-122.4194 + -118.2437 + -74.0060) / 3.0 185 )); 186 } 187 188 #[test] 189 fn country_center_returns_not_found_for_missing_country() { 190 let geocoder = open_fixture_geocoder(); 191 192 let err = geocoder 193 .country_center("ZZ") 194 .expect_err("missing country should return not found"); 195 assert_country_center_not_found(err, "ZZ"); 196 } 197 198 #[test] 199 fn reverse_country_and_country_list_report_missing_schema_errors() { 200 let geocoder = open_empty_geocoder(); 201 202 let reverse_err = geocoder 203 .reverse( 204 GeocoderPoint { 205 lat: 37.7749, 206 lng: -122.4194, 207 }, 208 None, 209 ) 210 .expect_err("reverse should fail without schema"); 211 assert_sqlite_error_contains(reverse_err, "no such"); 212 213 let country_err = geocoder 214 .country("US") 215 .expect_err("country should fail without schema"); 216 assert_sqlite_error_contains(country_err, "no such"); 217 218 let country_list_err = geocoder 219 .country_list() 220 .expect_err("country_list should fail without schema"); 221 assert_sqlite_error_contains(country_list_err, "no such"); 222 } 223 224 #[test] 225 fn country_center_reports_missing_schema_errors() { 226 let geocoder = open_empty_geocoder(); 227 228 let err = geocoder 229 .country_center("US") 230 .expect_err("country_center should fail without schema"); 231 assert_sqlite_error_contains(err, "no such"); 232 } 233 234 #[test] 235 fn reverse_and_country_propagate_row_mapping_errors() { 236 let geocoder = open_reverse_country_row_error_geocoder(); 237 238 let reverse_err = geocoder 239 .reverse( 240 GeocoderPoint { 241 lat: 37.7749, 242 lng: -122.4194, 243 }, 244 Some(GeocoderReverseOptions { 245 limit: 1, 246 degree_offset: 10.0, 247 }), 248 ) 249 .expect_err("reverse should fail on invalid row mapping"); 250 assert_sqlite_error_contains(reverse_err, "Invalid column type"); 251 252 let country_err = geocoder 253 .country("US") 254 .expect_err("country should fail on invalid row mapping"); 255 assert_sqlite_error_contains(country_err, "Invalid column type"); 256 } 257 258 #[test] 259 fn country_list_propagates_aggregate_row_mapping_errors() { 260 let geocoder = open_country_list_row_error_geocoder(); 261 262 let err = geocoder 263 .country_list() 264 .expect_err("country_list should fail on null aggregate row"); 265 assert_sqlite_error_contains(err, "Invalid column type"); 266 } 267 268 fn open_fixture_geocoder() -> Geocoder { 269 let path = build_fixture_database(); 270 Geocoder::open_path(&path).expect("open geocoder") 271 } 272 273 fn open_high_latitude_geocoder() -> Geocoder { 274 let path = build_high_latitude_database(); 275 Geocoder::open_path(&path).expect("open geocoder") 276 } 277 278 fn open_empty_geocoder() -> Geocoder { 279 let temp = NamedTempFile::new().expect("temp db"); 280 let path = temp.into_temp_path(); 281 Geocoder::open_path(&path).expect("open empty geocoder") 282 } 283 284 fn open_reverse_country_row_error_geocoder() -> Geocoder { 285 let temp = NamedTempFile::new().expect("temp db"); 286 let path = temp.into_temp_path(); 287 seed_reverse_country_row_error_database(path.to_str().expect("utf-8 temp path")); 288 Geocoder::open_path(&path).expect("open invalid row geocoder") 289 } 290 291 fn open_country_list_row_error_geocoder() -> Geocoder { 292 let temp = NamedTempFile::new().expect("temp db"); 293 let path = temp.into_temp_path(); 294 seed_country_list_row_error_database(path.to_str().expect("utf-8 temp path")); 295 Geocoder::open_path(&path).expect("open aggregate error geocoder") 296 } 297 298 fn build_fixture_database() -> tempfile::TempPath { 299 let temp = NamedTempFile::new().expect("temp db"); 300 let path = temp.into_temp_path(); 301 seed_fixture_database(path.to_str().expect("utf-8 temp path")); 302 path 303 } 304 305 fn build_high_latitude_database() -> tempfile::TempPath { 306 let temp = NamedTempFile::new().expect("temp db"); 307 let path = temp.into_temp_path(); 308 seed_high_latitude_database(path.to_str().expect("utf-8 temp path")); 309 path 310 } 311 312 fn seed_fixture_database(path: &str) { 313 let conn = Connection::open(path).expect("open fixture database"); 314 seed_schema(&conn); 315 316 insert_country(&conn, "US", "United States"); 317 insert_country(&conn, "BR", "Brazil"); 318 319 insert_admin1(&conn, "US", 6, "California"); 320 insert_admin1(&conn, "US", 36, "New York"); 321 insert_admin1(&conn, "BR", 27, "Sao Paulo"); 322 323 insert_feature(&conn, 1, "San Francisco", "US", 6, 37.7749, -122.4194); 324 insert_feature(&conn, 2, "Los Angeles", "US", 6, 34.0522, -118.2437); 325 insert_feature(&conn, 3, "New York City", "US", 36, 40.7128, -74.0060); 326 insert_feature(&conn, 4, "Sao Paulo", "BR", 27, -23.5505, -46.6333); 327 } 328 329 fn seed_high_latitude_database(path: &str) { 330 let conn = Connection::open(path).expect("open fixture database"); 331 seed_schema(&conn); 332 333 insert_country(&conn, "NO", "Norway"); 334 insert_admin1(&conn, "NO", 1, "Nord"); 335 336 insert_feature(&conn, 1, "Polar East", "NO", 1, 75.02, 0.10); 337 insert_feature(&conn, 2, "Polar North", "NO", 1, 75.05, 0.05); 338 } 339 340 fn seed_reverse_country_row_error_database(path: &str) { 341 let conn = Connection::open(path).expect("open invalid row fixture database"); 342 conn.execute_batch( 343 r#" 344 CREATE TABLE geonames( 345 id INTEGER, 346 name TEXT, 347 admin1_id INTEGER, 348 admin1_name TEXT, 349 country_id TEXT, 350 country_name TEXT, 351 latitude REAL, 352 longitude REAL 353 ); 354 CREATE TABLE coordinates( 355 feature_id INTEGER, 356 latitude REAL, 357 longitude REAL 358 ); 359 "#, 360 ) 361 .expect("create invalid row schema"); 362 conn.execute( 363 "INSERT INTO geonames (id, name, admin1_id, admin1_name, country_id, country_name, latitude, longitude) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", 364 rusqlite::params![1_i64, Option::<String>::None, Option::<i64>::None, Option::<String>::None, "US", "United States", 37.7749_f64, -122.4194_f64], 365 ) 366 .expect("insert invalid reverse/country row"); 367 conn.execute( 368 "INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (?1, ?2, ?3)", 369 (1_i64, 37.7749_f64, -122.4194_f64), 370 ) 371 .expect("insert invalid reverse/country coordinate"); 372 } 373 374 fn seed_country_list_row_error_database(path: &str) { 375 let conn = Connection::open(path).expect("open aggregate error fixture database"); 376 conn.execute_batch( 377 r#" 378 CREATE TABLE geonames( 379 country_id TEXT, 380 country_name TEXT, 381 latitude REAL, 382 longitude REAL 383 ); 384 "#, 385 ) 386 .expect("create aggregate error schema"); 387 conn.execute( 388 "INSERT INTO geonames (country_id, country_name, latitude, longitude) VALUES (?1, ?2, ?3, ?4)", 389 rusqlite::params!["US", "United States", Option::<f64>::None, Option::<f64>::None], 390 ) 391 .expect("insert aggregate error row"); 392 } 393 394 fn seed_schema(conn: &Connection) { 395 conn.execute_batch( 396 r#" 397 CREATE TABLE countries( 398 id TEXT, 399 name TEXT, 400 PRIMARY KEY (id) 401 ); 402 CREATE TABLE admin1( 403 country_id TEXT, 404 id INTEGER, 405 name TEXT, 406 PRIMARY KEY (country_id, id) 407 ); 408 CREATE TABLE features( 409 id INTEGER, 410 name TEXT, 411 country_id TEXT, 412 admin1_id INTEGER, 413 PRIMARY KEY (id) 414 ); 415 CREATE TABLE coordinates( 416 feature_id INTEGER, 417 latitude REAL, 418 longitude REAL, 419 PRIMARY KEY (feature_id) 420 ); 421 CREATE INDEX coordinates_lat_lng ON coordinates (latitude, longitude); 422 CREATE VIEW geonames AS 423 SELECT 424 features.id, 425 features.name, 426 admin1.id AS admin1_id, 427 admin1.name AS admin1_name, 428 countries.id AS country_id, 429 countries.name AS country_name, 430 coordinates.latitude AS latitude, 431 coordinates.longitude AS longitude 432 FROM features 433 LEFT JOIN countries ON features.country_id = countries.id 434 LEFT JOIN admin1 ON features.country_id = admin1.country_id AND features.admin1_id = admin1.id 435 JOIN coordinates ON features.id = coordinates.feature_id; 436 "#, 437 ) 438 .expect("create fixture schema"); 439 } 440 441 fn insert_country(conn: &Connection, id: &str, name: &str) { 442 conn.execute( 443 "INSERT INTO countries (id, name) VALUES (?1, ?2)", 444 (id, name), 445 ) 446 .expect("insert country"); 447 } 448 449 fn insert_admin1(conn: &Connection, country_id: &str, id: i64, name: &str) { 450 conn.execute( 451 "INSERT INTO admin1 (country_id, id, name) VALUES (?1, ?2, ?3)", 452 (country_id, id, name), 453 ) 454 .expect("insert admin1"); 455 } 456 457 fn insert_feature( 458 conn: &Connection, 459 id: i64, 460 name: &str, 461 country_id: &str, 462 admin1_id: i64, 463 latitude: f64, 464 longitude: f64, 465 ) { 466 conn.execute( 467 "INSERT INTO features (id, name, country_id, admin1_id) VALUES (?1, ?2, ?3, ?4)", 468 (id, name, country_id, admin1_id), 469 ) 470 .expect("insert feature"); 471 conn.execute( 472 "INSERT INTO coordinates (feature_id, latitude, longitude) VALUES (?1, ?2, ?3)", 473 (id, latitude, longitude), 474 ) 475 .expect("insert coordinate"); 476 } 477 478 fn approx_eq(left: f64, right: f64) -> bool { 479 (left - right).abs() < 0.000_001 480 } 481 482 fn assert_sqlite_error_contains(err: GeocoderError, needle: &str) { 483 match err { 484 GeocoderError::Sqlite(inner) => assert!( 485 inner.to_string().contains(needle), 486 "expected sqlite error containing {needle:?}, got {inner}" 487 ), 488 other => panic!("expected sqlite error, got {other}"), 489 } 490 } 491 492 fn assert_country_center_not_found(err: GeocoderError, country_id: &str) { 493 match err { 494 GeocoderError::CountryCenterNotFound { country_id: actual } => { 495 assert_eq!(actual, country_id); 496 } 497 other => panic!("expected CountryCenterNotFound, got {other}"), 498 } 499 }