diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..20c400c --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,68 @@ +image: 3.10.8-slim-buster + +variables: + PACKAGE_REGISTRY_URL: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/linux/$CI_COMMIT_TAG/" + +.deploy: + rules: + # Regex magic copied from Neshura/page-test, only deploys on x.y.z or higher (x.y) Tags + - if: $CI_COMMIT_TAG =~ /^((([\d])+\.){1,2}[\d]+)\s*$/ && $CI_COMMIT_TAG + +stages: + - build + - upload + - release + +## Docker steps + +build: + image: rust:latest + stage: build + + variables: + IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_BRANCH + CACHING: + script: + - echo "Compiling the code..." + - cargo build -r + - echo "Compile complete." + after_script: + - echo JOB_ID=$CI_JOB_ID >> job.env + - mkdir ./artifacts + - cp /builds/Neshura/chellaris-rust-api/target/release/chellaris-rust-api ./artifacts/ + artifacts: + paths: + - ./artifacts/ + reports: + dotenv: job.env + rules: + - !reference [.deploy, rules] + +upload: + needs: + - job: build + artifacts: true + image: curlimages/curl:latest + stage: upload + script: + - | + curl --header "JOB-TOKEN: ${CI_JOB_TOKEN}" --upload-file artifacts/chellaris-rust-api "${PACKAGE_REGISTRY_URL}/chellaris-rust-api" + rules: + - !reference [.deploy, rules] + + +Tag Release: + stage: release + image: registry.gitlab.com/gitlab-org/release-cli:latest + rules: + - !reference [.deploy, rules] + script: + - apk add curl + - echo "running Release Job, attaching Artifact from Job $JOB_ID" + release: + tag_name: '$CI_COMMIT_TAG' + description: '$CI_COMMIT_TAG' + assets: + links: + - name: "chellaris-rust-api" + url: "${PACKAGE_REGISTRY_URL}/chellaris-rust-api" diff --git a/src/main.rs b/src/main.rs index 4e01dc3..a4e3796 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ use crate::db::connect_postgres; mod db; mod v2; +mod v3; macro_rules! api_base { () => { @@ -75,6 +76,15 @@ async fn postgres_watchdog(pool: PgPool, is_alive: Arc, shutdown: Ar }; let passed = (Local::now() - start).to_std().expect(&format!("Unable to get Time Difference for '{}' and '{}'", start, Local::now())); + + thread::sleep(Duration::from_secs(5) - passed); + if shutdown.load(Ordering::Relaxed) { + break; + } + thread::sleep(Duration::from_secs(10) - passed); + if shutdown.load(Ordering::Relaxed) { + break; + } thread::sleep(Duration::from_secs(15) - passed); } is_alive.store(false, Ordering::Relaxed); @@ -121,7 +131,21 @@ async fn main() -> Result<()> { struct ApiDocV2; #[derive(OpenApi)] - #[openapi(paths(), components(schemas()))] + #[openapi( + paths( + v3::full_view_data, + ), + components(schemas( + v3::schemas::FullViewData, + v3::schemas::Ethic, + v3::schemas::EmpireEthic, + v3::schemas::ChellarisGame, + v3::schemas::Species, + v3::schemas::ChellarisGameGroup, + v3::schemas::Portrait, + v3::schemas::ChellarisEmpire + )) + )] struct ApiDocV3; let openapi_urls = vec![ @@ -156,6 +180,8 @@ async fn main() -> Result<()> { .service(v2::games) .service(v2::portrait_groups) .service(v2::portraits) + // API v3 Endpoints + .service(v3::full_view_data) // Swagger UI .service( SwaggerUi::new(concat!(api_base!(), "/swagger/{_:.*}")) diff --git a/src/v3/mod.rs b/src/v3/mod.rs new file mode 100644 index 0000000..39f107e --- /dev/null +++ b/src/v3/mod.rs @@ -0,0 +1,165 @@ +use std::{vec, collections::HashMap}; + +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 = FullViewData), + ), + security( + ("api_key" = []) + ), +)] +#[get("/full_view_data")] +pub(crate) async fn full_view_data( + data: web::Data +) -> impl Responder { + let start = chrono::Local::now(); // DEBUG + + // SQL Queries + // TODO: Optimize by utilizing some SQL magic, for now this has to do + + let db_games: Vec = sqlx::query_as( + "SELECT * FROM public.games ORDER BY id", + ) + .fetch_all(&data.db) + .await.expect("Error Fetching Data from DB"); + + let db_groups: Vec = sqlx::query_as( + "SELECT * FROM public.game_groups ORDER BY id", + ) + .fetch_all(&data.db) + .await.expect("Error Fetching Data from DB"); + + let db_empires: Vec = sqlx::query_as( + "SELECT * FROM public.empires ORDER BY id", + ) + .fetch_all(&data.db) + .await.expect("Error Fetching Data from DB"); + + let db_ethics: Vec = sqlx::query_as( + "SELECT * FROM public.ethics ORDER BY id", + ) + .fetch_all(&data.db) + .await.expect("Error Fetching Data from DB"); + + let db_empire_ethics: Vec = sqlx::query_as( + "SELECT * FROM public.empire_ethics ORDER BY empires_id", + ) + .fetch_all(&data.db) + .await.expect("Error Fetching Data from DB"); + + let db_portrait_groups: Vec = sqlx::query_as( + "SELECT * FROM public.portrait_groups ORDER BY id", + ) + .fetch_all(&data.db) + .await.expect("Error Fetching Data from DB"); + + let db_portraits: Vec = 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: schemas::FullViewData = schemas::FullViewData { + games: HashMap::new(), + ethics: HashMap::new(), + species: HashMap::new(), + }; + + // Data processing + // Species Vector + db_portrait_groups.iter().for_each(|species| { + let new_data = schemas::Species { + id: species.id, + displayName: species.name.clone(), + portraits: HashMap::new(), + }; + + parsed_data.species.entry(species.id).and_modify(|d| *d = new_data.clone()).or_insert(new_data); + }); + + db_portraits.iter().for_each(|portrait| { + let new_data = schemas::Portrait { + id: portrait.id, + hires: portrait.hires.clone(), + lores: portrait.lores.clone(), + }; + + parsed_data.species.get_mut(&portrait.group_id).unwrap().portraits.entry(portrait.id).and_modify(|d| *d = new_data.clone()).or_insert(new_data); + }); + + // Games Vector and Children + db_games.iter().for_each(|game| { + let new_data = schemas::ChellarisGame { + id: game.id, + name: game.name.clone(), + empires: HashMap::new(), + groups: HashMap::new(), + }; + + parsed_data.games.entry(game.id).and_modify(|d| *d = new_data.clone()).or_insert(new_data); + }); + + db_groups.iter().for_each(|group| { + let new_data = schemas::ChellarisGameGroup { + id: group.id, + name: group.name.clone(), + }; + + parsed_data.games.get_mut(&group.game_id).unwrap().groups.entry(group.id).and_modify(|d| *d = new_data.clone()).or_insert(new_data); + }); + + db_empires.iter().for_each(|empire| { + let new_data = schemas::ChellarisEmpire { + id: empire.id, + gestalt: empire.gestalt.unwrap_or(false), + machine: false, // TODO overwrite this later on with correct data + group: empire.group_id, + empire_portrait: empire.empire_portrait_id, + empire_portrait_group: empire.empire_portrait_group_id, + discord_user: empire.discord_user.clone(), + ethics: HashMap::new(), + }; + + parsed_data.games.get_mut(&empire.group_game_id).unwrap().empires.entry(empire.id).and_modify(|d| *d = new_data.clone()).or_insert(new_data); + }); + + // Ethics Vector + db_ethics.iter().for_each(|ethic| { + let new_data = schemas::Ethic { + id: ethic.id, + displayName: ethic.name.clone(), + machine: ethic.machine_ethic, + }; + + parsed_data.ethics.entry(ethic.id).and_modify(|d| *d = new_data.clone()).or_insert(new_data); + }); + + println!("{:#?}", parsed_data.ethics); // DEBUG + + db_empire_ethics.iter().for_each(|empire_ethic| { + let game_id = empire_ethic.empires_group_game_id; + let id = empire_ethic.empires_id; + + let new_data = schemas::EmpireEthic { + id: empire_ethic.ethics_id, + displayName: parsed_data.ethics[&empire_ethic.ethics_id].displayName.clone(), + machine: parsed_data.ethics[&empire_ethic.ethics_id].machine, + fanatic: empire_ethic.ethics_fanatic, + }; + + parsed_data.games.get_mut(&game_id).unwrap().empires.get_mut(&id).unwrap().ethics.entry(empire_ethic.ethics_id).and_modify(|d| *d = new_data.clone()).or_insert(new_data); + }); + + println!("{:?} ms", (chrono::Local::now() - start).to_std().unwrap()); // DEBUG + + Json(parsed_data) +} diff --git a/src/v3/schemas.rs b/src/v3/schemas.rs new file mode 100644 index 0000000..6b3fe9b --- /dev/null +++ b/src/v3/schemas.rs @@ -0,0 +1,71 @@ +use std::collections::HashMap; + +use serde::{Serialize, Deserialize}; +use utoipa::{ToSchema, IntoParams}; + +#[derive(Serialize, Deserialize, ToSchema, Debug, IntoParams)] +pub struct AuthParams { + pub token: Option, +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct FullViewData { + pub games: HashMap, + pub ethics: HashMap, + pub species: HashMap, +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct ChellarisGame { + pub id: i32, + pub name: String, + pub groups: HashMap, + pub empires: HashMap, +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct ChellarisGameGroup { + pub id: i32, + pub name: String, +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct ChellarisEmpire { + pub id: i32, + pub gestalt: bool, + pub machine: bool, + pub group: i32, + pub empire_portrait: i32, + pub empire_portrait_group: i32, + pub discord_user: Option, + pub ethics: HashMap, +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct Ethic { + pub id: i32, + pub displayName: String, + pub machine: bool, +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct EmpireEthic { + pub id: i32, + pub displayName: String, + pub machine: bool, + pub fanatic: bool, +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct Species { + pub id: i32, + pub displayName: String, + pub portraits: HashMap, +} + +#[derive(Serialize, ToSchema, Debug, Clone)] +pub struct Portrait { + pub id: i32, + pub hires: String, + pub lores: Option, +} \ No newline at end of file