Overhaul refactor. Still things in progress

This commit is contained in:
2025-04-05 22:42:13 -04:00
parent 310d1eaad8
commit 769762dfa7
133 changed files with 119890 additions and 8784 deletions

233
api/src/airports/delete.rs Normal file
View File

@@ -0,0 +1,233 @@
use std::fmt::Display;
use std::str::FromStr;
use crate::db;
use log::error;
use serde::{Deserialize, Serialize};
use sqlx::postgres::types::PgPoint;
use crate::error::ApiResult;
const TABLE_NAME: &str = "airports";
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
struct RunwayDb {
pub icao: String,
pub id: String,
pub length_ft: f32,
pub width_ft: f32,
pub surface: String,
}
#[derive(Debug)]
pub struct AirportFilter {
pub icaos: Option<Vec<String>>,
pub name: Option<String>,
// pub bounds: Option<Polygon<Point>>,
pub categories: Option<Vec<AirportCategory>>,
pub has_metar: Option<bool>,
}
impl Default for AirportFilter {
fn default() -> Self {
AirportFilter {
icaos: None,
name: None,
// bounds: None,
categories: None,
has_metar: None,
}
}
}
impl AirportDb {
pub async fn find_all(_filter: &AirportFilter, _limit: i32, _page: i32) -> ApiResult<Vec<Self>> {
let pool = db::pool();
let airports: Vec<Self> = sqlx::query_as::<_, Self>(&format!(
"SELECT * FROM {}",
TABLE_NAME
))
.fetch_all(pool)
.await?;
Ok(airports)
}
pub async fn count(_filter: &AirportFilter) -> ApiResult<i64> {
let pool = db::pool();
let count: i64 = sqlx::query_scalar::<_, i64>(&format!(
"SELECT COUNT(*) FROM {}",
TABLE_NAME
))
.fetch_one(pool)
.await?;
Ok(count)
}
// fn build_query<'a>(
// mut query: QueryBuilder<'a, Postgres>,
// filter: &'a AirportFilter,
// ) -> QueryBuilder<'a, Postgres> {
// if let Some(bounds) = &filter.bounds {
// // convert bounds to a WKT polygon
// if bounds.rings.len() > 1 {
// return Err(ApiError {
// status: 400,
// message: "Only one polygon is allowed".to_string(),
// });
// } else {
// let mut points: Vec<String> = vec![];
// bounds.rings.iter().for_each(|ring| {
// ring.iter().for_each(|point| {
// points.push(format!("{} {}", point.get_x(), point.get_y()));
// });
// });
// let bounds = format!("POLYGON(({}))", points.join(","));
// query.push(format!(
// "ST_Contains(ST_GeomFromText('{}', 4326), point)",
// bounds
// ));
// }
// }
// if let Some(categories) = &filter.categories {
// query.push(format!(
// "({})",
// categories
// .iter()
// .map(|category| format!("category = '{}'", category.to_string()))
// .collect::<Vec<String>>()
// .join(" OR ")
// ));
// }
//
// fn sanitize_icao(icao: &str) -> String {
// // Sanitize search to only allow [a-zA-Z0-9-\\s]
// icao
// .chars()
// .filter(|c| c.is_alphanumeric() || *c == '-' || *c == ' ')
// .collect::<String>()
// }
//
// if &filter.icaos.is_some() == &true && &filter.name.is_some() == &true {
// let icaos = filter.icaos.as_ref().unwrap();
// let name = sanitize_icao(filter.name.as_ref().unwrap());
// let icao_part = format!(
// "({})",
// icaos
// .iter()
// .map(|icao| format!("icao ILIKE '{}'", sanitize_icao(icao)))
// .collect::<Vec<String>>()
// .join(" OR ")
// );
// let name_part = format!("name ILIKE '%{}%'", name);
// parts.push(format!("({} OR {})", icao_part, name_part));
// } else if let Some(icaos) = &filter.icaos {
// parts.push(format!(
// "({})",
// icaos
// .iter()
// .map(|icao| format!("icao ILIKE '{}'", sanitize_icao(icao)))
// .collect::<Vec<String>>()
// .join(" OR ")
// ));
// } else if let Some(name) = &filter.name {
// let search = sanitize_icao(name);
// parts.push(format!("name ILIKE '%{}%'", search));
// }
// if let Some(has_metar) = &filter.has_metar {
// parts.push(format!("has_metar = {}", has_metar));
// }
//
// if parts.len() > 0 {
// query = format!("{} WHERE {}", query, parts.join(" AND "));
// }
//
// return Ok(query);
// }
pub async fn find_by_icao(icao: &str) -> ApiResult<Self> {
let pool = db::pool();
let airport =
sqlx::query_as::<_, Self>(&format!("SELECT * FROM {} WHERE icao = $1", TABLE_NAME))
.bind(icao)
.fetch_one(pool)
.await?;
Ok(airport)
}
pub async fn insert(&self) -> ApiResult<()> {
let pool = db::pool();
sqlx::query(&format!(
"INSERT INTO {} (
icao,
category,
name,
elevation_ft,
iso_country,
iso_region,
municipality,
has_metar,
point,
data
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
)",
TABLE_NAME
))
.bind(self.icao.clone())
.bind(self.category.clone())
.bind(&self.name)
.bind(self.elevation_ft)
.bind(self.iso_country.clone())
.bind(self.iso_region.clone())
.bind(self.municipality.clone())
.bind(self.has_metar.clone())
// .bind(self.point.clone())
.bind(self.data.clone())
.execute(pool)
.await?;
Ok(())
}
// pub fn insert_vec(airports: Vec<Self>) -> ApiResult<Vec<Self>> {
// let mut conn: r2d2::PooledConnection<diesel::r2d2::ConnectionManager<PgConnection>> =
// db::connection()?;
// let mut inserted_airports: Vec<Self> = vec![];
// for airport in airports {
// let airport = Self::from(airport);
// let airport = diesel::insert_into(airports::table)
// .values(airport)
// .on_conflict_do_nothing()
// .get_result(&mut conn)?;
// inserted_airports.push(airport);
// }
// Ok(inserted_airports)
// }
pub async fn update(&self) -> ApiResult<()> {
// let mut conn = db::pool()?;
// let airport = diesel::update(airports::table)
// .filter(airports::icao.eq(airport.icao.clone()))
// .set(airport)
// .get_result(&mut conn)?;
// Ok(airport)
Ok(())
}
pub async fn delete_all() -> ApiResult<()> {
Ok(())
}
pub async fn delete_by_icao(_icao: &str) -> ApiResult<()> {
// let mut conn = db::pool()?;
// let res = match icao {
// Some(icao) => {
// diesel::delete(airports::table.filter(airports::icao.eq(icao))).execute(&mut conn)?
// }
// None => diesel::delete(airports::table).execute(&mut conn)?,
// };
// Ok(res)
Ok(())
}
}

