updating api formats

This commit is contained in:
2023-09-16 22:39:02 -04:00
parent 7d9d32558f
commit 4a3225c3f9
23 changed files with 522 additions and 79 deletions

View File

@@ -45,7 +45,7 @@ dependencies = [
"actix-service",
"actix-utils",
"ahash",
"base64",
"base64 0.21.4",
"bitflags 2.4.0",
"brotli",
"bytes",
@@ -73,6 +73,22 @@ dependencies = [
"zstd",
]
[[package]]
name = "actix-identity"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1224c9f9593dc27c9077b233ce04adedc1d7febcfc35ee9f53ea3c24df180bec"
dependencies = [
"actix-service",
"actix-session",
"actix-utils",
"actix-web",
"anyhow",
"futures-core",
"serde",
"tracing",
]
[[package]]
name = "actix-macros"
version = "0.2.4"
@@ -135,6 +151,23 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "actix-session"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43da8b818ae1f11049a4d218975345fe8e56ce5a5f92c11f972abcff5ff80e87"
dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"anyhow",
"async-trait",
"derive_more",
"serde",
"serde_json",
"tracing",
]
[[package]]
name = "actix-utils"
version = "3.0.1"
@@ -212,6 +245,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "209b47e8954a928e1d72e86eca7000ebb6655fe1436d33eefc2201cad027e237"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]]
name = "ahash"
version = "0.8.3"
@@ -263,6 +331,23 @@ dependencies = [
"libc",
]
[[package]]
name = "anyhow"
version = "1.0.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
[[package]]
name = "async-trait"
version = "0.1.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.32",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@@ -284,6 +369,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]]
name = "base64"
version = "0.21.4"
@@ -390,6 +481,16 @@ dependencies = [
"windows-targets",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -402,7 +503,14 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"aes-gcm",
"base64 0.20.0",
"hkdf",
"hmac",
"percent-encoding",
"rand",
"sha2",
"subtle",
"time",
"version_check",
]
@@ -448,9 +556,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"rand_core",
"typenum",
]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "deranged"
version = "0.3.8"
@@ -526,6 +644,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@@ -689,6 +808,16 @@ dependencies = [
"wasi",
]
[[package]]
name = "ghash"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]]
name = "gimli"
version = "0.28.0"
@@ -732,6 +861,24 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
[[package]]
name = "hkdf"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
dependencies = [
"hmac",
]
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "http"
version = "0.2.9"
@@ -862,6 +1009,15 @@ dependencies = [
"hashbrown 0.14.0",
]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]]
name = "ipnet"
version = "2.8.0"
@@ -1068,6 +1224,12 @@ version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "opaque-debug"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.57"
@@ -1165,6 +1327,18 @@ version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "polyval"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "postgis_diesel"
version = "2.2.1"
@@ -1304,7 +1478,7 @@ version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [
"base64",
"base64 0.21.4",
"bytes",
"encoding_rs",
"futures-core",
@@ -1485,6 +1659,17 @@ dependencies = [
"digest",
]
[[package]]
name = "sha2"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.1"
@@ -1529,6 +1714,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]]
name = "syn"
version = "1.0.109"
@@ -1763,6 +1954,16 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]]
name = "url"
version = "2.4.1"
@@ -1882,6 +2083,7 @@ name = "weather-service"
version = "0.1.0"
dependencies = [
"actix-cors",
"actix-identity",
"actix-rt",
"actix-web",
"chrono",

View File

@@ -8,6 +8,8 @@ edition = "2021"
[dependencies]
actix-web = "4.4.0"
actix-rt = "2.9.0"
actix-cors = "0.6.4"
actix-identity = "0.5.0"
chrono = { version = "0.4.30", features = ["serde"] }
dotenv = "0.15.0"
diesel = { version = "2.0", features = ["postgres", "r2d2", "uuid", "chrono"] }
@@ -24,4 +26,3 @@ serde_json = "1.0.105"
tokio = { version = "1.32.0", features = ["macros", "rt"] }
uuid = { version = "1.4.1", features = ["serde", "v4"] }
log = "0.4.20"
actix-cors = "0.6.4"

View File

@@ -27,7 +27,7 @@ clean:
rm -rf target
clean-db: ## Remove database and Cargo packages
docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"' || true
docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "DROP DATABASE IF EXISTS \"${DATABASE_NAME}\";"'
docker exec -i ${DATABASE_CONTAINER} sh -c 'PGPASSWORD=${DATABASE_PASSWORD} psql -U ${DATABASE_USER} -d postgres -c "CREATE DATABASE \"${DATABASE_NAME}\";"' || true

56
weather-service/README.md Normal file
View File

@@ -0,0 +1,56 @@
## REST API
The REST API for the weather service is described below.
### Import Airports
---
#### Request
`GET /import`
#### Response
```
```
### Get Airports
---
#### Request
`GET /airports`
#### Response
```
```
### Get Airport
---
#### Request
`GET /airports/{icao}`
#### Response
```
```
### Create Airport
---
#### Request
`CREATE /airports`
#### Response
```
```
### Update Airport
---
#### Request
`PUT /airports/{icao}`
#### Response
```
```
### Delete Airport
---
#### Request
`DELETE /airports/{icao}`
#### Response
```
```

View File

@@ -0,0 +1 @@
DROP TABLE users;

View File

@@ -0,0 +1,11 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE IF NOT EXISTS users (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
email TEXT NOT NULL,
username TEXT NOT NULL,
password TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
favorites TEXT[]
);

View File

@@ -1,15 +1,15 @@
use crate::db;
use crate::error_handler::CustomError;
use crate::error_handler::ServiceError;
use crate::schema::airports;
use diesel::prelude::*;
use log::trace;
// use log::trace;
use postgis_diesel::types::*;
use postgis_diesel::functions::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, AsChangeset, Insertable)]
#[diesel(table_name = airports)]
pub struct Airport {
pub struct InsertAirport {
pub icao: String,
pub category: String,
pub full_name: String,
@@ -26,7 +26,7 @@ pub struct Airport {
#[derive(Serialize, Deserialize, Queryable, QueryableByName)]
#[diesel(table_name = airports)]
pub struct Airports {
pub struct QueryAirport {
pub icao: String,
pub id: i32,
pub category: String,
@@ -42,13 +42,13 @@ pub struct Airports {
pub point: Point
}
impl Airports {
pub fn get_all(bounds: Option<Polygon<Point>>, category: Option<String>, filter: Option<String>, limit: i32, page: i32) -> Result<Vec<Self>, CustomError> {
impl QueryAirport {
pub fn get_all(bounds: Option<Polygon<Point>>, category: Option<String>, filter: Option<String>, limit: i32, page: i32) -> Result<Vec<Self>, ServiceError> {
let mut conn = db::connection()?;
let mut query = airports::table
.limit(limit as i64)
.into_boxed();
query = query.filter(airports::id.gt(page * limit));
query = query.filter(airports::id.gt(std::cmp::max(1, page - 1) * limit));
if let Some(bounds) = bounds {
query = query.filter(st_contains(bounds, airports::point));
@@ -62,28 +62,28 @@ impl Airports {
.or(airports::full_name.ilike(format!("%{}%", filter)))
)
}
let debug = diesel::debug_query::<diesel::pg::Pg, _>(&query);
trace!("{}", debug);
let airports: Vec<Airports> = query.order(airports::category.asc()).load::<Airports>(&mut conn)?;
// let debug = diesel::debug_query::<diesel::pg::Pg, _>(&query);
// trace!("{}", debug);
let airports: Vec<QueryAirport> = query.order(airports::category.asc()).load::<QueryAirport>(&mut conn)?;
Ok(airports)
}
pub fn find(icao: String) -> Result<Self, CustomError> {
pub fn find(icao: String) -> Result<Self, ServiceError> {
let mut conn = db::connection()?;
let airport = airports::table.filter(airports::icao.eq(icao)).first(&mut conn)?;
Ok(airport)
}
pub fn create(airport: Airport) -> Result<Self, CustomError> {
pub fn create(airport: InsertAirport) -> Result<Self, ServiceError> {
let mut conn = db::connection()?;
let airport = Airport::from(airport);
let airport = InsertAirport::from(airport);
let airport = diesel::insert_into(airports::table)
.values(airport)
.get_result(&mut conn)?;
Ok(airport)
}
pub fn update(id: i32, airport: Airport) -> Result<Self, CustomError> {
pub fn update(id: i32, airport: InsertAirport) -> Result<Self, ServiceError> {
let mut conn = db::connection()?;
let airport = diesel::update(airports::table)
.filter(airports::id.eq(id))
@@ -92,7 +92,7 @@ pub fn update(id: i32, airport: Airport) -> Result<Self, CustomError> {
Ok(airport)
}
pub fn delete(id: i32) -> Result<usize, CustomError> {
pub fn delete(id: i32) -> Result<usize, ServiceError> {
let mut conn = db::connection()?;
let res = diesel::delete(airports::table.filter(airports::id.eq(id))).execute(&mut conn)?;
Ok(res)

View File

@@ -1,4 +1,4 @@
use crate::{airports::{Airport, Airports}, db::{self, Metadata}};
use crate::{airports::{InsertAirport, QueryAirport}, db::{self, Metadata}};
use actix_web::{delete, get, post, put, web, HttpResponse, HttpRequest};
use log::{error, warn};
use postgis_diesel::types::{Polygon, Point};
@@ -21,7 +21,7 @@ async fn import() -> HttpResponse {
#[derive(Serialize, Deserialize)]
pub struct AirportsResponse {
pub data: Vec<Airports>,
pub data: Vec<QueryAirport>,
pub meta: Metadata
}
@@ -82,7 +82,7 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
None => None
};
match web::block(move || Airports::get_all(polygon, category, filter, params.limit, params.page)).await.unwrap() {
match web::block(move || QueryAirport::get_all(polygon, category, filter, params.limit, params.page)).await.unwrap() {
Ok(a) => HttpResponse::Ok().json(AirportsResponse {
data: a,
meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 }
@@ -96,13 +96,13 @@ async fn get_all(req: HttpRequest) -> HttpResponse {
#[derive(Serialize, Deserialize)]
pub struct AirportResponse {
pub data: Airports,
pub data: QueryAirport,
pub meta: Metadata
}
#[get("/airports/{icao}")]
async fn get(icao: web::Path<String>) -> HttpResponse {
match Airports::find(icao.into_inner()) {
match QueryAirport::find(icao.into_inner()) {
Ok(a) => HttpResponse::Ok().json(AirportResponse {
data: a,
meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 }
@@ -115,8 +115,8 @@ async fn get(icao: web::Path<String>) -> HttpResponse {
}
#[post("/airports")]
async fn create(airport: web::Json<Airport>) -> HttpResponse {
match Airports::create(airport.into_inner()) {
async fn create(airport: web::Json<InsertAirport>) -> HttpResponse {
match QueryAirport::create(airport.into_inner()) {
Ok(a) => HttpResponse::Created().json(a),
Err(err) => {
error!("{}", err);
@@ -125,9 +125,9 @@ async fn create(airport: web::Json<Airport>) -> HttpResponse {
}
}
#[put("/airports/{id}")]
async fn update(id: web::Path<i32>, airport: web::Json<Airport>) -> HttpResponse {
match Airports::update(id.into_inner(), airport.into_inner()) {
#[put("/airports/{icao}")]
async fn update(icao: web::Path<i32>, airport: web::Json<InsertAirport>) -> HttpResponse {
match QueryAirport::update(icao.into_inner(), airport.into_inner()) {
Ok(a) => HttpResponse::Ok().json(a),
Err(err) => {
error!("{}", err);
@@ -136,9 +136,9 @@ async fn update(id: web::Path<i32>, airport: web::Json<Airport>) -> HttpResponse
}
}
#[delete("/airports/{id}")]
async fn delete(id: web::Path<i32>) -> HttpResponse {
match Airports::delete(id.into_inner()) {
#[delete("/airports/{icao}")]
async fn delete(icao: web::Path<i32>) -> HttpResponse {
match QueryAirport::delete(icao.into_inner()) {
Ok(_) => HttpResponse::NoContent().finish(),
Err(err) => {
error!("{}", err);

View File

View File

@@ -1,4 +1,4 @@
use crate::{error_handler::CustomError, airports::{Airport, Airports}};
use crate::{error_handler::ServiceError, airports::{InsertAirport, QueryAirport}};
use diesel::{r2d2::ConnectionManager, PgConnection};
use serde::{Deserialize, Serialize};
use crate::diesel_migrations::MigrationHarness;
@@ -34,18 +34,18 @@ pub fn init() {
};
}
pub fn connection() -> Result<DbConnection, CustomError> {
pub fn connection() -> Result<DbConnection, ServiceError> {
POOL.get()
.map_err(|e| CustomError::new(500, format!("Failed getting db connection: {}", e)))
.map_err(|e| ServiceError::new(500, format!("Failed getting db connection: {}", e)))
}
pub fn import_data() {
let path = "airport-codes.json";
debug!("Importing data from {}", path);
let contents: String = std::fs::read_to_string(path).expect("Failed to read file");
let airports: Vec<Airport> = serde_json::from_str(&contents).expect("JSON was not well formed.");
let airports: Vec<InsertAirport> = serde_json::from_str(&contents).expect("JSON was not well formed.");
for airport in airports {
match Airports::create(airport) {
match QueryAirport::create(airport) {
Ok(_) => {},
Err(err) => error!("Error inserting airport; {}", err)
};

View File

@@ -7,14 +7,14 @@ use serde_json::json;
use std::fmt;
#[derive(Debug, Deserialize, Serialize)]
pub struct CustomError {
pub struct ServiceError {
pub error_status_code: u16,
pub error_message: String,
}
impl CustomError {
pub fn new(error_status_code: u16, error_message: String) -> CustomError {
CustomError {
impl ServiceError {
pub fn new(error_status_code: u16, error_message: String) -> ServiceError {
ServiceError {
error_status_code,
error_message,
}
@@ -32,25 +32,25 @@ impl CustomError {
}
}
impl fmt::Display for CustomError {
impl fmt::Display for ServiceError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(self.error_message.as_str())
}
}
impl From<DieselError> for CustomError {
fn from(error: DieselError) -> CustomError {
impl From<DieselError> for ServiceError {
fn from(error: DieselError) -> ServiceError {
match error {
DieselError::DatabaseError(_, err) => CustomError::new(409, err.message().to_string()),
DieselError::DatabaseError(_, err) => ServiceError::new(409, err.message().to_string()),
DieselError::NotFound => {
CustomError::new(404, "The airport record was not found".to_string())
ServiceError::new(404, "The record was not found".to_string())
}
err => CustomError::new(500, format!("Unknown Diesel error: {}", err)),
err => ServiceError::new(500, format!("Unknown Diesel error: {}", err)),
}
}
}
impl ResponseError for CustomError {
impl ResponseError for ServiceError {
fn error_response(&self) -> HttpResponse {
let status_code = match StatusCode::from_u16(self.error_status_code) {
Ok(status_code) => status_code,

View File

@@ -1,3 +1,4 @@
extern crate actix_web;
extern crate diesel;
#[macro_use]
extern crate diesel_migrations;
@@ -8,12 +9,13 @@ use dotenv::dotenv;
use env_logger::Env;
use listenfd::ListenFd;
use log::debug;
use std::env;
mod airports;
mod auth;
mod db;
mod error_handler;
mod metars;
mod users;
mod schema;
#[actix_rt::main]
@@ -34,6 +36,7 @@ async fn main() -> std::io::Result<()> {
App::new()
.configure(airports::init_routes)
.configure(metars::init_routes)
.configure(users::init_routes)
.wrap(cors)
.wrap(Logger::default())
});
@@ -41,8 +44,8 @@ async fn main() -> std::io::Result<()> {
server = match listenfd.take_tcp_listener(0)? {
Some(listener) => server.listen(listener)?,
None => {
let host = env::var("HOST").expect("Please set host in .env");
let port = env::var("PORT").expect("Please set port in .env");
let host = std::env::var("HOST").expect("Please set host in .env");
let port = std::env::var("PORT").expect("Please set port in .env");
debug!("Binding server to {}:{}", host, port);
server.bind(format!("{}:{}", host, port))?
}

View File

@@ -1,4 +1,4 @@
use crate::{error_handler::CustomError, db};
use crate::{error_handler::ServiceError, db};
use crate::schema::metars;
use diesel::{prelude::*, sql_query};
use log::{warn, trace};
@@ -15,7 +15,7 @@ pub struct QualityControlFlags {
#[derive(Serialize, Deserialize, AsChangeset, Insertable)]
#[diesel(table_name = metars)]
pub struct Metar {
pub struct InsertMetar {
pub raw_text: String,
pub station_id: String,
pub observation_time: String,
@@ -42,10 +42,10 @@ pub struct Metar {
pub elevation_m: i32
}
impl Metar {
pub fn parse(input: String) -> Result<Vec<Self>, CustomError> {
impl InsertMetar {
pub fn parse(input: String) -> Result<Vec<Self>, ServiceError> {
if input.is_empty() {
return Err(CustomError::new(500, "Input is empty".to_string()))
return Err(ServiceError::new(500, "Input is empty".to_string()))
}
let mut reader = Reader::from_str(&input);
@@ -109,7 +109,7 @@ impl Metar {
#[derive(Serialize, Deserialize, Queryable, QueryableByName)]
#[diesel(table_name = metars)]
pub struct Metars {
pub struct QueryMetar {
pub id: i32,
pub raw_text: String,
pub station_id: String,
@@ -137,8 +137,8 @@ pub struct Metars {
pub elevation_m: i32
}
impl Metars {
pub async fn get_all(icaos: String) -> Result<Vec<Self>, CustomError> {
impl QueryMetar {
pub async fn get_all(icaos: String) -> Result<Vec<Self>, ServiceError> {
if icaos.is_empty() {
return Ok(vec![]);
}
@@ -146,12 +146,12 @@ impl Metars {
let station_query: Vec<String> = station_icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect();
let mut conn = db::connection()?;
let mut db_metars: Vec<Metars> = match sql_query(format!("SELECT DISTINCT ON (station_id) * FROM metars WHERE station_id IN ({}) ORDER BY station_id, observation_time DESC", station_query.join(","))).load(&mut conn) {
let mut db_metars: Vec<QueryMetar> = match sql_query(format!("SELECT DISTINCT ON (station_id) * FROM metars WHERE station_id IN ({}) ORDER BY station_id, observation_time DESC", station_query.join(","))).load(&mut conn) {
Ok(m) => m,
Err(err) => return Err(CustomError { error_status_code: 500, error_message: format!("{}", err) })
Err(err) => return Err(ServiceError { error_status_code: 500, error_message: format!("{}", err) })
};
fn get_missing_metar_icaos(db_metars: &Vec<Metars>, station_icaos: Vec<&str>) -> Vec<String> {
fn get_missing_metar_icaos(db_metars: &Vec<QueryMetar>, station_icaos: Vec<&str>) -> Vec<String> {
let mut missing_metar_icaos: Vec<String> = vec![];
let current_time = chrono::Local::now().naive_local().timestamp();
let db_metars_set: HashSet<&str> = db_metars.iter().map(|icao| icao.station_id.as_str()).collect();
@@ -182,10 +182,10 @@ impl Metars {
trace!("Retrieving missing METAR data for {:?}", missing_icaos);
let missing_icaos_string: Vec<String> = missing_icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect();
let url = format!("https://beta.aviationweather.gov/cgi-bin/data/metar.php?ids={}&format=xml", missing_icaos_string.join(","));
let metars: Vec<Metar> = match reqwest::get(url).await {
let metars: Vec<InsertMetar> = match reqwest::get(url).await {
Ok(r) => match r.text().await {
Ok(r) => {
match Metar::parse(r) {
match InsertMetar::parse(r) {
Ok(m) => m,
Err(err) => {
warn!("{}", err);

View File

@@ -1,18 +1,18 @@
use crate::{error_handler::CustomError, db::Metadata};
use crate::metars::Metars;
use crate::{error_handler::ServiceError, db::Metadata};
use crate::metars::QueryMetar;
use actix_web::{get, web, HttpResponse, Responder};
use log::error;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct MetarsResponse {
pub data: Vec<Metars>,
pub data: Vec<QueryMetar>,
pub meta: Metadata
}
#[get("metars/{ids}")]
async fn get_all(ids: web::Path<String>) -> impl Responder {
let airports = match web::block(|| Ok::<_, CustomError>(async {Metars::get_all(ids.into_inner()).await}))
let airports = match web::block(|| Ok::<_, ServiceError>(async {QueryMetar::get_all(ids.into_inner()).await}))
.await
.unwrap()
.unwrap()

View File

@@ -1,6 +1,6 @@
diesel::table! {
use postgis_diesel::sql_types::*;
use diesel::sql_types::*;
use postgis_diesel::sql_types::*;
airports (icao) {
icao -> Text,
id -> Integer,
@@ -43,3 +43,15 @@ diesel::table! {
elevation_m -> Integer,
}
}
diesel::table! {
use diesel::sql_types::*;
use crate::users::PgUserType;
users (id) {
id -> Uuid,
first_name -> Text,
last_name -> Text,
user_type -> PgUserType,
favorites -> Array<Text>
}
}

View File

@@ -0,0 +1,7 @@
mod model;
mod routes;
mod user_type;
pub use user_type::PgUserType;
pub use model::*;
pub use routes::init_routes;

View File

@@ -0,0 +1,50 @@
use std::{future::Future, pin::Pin, sync::RwLock};
use actix_web::{dev::Payload, error::ErrorUnauthorized, web, Error, FromRequest, HttpRequest};
use actix_identity::Identity;
use diesel::{query_builder::AsChangeset, prelude::Insertable};
use log::warn;
use crate::schema::users;
use serde::{Serialize, Deserialize};
use super::user_type::UserType;
#[derive(Serialize, Deserialize, AsChangeset, Insertable)]
#[diesel(table_name = users)]
pub struct InsertUser {
first_name: String,
last_name: String,
user_type: UserType,
favorites: Vec<String>
}
// impl FromRequest for InsertUser {
// type Config = ();
// type Error = Error;
// type Future = Pin<Box<dyn Future<Output = Result<Self, Error>>>>;
// fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
// let fut = Identity::from_request(req, pl);
// let sessions: Option<&web::Data<RwLock<Sessions>>> = req.app_data();
// if sessions.is_none() {
// warn!("sessions is empty(none)!");
// return Box::pin(async { Err(ErrorUnauthorized("unauthorized")) });
// }
// let sessions = sessions.unwrap().clone();
// Box::pin(async move {
// if let Some(identity) = fut.await?.identity() {
// if let Some(user) = sessions
// .read()
// .unwrap()
// .map
// .get(&identity)
// .map(|x| x.clone())
// {
// return Ok(user);
// }
// };
// Err(ErrorUnauthorized("unauthorized"))
// })
// }
// }

View File

@@ -0,0 +1,51 @@
use actix_web::{get, post, delete, put, web, HttpResponse};
#[get("users")]
async fn get() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[get("users/{id}")]
async fn get_all() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[post("users")]
async fn create() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[delete("users")]
async fn delete() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[put("users")]
async fn update() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[get("users/favorites")]
async fn get_favorites() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[post("users/favorites")]
async fn add_favorite() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
#[delete("users/favorites")]
async fn delete_favorite() -> HttpResponse {
HttpResponse::NotImplemented().finish()
}
pub fn init_routes(config: &mut web::ServiceConfig) {
config.service(get);
config.service(create);
config.service(delete);
config.service(update);
config.service(get_favorites);
config.service(add_favorite);
config.service(delete_favorite);
}

View File

@@ -0,0 +1,35 @@
use std::io::Write;
use diesel::{sql_types::SqlType, deserialize::{FromSqlRow, FromSql, self}, expression::AsExpression, serialize::{ToSql, Output, self, IsNull}, pg::{Pg, PgValue}};
use serde::{Serialize, Deserialize};
#[derive(SqlType)]
#[diesel(postgres_type(name = "User_Type"))]
pub struct PgUserType;
#[derive(Serialize, Deserialize, Debug, PartialEq, FromSqlRow, AsExpression, Eq)]
#[diesel(sql_type = PgUserType)]
pub enum UserType {
Admin,
User,
}
impl ToSql<PgUserType, Pg> for UserType {
fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Pg>) -> serialize::Result {
match *self {
Self::Admin => out.write_all(b"admin")?,
Self::User => out.write_all(b"user")?,
}
Ok(IsNull::No)
}
}
impl FromSql<PgUserType, Pg> for UserType {
fn from_sql(bytes: PgValue<'_>) -> deserialize::Result<Self> {
match bytes.as_bytes() {
b"admin" => Ok(Self::Admin),
b"user" => Ok(Self::User),
_ => Err("Unrecognized enum variant".into()),
}
}
}

View File

@@ -1,5 +1,7 @@
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': {},
tailwindcss: {},
autoprefixer: {}
}

View File

@@ -118,7 +118,15 @@ export default function MapTiles() {
position={[airport.point.y, airport.point.x]}
icon={icon(airport)}
eventHandlers={{
click: () => handleOpen(airport)
click: () => {
mapEvents.eachLayer((l) => {
if (l.getTooltip() && l.isTooltipOpen()) {
console.log('l', l);
l.closeTooltip();
}
});
handleOpen(airport);
}
}}
>
{!isOpen && (

View File

@@ -36,12 +36,16 @@ export default function MetarDialog({ airport, isOpen, onClose }: MetarDialogPro
}
function windColor(metar: Metar | undefined) {
if (Number(metar?.wind_speed_kt) <= 9) {
return 'bg-green-300';
} else if (Number(metar?.wind_speed_kt) > 9) {
return 'bg-orange-300';
} else if (Number(metar?.wind_speed_kt) > 12) {
return 'bg-red-300';
if (metar) {
if (Number(metar.wind_speed_kt) <= 9) {
return 'bg-green-300';
} else if (Number(metar.wind_speed_kt) <= 12) {
return 'bg-orange-300';
} else {
return 'bg-red-300';
}
} else {
return 'gb-gray-100';
}
}
return (

View File

@@ -9,8 +9,8 @@ export default function Map({ className = '' }: { className?: string }) {
<MapContainer
center={[38.7209, -77.5133]}
zoom={8}
maxZoom={12}
minZoom={1}
maxZoom={14} // Zoomed in
minZoom={3} // Zoomed out
id='map-container'
style={{ height: '94.5vh' }}
className={`${className} overflow-y-hidden overflow-x-hidden`}