use std::error::Error;
use std::{fmt, fs};
use crate::types::{GenericOfp, Ofp};
use regex::Regex;
mod converter;
pub mod types;
static SIMBRIEF_URL: &str = "https://www.simbrief.com/api/xml.fetcher.php?";
static JSON_SUFFIX: &str = "&json=1";
static PLACEHOLDER: &str = "---";
pub async fn load_ofp_online(user_id: &str, user_name: &str) -> Result<Ofp, Box<dyn Error>> {
let url = build_url(user_id, user_name)?;
let response = reqwest::get(url).await?;
let mut content = response.text().await?;
content = sanitize_ofp(&content);
serde_json::from_str(&content).map_err(Into::into)
}
pub async fn load_generic_ofp_online(
user_id: &str,
user_name: &str,
) -> Result<GenericOfp, Box<dyn Error>> {
let ofp = load_ofp_online(user_id, user_name).await?;
Ok(extract_generic_ofp(ofp))
}
pub fn extract_generic_ofp(ofp: Ofp) -> GenericOfp {
let user_id = ofp.params.user_id;
let time = ofp.params.time_generated;
let airac = ofp.params.airac;
let airline_icao = ofp.general.icao_airline;
let flight_number = ofp.general.flight_number;
let callsign = if ofp.atc.is_some() {
ofp.atc.unwrap().callsign
} else {
PLACEHOLDER.to_string()
};
let costindex = ofp.general.costindex;
let initial_altitude = ofp.general.initial_altitude;
let stepclimb = ofp.general.stepclimb_string;
let route_distance = ofp.general.route_distance;
let total_burn = ofp.general.total_burn;
let route = ofp.general.route;
let origin_icao = ofp.origin.icao_code;
let origin_name = ofp.origin.name;
let origin_plan_rwy = ofp.origin.plan_rwy.unwrap_or("---".to_string());
let origin_procedure = ofp
.navlog
.clone()
.map_or(PLACEHOLDER.to_string(), |navlog| {
navlog
.fixes
.first()
.map_or(PLACEHOLDER.to_string(), |fix| fix.via_airway.clone())
});
let destination_icao = ofp.destination.icao_code;
let destination_name = ofp.destination.name;
let destination_plan_rwy = ofp.destination.plan_rwy.unwrap_or("---".to_string());
let destination_procedure = ofp
.navlog
.clone()
.map_or(PLACEHOLDER.to_string(), |navlog| {
navlog
.fixes
.last()
.map_or(PLACEHOLDER.to_string(), |fix| fix.via_airway.clone())
});
let mut alternates: Vec<String> = Vec::new();
if ofp.alternates.is_some() {
for alternate in ofp.alternates.unwrap() {
alternates.push(alternate.icao_code);
}
}
if ofp.takeoff_altn.is_some() {
alternates.push(ofp.takeoff_altn.unwrap().icao_code);
}
if ofp.enroute_altn.is_some() {
alternates.push(ofp.enroute_altn.unwrap().icao_code);
}
GenericOfp {
user_id,
time,
airac,
airline_icao,
flight_number,
callsign,
costindex,
initial_altitude,
stepclimb,
route_distance,
total_burn,
route,
origin_icao,
origin_name,
origin_plan_rwy,
origin_procedure,
destination_icao,
destination_name,
destination_plan_rwy,
destination_procedure,
alternates,
}
}
fn build_url(user_id: &str, user_name: &str) -> Result<String, InvalidArgumentError> {
if !user_id.is_empty() {
Ok(format!("{SIMBRIEF_URL}userid={user_id}{JSON_SUFFIX}"))
} else if !user_name.is_empty() {
Ok(format!("{SIMBRIEF_URL}username={user_name}{JSON_SUFFIX}"))
} else {
Err(InvalidArgumentError::new("user_id and user_name are empty"))
}
}
pub fn load_ofp_from_file(file_path: &str) -> Result<Ofp, Box<dyn Error>> {
let mut content = fs::read_to_string(file_path)?;
content = sanitize_ofp(&content);
let ofp: Ofp = serde_json::from_str(&content)?;
Ok(ofp)
}
pub fn save_ofp_to_file(file_path: &str, ofp: &Ofp) -> Result<(), FileSaveError> {
let result = serde_json::to_string(ofp);
match result {
Ok(content) => match fs::write(file_path, content) {
Ok(()) => Ok(()),
Err(_) => Err(FileSaveError::new(
"Could not write the serialized Ofp to the disk. io::Error",
)),
},
Err(_) => Err(FileSaveError::new("Could not serialize the Ofp")),
}
}
fn sanitize_ofp(input: &str) -> String {
let regex_sigmet_open = Regex::new(r#""sigmet":\{"#).unwrap();
let regex_sigmet_close = Regex::new(r#"}},"text""#).unwrap();
let regex_altn_metar_open = Regex::new(r#""altn_metar":""#).unwrap();
let regex_altn_metar_close = Regex::new(r#"","altn_taf""#).unwrap();
let regex_altn_taf_open = Regex::new(r#""altn_taf":""#).unwrap();
let regex_altn_taf_close = Regex::new(r#"","toaltn_metar""#).unwrap();
let regex_altn_open = Regex::new(r#""alternate":\{"#).unwrap();
let regex_altn_close = Regex::new(r#"},"takeoff_altn""#).unwrap();
let regex_altn_empty = Regex::new(r#""alternate":\[\{}],"#).unwrap();
let regex_atis_empty = Regex::new(r#","atis":\{}"#).unwrap();
let regex_atis_open = Regex::new(r#""atis":\{"#).unwrap();
let regex_atis_close = Regex::new(r#"},"notam""#).unwrap();
let regex_notam_empty = Regex::new(r#","notam":\{}"#).unwrap();
let regex_notam_open = Regex::new(r#""notam":\{"#).unwrap();
let regex_notam_close = Regex::new(r#""notam_is_obstacle":\{}}},"#).unwrap();
let regex_fir_open = Regex::new(r#""fir":\{"#).unwrap();
let regex_fir_close0 = Regex::new(r#"}}},\{"ident":"#).unwrap();
let regex_fir_close1 = Regex::new(r#"}}}]},"etops""#).unwrap();
let regex_fir_altn_open = Regex::new(r#""fir_altn":""#).unwrap();
let regex_fir_altn_close = Regex::new(r#"","fir_etops""#).unwrap();
let regex_fir_enroute_open = Regex::new(r#""fir_enroute":""#).unwrap();
let regex_fir_enroute_close = Regex::new(r#""},"aircraft""#).unwrap();
let regex_dx_rmk_open = Regex::new(r#""dx_rmk":""#).unwrap();
let regex_dx_rmk_close = Regex::new(r#"","sys_rmk"#).unwrap();
let regex_empty_open = Regex::new(r#""([^"]*)":\{},"#).unwrap();
let regex_empty_close = Regex::new(r#","([^"]*)":\{}}"#).unwrap();
let regex_impacts = Regex::new(r#""impacts":\{"zfw_minus_1000":\{}},"#).unwrap();
let temp_sigmet_open = regex_sigmet_open.replace_all(input, "\"sigmet\":[{");
let temp_sigmet_close = regex_sigmet_close.replace_all(&temp_sigmet_open, "}]},\"text\"");
let temp_altn_metar_open =
regex_altn_metar_open.replace_all(&temp_sigmet_close, "\"altn_metar\":[\"");
let temp_altn_metar_close =
regex_altn_metar_close.replace_all(&temp_altn_metar_open, "\"],\"altn_taf\"");
let temp_altn_taf_open =
regex_altn_taf_open.replace_all(&temp_altn_metar_close, "\"altn_taf\":[\"");
let temp_altn_taf_close =
regex_altn_taf_close.replace_all(&temp_altn_taf_open, "\"],\"toaltn_metar\"");
let temp_altn_open = regex_altn_open.replace_all(&temp_altn_taf_close, "\"alternate\":[{");
let temp_altn_close = regex_altn_close.replace_all(&temp_altn_open, "}],\"takeoff_altn\"");
let temp_altn_empty = regex_altn_empty.replace_all(&temp_altn_close, "");
let temp_atis_empty = regex_atis_empty.replace_all(&temp_altn_empty, "");
let temp_atis_open = regex_atis_open.replace_all(&temp_atis_empty, "\"atis\":[{");
let temp_atis_close = regex_atis_close.replace_all(&temp_atis_open, "}],\"notam\"");
let temp_notam_empty = regex_notam_empty.replace_all(&temp_atis_close, "");
let temp_notam_open = regex_notam_open.replace_all(&temp_notam_empty, "\"notam\":[{");
let temp_notam_close =
regex_notam_close.replace_all(&temp_notam_open, "\"notam_is_obstacle\":{}}]},");
let temp_fir_open = regex_fir_open.replace_all(&temp_notam_close, "\"fir\":[{");
let temp_fir_close0 = regex_fir_close0.replace_all(&temp_fir_open, "}]}},{\"ident\":");
let temp_fir_close1 = regex_fir_close1.replace_all(&temp_fir_close0, "}]}}]},\"etops\"");
let temp_fir_altn_open = regex_fir_altn_open.replace_all(&temp_fir_close1, "\"fir_altn\":[\"");
let temp_fir_altn_close =
regex_fir_altn_close.replace_all(&temp_fir_altn_open, "\"],\"fir_etops\"");
let temp_fir_enroute_open =
regex_fir_enroute_open.replace_all(&temp_fir_altn_close, "\"fir_enroute\":[\"");
let temp_fir_enroute_close =
regex_fir_enroute_close.replace_all(&temp_fir_enroute_open, "\"]},\"aircraft\"");
let temp_dx_rmk_open = regex_dx_rmk_open.replace_all(&temp_fir_enroute_close, "\"dx_rmk\":[\"");
let temp_dx_rmk_close = regex_dx_rmk_close.replace_all(&temp_dx_rmk_open, "\"],\"sys_rmk");
let temp_empty_open = regex_empty_open.replace_all(&temp_dx_rmk_close, "");
let temp_empty_close = regex_empty_close.replace_all(&temp_empty_open, "}");
let temp_impacts = regex_impacts.replace_all(&temp_empty_close, "");
let result = temp_impacts.into_owned(); let _ = fs::write("res/test/temp.json", result.clone()); result }
#[derive(Debug)]
pub struct InvalidArgumentError {
details: String,
}
impl InvalidArgumentError {
fn new(msg: &str) -> InvalidArgumentError {
InvalidArgumentError {
details: msg.to_string(),
}
}
}
impl fmt::Display for InvalidArgumentError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for InvalidArgumentError {
fn description(&self) -> &str {
&self.details
}
}
#[derive(Debug)]
pub struct FileSaveError {
details: String,
}
impl FileSaveError {
fn new(msg: &str) -> FileSaveError {
FileSaveError {
details: msg.to_string(),
}
}
}
impl fmt::Display for FileSaveError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.details)
}
}
impl Error for FileSaveError {
fn description(&self) -> &str {
&self.details
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
use chrono::{DateTime, Utc};
use crate::types::Ofp;
use crate::{load_ofp_from_file, load_ofp_online, save_ofp_to_file};
use tokio_macros::test;
static FILE_PATH_TEST: &str = "res/test/";
#[test] async fn test_load_ofp_online() {
let result: Result<Ofp, Box<dyn Error>> = load_ofp_online("353738", "").await;
assert!(result.is_ok());
let result: Result<Ofp, Box<dyn Error>> = load_ofp_online("", "greluc").await;
assert!(result.is_ok());
}
#[test]
async fn test_load_ofp_from_file_4alternate_takeoff_enroute_etops() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}EDDFKSFO_XML_1720717760.json", FILE_PATH_TEST));
assert!(result.is_ok());
let ofp = result.unwrap();
assert_eq!(353738u32, ofp.fetch.user_id);
assert_eq!(
-7i8,
ofp.alternates
.unwrap()
.first()
.unwrap()
.avg_wind_comp
.unwrap()
);
assert_eq!(
DateTime::<Utc>::from_timestamp(1720719600i64, 0)
.unwrap()
.to_utc(),
ofp.times.sched_out
);
}
#[test]
async fn test_load_ofp_from_file_0alternate() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}EDDFEGLL_XML_1720718078.json", FILE_PATH_TEST));
assert!(result.is_ok());
}
#[test]
async fn test_load_ofp_from_file_1alternate() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}EDDFLGAV_XML_1720718194.json", FILE_PATH_TEST));
assert!(result.is_ok());
}
#[test]
async fn test_load_ofp_from_file_4alternate_nonavlog() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}EDDKEDDC_XML_1720852477.json", FILE_PATH_TEST));
assert!(result.is_ok());
}
#[test]
async fn test_load_ofp_from_file_4alternate_nooptional() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}KLASKSFO_XML_1720853074.json", FILE_PATH_TEST));
assert!(result.is_ok());
}
#[test]
async fn test_fir_altn_multiple() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}LLBGEDDM_XML_1728041926.json", FILE_PATH_TEST));
assert!(result.is_ok());
}
#[test]
async fn test_load_ofp_from_file_new0() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}LOWWLGKR_XML_1721458468.json", FILE_PATH_TEST));
assert!(result.is_ok());
}
#[test]
async fn test_load_ofp_from_file_atis_not_issued() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}EDDPEDDF_XML_1722018273.json", FILE_PATH_TEST));
assert!(result.is_ok());
}
#[test]
async fn test_save_ofp_to_file_4alternate_takeoff_enroute_etops() {
let result: Result<Ofp, Box<dyn Error>> =
load_ofp_from_file(&format!("{}EDDFKSFO_XML_1720717760.json", FILE_PATH_TEST));
assert!(result.is_ok());
let ofp = result.unwrap();
let result = save_ofp_to_file("res/test/save.json", &ofp);
assert!(result.is_ok())
}
}