View File

@@ -1,432 +0,0 @@
use std::fmt::Display;
use std::str::FromStr;
use crate::db;
use crate::error::{ApiError, ApiResult};
use crate::db::schema::airports;
use diesel::prelude::*;
use diesel::sql_query;
use log::error;
use postgis_diesel::types::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct Runway {
pub id: String,
pub length_ft: f32,
pub width_ft: f32,
pub surface: String,
}
#[derive(Serialize, Deserialize)]
pub struct Frequency {
pub id: String,
pub frequency_mhz: f32,
}
#[derive(Serialize, Deserialize)]
pub struct Airport {
pub icao: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub iata: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local: Option<String>,
pub name: String,
pub category: AirportCategory,
pub iso_country: String,
pub iso_region: String,
pub municipality: String,
pub elevation_ft: f32,
pub latitude: f64,
pub longitude: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_tower: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_beacon: Option<bool>,
pub runways: Vec<Runway>,
pub frequencies: Vec<Frequency>,
pub public: bool,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AirportMetarCache {
pub icao: String,
pub has_metar: bool,
pub last_checked: chrono::NaiveDateTime,
}
impl Into<QueryAirport> for Airport {
fn into(self) -> QueryAirport {
return QueryAirport {
icao: self.icao.clone(),
category: self.category.clone().to_string(),
name: self.name.clone(),
elevation_ft: self.elevation_ft,
iso_country: self.iso_country.clone(),
iso_region: self.iso_region.clone(),
municipality: self.municipality.clone(),
has_metar: false,
point: Point::new(self.longitude, self.latitude, Some(4326)),
data: match serde_json::to_value(&self) {
Ok(d) => d,
Err(err) => {
error!("{}", err);
serde_json::Value::Null
}
},
};
}
}
impl From<QueryAirport> for Airport {
fn from(airport: QueryAirport) -> Self {
serde_json::from_value(airport.data).unwrap()
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum AirportCategory {
#[serde(rename = "small_airport")]
Small,
#[serde(rename = "medium_airport")]
Medium,
#[serde(rename = "large_airport")]
Large,
#[serde(rename = "heliport")]
Heliport,
#[serde(rename = "closed")]
Closed,
#[serde(rename = "seaplane_base")]
Seaplane,
#[serde(rename = "balloonport")]
Balloonport,
#[serde(rename = "unknown")]
Unknown,
}
impl FromStr for AirportCategory {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"small_airport" => Ok(AirportCategory::Small),
"medium_airport" => Ok(AirportCategory::Medium),
"large_airport" => Ok(AirportCategory::Large),
"heliport" => Ok(AirportCategory::Heliport),
"closed" => Ok(AirportCategory::Closed),
"seaplane_base" => Ok(AirportCategory::Seaplane),
"balloonport" => Ok(AirportCategory::Balloonport),
_ => Ok(AirportCategory::Unknown),
}
}
}
impl Display for AirportCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AirportCategory::Small => write!(f, "small_airport"),
AirportCategory::Medium => write!(f, "medium_airport"),
AirportCategory::Large => write!(f, "large_airport"),
AirportCategory::Heliport => write!(f, "heliport"),
AirportCategory::Closed => write!(f, "closed"),
AirportCategory::Seaplane => write!(f, "seaplane_base"),
AirportCategory::Balloonport => write!(f, "balloonport"),
AirportCategory::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Serialize, Deserialize, AsChangeset, Insertable, Queryable, QueryableByName)]
#[diesel(table_name = airports)]
pub struct QueryAirport {
pub icao: String,
pub category: String,
pub name: String,
pub elevation_ft: f32,
pub iso_country: String,
pub iso_region: String,
pub municipality: String,
pub has_metar: bool,
pub point: Point,
pub data: serde_json::Value,
}
#[derive(Debug)]
pub struct QueryFilters {
pub icaos: Option<Vec<String>>,
pub name: Option<String>,
pub bounds: Option<Polygon<Point>>,
pub categories: Option<Vec<AirportCategory>>,
pub order_field: Option<QueryOrderField>,
pub order_by: Option<QueryOrderBy>,
pub has_metar: Option<bool>,
}
impl Default for QueryFilters {
fn default() -> Self {
QueryFilters {
icaos: None,
name: None,
bounds: None,
categories: None,
order_field: None,
order_by: None,
has_metar: None,
}
}
}
#[derive(Debug)]
pub enum QueryOrderBy {
Asc,
Desc,
}
impl FromStr for QueryOrderBy {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"asc" => Ok(QueryOrderBy::Asc),
"desc" => Ok(QueryOrderBy::Desc),
_ => Err(()),
}
}
}
#[derive(Debug)]
pub enum QueryOrderField {
Icao,
Name,
Category,
Country,
Region,
Municipality,
}
impl FromStr for QueryOrderField {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"icao" => Ok(QueryOrderField::Icao),
"name" => Ok(QueryOrderField::Name),
"category" => Ok(QueryOrderField::Category),
"iso_country" => Ok(QueryOrderField::Country),
"iso_region" => Ok(QueryOrderField::Region),
"municipality" => Ok(QueryOrderField::Municipality),
_ => Err(()),
}
}
}
impl QueryAirport {
pub fn get_all(filters: &QueryFilters, limit: i32, page: i32) -> ApiResult<Vec<Self>> {
let mut conn = db::connection()?;
let mut query: String = "SELECT * FROM airports".to_string();
query = format!("{} {}", query, QueryAirport::build_filter_query(&filters)?);
query = format!("{} ORDER BY has_metar DESC", query);
if let Some(order_by) = &filters.order_by {
match order_by {
QueryOrderBy::Asc => {
if let Some(order_field) = &filters.order_field {
query = match order_field {
QueryOrderField::Icao => format!("{}, icao ASC", query),
QueryOrderField::Name => format!("{}, name ASC", query),
QueryOrderField::Category => format!("{}, category ASC", query),
QueryOrderField::Country => format!("{}, iso_country ASC", query),
QueryOrderField::Region => format!("{}, iso_region ASC", query),
QueryOrderField::Municipality => format!("{}, municipality ASC", query),
};
};
}
QueryOrderBy::Desc => {
if let Some(order_field) = &filters.order_field {
query = match order_field {
QueryOrderField::Icao => format!("{}, icao DESC", query),
QueryOrderField::Name => format!("{}, name DESC", query),
QueryOrderField::Category => format!("{}, category DESC", query),
QueryOrderField::Country => format!("{}, iso_country DESC", query),
QueryOrderField::Region => format!("{}, iso_region DESC", query),
QueryOrderField::Municipality => format!("{}, municipality DESC", query),
};
};
}
}
}
// Limit query to page and limit
query = format!("{} LIMIT {} OFFSET {}", query, limit, (page - 1) * limit);
let airports: Vec<QueryAirport> = match sql_query(query).load(&mut conn) {
Ok(a) => a,
Err(err) => {
return Err(ApiError {
status: 500,
message: format!("{}", err),
})
}
};
Ok(airports)
}
pub fn get_count(filters: &QueryFilters) -> ApiResult<i64> {
let mut conn = db::connection()?;
let mut query = "SELECT COUNT(*) FROM airports".to_string();
query = format!("{} {}", query, QueryAirport::build_filter_query(&filters)?);
// TODO: Fix this to use get_result() instead of building this table to do the load()
diesel::table! {
airports (count) {
count -> BigInt,
}
}
#[derive(Debug, Queryable, QueryableByName)]
#[diesel(table_name = airports)]
struct Count {
count: i64,
}
let count: Vec<Count> = match sql_query(query).load(&mut conn) {
Ok(a) => a,
Err(err) => {
return Err(ApiError {
status: 500,
message: format!("{}", err),
})
}
};
return Ok(count[0].count);
}
// TODO: Unsafe query, need to sanitize inputs
fn build_filter_query(filters: &QueryFilters) -> ApiResult<String> {
let mut query = "".to_string();
let mut parts: Vec<String> = vec![];
if let Some(bounds) = &filters.bounds {
// convert bounds to a WKT polygon
if bounds.rings.len() > 1 {
return Err(ApiError {
status: 400,
message: "Only one polygon is allowed".to_string(),
});
} else {
let mut points: Vec<String> = vec![];
bounds.rings.iter().for_each(|ring| {
ring.iter().for_each(|point| {
points.push(format!("{} {}", point.get_x(), point.get_y()));
});
});
let bounds = format!("POLYGON(({}))", points.join(","));
parts.push(format!(
"ST_Contains(ST_GeomFromText('{}', 4326), point)",
bounds
));
}
}
if let Some(categories) = &filters.categories {
parts.push(format!(
"({})",
categories
.iter()
.map(|category| format!("category = '{}'", category.to_string()))
.collect::<Vec<String>>()
.join(" OR ")
));
}
fn sanitize_icao(icao: &str) -> String {
// Sanitize search to only allow [a-zA-Z0-9-\\s]
icao
.chars()
.filter(|c| c.is_alphanumeric() || *c == '-' || *c == ' ')
.collect::<String>()
}
if &filters.icaos.is_some() == &true && &filters.name.is_some() == &true {
let icaos = filters.icaos.as_ref().unwrap();
let name = sanitize_icao(filters.name.as_ref().unwrap());
let icao_part = format!(
"({})",
icaos
.iter()
.map(|icao| format!("icao ILIKE '{}'", sanitize_icao(icao)))
.collect::<Vec<String>>()
.join(" OR ")
);
let name_part = format!("name ILIKE '%{}%'", name);
parts.push(format!("({} OR {})", icao_part, name_part));
} else if let Some(icaos) = &filters.icaos {
parts.push(format!(
"({})",
icaos
.iter()
.map(|icao| format!("icao ILIKE '{}'", sanitize_icao(icao)))
.collect::<Vec<String>>()
.join(" OR ")
));
} else if let Some(name) = &filters.name {
let search = sanitize_icao(name);
parts.push(format!("name ILIKE '%{}%'", search));
}
if let Some(has_metar) = &filters.has_metar {
parts.push(format!("has_metar = {}", has_metar));
}
if parts.len() > 0 {
query = format!("{} WHERE {}", query, parts.join(" AND "));
}
return Ok(query);
}
pub fn get(icao: &str) -> ApiResult<Self> {
let mut conn = db::connection()?;
let airport = airports::table
.filter(airports::icao.eq(icao))
.first(&mut conn)?;
Ok(airport)
}
pub fn insert(airport: Self) -> ApiResult<Self> {
let mut conn: r2d2::PooledConnection<diesel::r2d2::ConnectionManager<PgConnection>> =
db::connection()?;
let airport = Self::from(airport);
let airport = diesel::insert_into(airports::table)
.values(airport)
.on_conflict_do_nothing()
.get_result(&mut conn)?;
Ok(airport)
}
pub fn insert_all(airports: Vec<Self>) -> ApiResult<Vec<Self>> {
let mut conn: r2d2::PooledConnection<diesel::r2d2::ConnectionManager<PgConnection>> =
db::connection()?;
let mut inserted_airports: Vec<Self> = vec![];
for airport in airports {
let airport = Self::from(airport);
let airport = diesel::insert_into(airports::table)
.values(airport)
.on_conflict_do_nothing()
.get_result(&mut conn)?;
inserted_airports.push(airport);
}
Ok(inserted_airports)
}
pub fn update(airport: Self) -> ApiResult<Self> {
let mut conn = db::connection()?;
let airport = diesel::update(airports::table)
.filter(airports::icao.eq(airport.icao.clone()))
.set(airport)
.get_result(&mut conn)?;
Ok(airport)
}
pub fn delete(icao: Option<String>) -> ApiResult<usize> {
let mut conn = db::connection()?;
let res = match icao {
Some(icao) => {
diesel::delete(airports::table.filter(airports::icao.eq(icao))).execute(&mut conn)?
}
None => diesel::delete(airports::table).execute(&mut conn)?,
};
Ok(res)
}
}

