Feature Parity with python variant
This commit is contained in:
parent
83bf813bb1
commit
e19187d6c7
11 changed files with 3585 additions and 1 deletions
21
src/db/mod.rs
Normal file
21
src/db/mod.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
use std::error::Error;
|
||||
|
||||
use sqlx::{Postgres, postgres::PgConnectOptions, PgPool, Pool};
|
||||
|
||||
use crate::{PostgresConfig};
|
||||
|
||||
pub(crate) mod schemas;
|
||||
|
||||
pub(crate) async fn connect_postgres(config: PostgresConfig) -> Result<Pool<Postgres>, Box<dyn Error>> {
|
||||
|
||||
let connection_settings = PgConnectOptions::new()
|
||||
.host(&config.host)
|
||||
.port(config.port)
|
||||
.username(&config.user)
|
||||
.password(&config.password)
|
||||
.database(&config.db);
|
||||
|
||||
let pool = PgPool::connect_with(connection_settings).await?;
|
||||
|
||||
return Ok(pool);
|
||||
}
|
57
src/db/schemas.rs
Normal file
57
src/db/schemas.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use serde::Serialize;
|
||||
use sqlx::FromRow;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct PortraitGroup {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct Portrait {
|
||||
pub id: i32,
|
||||
pub group_id: i32,
|
||||
pub hires: String,
|
||||
pub lores: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct Game {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct GameGroup {
|
||||
pub id: i32,
|
||||
pub game_id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct Ethic {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub machine_ethic: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct Empire {
|
||||
pub id: i32,
|
||||
pub discord_user: Option<String>,
|
||||
pub group_id: i32,
|
||||
pub gestalt: Option<bool>, // TODO: make nn in DB schema
|
||||
pub empire_portrait_id: i32,
|
||||
pub empire_portrait_group_id: i32,
|
||||
pub group_game_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct EmpireEthic {
|
||||
pub empires_id: i32,
|
||||
pub empires_group_id: i32,
|
||||
pub empires_group_game_id: i32,
|
||||
pub ethics_id: i32,
|
||||
pub ethics_fanatic: bool,
|
||||
}
|
211
src/main.rs
Normal file
211
src/main.rs
Normal file
|
@ -0,0 +1,211 @@
|
|||
use std::{error::Error, fs, net::Ipv4Addr, net::Ipv6Addr, thread, time::Duration, sync::{Mutex, Arc, atomic::{AtomicBool, Ordering}}};
|
||||
|
||||
use chrono::Local;
|
||||
|
||||
use actix_web::{middleware::Logger, web, App, HttpServer, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{PgPool, Pool, Postgres, Connection};
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::{Config, SwaggerUi, Url};
|
||||
|
||||
use crate::db::connect_postgres;
|
||||
|
||||
mod db;
|
||||
mod v2;
|
||||
|
||||
macro_rules! api_base {
|
||||
() => {
|
||||
"/api"
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! api_base_2 {
|
||||
() => {
|
||||
"/api/v2"
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! api_base_3 {
|
||||
() => {
|
||||
"/api/v3"
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub(crate) struct ConfigToml {
|
||||
database: PostgresConfig,
|
||||
auth: AuthenticationTokens,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub(crate) struct PostgresConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
user: String,
|
||||
password: String,
|
||||
db: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub(crate) struct AuthenticationTokens {
|
||||
moderator: String,
|
||||
admin: String
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
db: Pool<Postgres>,
|
||||
auth_tokens: AuthenticationTokens,
|
||||
}
|
||||
|
||||
async fn postgres_watchdog(pool: PgPool, is_alive: Arc<AtomicBool>, shutdown: Arc<AtomicBool>) {
|
||||
loop {
|
||||
if shutdown.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
let start = Local::now();
|
||||
|
||||
let mut conn = match pool.acquire().await {
|
||||
Ok(data) => data,
|
||||
Err(_) => break,
|
||||
};
|
||||
|
||||
match conn.ping().await {
|
||||
Ok(_) => {eprintln!("Pinged DB Server at {}", Local::now().format("%H:%M:%S"))},
|
||||
Err(_) => todo!(),
|
||||
};
|
||||
|
||||
let passed = (Local::now() - start).to_std().expect(&format!("Unable to get Time Difference for '{}' and '{}'", start, Local::now()));
|
||||
thread::sleep(Duration::from_secs(15) - passed);
|
||||
}
|
||||
is_alive.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let shutdown: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let shutdown_clone = Arc::clone(&shutdown);
|
||||
ctrlc::set_handler(move || {
|
||||
eprintln!("Ctrl-C received");
|
||||
shutdown_clone.store(true, Ordering::Relaxed)
|
||||
})
|
||||
.expect("Error setting Ctrl-C handler");
|
||||
|
||||
let toml_str = fs::read_to_string("config.toml").expect("Failed to read config.toml");
|
||||
|
||||
let config: ConfigToml = toml::from_str(&toml_str).expect("Failed to parse config.toml");
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(
|
||||
v2::empire_ethics,
|
||||
v2::empires,
|
||||
v2::ethics,
|
||||
v2::game_groups,
|
||||
v2::games,
|
||||
v2::portrait_groups,
|
||||
v2::portraits
|
||||
),
|
||||
components(schemas(
|
||||
v2::schemas::PortraitGroup,
|
||||
v2::schemas::Portrait,
|
||||
v2::schemas::Ethic,
|
||||
v2::schemas::Empire,
|
||||
v2::schemas::EmpireEthic,
|
||||
v2::schemas::Game,
|
||||
v2::schemas::GameGroup
|
||||
))
|
||||
)]
|
||||
struct ApiDocV2;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(paths(), components(schemas()))]
|
||||
struct ApiDocV3;
|
||||
|
||||
let openapi_urls = vec![
|
||||
Url::new("v2", concat!(api_base_2!(), "/openapi.json")),
|
||||
Url::new("v3", concat!(api_base_3!(), "/openapi.json")),
|
||||
];
|
||||
|
||||
let is_alive: Arc<AtomicBool> = Arc::new(AtomicBool::new(true));
|
||||
|
||||
loop {
|
||||
let db_auth_tokens = config.auth.clone();
|
||||
let pool = connect_postgres(config.database.clone()).await.unwrap();
|
||||
let pool_copy = pool.clone();
|
||||
|
||||
let swagger_config = Config::new(openapi_urls.clone());
|
||||
|
||||
let mut openapi_v2 = ApiDocV2::openapi();
|
||||
openapi_v2.info.title = "Chellaris Rust API".to_string();
|
||||
|
||||
let mut openapi_v3 = ApiDocV3::openapi();
|
||||
openapi_v3.info.title = "Chellaris Rust API".to_string();
|
||||
|
||||
let server = HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(AppState { db: pool.clone(), auth_tokens: db_auth_tokens.clone() }))
|
||||
.wrap(Logger::default())
|
||||
// API v2 Endpoints
|
||||
.service(v2::empire_ethics)
|
||||
.service(v2::empires)
|
||||
.service(v2::ethics)
|
||||
.service(v2::game_groups)
|
||||
.service(v2::games)
|
||||
.service(v2::portrait_groups)
|
||||
.service(v2::portraits)
|
||||
// Swagger UI
|
||||
.service(
|
||||
SwaggerUi::new(concat!(api_base!(), "/swagger/{_:.*}"))
|
||||
.urls(vec![
|
||||
(
|
||||
Url::new("v2", concat!(api_base_2!(), "/openapi.json")),
|
||||
openapi_v2.clone(),
|
||||
),
|
||||
(
|
||||
Url::new("v3", concat!(api_base_3!(), "/openapi.json")),
|
||||
openapi_v3.clone(),
|
||||
),
|
||||
])
|
||||
.config(swagger_config.clone()),
|
||||
)
|
||||
})
|
||||
.bind((Ipv6Addr::UNSPECIFIED, 8080)).expect("Port or IP already occupied")
|
||||
.run();
|
||||
|
||||
let server_thread = tokio::spawn(async {
|
||||
eprintln!("Awaiting server");
|
||||
let _ = server.await;
|
||||
println!("Stopped awaiting server");
|
||||
});
|
||||
|
||||
println!("Started Serving API on: ");
|
||||
println!(" -> http://[{}]:{}", Ipv6Addr::UNSPECIFIED, 8080);
|
||||
println!(" -> http://{}:{}", Ipv4Addr::UNSPECIFIED, 8080);
|
||||
|
||||
let is_alive_clone = Arc::clone(&is_alive);
|
||||
let shutdown_clone = Arc::clone(&shutdown);
|
||||
let _ = tokio::spawn(async move { postgres_watchdog(pool_copy, is_alive_clone, shutdown_clone).await });
|
||||
|
||||
//watchdog_thread.await;
|
||||
|
||||
while is_alive.load(Ordering::Relaxed) {
|
||||
let thread = tokio::spawn(async {
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
});
|
||||
|
||||
let _ = thread.await;
|
||||
}
|
||||
|
||||
if shutdown.load(Ordering::Relaxed) {
|
||||
break;
|
||||
}
|
||||
|
||||
eprintln!("Connection died, restarting Server");
|
||||
server_thread.abort();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
260
src/v2/mod.rs
Normal file
260
src/v2/mod.rs
Normal file
|
@ -0,0 +1,260 @@
|
|||
use actix_web::{web::{self, Json}, Responder, get};
|
||||
|
||||
use crate::{AppState, db};
|
||||
|
||||
pub(crate) mod schemas;
|
||||
|
||||
#[utoipa::path(
|
||||
params(),
|
||||
responses(
|
||||
(status = 200, description = "OK", body = EmpireEthic),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[get("/empire_ethics")]
|
||||
pub(crate) async fn empire_ethics(
|
||||
data: web::Data<AppState>
|
||||
) -> impl Responder {
|
||||
let db_data: Vec<db::schemas::EmpireEthic> = sqlx::query_as(
|
||||
"SELECT * FROM public.empire_ethics ORDER BY empires_id",
|
||||
)
|
||||
.fetch_all(&data.db)
|
||||
.await.expect("Error Fetching Data from DB");
|
||||
|
||||
let mut parsed_data: Vec<schemas::EmpireEthic> = vec![];
|
||||
|
||||
db_data.iter().for_each(|entry| {
|
||||
let new_data = schemas::EmpireEthic {
|
||||
empires_id: entry.empires_id,
|
||||
empires_group_game_id: entry.empires_group_game_id,
|
||||
empires_group_id: entry.empires_group_id,
|
||||
ethics_id: entry.ethics_id,
|
||||
ethics_fanatic: entry.ethics_fanatic
|
||||
};
|
||||
|
||||
parsed_data.push(new_data);
|
||||
});
|
||||
|
||||
Json(parsed_data)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
params(
|
||||
schemas::AuthParams
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "OK", body = Empire),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[get("/empires")]
|
||||
pub(crate) async fn empires(
|
||||
data: web::Data<AppState>,
|
||||
params: web::Query<schemas::AuthParams>
|
||||
) -> impl Responder {
|
||||
let params: schemas::AuthParams = params.into_inner();
|
||||
|
||||
let db_data: Vec<db::schemas::Empire> = sqlx::query_as(
|
||||
"SELECT * FROM public.empires ORDER BY id",
|
||||
)
|
||||
.fetch_all(&data.db)
|
||||
.await.expect("Error Fetching Data from DB");
|
||||
|
||||
let mut parsed_data: Vec<schemas::Empire> = vec![];
|
||||
|
||||
db_data.iter().for_each(|entry| {
|
||||
let mut new_data = schemas::Empire {
|
||||
id: entry.id,
|
||||
group_id: entry.group_id,
|
||||
empire_portrait_group_id: entry.empire_portrait_group_id,
|
||||
empire_portrait_id: entry.empire_portrait_id,
|
||||
group_game_id: entry.group_game_id,
|
||||
discord_user: None,
|
||||
gestalt: entry.gestalt
|
||||
};
|
||||
|
||||
if let Some(auth_token) = params.token.clone() {
|
||||
if auth_token == data.auth_tokens.admin || auth_token == data.auth_tokens.moderator {
|
||||
new_data.discord_user = entry.discord_user.clone();
|
||||
}
|
||||
}
|
||||
|
||||
parsed_data.push(new_data);
|
||||
});
|
||||
|
||||
Json(parsed_data)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
params(),
|
||||
responses(
|
||||
(status = 200, description = "OK", body = Ethic),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[get("/ethics")]
|
||||
pub(crate) async fn ethics(
|
||||
data: web::Data<AppState>
|
||||
) -> impl Responder {
|
||||
let db_data: Vec<db::schemas::Ethic> = sqlx::query_as(
|
||||
"SELECT * FROM public.ethics ORDER BY id",
|
||||
)
|
||||
.fetch_all(&data.db)
|
||||
.await.expect("Error Fetching Data from DB");
|
||||
|
||||
let mut parsed_data: Vec<schemas::Ethic> = vec![];
|
||||
|
||||
db_data.iter().for_each(|entry| {
|
||||
let new_data = schemas::Ethic {
|
||||
id: entry.id,
|
||||
machine_ethic: entry.machine_ethic,
|
||||
name: entry.name.clone()
|
||||
};
|
||||
|
||||
parsed_data.push(new_data);
|
||||
});
|
||||
|
||||
Json(parsed_data)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
params(),
|
||||
responses(
|
||||
(status = 200, description = "OK", body = GameGroup),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[get("/game_groups")]
|
||||
pub(crate) async fn game_groups(
|
||||
data: web::Data<AppState>
|
||||
) -> impl Responder {
|
||||
let db_data: Vec<db::schemas::GameGroup> = sqlx::query_as(
|
||||
"SELECT * FROM public.game_groups ORDER BY id",
|
||||
)
|
||||
.fetch_all(&data.db)
|
||||
.await.expect("Error Fetching Data from DB");
|
||||
|
||||
let mut parsed_data: Vec<schemas::GameGroup> = vec![];
|
||||
|
||||
db_data.iter().for_each(|entry| {
|
||||
let new_data = schemas::GameGroup {
|
||||
game_id: entry.game_id,
|
||||
id: entry.id,
|
||||
name: entry.name.clone()
|
||||
};
|
||||
|
||||
parsed_data.push(new_data);
|
||||
});
|
||||
|
||||
Json(parsed_data)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
params(),
|
||||
responses(
|
||||
(status = 200, description = "OK", body = Game),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[get("/games")]
|
||||
pub(crate) async fn games(
|
||||
data: web::Data<AppState>
|
||||
) -> impl Responder {
|
||||
let db_data: Vec<db::schemas::Game> = sqlx::query_as(
|
||||
"SELECT * FROM public.games ORDER BY id",
|
||||
)
|
||||
.fetch_all(&data.db)
|
||||
.await.expect("Error Fetching Data from DB");
|
||||
|
||||
let mut parsed_data: Vec<schemas::Game> = vec![];
|
||||
|
||||
db_data.iter().for_each(|entry| {
|
||||
let new_data = schemas::Game {
|
||||
id: entry.id,
|
||||
name: entry.name.clone()
|
||||
};
|
||||
|
||||
parsed_data.push(new_data);
|
||||
});
|
||||
|
||||
Json(parsed_data)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
params(),
|
||||
responses(
|
||||
(status = 200, description = "OK", body = PortraitGroup),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[get("/portrait_groups")]
|
||||
pub(crate) async fn portrait_groups(
|
||||
data: web::Data<AppState>
|
||||
) -> impl Responder {
|
||||
let db_data: Vec<db::schemas::PortraitGroup> = sqlx::query_as(
|
||||
"SELECT * FROM public.portrait_groups ORDER BY id",
|
||||
)
|
||||
.fetch_all(&data.db)
|
||||
.await.expect("Error Fetching Data from DB");
|
||||
|
||||
let mut parsed_data: Vec<schemas::PortraitGroup> = vec![];
|
||||
|
||||
db_data.iter().for_each(|entry| {
|
||||
let new_data = schemas::PortraitGroup {
|
||||
id: entry.id,
|
||||
name: entry.name.clone()
|
||||
};
|
||||
|
||||
parsed_data.push(new_data);
|
||||
});
|
||||
|
||||
Json(parsed_data)
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
params(),
|
||||
responses(
|
||||
(status = 200, description = "OK", body = Portrait),
|
||||
),
|
||||
security(
|
||||
("api_key" = [])
|
||||
),
|
||||
)]
|
||||
#[get("/portraits")]
|
||||
pub(crate) async fn portraits(
|
||||
data: web::Data<AppState>
|
||||
) -> impl Responder {
|
||||
let db_data: Vec<db::schemas::Portrait> = sqlx::query_as(
|
||||
"SELECT * FROM public.portraits ORDER BY id",
|
||||
)
|
||||
.fetch_all(&data.db)
|
||||
.await.expect("Error Fetching Data from DB");
|
||||
|
||||
let mut parsed_data: Vec<schemas::Portrait> = vec![];
|
||||
|
||||
db_data.iter().for_each(|entry| {
|
||||
let new_data = schemas::Portrait {
|
||||
id: entry.id,
|
||||
group_id: entry.group_id,
|
||||
hires: entry.hires.clone(),
|
||||
lores: entry.lores.clone()
|
||||
};
|
||||
|
||||
parsed_data.push(new_data);
|
||||
});
|
||||
|
||||
Json(parsed_data)
|
||||
}
|
||||
|
62
src/v2/schemas.rs
Normal file
62
src/v2/schemas.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use serde::{Serialize, Deserialize};
|
||||
use sqlx::FromRow;
|
||||
use utoipa::{ToSchema, IntoParams};
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug, FromRow, IntoParams)]
|
||||
pub struct AuthParams {
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct PortraitGroup {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct Portrait {
|
||||
pub id: i32,
|
||||
pub group_id: i32,
|
||||
pub hires: String,
|
||||
pub lores: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct Game {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct GameGroup {
|
||||
pub id: i32,
|
||||
pub game_id: i32,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct Ethic {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub machine_ethic: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct Empire {
|
||||
pub id: i32,
|
||||
pub discord_user: Option<String>,
|
||||
pub group_id: i32,
|
||||
pub gestalt: Option<bool>, // TODO: make nn in DB schema
|
||||
pub empire_portrait_id: i32,
|
||||
pub empire_portrait_group_id: i32,
|
||||
pub group_game_id: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, ToSchema, Debug, FromRow)]
|
||||
pub struct EmpireEthic {
|
||||
pub empires_id: i32,
|
||||
pub empires_group_id: i32,
|
||||
pub empires_group_game_id: i32,
|
||||
pub ethics_id: i32,
|
||||
pub ethics_fanatic: bool,
|
||||
}
|
Reference in a new issue