1use flate2::read::GzDecoder;
25use once_cell::sync::Lazy;
26use serde::{Deserialize, Deserializer, Serialize};
27use std::collections::HashMap;
28use std::f64::consts::PI;
29use std::io::Read;
30
31static COMPRESSED_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/airports.json.gz"));
33
34static AIRPORTS: Lazy<Vec<Airport>> = Lazy::new(|| {
35 let mut decoder = GzDecoder::new(COMPRESSED_DATA);
36 let mut json_str = String::new();
37 decoder
38 .read_to_string(&mut json_str)
39 .expect("Failed to decompress airport data");
40 serde_json::from_str(&json_str).expect("Failed to parse airport data")
41});
42
43fn deserialize_opt_i64<'de, D>(deserializer: D) -> std::result::Result<Option<i64>, D::Error>
49where
50 D: Deserializer<'de>,
51{
52 #[derive(Deserialize)]
53 #[serde(untagged)]
54 enum NumOrStr {
55 Int(i64),
56 Float(f64),
57 Str(String),
58 }
59 match NumOrStr::deserialize(deserializer)? {
60 NumOrStr::Int(n) => Ok(Some(n)),
61 NumOrStr::Float(f) => Ok(Some(f as i64)),
62 NumOrStr::Str(s) if s.is_empty() => Ok(None),
63 NumOrStr::Str(s) => s.parse::<i64>().ok().map_or(Ok(None), |n| Ok(Some(n))),
64 }
65}
66
67fn deserialize_opt_f64<'de, D>(deserializer: D) -> std::result::Result<Option<f64>, D::Error>
69where
70 D: Deserializer<'de>,
71{
72 #[derive(Deserialize)]
73 #[serde(untagged)]
74 enum NumOrStr {
75 Int(i64),
76 Float(f64),
77 Str(String),
78 }
79 match NumOrStr::deserialize(deserializer)? {
80 NumOrStr::Int(n) => Ok(Some(n as f64)),
81 NumOrStr::Float(f) => Ok(Some(f)),
82 NumOrStr::Str(s) if s.is_empty() => Ok(None),
83 NumOrStr::Str(s) => s.parse::<f64>().ok().map_or(Ok(None), |n| Ok(Some(n))),
84 }
85}
86
87fn deserialize_string_or_int<'de, D>(deserializer: D) -> std::result::Result<String, D::Error>
89where
90 D: Deserializer<'de>,
91{
92 #[derive(Deserialize)]
93 #[serde(untagged)]
94 enum StringOrInt {
95 Str(String),
96 Int(i64),
97 }
98 match StringOrInt::deserialize(deserializer)? {
99 StringOrInt::Str(s) => Ok(s),
100 StringOrInt::Int(n) => Ok(n.to_string()),
101 }
102}
103
104fn deserialize_bool_from_string<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error>
106where
107 D: Deserializer<'de>,
108{
109 let s = String::deserialize(deserializer)?;
110 Ok(s.eq_ignore_ascii_case("true") || s.eq_ignore_ascii_case("yes"))
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct Airport {
120 pub iata: String,
122 #[serde(deserialize_with = "deserialize_string_or_int")]
124 pub icao: String,
125 #[serde(rename = "time")]
127 pub timezone: String,
128 #[serde(deserialize_with = "deserialize_opt_f64")]
130 pub utc: Option<f64>,
131 pub country_code: String,
133 pub continent: String,
135 pub airport: String,
137 pub latitude: f64,
139 pub longitude: f64,
141 #[serde(deserialize_with = "deserialize_opt_i64")]
143 pub elevation_ft: Option<i64>,
144 #[serde(rename = "type")]
146 pub airport_type: String,
147 #[serde(deserialize_with = "deserialize_bool_from_string")]
149 pub scheduled_service: bool,
150 #[serde(default)]
152 pub wikipedia: String,
153 #[serde(default)]
155 pub website: String,
156 #[serde(deserialize_with = "deserialize_opt_i64")]
158 pub runway_length: Option<i64>,
159 #[serde(default)]
161 pub flightradar24_url: String,
162 #[serde(default)]
164 pub radarbox_url: String,
165 #[serde(default)]
167 pub flightaware_url: String,
168}
169
170#[derive(Debug, Clone, Serialize)]
176pub struct AirportLinks {
177 pub website: Option<String>,
178 pub wikipedia: Option<String>,
179 pub flightradar24: Option<String>,
180 pub radarbox: Option<String>,
181 pub flightaware: Option<String>,
182}
183
184#[derive(Debug, Clone, Serialize)]
186pub struct CountryStats {
187 pub total: usize,
188 pub by_type: HashMap<String, usize>,
189 pub with_scheduled_service: usize,
190 pub average_runway_length: f64,
191 pub average_elevation: f64,
192 pub timezones: Vec<String>,
193}
194
195#[derive(Debug, Clone, Serialize)]
197pub struct ContinentStats {
198 pub total: usize,
199 pub by_type: HashMap<String, usize>,
200 pub by_country: HashMap<String, usize>,
201 pub with_scheduled_service: usize,
202 pub average_runway_length: f64,
203 pub average_elevation: f64,
204 pub timezones: Vec<String>,
205}
206
207#[derive(Debug, Clone, Serialize)]
209pub struct AirportInfo {
210 pub code: String,
211 pub name: String,
212 pub iata: String,
213 pub icao: String,
214}
215
216#[derive(Debug, Clone, Serialize)]
218pub struct DistanceMatrix {
219 pub airports: Vec<AirportInfo>,
220 pub distances: HashMap<String, HashMap<String, f64>>,
221}
222
223#[derive(Debug, Clone)]
225pub struct NearbyAirport {
226 pub airport: Airport,
227 pub distance: f64,
229}
230
231#[derive(Debug, Clone, Default)]
233pub struct AirportFilter {
234 pub country_code: Option<String>,
235 pub continent: Option<String>,
236 pub airport_type: Option<String>,
237 pub has_scheduled_service: Option<bool>,
238 pub min_runway_ft: Option<i64>,
239}
240
241#[derive(Debug, Clone)]
243pub enum AirportError {
244 NotFound(String),
246 InvalidInput(String),
248}
249
250impl std::fmt::Display for AirportError {
251 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252 match self {
253 AirportError::NotFound(msg) => write!(f, "{}", msg),
254 AirportError::InvalidInput(msg) => write!(f, "{}", msg),
255 }
256 }
257}
258
259impl std::error::Error for AirportError {}
260
261pub type Result<T> = std::result::Result<T, AirportError>;
262
263fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
268 const EARTH_RADIUS_KM: f64 = 6371.0;
269 let d_lat = (lat2 - lat1) * PI / 180.0;
270 let d_lon = (lon2 - lon1) * PI / 180.0;
271 let lat1_rad = lat1 * PI / 180.0;
272 let lat2_rad = lat2 * PI / 180.0;
273
274 let a =
275 (d_lat / 2.0).sin().powi(2) + lat1_rad.cos() * lat2_rad.cos() * (d_lon / 2.0).sin().powi(2);
276 let c = 2.0 * a.sqrt().asin();
277 EARTH_RADIUS_KM * c
278}
279
280fn type_matches(airport_type: &str, filter_type: &str) -> bool {
285 let filter_lower = filter_type.to_lowercase();
286 if filter_lower == "airport" {
287 let at = airport_type.to_lowercase();
288 at == "large_airport" || at == "medium_airport" || at == "small_airport"
289 } else {
290 airport_type.eq_ignore_ascii_case(&filter_lower)
291 }
292}
293
294fn matches_filter(airport: &Airport, filter: &AirportFilter) -> bool {
296 if let Some(ref cc) = filter.country_code {
297 if !airport.country_code.eq_ignore_ascii_case(cc) {
298 return false;
299 }
300 }
301 if let Some(ref cont) = filter.continent {
302 if !airport.continent.eq_ignore_ascii_case(cont) {
303 return false;
304 }
305 }
306 if let Some(ref t) = filter.airport_type {
307 if !type_matches(&airport.airport_type, t) {
308 return false;
309 }
310 }
311 if let Some(has_service) = filter.has_scheduled_service {
312 if airport.scheduled_service != has_service {
313 return false;
314 }
315 }
316 if let Some(min_runway) = filter.min_runway_ft {
317 let runway = airport.runway_length.unwrap_or(0);
318 if runway < min_runway {
319 return false;
320 }
321 }
322 true
323}
324
325fn non_empty(s: &str) -> Option<String> {
326 if s.is_empty() {
327 None
328 } else {
329 Some(s.to_string())
330 }
331}
332
333pub struct AirportData;
343
344impl AirportData {
345 pub fn new() -> Self {
349 AirportData
350 }
351
352 pub fn all_airports(&self) -> &[Airport] {
354 &AIRPORTS
355 }
356
357 pub fn get_airport_by_iata(&self, iata_code: &str) -> Result<&Airport> {
373 let code = iata_code.trim().to_uppercase();
374 AIRPORTS
375 .iter()
376 .find(|a| a.iata.eq_ignore_ascii_case(&code))
377 .ok_or_else(|| {
378 AirportError::NotFound(format!("No data found for IATA code: {}", iata_code))
379 })
380 }
381
382 pub fn get_airports_by_iata(&self, iata_code: &str) -> Vec<&Airport> {
386 let code = iata_code.trim().to_uppercase();
387 AIRPORTS
388 .iter()
389 .filter(|a| a.iata.eq_ignore_ascii_case(&code))
390 .collect()
391 }
392
393 pub fn get_airport_by_icao(&self, icao_code: &str) -> Result<&Airport> {
397 let code = icao_code.trim().to_uppercase();
398 AIRPORTS
399 .iter()
400 .find(|a| a.icao.eq_ignore_ascii_case(&code))
401 .ok_or_else(|| {
402 AirportError::NotFound(format!("No data found for ICAO code: {}", icao_code))
403 })
404 }
405
406 pub fn get_airports_by_icao(&self, icao_code: &str) -> Vec<&Airport> {
408 let code = icao_code.trim().to_uppercase();
409 AIRPORTS
410 .iter()
411 .filter(|a| a.icao.eq_ignore_ascii_case(&code))
412 .collect()
413 }
414
415 pub fn search_by_name(&self, query: &str) -> Result<Vec<&Airport>> {
419 let q = query.trim();
420 if q.len() < 2 {
421 return Err(AirportError::InvalidInput(
422 "Search query must be at least 2 characters".to_string(),
423 ));
424 }
425 let lower = q.to_lowercase();
426 let results: Vec<&Airport> = AIRPORTS
427 .iter()
428 .filter(|a| a.airport.to_lowercase().contains(&lower))
429 .collect();
430 Ok(results)
431 }
432
433 pub fn find_nearby_airports(&self, lat: f64, lon: f64, radius_km: f64) -> Vec<NearbyAirport> {
441 let mut results: Vec<NearbyAirport> = AIRPORTS
442 .iter()
443 .filter_map(|a| {
444 let dist = haversine_distance(lat, lon, a.latitude, a.longitude);
445 if dist <= radius_km {
446 Some(NearbyAirport {
447 airport: a.clone(),
448 distance: dist,
449 })
450 } else {
451 None
452 }
453 })
454 .collect();
455 results.sort_by(|a, b| {
456 a.distance
457 .partial_cmp(&b.distance)
458 .unwrap_or(std::cmp::Ordering::Equal)
459 });
460 results
461 }
462
463 pub fn calculate_distance(&self, code1: &str, code2: &str) -> Result<f64> {
467 let a1 = self.resolve_airport(code1)?;
468 let a2 = self.resolve_airport(code2)?;
469 Ok(haversine_distance(
470 a1.latitude,
471 a1.longitude,
472 a2.latitude,
473 a2.longitude,
474 ))
475 }
476
477 pub fn find_nearest_airport(
481 &self,
482 lat: f64,
483 lon: f64,
484 filter: Option<&AirportFilter>,
485 ) -> Result<NearbyAirport> {
486 let mut best: Option<NearbyAirport> = None;
487
488 for airport in AIRPORTS.iter() {
489 if let Some(f) = filter {
490 if !matches_filter(airport, f) {
491 continue;
492 }
493 }
494 let dist = haversine_distance(lat, lon, airport.latitude, airport.longitude);
495 let is_closer = match &best {
496 Some(b) => dist < b.distance,
497 None => true,
498 };
499 if is_closer {
500 best = Some(NearbyAirport {
501 airport: airport.clone(),
502 distance: dist,
503 });
504 }
505 }
506
507 best.ok_or_else(|| {
508 AirportError::NotFound("No airport found matching the criteria".to_string())
509 })
510 }
511
512 pub fn get_airports_by_country_code(&self, country_code: &str) -> Vec<&Airport> {
518 let cc = country_code.trim().to_uppercase();
519 AIRPORTS
520 .iter()
521 .filter(|a| a.country_code.eq_ignore_ascii_case(&cc))
522 .collect()
523 }
524
525 pub fn get_airports_by_continent(&self, continent_code: &str) -> Vec<&Airport> {
529 let cc = continent_code.trim().to_uppercase();
530 AIRPORTS
531 .iter()
532 .filter(|a| a.continent.eq_ignore_ascii_case(&cc))
533 .collect()
534 }
535
536 pub fn get_airports_by_type(&self, airport_type: &str) -> Vec<&Airport> {
542 AIRPORTS
543 .iter()
544 .filter(|a| type_matches(&a.airport_type, airport_type))
545 .collect()
546 }
547
548 pub fn get_airports_by_timezone(&self, timezone: &str) -> Vec<&Airport> {
550 AIRPORTS.iter().filter(|a| a.timezone == timezone).collect()
551 }
552
553 pub fn find_airports(&self, filter: &AirportFilter) -> Vec<&Airport> {
555 AIRPORTS
556 .iter()
557 .filter(|a| matches_filter(a, filter))
558 .collect()
559 }
560
561 pub fn get_autocomplete_suggestions(&self, query: &str) -> Vec<&Airport> {
570 let q = query.trim();
571 if q.len() < 2 {
572 return Vec::new();
573 }
574 let lower = q.to_lowercase();
575 AIRPORTS
576 .iter()
577 .filter(|a| {
578 a.airport.to_lowercase().contains(&lower) || a.iata.to_lowercase().contains(&lower)
579 })
580 .take(10)
581 .collect()
582 }
583
584 pub fn get_airport_links(&self, code: &str) -> Result<AirportLinks> {
588 let airport = self.resolve_airport(code)?;
589 Ok(AirportLinks {
590 website: non_empty(&airport.website),
591 wikipedia: non_empty(&airport.wikipedia),
592 flightradar24: non_empty(&airport.flightradar24_url),
593 radarbox: non_empty(&airport.radarbox_url),
594 flightaware: non_empty(&airport.flightaware_url),
595 })
596 }
597
598 pub fn get_airport_stats_by_country(&self, country_code: &str) -> Result<CountryStats> {
604 let airports = self.get_airports_by_country_code(country_code);
605 if airports.is_empty() {
606 return Err(AirportError::NotFound(format!(
607 "No airports found for country code: {}",
608 country_code
609 )));
610 }
611
612 let mut by_type: HashMap<String, usize> = HashMap::new();
613 let mut with_scheduled_service = 0usize;
614 let mut runway_sum = 0f64;
615 let mut runway_count = 0usize;
616 let mut elevation_sum = 0f64;
617 let mut elevation_count = 0usize;
618 let mut tz_set: Vec<String> = Vec::new();
619
620 for a in &airports {
621 *by_type.entry(a.airport_type.clone()).or_insert(0) += 1;
622 if a.scheduled_service {
623 with_scheduled_service += 1;
624 }
625 if let Some(r) = a.runway_length {
626 if r > 0 {
627 runway_sum += r as f64;
628 runway_count += 1;
629 }
630 }
631 if let Some(e) = a.elevation_ft {
632 elevation_sum += e as f64;
633 elevation_count += 1;
634 }
635 if !tz_set.contains(&a.timezone) {
636 tz_set.push(a.timezone.clone());
637 }
638 }
639
640 tz_set.sort();
641
642 Ok(CountryStats {
643 total: airports.len(),
644 by_type,
645 with_scheduled_service,
646 average_runway_length: if runway_count > 0 {
647 (runway_sum / runway_count as f64).round()
648 } else {
649 0.0
650 },
651 average_elevation: if elevation_count > 0 {
652 (elevation_sum / elevation_count as f64).round()
653 } else {
654 0.0
655 },
656 timezones: tz_set,
657 })
658 }
659
660 pub fn get_airport_stats_by_continent(&self, continent_code: &str) -> Result<ContinentStats> {
662 let airports = self.get_airports_by_continent(continent_code);
663 if airports.is_empty() {
664 return Err(AirportError::NotFound(format!(
665 "No airports found for continent code: {}",
666 continent_code
667 )));
668 }
669
670 let mut by_type: HashMap<String, usize> = HashMap::new();
671 let mut by_country: HashMap<String, usize> = HashMap::new();
672 let mut with_scheduled_service = 0usize;
673 let mut runway_sum = 0f64;
674 let mut runway_count = 0usize;
675 let mut elevation_sum = 0f64;
676 let mut elevation_count = 0usize;
677 let mut tz_set: Vec<String> = Vec::new();
678
679 for a in &airports {
680 *by_type.entry(a.airport_type.clone()).or_insert(0) += 1;
681 *by_country.entry(a.country_code.clone()).or_insert(0) += 1;
682 if a.scheduled_service {
683 with_scheduled_service += 1;
684 }
685 if let Some(r) = a.runway_length {
686 if r > 0 {
687 runway_sum += r as f64;
688 runway_count += 1;
689 }
690 }
691 if let Some(e) = a.elevation_ft {
692 elevation_sum += e as f64;
693 elevation_count += 1;
694 }
695 if !tz_set.contains(&a.timezone) {
696 tz_set.push(a.timezone.clone());
697 }
698 }
699
700 tz_set.sort();
701
702 Ok(ContinentStats {
703 total: airports.len(),
704 by_type,
705 by_country,
706 with_scheduled_service,
707 average_runway_length: if runway_count > 0 {
708 (runway_sum / runway_count as f64).round()
709 } else {
710 0.0
711 },
712 average_elevation: if elevation_count > 0 {
713 (elevation_sum / elevation_count as f64).round()
714 } else {
715 0.0
716 },
717 timezones: tz_set,
718 })
719 }
720
721 pub fn get_largest_airports_by_continent(
726 &self,
727 continent_code: &str,
728 limit: usize,
729 sort_by: &str,
730 ) -> Vec<Airport> {
731 let mut airports: Vec<Airport> = self
732 .get_airports_by_continent(continent_code)
733 .into_iter()
734 .cloned()
735 .collect();
736
737 match sort_by.to_lowercase().as_str() {
738 "elevation" => {
739 airports.sort_by(|a, b| {
740 let ea = a.elevation_ft.unwrap_or(0);
741 let eb = b.elevation_ft.unwrap_or(0);
742 eb.cmp(&ea)
743 });
744 }
745 _ => {
746 airports.sort_by(|a, b| {
748 let ra = a.runway_length.unwrap_or(0);
749 let rb = b.runway_length.unwrap_or(0);
750 rb.cmp(&ra)
751 });
752 }
753 }
754
755 airports.truncate(limit);
756 airports
757 }
758
759 pub fn get_multiple_airports(&self, codes: &[&str]) -> Vec<Option<&Airport>> {
767 codes
768 .iter()
769 .map(|code| self.resolve_airport(code).ok())
770 .collect()
771 }
772
773 pub fn calculate_distance_matrix(&self, codes: &[&str]) -> Result<DistanceMatrix> {
777 if codes.len() < 2 {
778 return Err(AirportError::InvalidInput(
779 "At least 2 airport codes are required for a distance matrix".to_string(),
780 ));
781 }
782
783 let mut resolved: Vec<(&Airport, String)> = Vec::new();
784 for code in codes {
785 let airport = self.resolve_airport(code).map_err(|_| {
786 AirportError::NotFound(format!("Airport not found for code: {}", code))
787 })?;
788 resolved.push((airport, code.to_string()));
789 }
790
791 let airport_infos: Vec<AirportInfo> = resolved
792 .iter()
793 .map(|(a, code)| AirportInfo {
794 code: code.to_string(),
795 name: a.airport.clone(),
796 iata: a.iata.clone(),
797 icao: a.icao.clone(),
798 })
799 .collect();
800
801 let mut distances: HashMap<String, HashMap<String, f64>> = HashMap::new();
802 for (a1, code1) in &resolved {
803 let mut inner: HashMap<String, f64> = HashMap::new();
804 for (a2, code2) in &resolved {
805 if code1 == code2 {
806 inner.insert(code2.clone(), 0.0);
807 } else {
808 let dist =
809 haversine_distance(a1.latitude, a1.longitude, a2.latitude, a2.longitude);
810 inner.insert(code2.clone(), dist.round());
811 }
812 }
813 distances.insert(code1.clone(), inner);
814 }
815
816 Ok(DistanceMatrix {
817 airports: airport_infos,
818 distances,
819 })
820 }
821
822 pub fn validate_iata_code(&self, code: &str) -> bool {
830 let trimmed = code.trim();
831 if trimmed.len() != 3 || !trimmed.chars().all(|c| c.is_ascii_uppercase()) {
832 return false;
833 }
834 AIRPORTS
835 .iter()
836 .any(|a| a.iata.eq_ignore_ascii_case(trimmed))
837 }
838
839 pub fn validate_icao_code(&self, code: &str) -> bool {
843 let trimmed = code.trim();
844 if trimmed.len() != 4
845 || !trimmed
846 .chars()
847 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
848 {
849 return false;
850 }
851 AIRPORTS
852 .iter()
853 .any(|a| a.icao.eq_ignore_ascii_case(trimmed))
854 }
855
856 pub fn get_airport_count(&self, filter: Option<&AirportFilter>) -> usize {
860 match filter {
861 Some(f) => AIRPORTS.iter().filter(|a| matches_filter(a, f)).count(),
862 None => AIRPORTS.len(),
863 }
864 }
865
866 pub fn is_airport_operational(&self, code: &str) -> Result<bool> {
870 let airport = self.resolve_airport(code)?;
871 Ok(airport.scheduled_service)
872 }
873
874 fn resolve_airport(&self, code: &str) -> Result<&Airport> {
880 let trimmed = code.trim();
881 if let Some(a) = AIRPORTS
883 .iter()
884 .find(|a| !a.iata.is_empty() && a.iata.eq_ignore_ascii_case(trimmed))
885 {
886 return Ok(a);
887 }
888 if let Some(a) = AIRPORTS
890 .iter()
891 .find(|a| !a.icao.is_empty() && a.icao.eq_ignore_ascii_case(trimmed))
892 {
893 return Ok(a);
894 }
895 Err(AirportError::NotFound(format!(
896 "No airport found for code: {}",
897 code
898 )))
899 }
900}
901
902impl Default for AirportData {
903 fn default() -> Self {
904 Self::new()
905 }
906}
907
908#[cfg(test)]
913mod tests {
914 use super::*;
915
916 fn db() -> AirportData {
917 AirportData::new()
918 }
919
920 #[test]
924 fn test_get_airport_by_iata_lhr() {
925 let d = db();
926 let airport = d.get_airport_by_iata("LHR").unwrap();
927 assert_eq!(airport.iata, "LHR");
928 assert!(airport.airport.contains("Heathrow"));
929 }
930
931 #[test]
935 fn test_get_airport_by_icao_egll() {
936 let d = db();
937 let airport = d.get_airport_by_icao("EGLL").unwrap();
938 assert_eq!(airport.icao, "EGLL");
939 assert!(airport.airport.contains("Heathrow"));
940 }
941
942 #[test]
946 fn test_get_airports_by_country_code_us() {
947 let d = db();
948 let airports = d.get_airports_by_country_code("US");
949 assert!(airports.len() > 100);
950 assert_eq!(airports[0].country_code, "US");
951 }
952
953 #[test]
957 fn test_get_airports_by_continent_eu() {
958 let d = db();
959 let airports = d.get_airports_by_continent("EU");
960 assert!(airports.len() > 100);
961 assert!(airports.iter().all(|a| a.continent == "EU"));
962 }
963
964 #[test]
968 fn test_find_nearby_airports_london() {
969 let d = db();
970 let nearby = d.find_nearby_airports(51.5074, -0.1278, 50.0);
971 assert!(!nearby.is_empty());
972 assert!(nearby.iter().any(|n| n.airport.iata == "LHR"));
973 }
974
975 #[test]
979 fn test_get_airports_by_type_large() {
980 let d = db();
981 let airports = d.get_airports_by_type("large_airport");
982 assert!(airports.len() > 10);
983 assert!(airports.iter().all(|a| a.airport_type == "large_airport"));
984 }
985
986 #[test]
987 fn test_get_airports_by_type_medium() {
988 let d = db();
989 let airports = d.get_airports_by_type("medium_airport");
990 assert!(airports.len() > 10);
991 assert!(airports.iter().all(|a| a.airport_type == "medium_airport"));
992 }
993
994 #[test]
995 fn test_get_airports_by_type_airport_generic() {
996 let d = db();
997 let airports = d.get_airports_by_type("airport");
998 assert!(airports.len() > 50);
999 assert!(airports.iter().all(|a| a.airport_type.contains("airport")));
1000 }
1001
1002 #[test]
1003 fn test_get_airports_by_type_heliport() {
1004 let d = db();
1005 let heliports = d.get_airports_by_type("heliport");
1006 for h in &heliports {
1007 assert_eq!(h.airport_type, "heliport");
1008 }
1009 }
1010
1011 #[test]
1012 fn test_get_airports_by_type_seaplane_base() {
1013 let d = db();
1014 let bases = d.get_airports_by_type("seaplane_base");
1015 for b in &bases {
1016 assert_eq!(b.airport_type, "seaplane_base");
1017 }
1018 }
1019
1020 #[test]
1021 fn test_get_airports_by_type_case_insensitive() {
1022 let d = db();
1023 let upper = d.get_airports_by_type("LARGE_AIRPORT");
1024 let lower = d.get_airports_by_type("large_airport");
1025 assert_eq!(upper.len(), lower.len());
1026 assert!(!upper.is_empty());
1027 }
1028
1029 #[test]
1030 fn test_get_airports_by_type_nonexistent() {
1031 let d = db();
1032 let airports = d.get_airports_by_type("nonexistent_type");
1033 assert!(airports.is_empty());
1034 }
1035
1036 #[test]
1040 fn test_autocomplete_london() {
1041 let d = db();
1042 let suggestions = d.get_autocomplete_suggestions("London");
1043 assert!(!suggestions.is_empty());
1044 assert!(suggestions.len() <= 10);
1045 assert!(suggestions.iter().any(|a| a.iata == "LHR"));
1046 }
1047
1048 #[test]
1052 fn test_calculate_distance_lhr_jfk() {
1053 let d = db();
1054 let dist = d.calculate_distance("LHR", "JFK").unwrap();
1055 assert!((dist - 5541.0).abs() < 50.0);
1057 }
1058
1059 #[test]
1063 fn test_find_airports_gb_airport() {
1064 let d = db();
1065 let filter = AirportFilter {
1066 country_code: Some("GB".to_string()),
1067 airport_type: Some("airport".to_string()),
1068 ..Default::default()
1069 };
1070 let airports = d.find_airports(&filter);
1071 assert!(airports
1074 .iter()
1075 .all(|a| a.country_code == "GB" && a.airport_type.contains("airport")));
1076 }
1077
1078 #[test]
1079 fn test_find_airports_scheduled_service() {
1080 let d = db();
1081 let with_service = d.find_airports(&AirportFilter {
1082 has_scheduled_service: Some(true),
1083 ..Default::default()
1084 });
1085 let without_service = d.find_airports(&AirportFilter {
1086 has_scheduled_service: Some(false),
1087 ..Default::default()
1088 });
1089 assert!(with_service.len() + without_service.len() > 0);
1090
1091 if !with_service.is_empty() {
1092 assert!(with_service.iter().all(|a| a.scheduled_service));
1093 }
1094 if !without_service.is_empty() {
1095 assert!(without_service.iter().all(|a| !a.scheduled_service));
1096 }
1097 }
1098
1099 #[test]
1103 fn test_get_airports_by_timezone_london() {
1104 let d = db();
1105 let airports = d.get_airports_by_timezone("Europe/London");
1106 assert!(airports.len() > 10);
1107 assert!(airports.iter().all(|a| a.timezone == "Europe/London"));
1108 }
1109
1110 #[test]
1114 fn test_get_airport_links_lhr() {
1115 let d = db();
1116 let links = d.get_airport_links("LHR").unwrap();
1117 assert!(links
1118 .wikipedia
1119 .as_deref()
1120 .unwrap_or("")
1121 .contains("Heathrow_Airport"));
1122 assert!(links.website.is_some());
1123 }
1124
1125 #[test]
1126 fn test_get_airport_links_hnd() {
1127 let d = db();
1128 let links = d.get_airport_links("HND").unwrap();
1129 assert!(links
1130 .wikipedia
1131 .as_deref()
1132 .unwrap_or("")
1133 .contains("Tokyo_International_Airport"));
1134 assert!(links.website.is_some());
1135 }
1136
1137 #[test]
1141 fn test_stats_by_country_sg() {
1142 let d = db();
1143 let stats = d.get_airport_stats_by_country("SG").unwrap();
1144 assert!(stats.total > 0);
1145 assert!(!stats.timezones.is_empty());
1146 }
1147
1148 #[test]
1149 fn test_stats_by_country_us() {
1150 let d = db();
1151 let stats = d.get_airport_stats_by_country("US").unwrap();
1152 assert!(stats.total > 1000);
1153 assert!(stats.by_type.contains_key("large_airport"));
1154 assert!(*stats.by_type.get("large_airport").unwrap() > 0);
1155 }
1156
1157 #[test]
1158 fn test_stats_by_country_invalid() {
1159 let d = db();
1160 let result = d.get_airport_stats_by_country("XYZ");
1161 assert!(result.is_err());
1162 }
1163
1164 #[test]
1168 fn test_stats_by_continent_as() {
1169 let d = db();
1170 let stats = d.get_airport_stats_by_continent("AS").unwrap();
1171 assert!(stats.total > 100);
1172 assert!(stats.by_country.len() > 10);
1173 }
1174
1175 #[test]
1176 fn test_stats_by_continent_eu() {
1177 let d = db();
1178 let stats = d.get_airport_stats_by_continent("EU").unwrap();
1179 assert!(stats.by_country.contains_key("GB"));
1180 assert!(stats.by_country.contains_key("FR"));
1181 assert!(stats.by_country.contains_key("DE"));
1182 }
1183
1184 #[test]
1188 fn test_largest_by_continent_runway() {
1189 let d = db();
1190 let airports = d.get_largest_airports_by_continent("AS", 5, "runway");
1191 assert!(airports.len() <= 5);
1192 assert!(!airports.is_empty());
1193 for i in 0..airports.len() - 1 {
1194 let r1 = airports[i].runway_length.unwrap_or(0);
1195 let r2 = airports[i + 1].runway_length.unwrap_or(0);
1196 assert!(r1 >= r2);
1197 }
1198 }
1199
1200 #[test]
1201 fn test_largest_by_continent_elevation() {
1202 let d = db();
1203 let airports = d.get_largest_airports_by_continent("SA", 5, "elevation");
1204 assert!(airports.len() <= 5);
1205 for i in 0..airports.len() - 1 {
1206 let e1 = airports[i].elevation_ft.unwrap_or(0);
1207 let e2 = airports[i + 1].elevation_ft.unwrap_or(0);
1208 assert!(e1 >= e2);
1209 }
1210 }
1211
1212 #[test]
1213 fn test_largest_by_continent_respects_limit() {
1214 let d = db();
1215 let airports = d.get_largest_airports_by_continent("EU", 3, "runway");
1216 assert!(airports.len() <= 3);
1217 }
1218
1219 #[test]
1223 fn test_get_multiple_airports_iata() {
1224 let d = db();
1225 let airports = d.get_multiple_airports(&["SIN", "LHR", "JFK"]);
1226 assert_eq!(airports.len(), 3);
1227 assert_eq!(airports[0].as_ref().unwrap().iata, "SIN");
1228 assert_eq!(airports[1].as_ref().unwrap().iata, "LHR");
1229 assert_eq!(airports[2].as_ref().unwrap().iata, "JFK");
1230 }
1231
1232 #[test]
1233 fn test_get_multiple_airports_mixed_codes() {
1234 let d = db();
1235 let airports = d.get_multiple_airports(&["SIN", "EGLL", "JFK"]);
1236 assert_eq!(airports.len(), 3);
1237 assert!(airports.iter().all(|a| a.is_some()));
1238 }
1239
1240 #[test]
1241 fn test_get_multiple_airports_with_invalid() {
1242 let d = db();
1243 let airports = d.get_multiple_airports(&["SIN", "INVALID", "LHR"]);
1244 assert_eq!(airports.len(), 3);
1245 assert!(airports[0].is_some());
1246 assert!(airports[1].is_none());
1247 assert!(airports[2].is_some());
1248 }
1249
1250 #[test]
1251 fn test_get_multiple_airports_empty() {
1252 let d = db();
1253 let airports = d.get_multiple_airports(&[]);
1254 assert!(airports.is_empty());
1255 }
1256
1257 #[test]
1261 fn test_distance_matrix() {
1262 let d = db();
1263 let matrix = d.calculate_distance_matrix(&["SIN", "LHR", "JFK"]).unwrap();
1264 assert_eq!(matrix.airports.len(), 3);
1265
1266 assert_eq!(matrix.distances["SIN"]["SIN"], 0.0);
1268 assert_eq!(matrix.distances["LHR"]["LHR"], 0.0);
1269 assert_eq!(matrix.distances["JFK"]["JFK"], 0.0);
1270
1271 assert_eq!(
1273 matrix.distances["SIN"]["LHR"],
1274 matrix.distances["LHR"]["SIN"]
1275 );
1276 assert_eq!(
1277 matrix.distances["SIN"]["JFK"],
1278 matrix.distances["JFK"]["SIN"]
1279 );
1280
1281 assert!(matrix.distances["SIN"]["LHR"] > 5000.0);
1283 assert!(matrix.distances["LHR"]["JFK"] > 3000.0);
1284 }
1285
1286 #[test]
1287 fn test_distance_matrix_too_few() {
1288 let d = db();
1289 let result = d.calculate_distance_matrix(&["SIN"]);
1290 assert!(result.is_err());
1291 }
1292
1293 #[test]
1294 fn test_distance_matrix_invalid_code() {
1295 let d = db();
1296 let result = d.calculate_distance_matrix(&["SIN", "INVALID"]);
1297 assert!(result.is_err());
1298 }
1299
1300 #[test]
1304 fn test_find_nearest_airport_sin() {
1305 let d = db();
1306 let nearest = d.find_nearest_airport(1.35019, 103.994003, None).unwrap();
1307 assert_eq!(nearest.airport.iata, "SIN");
1308 assert!(nearest.distance < 2.0);
1309 }
1310
1311 #[test]
1312 fn test_find_nearest_airport_with_type_filter() {
1313 let d = db();
1314 let filter = AirportFilter {
1315 airport_type: Some("large_airport".to_string()),
1316 ..Default::default()
1317 };
1318 let nearest = d
1319 .find_nearest_airport(51.5074, -0.1278, Some(&filter))
1320 .unwrap();
1321 assert_eq!(nearest.airport.airport_type, "large_airport");
1322 assert!(nearest.distance > 0.0);
1323 }
1324
1325 #[test]
1326 fn test_find_nearest_airport_with_type_and_country() {
1327 let d = db();
1328 let filter = AirportFilter {
1329 airport_type: Some("large_airport".to_string()),
1330 country_code: Some("US".to_string()),
1331 ..Default::default()
1332 };
1333 let nearest = d
1334 .find_nearest_airport(40.7128, -74.0060, Some(&filter))
1335 .unwrap();
1336 assert_eq!(nearest.airport.airport_type, "large_airport");
1337 assert_eq!(nearest.airport.country_code, "US");
1338 }
1339
1340 #[test]
1344 fn test_validate_iata_valid() {
1345 let d = db();
1346 assert!(d.validate_iata_code("SIN"));
1347 assert!(d.validate_iata_code("LHR"));
1348 assert!(d.validate_iata_code("JFK"));
1349 }
1350
1351 #[test]
1352 fn test_validate_iata_invalid() {
1353 let d = db();
1354 assert!(!d.validate_iata_code("XYZ"));
1355 assert!(!d.validate_iata_code("ZZZ"));
1356 }
1357
1358 #[test]
1359 fn test_validate_iata_bad_format() {
1360 let d = db();
1361 assert!(!d.validate_iata_code("ABCD"));
1362 assert!(!d.validate_iata_code("AB"));
1363 assert!(!d.validate_iata_code("abc"));
1364 assert!(!d.validate_iata_code(""));
1365 }
1366
1367 #[test]
1371 fn test_validate_icao_valid() {
1372 let d = db();
1373 assert!(d.validate_icao_code("WSSS"));
1374 assert!(d.validate_icao_code("EGLL"));
1375 assert!(d.validate_icao_code("KJFK"));
1376 }
1377
1378 #[test]
1379 fn test_validate_icao_invalid() {
1380 let d = db();
1381 assert!(!d.validate_icao_code("XXXX"));
1382 assert!(!d.validate_icao_code("ZZZ0"));
1383 }
1384
1385 #[test]
1386 fn test_validate_icao_bad_format() {
1387 let d = db();
1388 assert!(!d.validate_icao_code("ABC"));
1389 assert!(!d.validate_icao_code("ABCDE"));
1390 assert!(!d.validate_icao_code("abcd"));
1391 assert!(!d.validate_icao_code(""));
1392 }
1393
1394 #[test]
1398 fn test_get_airport_count_total() {
1399 let d = db();
1400 assert!(d.get_airport_count(None) > 5000);
1401 }
1402
1403 #[test]
1404 fn test_get_airport_count_by_type() {
1405 let d = db();
1406 let large_count = d.get_airport_count(Some(&AirportFilter {
1407 airport_type: Some("large_airport".to_string()),
1408 ..Default::default()
1409 }));
1410 let total = d.get_airport_count(None);
1411 assert!(large_count > 0);
1412 assert!(large_count < total);
1413 }
1414
1415 #[test]
1416 fn test_get_airport_count_by_country() {
1417 let d = db();
1418 let count = d.get_airport_count(Some(&AirportFilter {
1419 country_code: Some("US".to_string()),
1420 ..Default::default()
1421 }));
1422 assert!(count > 1000);
1423 }
1424
1425 #[test]
1426 fn test_get_airport_count_multiple_filters() {
1427 let d = db();
1428 let count = d.get_airport_count(Some(&AirportFilter {
1429 country_code: Some("US".to_string()),
1430 airport_type: Some("large_airport".to_string()),
1431 ..Default::default()
1432 }));
1433 assert!(count > 0);
1434 assert!(count < 200);
1435 }
1436
1437 #[test]
1441 fn test_is_airport_operational_true() {
1442 let d = db();
1443 assert!(d.is_airport_operational("SIN").unwrap());
1444 assert!(d.is_airport_operational("LHR").unwrap());
1445 assert!(d.is_airport_operational("JFK").unwrap());
1446 }
1447
1448 #[test]
1449 fn test_is_airport_operational_both_codes() {
1450 let d = db();
1451 assert!(d.is_airport_operational("SIN").unwrap());
1452 assert!(d.is_airport_operational("WSSS").unwrap());
1453 }
1454
1455 #[test]
1456 fn test_is_airport_operational_invalid() {
1457 let d = db();
1458 let result = d.is_airport_operational("INVALID");
1459 assert!(result.is_err());
1460 }
1461
1462 #[test]
1466 fn test_search_by_name() {
1467 let d = db();
1468 let results = d.search_by_name("Singapore").unwrap();
1469 assert!(!results.is_empty());
1470 }
1471}