Renamed directories
This commit is contained in:
2270
service/Cargo.lock
generated
Normal file
2270
service/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
service/Cargo.toml
Normal file
28
service/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "weather-service"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[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"] }
|
||||
postgis_diesel = { version = "2.2.1", features = ["serde"] }
|
||||
diesel_migrations = { version = "2.0", features = ["postgres"] }
|
||||
env_logger = "0.10.0"
|
||||
lazy_static = "1.4.0"
|
||||
listenfd = "1.0.1"
|
||||
quick-xml = { version = "0.30.0", features = ["serialize"] }
|
||||
r2d2 = "0.8.10"
|
||||
reqwest = "0.11.20"
|
||||
serde = {version = "1.0.188", features = ["derive"]}
|
||||
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"
|
||||
62
service/README.md
Normal file
62
service/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Aviation Weather
|
||||
|
||||
## UI
|
||||
|
||||
## Service
|
||||
|
||||
## 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
|
||||
```
|
||||
```
|
||||
65306
service/airport-codes.json
Normal file
65306
service/airport-codes.json
Normal file
File diff suppressed because it is too large
Load Diff
7
service/diesel.toml
Normal file
7
service/diesel.toml
Normal file
@@ -0,0 +1,7 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
||||
custom_type_derives = ["diesel::sql_types::SqlType", "std::fmt::Debug"]
|
||||
import_types = ["diesel::sql_types::*", "postgis_diesel::sql_types::*"]
|
||||
1
service/migrations/000000_create_airports/down.sql
Normal file
1
service/migrations/000000_create_airports/down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE airports;
|
||||
16
service/migrations/000000_create_airports/up.sql
Normal file
16
service/migrations/000000_create_airports/up.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||
CREATE TABLE IF NOT EXISTS airports (
|
||||
icao TEXT PRIMARY KEY NOT NULL,
|
||||
id INTEGER GENERATED ALWAYS AS IDENTITY,
|
||||
category TEXT NOT NULL,
|
||||
full_name TEXT NOT NULL,
|
||||
elevation_ft INTEGER,
|
||||
continent TEXT NOT NULL,
|
||||
iso_country TEXT NOT NULL,
|
||||
iso_region TEXT NOT NULL,
|
||||
municipality TEXT NOT NULL,
|
||||
gps_code TEXT NOT NULL,
|
||||
iata_code TEXT NOT NULL,
|
||||
local_code TEXT NOT NULL,
|
||||
point GEOMETRY(POINT,4326) NOT NULL
|
||||
);
|
||||
1
service/migrations/000001_create_metars/down.sql
Normal file
1
service/migrations/000001_create_metars/down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE metars;
|
||||
26
service/migrations/000001_create_metars/up.sql
Normal file
26
service/migrations/000001_create_metars/up.sql
Normal file
@@ -0,0 +1,26 @@
|
||||
CREATE TABLE IF NOT EXISTS metars (
|
||||
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||
raw_text TEXT NOT NULL,
|
||||
station_id TEXT NOT NULL,
|
||||
observation_time TEXT NOT NULL,
|
||||
latitude DOUBLE PRECISION NOT NULL,
|
||||
longitude DOUBLE PRECISION NOT NULL,
|
||||
temp_c DOUBLE PRECISION,
|
||||
dewpoint_c DOUBLE PRECISION,
|
||||
wind_dir_degrees TEXT,
|
||||
wind_speed_kt INTEGER,
|
||||
visibility_statute_mi TEXT,
|
||||
altim_in_hg DOUBLE PRECISION,
|
||||
sea_level_pressure_mb DOUBLE PRECISION,
|
||||
qcf_auto BOOLEAN,
|
||||
qcf_auto_station BOOLEAN,
|
||||
wx_string TEXT,
|
||||
sky_condition TEXT[],
|
||||
flight_category TEXT,
|
||||
three_hr_pressure_tendency_mb DOUBLE PRECISION,
|
||||
metar_type TEXT NOT NULL,
|
||||
max_t_c DOUBLE PRECISION,
|
||||
min_t_c DOUBLE PRECISION,
|
||||
precip_in DOUBLE PRECISION,
|
||||
elevation_m INTEGER NOT NULL
|
||||
);
|
||||
1
service/migrations/000002_create_users/down.sql
Normal file
1
service/migrations/000002_create_users/down.sql
Normal file
@@ -0,0 +1 @@
|
||||
DROP TABLE users;
|
||||
11
service/migrations/000002_create_users/up.sql
Normal file
11
service/migrations/000002_create_users/up.sql
Normal 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[]
|
||||
);
|
||||
5
service/src/airports/mod.rs
Normal file
5
service/src/airports/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
pub use model::*;
|
||||
pub use routes::init_routes;
|
||||
108
service/src/airports/model.rs
Normal file
108
service/src/airports/model.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use crate::db;
|
||||
use crate::error_handler::ServiceError;
|
||||
use crate::schema::airports;
|
||||
use diesel::prelude::*;
|
||||
// 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 InsertAirport {
|
||||
pub icao: String,
|
||||
pub category: String,
|
||||
pub full_name: String,
|
||||
pub elevation_ft: Option<i32>,
|
||||
pub continent: String,
|
||||
pub iso_country: String,
|
||||
pub iso_region: String,
|
||||
pub municipality: String,
|
||||
pub gps_code: String,
|
||||
pub iata_code: String,
|
||||
pub local_code: String,
|
||||
pub point: Point
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Queryable, QueryableByName)]
|
||||
#[diesel(table_name = airports)]
|
||||
pub struct QueryAirport {
|
||||
pub icao: String,
|
||||
pub id: i32,
|
||||
pub category: String,
|
||||
pub full_name: String,
|
||||
pub elevation_ft: Option<i32>,
|
||||
pub continent: String,
|
||||
pub iso_country: String,
|
||||
pub iso_region: String,
|
||||
pub municipality: String,
|
||||
pub gps_code: String,
|
||||
pub iata_code: String,
|
||||
pub local_code: String,
|
||||
pub point: Point
|
||||
}
|
||||
|
||||
impl QueryAirport {
|
||||
pub fn get_all(bounds: Option<Polygon<Point>>, category: Option<String>, filter: Option<String>, limit: Option<i32>, page: Option<i32>) -> Result<Vec<Self>, ServiceError> {
|
||||
let mut conn = db::connection()?;
|
||||
let limit = match limit {
|
||||
Some(l) => l,
|
||||
None => 100
|
||||
};
|
||||
let page = match page {
|
||||
Some(p) => p,
|
||||
None => 1
|
||||
};
|
||||
let mut query = airports::table
|
||||
.limit(limit as i64)
|
||||
.into_boxed();
|
||||
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));
|
||||
}
|
||||
if let Some(category) = category {
|
||||
query = query.filter(airports::category.eq(category));
|
||||
}
|
||||
if let Some(filter) = filter {
|
||||
query = query.filter(airports::icao
|
||||
.ilike(format!("%{}%", filter))
|
||||
.or(airports::full_name.ilike(format!("%{}%", filter)))
|
||||
)
|
||||
}
|
||||
// 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, ServiceError> {
|
||||
let mut conn = db::connection()?;
|
||||
let airport = airports::table.filter(airports::icao.eq(icao)).first(&mut conn)?;
|
||||
Ok(airport)
|
||||
}
|
||||
|
||||
pub fn create(airport: InsertAirport) -> Result<Self, ServiceError> {
|
||||
let mut conn = db::connection()?;
|
||||
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: InsertAirport) -> Result<Self, ServiceError> {
|
||||
let mut conn = db::connection()?;
|
||||
let airport = diesel::update(airports::table)
|
||||
.filter(airports::id.eq(id))
|
||||
.set(airport)
|
||||
.get_result(&mut conn)?;
|
||||
Ok(airport)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
157
service/src/airports/routes.rs
Normal file
157
service/src/airports/routes.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
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};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct GetAllParameters {
|
||||
filter: Option<String>,
|
||||
bounds: Option<String>,
|
||||
category: Option<String>,
|
||||
limit: Option<i32>,
|
||||
page: Option<i32>
|
||||
}
|
||||
|
||||
#[get("/import")]
|
||||
async fn import() -> HttpResponse {
|
||||
db::import_data();
|
||||
HttpResponse::Ok().body({})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AirportsResponse {
|
||||
pub data: Vec<QueryAirport>,
|
||||
pub meta: Metadata
|
||||
}
|
||||
|
||||
#[get("/airports")]
|
||||
async fn get_all(req: HttpRequest) -> HttpResponse {
|
||||
let params = web::Query::<GetAllParameters>::from_query(req.query_string()).unwrap();
|
||||
let polygon: Option<Polygon<Point>> = match ¶ms.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
|
||||
};
|
||||
let category = match ¶ms.category {
|
||||
Some(c) => Some(c.to_string()),
|
||||
None => None
|
||||
};
|
||||
let filter = match ¶ms.filter {
|
||||
Some(f) => Some(f.to_string()),
|
||||
None => None
|
||||
};
|
||||
|
||||
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 }
|
||||
}),
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
err.to_http_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AirportResponse {
|
||||
pub data: QueryAirport,
|
||||
pub meta: Metadata
|
||||
}
|
||||
|
||||
#[get("/airports/{icao}")]
|
||||
async fn get(icao: web::Path<String>) -> HttpResponse {
|
||||
match QueryAirport::find(icao.into_inner()) {
|
||||
Ok(a) => HttpResponse::Ok().json(AirportResponse {
|
||||
data: a,
|
||||
meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 }
|
||||
}),
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
err.to_http_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/airports")]
|
||||
async fn create(airport: web::Json<InsertAirport>) -> HttpResponse {
|
||||
match QueryAirport::create(airport.into_inner()) {
|
||||
Ok(a) => HttpResponse::Created().json(a),
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
err.to_http_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
err.to_http_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
err.to_http_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
config.service(get_all);
|
||||
config.service(get);
|
||||
config.service(create);
|
||||
config.service(update);
|
||||
config.service(delete);
|
||||
config.service(import);
|
||||
}
|
||||
0
service/src/auth/mod.rs
Normal file
0
service/src/auth/mod.rs
Normal file
68
service/src/db.rs
Normal file
68
service/src/db.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
use crate::{error_handler::ServiceError, airports::{InsertAirport, QueryAirport}};
|
||||
use diesel::{r2d2::ConnectionManager, PgConnection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::diesel_migrations::MigrationHarness;
|
||||
use lazy_static::lazy_static;
|
||||
use log::{error, debug, info};
|
||||
use r2d2;
|
||||
use std::env;
|
||||
|
||||
type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||
pub type DbConnection = r2d2::PooledConnection<ConnectionManager<PgConnection>>;
|
||||
|
||||
pub const MIGRATIONS: diesel_migrations::EmbeddedMigrations = embed_migrations!();
|
||||
|
||||
lazy_static! {
|
||||
static ref POOL: Pool = {
|
||||
let username = env::var("DATABASE_USER").expect("Database username is not set");
|
||||
let password = env::var("DATABASE_PASSWORD").expect("Database password is not set");
|
||||
let host = env::var("DATABASE_HOST").expect("Database host is not set");
|
||||
let name = env::var("DATABASE_NAME").expect("Database name is not set");
|
||||
let port = env::var("DATABASE_PORT").expect("Database port is not set");
|
||||
let url = format!("postgres://{}:{}@{}:{}/{}", username, password, host, port, name);
|
||||
let manager = ConnectionManager::<PgConnection>::new(url);
|
||||
Pool::builder().test_on_check_out(true).build(manager).expect("Failed to create db pool")
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init() {
|
||||
lazy_static::initialize(&POOL);
|
||||
let mut pool: DbConnection = connection().expect("Failed to get db connection");
|
||||
match pool.run_pending_migrations(MIGRATIONS) {
|
||||
Ok(_) => info!("Database initialized"),
|
||||
Err(err) => error!("Failed to initialize database; {}", err)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn connection() -> Result<DbConnection, ServiceError> {
|
||||
POOL.get()
|
||||
.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<InsertAirport> = serde_json::from_str(&contents).expect("JSON was not well formed.");
|
||||
for airport in airports {
|
||||
match QueryAirport::create(airport) {
|
||||
Ok(_) => {},
|
||||
Err(err) => error!("Error inserting airport; {}", err)
|
||||
};
|
||||
}
|
||||
debug!("Import complete");
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Metadata {
|
||||
pub page: i32,
|
||||
pub limit: i32,
|
||||
pub pages: i32,
|
||||
pub total: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct Coordinate {
|
||||
pub lon: f64,
|
||||
pub lat: f64
|
||||
}
|
||||
67
service/src/error_handler.rs
Normal file
67
service/src/error_handler.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{HttpResponse, ResponseError};
|
||||
use diesel::result::Error as DieselError;
|
||||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ServiceError {
|
||||
pub error_status_code: u16,
|
||||
pub error_message: String,
|
||||
}
|
||||
|
||||
impl ServiceError {
|
||||
pub fn new(error_status_code: u16, error_message: String) -> ServiceError {
|
||||
ServiceError {
|
||||
error_status_code,
|
||||
error_message,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_http_response(&self) -> HttpResponse {
|
||||
let status_code = match StatusCode::from_u16(self.error_status_code) {
|
||||
Ok(s) => s,
|
||||
Err(err) => {
|
||||
warn!("{}", err);
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
}
|
||||
};
|
||||
HttpResponse::build(status_code).body(self.error_message.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
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 ServiceError {
|
||||
fn from(error: DieselError) -> ServiceError {
|
||||
match error {
|
||||
DieselError::DatabaseError(_, err) => ServiceError::new(409, err.message().to_string()),
|
||||
DieselError::NotFound => {
|
||||
ServiceError::new(404, "The record was not found".to_string())
|
||||
}
|
||||
err => ServiceError::new(500, format!("Unknown Diesel error: {}", err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
let error_message = match status_code.as_u16() < 500 {
|
||||
true => self.error_message.clone(),
|
||||
false => "Internal server error".to_string(),
|
||||
};
|
||||
|
||||
HttpResponse::build(status_code).json(json!({ "message": error_message }))
|
||||
}
|
||||
}
|
||||
54
service/src/main.rs
Normal file
54
service/src/main.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
extern crate actix_web;
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
use actix_cors::Cors;
|
||||
use actix_web::{App, HttpServer, middleware::Logger};
|
||||
use dotenv::dotenv;
|
||||
use env_logger::Env;
|
||||
use listenfd::ListenFd;
|
||||
use log::debug;
|
||||
|
||||
mod airports;
|
||||
mod auth;
|
||||
mod db;
|
||||
mod error_handler;
|
||||
mod metars;
|
||||
mod users;
|
||||
mod schema;
|
||||
|
||||
#[actix_rt::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
dotenv().ok();
|
||||
if std::env::var_os("RUST_LOG").is_none() {
|
||||
std::env::set_var("RUST_LOG", "info,actix=info,diesel_migrations=warn,reqwest=warn,hyper=warn");
|
||||
}
|
||||
env_logger::init_from_env(Env::default().default_filter_or("info"));
|
||||
db::init();
|
||||
|
||||
let mut listenfd = ListenFd::from_env();
|
||||
let mut server = HttpServer::new(|| {
|
||||
let cors = Cors::default()
|
||||
.allow_any_origin()
|
||||
.allow_any_method()
|
||||
.allow_any_header();
|
||||
App::new()
|
||||
.configure(airports::init_routes)
|
||||
.configure(metars::init_routes)
|
||||
.configure(users::init_routes)
|
||||
.wrap(cors)
|
||||
.wrap(Logger::default())
|
||||
});
|
||||
|
||||
server = match listenfd.take_tcp_listener(0)? {
|
||||
Some(listener) => server.listen(listener)?,
|
||||
None => {
|
||||
let host = std::env::var("SERVICE_HOST").expect("Please set host in .env");
|
||||
let port = std::env::var("SERVICE_PORT").expect("Please set port in .env");
|
||||
debug!("Binding server to {}:{}", host, port);
|
||||
server.bind(format!("{}:{}", host, port))?
|
||||
}
|
||||
};
|
||||
server.run().await
|
||||
}
|
||||
5
service/src/metars/mod.rs
Normal file
5
service/src/metars/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
pub use model::*;
|
||||
pub use routes::init_routes;
|
||||
382
service/src/metars/model.rs
Normal file
382
service/src/metars/model.rs
Normal file
@@ -0,0 +1,382 @@
|
||||
use crate::{error_handler::ServiceError, db};
|
||||
use crate::schema::metars::{self};
|
||||
use diesel::{prelude::*, sql_query};
|
||||
use log::{warn, trace};
|
||||
use std::collections::HashSet;
|
||||
use std::io::BufRead;
|
||||
use quick_xml::{Reader, events::{Event, BytesStart}, Writer, de::Deserializer};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct QualityControlFlags {
|
||||
pub auto: Option<bool>,
|
||||
pub auto_station: Option<bool>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SkyCondition {
|
||||
#[serde(rename = "@sky_cover")]
|
||||
pub sky_cover: String,
|
||||
#[serde(rename = "@cloud_base_ft_agl")]
|
||||
pub cloud_base_ft_agl: Option<i32>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Metar {
|
||||
pub raw_text: String,
|
||||
pub station_id: String,
|
||||
pub observation_time: String,
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub temp_c: Option<f64>,
|
||||
pub dewpoint_c: Option<f64>,
|
||||
pub wind_dir_degrees: Option<String>,
|
||||
pub wind_speed_kt: Option<i32>,
|
||||
pub visibility_statute_mi: Option<String>,
|
||||
pub altim_in_hg: Option<f64>,
|
||||
pub sea_level_pressure_mb: Option<f64>,
|
||||
pub quality_control_flags: Option<QualityControlFlags>,
|
||||
pub wx_string: Option<String>,
|
||||
pub sky_condition: Option<Vec<SkyCondition>>,
|
||||
pub flight_category: String,
|
||||
pub three_hr_pressure_tendency_mb: Option<f64>,
|
||||
pub metar_type: String,
|
||||
#[serde(rename = "maxT_c")]
|
||||
pub max_t_c: Option<f64>,
|
||||
#[serde(rename = "minT_c")]
|
||||
pub min_t_c: Option<f64>,
|
||||
pub precip_in: Option<f64>,
|
||||
pub elevation_m: i32
|
||||
}
|
||||
|
||||
impl Metar {
|
||||
fn parse(input: String) -> Result<Vec<Self>, ServiceError> {
|
||||
if input.is_empty() {
|
||||
return Err(ServiceError::new(500, "Input is empty".to_string()))
|
||||
}
|
||||
|
||||
let mut reader = Reader::from_str(&input);
|
||||
let mut buf = Vec::new();
|
||||
let mut junk_buf: Vec<u8> = Vec::new();
|
||||
let mut metars: Vec<Self> = vec![];
|
||||
|
||||
loop {
|
||||
match reader.read_event_into(&mut buf) {
|
||||
Err(e) => panic!("Error at position: {}: {:?}", reader.buffer_position(), e),
|
||||
Ok(Event::Eof) => break,
|
||||
Ok(Event::Start(e)) => {
|
||||
match e.name().as_ref() {
|
||||
b"METAR" => {
|
||||
let metar_bytes = Self::read_to_end_into_buffer(&mut reader, &e, &mut junk_buf).unwrap();
|
||||
let str = std::str::from_utf8(&metar_bytes).unwrap();
|
||||
let mut deserializer = Deserializer::from_str(str);
|
||||
match Self::deserialize(&mut deserializer) {
|
||||
Ok(m) => metars.push(m),
|
||||
Err(err) => warn!("Error deserializing; {}", err)
|
||||
};
|
||||
},
|
||||
_ => ()
|
||||
}
|
||||
},
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(metars)
|
||||
}
|
||||
|
||||
// https://capnfabs.net/posts/parsing-huge-xml-quickxml-rust-serde/
|
||||
fn read_to_end_into_buffer<R: BufRead>(reader: &mut Reader<R>, start_tag: &BytesStart, junk_buf: &mut Vec<u8>) -> Result<Vec<u8>, quick_xml::Error> {
|
||||
let mut depth = 0;
|
||||
let mut output_buf: Vec<u8> = Vec::new();
|
||||
let mut w = Writer::new(&mut output_buf);
|
||||
let tag_name = start_tag.name();
|
||||
w.write_event(Event::Start(start_tag.clone()))?;
|
||||
loop {
|
||||
junk_buf.clear();
|
||||
let event = reader.read_event_into(junk_buf)?;
|
||||
w.write_event(&event)?;
|
||||
|
||||
match event {
|
||||
Event::Start(e) if e.name() == tag_name => depth += 1,
|
||||
Event::End(e) if e.name() == tag_name => {
|
||||
if depth == 0 {
|
||||
return Ok(output_buf);
|
||||
}
|
||||
depth -= 1;
|
||||
}
|
||||
Event::Eof => {
|
||||
panic!("EOF")
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_missing_metar_icaos(db_metars: &Vec<Self>, 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();
|
||||
let station_icaos_set: HashSet<&str> = station_icaos.to_owned().into_iter().collect();
|
||||
for difference in db_metars_set.symmetric_difference(&station_icaos_set) {
|
||||
missing_metar_icaos.push(difference.to_string());
|
||||
}
|
||||
for metar in db_metars {
|
||||
match chrono::NaiveDateTime::parse_and_remainder(&metar.observation_time, "%Y-%m-%dT%H:%M:%S") {
|
||||
Ok((time, _)) => {
|
||||
if current_time > (time.timestamp() + 3600) {
|
||||
trace!("{} METAR data is outdated", metar.station_id);
|
||||
missing_metar_icaos.push(metar.station_id.to_string());
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Parsing METAR timestamp failed; {}", err);
|
||||
missing_metar_icaos.push(metar.station_id.to_string());
|
||||
}
|
||||
};
|
||||
}
|
||||
return missing_metar_icaos;
|
||||
}
|
||||
|
||||
async fn get_remote_metars(icaos: String) -> Vec<Metar> {
|
||||
let url = format!("https://beta.aviationweather.gov/cgi-bin/data/metar.php?ids={}&format=xml", icaos);
|
||||
match reqwest::get(url).await {
|
||||
Ok(r) => match r.text().await {
|
||||
Ok(r) => {
|
||||
match Metar::parse(r) {
|
||||
Ok(m) => m,
|
||||
Err(err) => {
|
||||
warn!("{}", err);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Unable to parse METAR request: {}", err);
|
||||
vec![]
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
warn!("Unable to get METAR request: {}", err);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn from_query(query_metars: Vec<QueryMetar>) -> Vec<Self> {
|
||||
let mut metars: Vec<Metar> = vec![];
|
||||
for metar in query_metars {
|
||||
let quality_control_flags = Some(QualityControlFlags {
|
||||
auto: metar.qcf_auto,
|
||||
auto_station: metar.qcf_auto_station
|
||||
});
|
||||
let sky_condition = match metar.sky_condition {
|
||||
Some(s) => {
|
||||
let mut sc: Vec<SkyCondition> = vec![];
|
||||
for string in s {
|
||||
let split: Vec<&str> = string.split_whitespace().collect();
|
||||
if split.len() == 1 {
|
||||
sc.push(SkyCondition { sky_cover: split[0].to_string(), cloud_base_ft_agl: None })
|
||||
} else if split.len() == 2 {
|
||||
let cloud_base = split[1].parse::<i32>().unwrap();
|
||||
sc.push(SkyCondition { sky_cover: split[0].to_string(), cloud_base_ft_agl: Some(cloud_base) })
|
||||
}
|
||||
}
|
||||
Some(sc)
|
||||
},
|
||||
None => None
|
||||
};
|
||||
metars.push(Metar {
|
||||
raw_text: metar.raw_text,
|
||||
station_id: metar.station_id,
|
||||
observation_time: metar.observation_time,
|
||||
latitude: metar.latitude,
|
||||
longitude: metar.longitude,
|
||||
temp_c: metar.temp_c,
|
||||
dewpoint_c: metar.dewpoint_c,
|
||||
wind_dir_degrees: metar.wind_dir_degrees,
|
||||
wind_speed_kt: metar.wind_speed_kt,
|
||||
visibility_statute_mi: metar.visibility_statute_mi,
|
||||
altim_in_hg: metar.altim_in_hg,
|
||||
sea_level_pressure_mb: metar.sea_level_pressure_mb,
|
||||
quality_control_flags,
|
||||
wx_string: metar.wx_string,
|
||||
sky_condition,
|
||||
flight_category: metar.flight_category,
|
||||
three_hr_pressure_tendency_mb: metar.three_hr_pressure_tendency_mb,
|
||||
metar_type: metar.metar_type,
|
||||
max_t_c: metar.max_t_c,
|
||||
min_t_c: metar.min_t_c,
|
||||
precip_in: metar.precip_in,
|
||||
elevation_m: metar.elevation_m
|
||||
})
|
||||
}
|
||||
return metars;
|
||||
}
|
||||
|
||||
fn to_insert(metars: &Vec<Self>) -> Vec<InsertMetar> {
|
||||
let mut insert_metars: Vec<InsertMetar> = vec![];
|
||||
for metar in metars {
|
||||
insert_metars.push(InsertMetar {
|
||||
raw_text: metar.raw_text.to_string(),
|
||||
station_id: metar.station_id.to_string(),
|
||||
observation_time: metar.observation_time.to_string(),
|
||||
latitude: metar.latitude,
|
||||
longitude: metar.longitude,
|
||||
temp_c: metar.temp_c,
|
||||
dewpoint_c: metar.dewpoint_c,
|
||||
wind_dir_degrees: match &metar.wind_dir_degrees {
|
||||
Some(m) => Some(m.to_string()),
|
||||
None => None
|
||||
},
|
||||
wind_speed_kt: metar.wind_speed_kt,
|
||||
visibility_statute_mi: match &metar.visibility_statute_mi {
|
||||
Some(m) => Some(m.to_string()),
|
||||
None => None
|
||||
},
|
||||
altim_in_hg: metar.altim_in_hg,
|
||||
sea_level_pressure_mb: metar.sea_level_pressure_mb,
|
||||
qcf_auto: match &metar.quality_control_flags {
|
||||
Some(m) => m.auto,
|
||||
None => None
|
||||
},
|
||||
qcf_auto_station: match &metar.quality_control_flags {
|
||||
Some(m) => m.auto_station,
|
||||
None => None
|
||||
},
|
||||
wx_string: match &metar.wx_string {
|
||||
Some(m) => Some(m.to_string()),
|
||||
None => None
|
||||
},
|
||||
sky_condition: match &metar.sky_condition {
|
||||
Some(s) => {
|
||||
let mut sc: Vec<String> = vec![];
|
||||
for condition in s {
|
||||
if let Some(cloud_base) = condition.cloud_base_ft_agl {
|
||||
sc.push(format!("{} {}", condition.sky_cover, cloud_base));
|
||||
} else {
|
||||
sc.push(format!("{}", condition.sky_cover));
|
||||
}
|
||||
}
|
||||
Some(sc)
|
||||
},
|
||||
None => None
|
||||
},
|
||||
flight_category: metar.flight_category.to_string(),
|
||||
three_hr_pressure_tendency_mb: metar.three_hr_pressure_tendency_mb,
|
||||
metar_type: metar.metar_type.to_string(),
|
||||
max_t_c: metar.max_t_c,
|
||||
min_t_c: metar.min_t_c,
|
||||
precip_in: metar.precip_in,
|
||||
elevation_m: metar.elevation_m
|
||||
});
|
||||
}
|
||||
return insert_metars;
|
||||
}
|
||||
|
||||
pub async fn get_all(icaos: String) -> Result<Vec<Self>, ServiceError> {
|
||||
if icaos.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
let station_icaos: Vec<&str> = icaos.split(',').collect();
|
||||
let mut db_metars = match QueryMetar::get_all(&station_icaos) {
|
||||
Ok(m) => Self::from_query(m),
|
||||
Err(err) => return Err(err)
|
||||
};
|
||||
|
||||
let missing_icaos = Self::get_missing_metar_icaos(&db_metars, &station_icaos);
|
||||
if missing_icaos.is_empty() {
|
||||
return Ok(db_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 mut missing_metars = Self::get_remote_metars(missing_icaos_string.join(",")).await;
|
||||
if missing_metars.len() > 0 {
|
||||
let insert_metars = Self::to_insert(&missing_metars);
|
||||
let mut conn = db::connection()?;
|
||||
match diesel::insert_into(metars::table).values(&insert_metars).execute(&mut conn) {
|
||||
Ok(rows) => trace!("Inserted {} metar rows", rows),
|
||||
Err(err) => warn!("Unable to insert metar data; {}", err)
|
||||
};
|
||||
}
|
||||
let mut metars: Vec<Metar> = vec![];
|
||||
metars.append(&mut missing_metars);
|
||||
metars.append(&mut db_metars);
|
||||
Ok(metars)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, AsChangeset, Insertable)]
|
||||
#[diesel(table_name = metars)]
|
||||
struct InsertMetar {
|
||||
raw_text: String,
|
||||
station_id: String,
|
||||
observation_time: String,
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
temp_c: Option<f64>,
|
||||
dewpoint_c: Option<f64>,
|
||||
wind_dir_degrees: Option<String>,
|
||||
wind_speed_kt: Option<i32>,
|
||||
visibility_statute_mi: Option<String>,
|
||||
altim_in_hg: Option<f64>,
|
||||
sea_level_pressure_mb: Option<f64>,
|
||||
qcf_auto: Option<bool>,
|
||||
qcf_auto_station: Option<bool>,
|
||||
wx_string: Option<String>,
|
||||
sky_condition: Option<Vec<String>>,
|
||||
flight_category: String,
|
||||
three_hr_pressure_tendency_mb: Option<f64>,
|
||||
metar_type: String,
|
||||
#[serde(rename = "maxT_c")]
|
||||
max_t_c: Option<f64>,
|
||||
#[serde(rename = "minT_c")]
|
||||
min_t_c: Option<f64>,
|
||||
precip_in: Option<f64>,
|
||||
elevation_m: i32
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Queryable, QueryableByName)]
|
||||
#[diesel(table_name = metars)]
|
||||
struct QueryMetar {
|
||||
id: i32,
|
||||
raw_text: String,
|
||||
station_id: String,
|
||||
observation_time: String,
|
||||
latitude: f64,
|
||||
longitude: f64,
|
||||
temp_c: Option<f64>,
|
||||
dewpoint_c: Option<f64>,
|
||||
wind_dir_degrees: Option<String>,
|
||||
wind_speed_kt: Option<i32>,
|
||||
visibility_statute_mi: Option<String>,
|
||||
altim_in_hg: Option<f64>,
|
||||
sea_level_pressure_mb: Option<f64>,
|
||||
qcf_auto: Option<bool>,
|
||||
qcf_auto_station: Option<bool>,
|
||||
wx_string: Option<String>,
|
||||
sky_condition: Option<Vec<String>>,
|
||||
flight_category: String,
|
||||
three_hr_pressure_tendency_mb: Option<f64>,
|
||||
metar_type: String,
|
||||
#[serde(rename = "maxT_c")]
|
||||
max_t_c: Option<f64>,
|
||||
#[serde(rename = "minT_c")]
|
||||
min_t_c: Option<f64>,
|
||||
precip_in: Option<f64>,
|
||||
elevation_m: i32
|
||||
}
|
||||
|
||||
impl QueryMetar {
|
||||
fn get_all(icaos: &Vec<&str>) -> Result<Vec<QueryMetar>, ServiceError> {
|
||||
let station_query: Vec<String> = icaos.iter().map(|icao| format!("'{}'", icao.to_string())).collect();
|
||||
|
||||
let mut conn = db::connection()?;
|
||||
let db_metars: Vec<Self> = 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(ServiceError { error_status_code: 500, error_message: format!("{}", err) })
|
||||
};
|
||||
return Ok(db_metars);
|
||||
}
|
||||
}
|
||||
34
service/src/metars/routes.rs
Normal file
34
service/src/metars/routes.rs
Normal file
@@ -0,0 +1,34 @@
|
||||
use crate::{error_handler::ServiceError, db::Metadata};
|
||||
use crate::metars::Metar;
|
||||
use actix_web::{get, web, HttpResponse};
|
||||
use log::error;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct MetarsResponse {
|
||||
pub data: Vec<Metar>,
|
||||
pub meta: Metadata
|
||||
}
|
||||
|
||||
#[get("metars/{ids}")]
|
||||
async fn get_all(ids: web::Path<String>) -> HttpResponse {
|
||||
let airports = match web::block(|| Ok::<_, ServiceError>(async {Metar::get_all(ids.into_inner()).await}))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.await {
|
||||
Ok(a) => a,
|
||||
Err(err) => {
|
||||
error!("{}", err);
|
||||
return err.to_http_response();
|
||||
}
|
||||
};
|
||||
HttpResponse::Ok().json(MetarsResponse {
|
||||
data: airports,
|
||||
meta: Metadata { page: 0, limit: 0, pages: 0, total: 0 }
|
||||
})
|
||||
}
|
||||
|
||||
pub fn init_routes(config: &mut web::ServiceConfig) {
|
||||
config.service(get_all);
|
||||
}
|
||||
60
service/src/schema.rs
Normal file
60
service/src/schema.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
diesel::table! {
|
||||
use diesel::sql_types::*;
|
||||
use postgis_diesel::sql_types::*;
|
||||
airports (icao) {
|
||||
icao -> Text,
|
||||
id -> Integer,
|
||||
category -> Text,
|
||||
full_name -> Text,
|
||||
elevation_ft -> Nullable<Integer>,
|
||||
continent -> Text,
|
||||
iso_country -> Text,
|
||||
iso_region -> Text,
|
||||
municipality -> Text,
|
||||
gps_code -> Text,
|
||||
iata_code -> Text,
|
||||
local_code -> Text,
|
||||
point -> Geometry,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
metars (id) {
|
||||
id -> Integer,
|
||||
raw_text -> Text,
|
||||
station_id -> Text,
|
||||
observation_time -> Text,
|
||||
latitude -> Double,
|
||||
longitude -> Double,
|
||||
temp_c -> Nullable<Double>,
|
||||
dewpoint_c -> Nullable<Double>,
|
||||
wind_dir_degrees -> Nullable<Text>,
|
||||
wind_speed_kt -> Nullable<Integer>,
|
||||
visibility_statute_mi -> Nullable<Text>,
|
||||
altim_in_hg -> Nullable<Double>,
|
||||
sea_level_pressure_mb -> Nullable<Double>,
|
||||
qcf_auto -> Nullable<Bool>,
|
||||
qcf_auto_station -> Nullable<Bool>,
|
||||
wx_string -> Nullable<Text>,
|
||||
sky_condition -> Nullable<Array<Text>>,
|
||||
flight_category -> Text,
|
||||
three_hr_pressure_tendency_mb -> Nullable<Double>,
|
||||
metar_type -> Text,
|
||||
max_t_c -> Nullable<Double>,
|
||||
min_t_c -> Nullable<Double>,
|
||||
precip_in -> Nullable<Double>,
|
||||
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>
|
||||
}
|
||||
}
|
||||
7
service/src/users/mod.rs
Normal file
7
service/src/users/mod.rs
Normal 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;
|
||||
50
service/src/users/model.rs
Normal file
50
service/src/users/model.rs
Normal 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"))
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
51
service/src/users/routes.rs
Normal file
51
service/src/users/routes.rs
Normal 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);
|
||||
}
|
||||
35
service/src/users/user_type.rs
Normal file
35
service/src/users/user_type.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user