View File

@@ -0,0 +1,288 @@
use std::str::FromStr;
use actix_web::web::Json;
use serde::{Deserialize, Serialize};
use sqlx::{Postgres, QueryBuilder};
use crate::airports::model::airport_category::AirportCategory;
use crate::airports::{Frequency, Runway, UpdateFrequency, UpdateRunway};
use crate::db;
use crate::error::ApiResult;
const TABLE_NAME: &str = "airports";
#[derive(Debug, Serialize, Deserialize)]
pub struct Airport {
pub icao: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub iata: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local: Option<String>,
pub name: String,
pub category: AirportCategory,
pub iso_country: String,
pub iso_region: String,
pub municipality: String,
pub elevation_ft: f32,
pub longitude: f32,
pub latitude: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_tower: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_beacon: Option<bool>,
pub runways: Vec<Runway>,
pub frequencies: Vec<Frequency>,
pub public: bool,
}
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
struct AirportRow {
pub icao: String,
pub iata: Option<String>,
pub local: Option<String>,
pub name: String,
pub category: String,
pub iso_country: String,
pub iso_region: String,
pub municipality: String,
pub elevation_ft: f32,
longitude: f32,
latitude: f32,
pub has_tower: Option<bool>,
pub has_beacon: Option<bool>,
pub public: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateAirport {
#[serde(skip_serializing_if = "Option::is_none")]
pub icao: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iata: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub local: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<AirportCategory>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iso_country: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub iso_region: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub municipality: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub elevation_ft: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub longitude: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub latitude: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_tower: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_beacon: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub runways: Option<Vec<UpdateRunway>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequencies: Option<Vec<UpdateFrequency>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub public: Option<bool>,
}
impl Into<AirportRow> for Airport {
fn into(self) -> AirportRow {
AirportRow {
icao: self.icao.clone(),
iata: self.iata.clone(),
local: self.local.clone(),
name: self.name.clone(),
category: self.category.clone().to_string(),
iso_country: self.iso_country.clone(),
iso_region: self.iso_region.clone(),
municipality: self.municipality.clone(),
elevation_ft: self.elevation_ft,
longitude: self.longitude,
latitude: self.latitude,
has_tower: self.has_tower,
has_beacon: self.has_beacon,
public: self.public,
}
}
}
impl From<AirportRow> for Airport {
fn from(airport: AirportRow) -> Self {
Airport {
icao: airport.icao.clone(),
iata: airport.iata.clone(),
local: airport.local.clone(),
name: airport.name.clone(),
category: match AirportCategory::from_str(&airport.category) {
Ok(c) => c,
Err(_) => {
log::error!("Invalid Airport category: {}", airport.category);
AirportCategory::Unknown
}
},
iso_country: airport.iso_country.clone(),
iso_region: airport.iso_region.clone(),
municipality: airport.municipality.clone(),
elevation_ft: airport.elevation_ft,
longitude: airport.longitude,
latitude: airport.latitude,
has_tower: airport.has_tower,
has_beacon: airport.has_beacon,
runways: vec![],
frequencies: vec![],
public: airport.public,
}
}
}
impl Airport {
pub async fn select(icao: &str) -> Option<Self> {
let pool = db::pool();
let airport: Option<AirportRow> = sqlx::query_as(&format!(
r#"
SELECT * FROM {} WHERE icao = $1
"#,
TABLE_NAME
))
.bind(icao)
.fetch_optional(pool)
.await
.unwrap_or_else(|err| {
log::error!("Unable to find airport '{}'", icao);
None
});
match airport {
Some(a) => Some(a.into()),
None => None,
}
}
pub async fn select_all() -> ApiResult<Vec<Self>> {
let pool = db::pool();
let airports: Vec<AirportRow> = sqlx::query_as(&format!(
r#"
SELECT * FROM {}
"#,
TABLE_NAME
))
.fetch_all(pool)
.await?;
Ok(airports.into_iter().map(From::from).collect())
}
pub async fn insert(&self) -> ApiResult<Self> {
let pool = db::pool();
let airport: AirportRow = sqlx::query_as(&format!(
r#"
INSERT INTO {} (
icao, iata, local, name, category, iso_country, iso_region, municipality,
elevation_ft, longitude, latitude, has_tower, has_beacon, public
)
VALUES (
$1, $2, $3, $4, $5, $6, $7,
$8, $9, $10, $11, $12, $13, $14
)
RETURNING *
"#,
TABLE_NAME,
))
.bind(self.icao.to_string())
.bind(&self.iata)
.bind(&self.local)
.bind(self.name.to_string())
.bind(self.category.to_string())
.bind(self.iso_country.to_string())
.bind(self.iso_region.to_string())
.bind(self.municipality.to_string())
.bind(self.elevation_ft)
.bind(self.longitude)
.bind(self.latitude)
.bind(self.has_tower)
.bind(self.has_beacon)
.bind(self.public)
.fetch_one(pool)
.await?;
Ok(airport.into())
}
pub async fn insert_all(airports: Vec<Self>) -> ApiResult<()> {
let pool = db::pool();
let airport_rows: Vec<AirportRow> = airports.into_iter().map(Into::into).collect();
// Define the maximum size of a single insertion batch.
let chunk_size = 1000;
for chunk in airport_rows.chunks(chunk_size) {
// Build a dynamic query for batch insertion.
let mut query_builder: QueryBuilder<Postgres> = QueryBuilder::new(
"INSERT INTO airports (icao, iata, local, name, category, \
iso_country, iso_region, municipality, elevation_ft, \
longitude, latitude, has_tower, has_beacon, public) ",
);
query_builder.push_values(chunk, |mut b, row| {
b.push_bind(&row.icao)
.push_bind(&row.iata)
.push_bind(&row.local)
.push_bind(&row.name)
.push_bind(&row.category)
.push_bind(&row.iso_country)
.push_bind(&row.iso_region)
.push_bind(&row.municipality)
.push_bind(row.elevation_ft)
.push_bind(row.longitude)
.push_bind(row.latitude)
.push_bind(row.has_tower)
.push_bind(row.has_beacon)
.push_bind(row.public);
});
let query = query_builder.build();
query.execute(pool).await?;
}
Ok(())
}
// TODO
pub async fn update(icao: &str, airport: &UpdateAirport) -> ApiResult<()> {
Ok(())
}
pub async fn delete(icao: &str) -> ApiResult<()> {
let pool = db::pool();
sqlx::query(&format!(
r#"
DELETE FROM {} WHERE icao = $1
"#,
TABLE_NAME
))
.bind(icao.to_string())
.execute(pool)
.await?;
Ok(())
}
pub async fn delete_all() -> ApiResult<()> {
let pool = db::pool();
sqlx::query(&format!(
r#"
DELETE FROM {} WHERE true
"#,
TABLE_NAME
))
.execute(pool)
.await?;
Ok(())
}
}

View File

@@ -0,0 +1,54 @@
use std::fmt::Display;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AirportCategory {
#[serde(rename = "small_airport")]
Small,
#[serde(rename = "medium_airport")]
Medium,
#[serde(rename = "large_airport")]
Large,
#[serde(rename = "heliport")]
Heliport,
#[serde(rename = "closed")]
Closed,
#[serde(rename = "seaplane_base")]
Seaplane,
#[serde(rename = "balloon_port")]
BalloonPort,
#[serde(rename = "unknown")]
Unknown,
}
impl FromStr for AirportCategory {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"small_airport" => Ok(AirportCategory::Small),
"medium_airport" => Ok(AirportCategory::Medium),
"large_airport" => Ok(AirportCategory::Large),
"heliport" => Ok(AirportCategory::Heliport),
"closed" => Ok(AirportCategory::Closed),
"seaplane_base" => Ok(AirportCategory::Seaplane),
"balloon_port" => Ok(AirportCategory::BalloonPort),
_ => Ok(AirportCategory::Unknown),
}
}
}
impl Display for AirportCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AirportCategory::Small => write!(f, "small_airport"),
AirportCategory::Medium => write!(f, "medium_airport"),
AirportCategory::Large => write!(f, "large_airport"),
AirportCategory::Heliport => write!(f, "heliport"),
AirportCategory::Closed => write!(f, "closed"),
AirportCategory::Seaplane => write!(f, "seaplane_base"),
AirportCategory::BalloonPort => write!(f, "balloon_port"),
AirportCategory::Unknown => write!(f, "unknown"),
}
}
}

View File

@@ -0,0 +1,15 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Frequency {
pub id: String,
pub frequency_mhz: f32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateFrequency {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub frequency_mhz: Option<f32>,
}

View File

@@ -0,0 +1,9 @@
mod airport;
mod airport_category;
mod frequency;
mod runway;
pub use airport::*;
pub use airport_category::*;
pub use frequency::*;
pub use runway::*;

View File

@@ -0,0 +1,21 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct Runway {
pub id: String,
pub length_ft: f32,
pub width_ft: f32,
pub surface: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateRunway {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub length_ft: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub width_ft: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub surface: Option<String>,
}

View File

@@ -2,15 +2,15 @@ use std::str::FromStr;
use futures_util::stream::StreamExt as _;
use crate::{
airports::{QueryAirport, QueryFilters, QueryOrderField, QueryOrderBy, Airport, AirportCategory},
db::{Response, Metadata},
airports::{Airport, AirportCategory},
db::Paged,
auth::{Auth, verify_role},
};
use actix_multipart::Multipart;
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest, ResponseError};
use log::{error, warn};
use postgis_diesel::types::{Polygon, Point};
use serde::{Serialize, Deserialize};
use crate::airports::UpdateAirport;
use crate::users::ADMIN_ROLE;
#[derive(Debug, Serialize, Deserialize)]
struct AirportsQuery {
@@ -27,7 +27,7 @@ struct AirportsQuery {
#[post("/import")]
async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
if let Err(err) = verify_role(&auth, "admin") {
if let Err(err) = verify_role(&auth, ADMIN_ROLE) {
return ResponseError::error_response(&err);
};
@@ -43,7 +43,7 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
let data = match chunk {
Ok(data) => data,
Err(err) => {
error!("Failed to get chunk: {}", err);
log::error!("Failed to get chunk: {}", err);
return ResponseError::error_response(&err);
}
};
@@ -54,14 +54,12 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
let airports: Vec<Airport> = match serde_json::from_slice(&bytes) {
Ok(a) => a,
Err(err) => {
error!("Failed to parse JSON: {}", err);
log::error!("Failed to parse JSON: {}", err);
return ResponseError::error_response(&err);
}
};
// Convert Vec<Airport> to Vec<QueryAirport> and insert into database
let query_airports: Vec<QueryAirport> = airports.into_iter().map(|a| a.into()).collect();
match QueryAirport::insert_all(query_airports) {
match Airport::insert_all(airports).await {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
@@ -71,220 +69,83 @@ async fn import_airports(mut payload: Multipart, auth: Auth) -> HttpResponse {
#[get("")]
async fn get_airports(req: HttpRequest) -> HttpResponse {
let params = web::Query::<AirportsQuery>::from_query(req.query_string()).unwrap();
let mut filters = QueryFilters::default();
filters.icaos = match &params.icaos {
Some(i) => Some(i.split(",").map(|s| s.to_string()).collect()),
None => None,
};
filters.name = params.name.clone();
filters.categories = match &params.categories {
Some(c) => Some(
c.split(",")
.map(|s| AirportCategory::from_str(s).unwrap())
.collect(),
),
None => None,
};
filters.bounds = match &params.bounds {
Some(b) => {
let bounds: Vec<&str> = b.split(",").collect();
if bounds.len() != 4 {
warn!("Expected 4 bounds, received {}: {}", bounds.len(), b);
return HttpResponse::UnprocessableEntity().body(format!(
"Received {}; expected NE_LAT,NE_LON,SW_LAT,SW_LON",
b
));
}
let ne_lat = match bounds[0].parse::<f64>() {
Ok(b) => b,
Err(err) => {
warn!("{}", err);
return HttpResponse::UnprocessableEntity().body(format!("{}", err));
}
};
let ne_lon = match bounds[1].parse::<f64>() {
Ok(b) => b,
Err(err) => {
warn!("{}", err);
return HttpResponse::UnprocessableEntity().body(format!("{}", err));
}
};
let sw_lat = match bounds[2].parse::<f64>() {
Ok(b) => b,
Err(err) => {
warn!("{}", err);
return HttpResponse::UnprocessableEntity().body(format!("{}", err));
}
};
let sw_lon = match bounds[3].parse::<f64>() {
Ok(b) => b,
Err(err) => {
warn!("{}", err);
return HttpResponse::UnprocessableEntity().body(format!("{}", err));
}
};
let mut polygon: Polygon<Point> = Polygon::new(Some(4326));
polygon.add_point(Point {
x: sw_lon,
y: sw_lat,
srid: Some(4326),
});
polygon.add_point(Point {
x: ne_lon,
y: sw_lat,
srid: Some(4326),
});
polygon.add_point(Point {
x: ne_lon,
y: ne_lat,
srid: Some(4326),
});
polygon.add_point(Point {
x: sw_lon,
y: ne_lat,
srid: Some(4326),
});
polygon.add_point(Point {
x: sw_lon,
y: sw_lat,
srid: Some(4326),
});
Some(polygon)
}
None => None,
};
filters.order_by = match &params.order_by {
Some(o) => Some(QueryOrderBy::from_str(&o).unwrap()),
None => None,
};
filters.order_field = match &params.order_field {
Some(o) => Some(QueryOrderField::from_str(&o).unwrap()),
None => None,
};
filters.has_metar = match &params.has_metar {
Some(h) => Some(h.parse::<bool>().unwrap()),
None => None,
};
let limit = match params.limit {
Some(l) => l,
None => 100,
};
let page = match params.page {
Some(p) => p,
None => 1,
};
let total = match QueryAirport::get_count(&filters) {
Ok(t) => t,
Err(_) => 0,
};
match web::block(move || QueryAirport::get_all(&filters, limit, page))
.await
.unwrap()
{
Ok(a) => {
// Convert Vec<QueryAirport> to Vec<Airport>
let mut airports: Vec<Airport> = vec![];
for airport in a {
airports.push(airport.into());
}
HttpResponse::Ok().json(Response {
data: airports,
meta: Some(Metadata { page, limit, total }),
})
}
match Airport::select_all().await {
Ok(airports) => HttpResponse::Ok().json(airports),
Err(err) => {
error!("{}", err);
err.to_http_response()
log::error!("{}", err);
ResponseError::error_response(&err)
}
}
}
#[get("/{icao}")]
async fn get_airport(icao: web::Path<String>) -> HttpResponse {
match QueryAirport::get(&icao.into_inner()) {
Ok(a) => {
let airport: Airport = a.into();
HttpResponse::Ok().json(airport)
}
Err(err) => {
error!("{}", err);
err.to_http_response()
}
match Airport::select(&icao.into_inner()).await {
Some(airport) => HttpResponse::Ok().json(airport),
None => HttpResponse::NotFound().finish(),
}
}
#[post("")]
async fn create_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
async fn insert_airport(airport: web::Json<Airport>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, ADMIN_ROLE) {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
let query_airport: QueryAirport = airport.into_inner().into();
match QueryAirport::insert(query_airport) {
Ok(a) => {
let airport: Airport = a.into();
HttpResponse::Ok().json(airport)
}
match airport.insert().await {
Ok(a) => HttpResponse::Ok().json(a),
Err(err) => {
error!("{}", err);
err.to_http_response()
log::error!("{}", err);
ResponseError::error_response(&err)
}
}
}
#[put("/{icao}")]
async fn update_airport(
_icao: web::Path<String>,
airport: web::Json<Airport>,
icao: web::Path<String>,
airport: web::Json<UpdateAirport>,
auth: Auth,
) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
let _ = match verify_role(&auth, ADMIN_ROLE) {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
let query_airport: QueryAirport = airport.into_inner().into();
match QueryAirport::update(query_airport) {
Ok(a) => {
let airport: Airport = a.into();
HttpResponse::Ok().json(airport)
}
match Airport::update(&icao.into_inner(), &airport.into_inner()).await {
Ok(a) => HttpResponse::Ok().json(a),
Err(err) => {
error!("{}", err);
err.to_http_response()
log::error!("{}", err);
ResponseError::error_response(&err)
}
}
}
#[delete("")]
async fn delete_airports(auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
let _ = match verify_role(&auth, ADMIN_ROLE) {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
match QueryAirport::delete(None) {
match Airport::delete_all().await {
Ok(_) => HttpResponse::NoContent().finish(),
Err(err) => {
error!("{}", err);
err.to_http_response()
log::error!("{}", err);
ResponseError::error_response(&err)
}
}
}
#[delete("/{icao}")]
async fn delete_airport(icao: web::Path<String>, auth: Auth) -> HttpResponse {
let _ = match verify_role(&auth, "admin") {
let _ = match verify_role(&auth, ADMIN_ROLE) {
Ok(_) => {}
Err(err) => return ResponseError::error_response(&err),
};
match QueryAirport::delete(Some(icao.into_inner())) {
match Airport::delete(&icao.into_inner()).await {
Ok(_) => HttpResponse::NoContent().finish(),
Err(err) => {
error!("{}", err);
err.to_http_response()
log::error!("{}", err);
ResponseError::error_response(&err)
}
}
}
@@ -295,7 +156,7 @@ pub fn init_routes(config: &mut web::ServiceConfig) {
.service(import_airports)
.service(get_airports)
.service(get_airport)
.service(create_airport)
.service(insert_airport)
.service(update_airport)
.service(delete_airports)
.service(delete_airport),