From cecaecb92e8247fe2fec4dbd9583e84dcbabc95c Mon Sep 17 00:00:00 2001 From: Katalina Okano Date: Fri, 9 Dec 2022 07:34:45 -0500 Subject: [PATCH] api rebuild --- Cargo.lock | 21 +-- services/api/Cargo.toml | 7 +- services/api/src/classes.rs | 140 +++++++++++++++++--- services/api/src/health.rs | 11 ++ services/api/src/main.rs | 5 +- services/api/src/population.rs | 85 ++++++++++++ services/api/src/query.rs | 43 ++----- services/api/src/utils.rs | 100 +++++++++++++++ services/api/src/vehicles.rs | 228 +++++++++++++++++++++++++++------ services/api/src/world.rs | 185 +++++++++++++++----------- services/api/src/zone.rs | 132 +++++++++++++++++++ 11 files changed, 782 insertions(+), 175 deletions(-) create mode 100644 services/api/src/population.rs create mode 100644 services/api/src/utils.rs create mode 100644 services/api/src/zone.rs diff --git a/Cargo.lock b/Cargo.lock index 80874be..4429ee7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,6 +49,7 @@ dependencies = [ "async-graphql-axum", "axum", "lazy_static", + "reqwest", "serde", "serde_json", "sqlx", @@ -64,9 +65,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" [[package]] name = "async-graphql" -version = "5.0.2" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5005cfd364b44d9cb55486b44184fe41a57b97339e17ce10db05f6b6093571d9" +checksum = "42bb92ffef089e5b61e90bcc004c9689554dfb5a150d88e81c7f6fef9e76eeae" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -96,9 +97,9 @@ dependencies = [ [[package]] name = "async-graphql-axum" -version = "5.0.2" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5554f6f1578ba1c65942ff7103c4c5421a47890a64666e1863d38bb9194ab091" +checksum = "077bf197d54397cff2324b79d86a7a21b2a83260e62e33eccae33009427897c9" dependencies = [ "async-graphql", "async-trait", @@ -113,9 +114,9 @@ dependencies = [ [[package]] name = "async-graphql-derive" -version = "5.0.2" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff995b9d89198740d3701f1e5f7101e2822d5f4f1f4db19e9a1a9314cb14364" +checksum = "4fa579c7cea32030600994d579554b257e10d5ad87705f3d150b49ee08bd629d" dependencies = [ "Inflector", "async-graphql-parser", @@ -129,9 +130,9 @@ dependencies = [ [[package]] name = "async-graphql-parser" -version = "5.0.2" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e73570e2270b9921a183df47760bea67a65afb145eaaa1b82a9c34b0c6209ff" +checksum = "3b67a5bea60997ca72908854655ae87f7970dc7d786d9a42fd1d17069fa42ebc" dependencies = [ "async-graphql-value", "pest", @@ -141,9 +142,9 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "5.0.2" +version = "5.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97013c726c11f29262f52e9487025a72bae58262bad3c26389936ce3bf143b11" +checksum = "79c2721eb88245ca055e148a3f03cb11a88535c206ac5a7c59e9edb22816320a" dependencies = [ "bytes", "indexmap", diff --git a/services/api/Cargo.toml b/services/api/Cargo.toml index 10ce14e..08997d5 100644 --- a/services/api/Cargo.toml +++ b/services/api/Cargo.toml @@ -8,10 +8,11 @@ edition = "2021" [dependencies] serde_json = "1.0.89" serde = "1.0.149" -async-graphql = { version = "5.0.2" } -async-graphql-axum = "5.0.2" +async-graphql = { version = "5.0.3" } +async-graphql-axum = "5.0.3" axum = "0.6.1" sqlx = { version = "0.6.2", features = [ "runtime-tokio-native-tls", "postgres" ] } tokio = { version = "1.23.0", features = [ "full" ] } tower-http = { version = "0.3.5", features = ["cors"] } -lazy_static = "1.4.0" \ No newline at end of file +lazy_static = "1.4.0" +reqwest = "0.11.13" \ No newline at end of file diff --git a/services/api/src/classes.rs b/services/api/src/classes.rs index d63a4e5..8629bfd 100644 --- a/services/api/src/classes.rs +++ b/services/api/src/classes.rs @@ -1,36 +1,140 @@ +use crate::utils::{Filters, IdOrNameBy}; use async_graphql::{Context, Object}; +use sqlx::{Pool, Postgres, Row}; +/// A specific with optional faction filter. +pub struct Class { + filters: Filters, + class_name: String, +} + +impl Class { + async fn fetch<'ctx>(&self, ctx: &Context<'ctx>, filters: Filters) -> i64 { + let pool = ctx.data::>().unwrap(); + + let sql = format!( + "SELECT count(distinct character_id) FROM classes WHERE time > now() - interval '15 minutes' AND class_id = $1 {};", + filters.sql(), + ); + + println!("{}", sql); + + let query: i64 = sqlx::query(sql.as_str()) + .bind(self.class_name.as_str()) + .fetch_one(pool) + .await + .unwrap() + .get(0); + + query + } +} + +#[Object] +impl Class { + async fn total<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.fetch(ctx, self.filters.clone()).await + } + async fn nc<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.fetch( + ctx, + Filters { + faction: Some(IdOrNameBy::Id(1)), + ..self.filters.clone() + }, + ) + .await + } + async fn tr<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.fetch( + ctx, + Filters { + faction: Some(IdOrNameBy::Id(2)), + ..self.filters.clone() + }, + ) + .await + } + async fn vs<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.fetch( + ctx, + Filters { + faction: Some(IdOrNameBy::Id(3)), + ..self.filters.clone() + }, + ) + .await + } +} + +/// Super-struct of each class. pub struct Classes { - world_id: String, + filters: Filters, } impl Classes { - pub fn new(world_id: String) -> Self { - Self { world_id } - } - async fn by_class<'ctx>(&self, ctx: &Context<'ctx>, class_name: &str) -> u32 { - 0 + pub fn new(filters: Option) -> Self { + Self { + filters: filters.unwrap_or_default(), + } } } #[Object] impl Classes { - async fn infiltrator<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_class(ctx, "infiltrator").await + async fn infiltrator(&self) -> Class { + Class { + filters: self.filters.clone(), + class_name: "infiltrator".to_string(), + } } - async fn light_assault<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_class(ctx, "light_assault").await + async fn light_assault(&self) -> Class { + Class { + filters: self.filters.clone(), + class_name: "light_assault".to_string(), + } } - async fn combat_medic<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_class(ctx, "combat_medic").await + async fn combat_medic(&self) -> Class { + Class { + filters: self.filters.clone(), + class_name: "combat_medic".to_string(), + } } - async fn engineer<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_class(ctx, "engineer").await + async fn engineer(&self) -> Class { + Class { + filters: self.filters.clone(), + class_name: "engineer".to_string(), + } } - async fn heavy_assault<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_class(ctx, "heavy_assault").await + async fn heavy_assault(&self) -> Class { + Class { + filters: self.filters.clone(), + class_name: "heavy_assault".to_string(), + } } - async fn max<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_class(ctx, "max").await + async fn max(&self) -> Class { + Class { + filters: self.filters.clone(), + class_name: "max".to_string(), + } + } +} + +#[derive(Default)] +pub struct ClassesQuery; + +#[Object] +impl ClassesQuery { + /// Get all classes + pub async fn classes(&self, filter: Option) -> Classes { + Classes::new(filter) + } + + /// Get a specific class + pub async fn class(&self, filter: Option, class_name: String) -> Class { + Class { + filters: filter.unwrap_or_default(), + class_name, + } } } diff --git a/services/api/src/health.rs b/services/api/src/health.rs index b598063..1c9ac2a 100644 --- a/services/api/src/health.rs +++ b/services/api/src/health.rs @@ -91,3 +91,14 @@ impl Health { } } } + +#[derive(Default)] +pub struct HealthQuery; + +#[Object] +impl HealthQuery { + /// Reports on the health of Saerro Listening Post + pub async fn health(&self) -> Health { + Health {} + } +} diff --git a/services/api/src/main.rs b/services/api/src/main.rs index 89cf067..8823ca4 100644 --- a/services/api/src/main.rs +++ b/services/api/src/main.rs @@ -1,8 +1,11 @@ mod classes; mod health; +mod population; mod query; +mod utils; mod vehicles; mod world; +mod zone; use async_graphql::{ http::GraphiQLSource, EmptyMutation, EmptySubscription, Request, Response, Schema, @@ -61,7 +64,7 @@ async fn main() { .unwrap_or("postgres://saerrouser:saerro321@localhost:5432/data".to_string()); let db = sqlx::PgPool::connect(&db_url).await.unwrap(); - let schema = Schema::build(query::Query, EmptyMutation, EmptySubscription) + let schema = Schema::build(query::Query::default(), EmptyMutation, EmptySubscription) .data(db.clone()) .finish(); diff --git a/services/api/src/population.rs b/services/api/src/population.rs new file mode 100644 index 0000000..4e7f9f7 --- /dev/null +++ b/services/api/src/population.rs @@ -0,0 +1,85 @@ +use crate::utils::Filters; +use async_graphql::{Context, Object}; +use sqlx::{Pool, Postgres, Row}; + +/// A filterable list of currently active players. +pub struct Population { + filters: Filters, +} + +impl Population { + pub fn new(filters: Option) -> Self { + Self { + filters: filters.unwrap_or_default(), + } + } +} + +impl Population { + async fn by_faction<'ctx>(&self, ctx: &Context<'ctx>, faction: i32) -> i64 { + let pool = ctx.data::>().unwrap(); + + let sql = format!( + "SELECT count(distinct character_id) FROM players WHERE time > now() - interval '15 minutes' AND faction_id = $1 {};", + self.filters.sql(), + ); + + println!("{}", sql); + + let query: i64 = sqlx::query(sql.as_str()) + .bind(faction) + .fetch_one(pool) + .await + .unwrap() + .get(0); + + query + } +} + +#[Object] +impl Population { + async fn total<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + let pool = ctx.data::>().unwrap(); + + let sql = format!( + "SELECT count(distinct character_id) FROM players WHERE time > now() - interval '15 minutes' {};", + self.filters.sql(), + ); + + println!("{}", sql); + + let query: i64 = sqlx::query(sql.as_str()) + .fetch_one(pool) + .await + .unwrap() + .get(0); + + query + } + async fn nc<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.by_faction(ctx, 1).await + } + async fn vs<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.by_faction(ctx, 2).await + } + async fn tr<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.by_faction(ctx, 3).await + } + async fn ns<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.by_faction(ctx, 4).await + } +} + +#[derive(Default)] +pub struct PopulationQuery; + +#[Object] +impl PopulationQuery { + /// A filterable list of currently active players. + /// This is a core query that others will use to filter by, + /// i.e. `emerald { population { total } }` is equivalent to `population(filter: { world: { name: "emerald" } }) { total }` + pub async fn population(&self, filter: Option) -> Population { + Population::new(filter) + } +} diff --git a/services/api/src/query.rs b/services/api/src/query.rs index 7e1bdee..d025142 100644 --- a/services/api/src/query.rs +++ b/services/api/src/query.rs @@ -1,30 +1,15 @@ -use crate::health::Health; -use crate::world::World; -use async_graphql::Object; +use crate::{ + classes::ClassesQuery, health::HealthQuery, population::PopulationQuery, + vehicles::VehicleQuery, world::WorldQuery, zone::ZoneQuery, +}; +use async_graphql::MergedObject; -pub struct Query; - -#[Object] -impl Query { - /// Returns a graph for the world with the given ID. - /// If the world does not exist, this will not fail. - async fn world(&self, id: String) -> World { - World { id: id.clone() } - } - - /// Returns a graph for the world specified by it's human name. - /// This is case-insensitive; but will not fail. - async fn world_by_name(&self, name: String) -> World { - World::from_name(name) - } - - /// Returns a graph of all known live play worlds. - async fn all_worlds(&self) -> Vec { - World::all_worlds() - } - - /// Reports on the health of Saerro Listening Post - async fn health(&self) -> Health { - Health {} - } -} +#[derive(MergedObject, Default)] +pub struct Query( + PopulationQuery, + VehicleQuery, + ClassesQuery, + WorldQuery, + ZoneQuery, + HealthQuery, +); diff --git a/services/api/src/utils.rs b/services/api/src/utils.rs new file mode 100644 index 0000000..dc3fadb --- /dev/null +++ b/services/api/src/utils.rs @@ -0,0 +1,100 @@ +use async_graphql::{InputObject, OneofObject}; +use lazy_static::lazy_static; +use std::collections::HashMap; + +lazy_static! { + pub static ref WORLD_IDS: HashMap = HashMap::from([ + ("connery".to_string(), 1), + ("miller".to_string(), 10), + ("cobalt".to_string(), 13), + ("emerald".to_string(), 17), + ("jaeger".to_string(), 19), + ("soltech".to_string(), 40), + ("genudine".to_string(), 1000), + ("ceres".to_string(), 2000), + ]); + pub static ref ID_TO_WORLD: HashMap = WORLD_IDS + .iter() + .map(|(name, id)| (id.to_owned(), name.to_owned())) + .collect(); + pub static ref FACTION_IDS: HashMap = HashMap::from([ + ("vs".to_string(), 1), + ("nc".to_string(), 2), + ("tr".to_string(), 3), + ("ns".to_string(), 4), + ]); + pub static ref ID_TO_FACTION: HashMap = FACTION_IDS + .iter() + .map(|(name, id)| (id.to_owned(), name.to_owned())) + .collect(); + pub static ref ZONE_IDS: HashMap = HashMap::from([ + ("indar".to_string(), 2), + ("hossin".to_string(), 4), + ("amerish".to_string(), 6), + ("esamir".to_string(), 8), + ("oshur".to_string(), 344), + ]); + pub static ref ID_TO_ZONE: HashMap = ZONE_IDS + .iter() + .map(|(name, id)| (id.to_owned(), name.to_owned())) + .collect(); +} + +/// Allows for one of the following: +/// - By ID, example: `{ id: 1 }` +/// - By name (case-insensitive), example: `{ name: "Connery" }` +#[derive(OneofObject, Clone)] +pub enum IdOrNameBy { + Id(i32), + Name(String), +} + +pub fn id_or_name_to_id(map: &HashMap, by: &IdOrNameBy) -> Option { + match by { + IdOrNameBy::Id(id) => Some(*id), + IdOrNameBy::Name(name) => map.get(&name.to_lowercase()).map(|id| *id), + } +} + +pub fn id_or_name_to_name(map: &HashMap, id: &IdOrNameBy) -> Option { + match id { + IdOrNameBy::Id(id) => map.get(id).map(|name| name.to_owned()), + IdOrNameBy::Name(name) => Some(name.to_owned()), + } +} + +/// A filter for core queries, allows for filtering by world, faction, and zone. +/// Omitting a field will not filter by that field, so for example: +/// `{ world: { id: 1 }, faction: { name: "VS" } }` +/// will filter by world ID 1 and faction name "VS", but also search in every continent. +#[derive(InputObject, Default, Clone)] +pub struct Filters { + /// The world to filter by, like Connery, Emerald, etc. + pub world: Option, + /// The faction to filter by, like VS, NC, TR, or NS + pub faction: Option, + /// The zone or continent to filter by, like Indar, Amerish, etc. + pub zone: Option, +} + +impl Filters { + pub fn sql(&self) -> String { + let mut sql = String::new(); + if let Some(world) = &self.world { + if let Some(world_id) = id_or_name_to_id(&WORLD_IDS, world) { + sql.push_str(&format!(" AND world_id = {}", world_id)); + } + } + if let Some(faction) = &self.faction { + if let Some(faction_id) = id_or_name_to_id(&FACTION_IDS, faction) { + sql.push_str(&format!(" AND faction_id = {}", faction_id)); + } + } + if let Some(zone) = &self.zone { + if let Some(zone_id) = id_or_name_to_id(&ZONE_IDS, zone) { + sql.push_str(&format!(" AND zone_id = {}", zone_id)); + } + } + sql + } +} diff --git a/services/api/src/vehicles.rs b/services/api/src/vehicles.rs index 03294b1..bf53dd2 100644 --- a/services/api/src/vehicles.rs +++ b/services/api/src/vehicles.rs @@ -1,73 +1,221 @@ +use crate::utils::{Filters, IdOrNameBy}; use async_graphql::{Context, Object}; +use sqlx::{Pool, Postgres, Row}; +/// A specific vehicle +pub struct Vehicle { + filters: Filters, + vehicle_name: String, +} + +impl Vehicle { + async fn fetch<'ctx>(&self, ctx: &Context<'ctx>, filters: Filters) -> i64 { + let pool = ctx.data::>().unwrap(); + + let sql = format!( + "SELECT count(distinct character_id) FROM vehicles WHERE time > now() - interval '15 minutes' AND vehicle_id = $1 {};", + filters.sql(), + ); + + println!("{}", sql); + + let query: i64 = sqlx::query(sql.as_str()) + .bind(self.vehicle_name.as_str()) + .fetch_one(pool) + .await + .unwrap() + .get(0); + + query + } +} + +#[Object] +impl Vehicle { + async fn total<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.fetch(ctx, self.filters.clone()).await + } + async fn nc<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.fetch( + ctx, + Filters { + faction: Some(IdOrNameBy::Id(1)), + ..self.filters.clone() + }, + ) + .await + } + async fn tr<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.fetch( + ctx, + Filters { + faction: Some(IdOrNameBy::Id(2)), + ..self.filters.clone() + }, + ) + .await + } + async fn vs<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + self.fetch( + ctx, + Filters { + faction: Some(IdOrNameBy::Id(3)), + ..self.filters.clone() + }, + ) + .await + } +} + +/// Super-struct for all vehicles. pub struct Vehicles { - world_id: String, + filters: Filters, } impl Vehicles { - pub fn new(world_id: String) -> Self { - Self { world_id } - } - async fn by_vehicle<'ctx>(&self, ctx: &Context<'ctx>, vehicle_name: &str) -> u32 { - 0 + pub fn new(filters: Option) -> Self { + Self { + filters: filters.unwrap_or_default(), + } } } #[Object] impl Vehicles { - async fn flash<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "flash").await + async fn total<'ctx>(&self, ctx: &Context<'ctx>) -> i64 { + let pool = ctx.data::>().unwrap(); + + let sql = format!( + "SELECT count(distinct character_id) FROM vehicles WHERE time > now() - interval '15 minutes' {};", + self.filters.sql(), + ); + + println!("{}", sql); + + let query: i64 = sqlx::query(sql.as_str()) + .fetch_one(pool) + .await + .unwrap() + .get(0); + + query } - async fn sunderer<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "sunderer").await + + // Transport + async fn flash(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "flash".to_string(), + } } - async fn ant<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "ant").await + async fn sunderer(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "sunderer".to_string(), + } } - async fn harasser<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "harasser").await + async fn ant(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "ant".to_string(), + } } - async fn javelin<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "javelin").await + async fn harasser(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "harasser".to_string(), + } + } + async fn javelin(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "javelin".to_string(), + } } // Tanks - async fn lightning<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "lightning").await + async fn lightning(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "javelin".to_string(), + } } - async fn prowler<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "prowler").await + async fn prowler(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "prowler".to_string(), + } } - async fn vanguard<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "vanguard").await + async fn vanguard(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "vanguard".to_string(), + } } - async fn magrider<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "magrider").await + async fn magrider(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "magrider".to_string(), + } } - async fn chimera<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "chimera").await + async fn chimera(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "chimera".to_string(), + } } // Air - async fn mosquito<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "mosquito").await + async fn mosquito(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "mosquito".to_string(), + } } - async fn liberator<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "liberator").await + async fn liberator(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "liberator".to_string(), + } } - async fn galaxy<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "galaxy").await + async fn galaxy(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "galaxy".to_string(), + } } - async fn valkyrie<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "valkyrie").await + async fn valkyrie(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "valkyrie".to_string(), + } } - async fn reaver<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "reaver").await + async fn reaver(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "reaver".to_string(), + } } - async fn scythe<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "scythe").await + async fn scythe(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "scythe".to_string(), + } } - async fn dervish<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_vehicle(ctx, "dervish").await + async fn dervish(&self) -> Vehicle { + Vehicle { + filters: self.filters.clone(), + vehicle_name: "dervish".to_string(), + } + } +} + +#[derive(Default)] +pub struct VehicleQuery; + +#[Object] +impl VehicleQuery { + pub async fn vehicles(&self, filter: Option) -> Vehicles { + Vehicles::new(filter) } } diff --git a/services/api/src/world.rs b/services/api/src/world.rs index b1012bb..fe38378 100644 --- a/services/api/src/world.rs +++ b/services/api/src/world.rs @@ -1,48 +1,25 @@ -use crate::{classes::Classes, vehicles::Vehicles}; -use async_graphql::{Context, Object}; -use lazy_static::lazy_static; -use std::collections::HashMap; +use crate::{ + classes::Classes, + population::Population, + utils::{id_or_name_to_id, id_or_name_to_name, Filters, IdOrNameBy, ID_TO_WORLD, WORLD_IDS}, + vehicles::Vehicles, + zone::Zones, +}; +use async_graphql::Object; -lazy_static! { - static ref WORLD_NAME_TO_ID: HashMap<&'static str, &'static str> = HashMap::from([ - ("connery", "1"), - ("miller", "10"), - ("cobalt", "13"), - ("emerald", "17"), - ("jaeger", "19"), - ("soltech", "40"), - ("genudine", "1000"), - ("ceres", "2000"), - ]); - static ref WORLD_ID_TO_NAME: HashMap<&'static str, &'static str> = HashMap::from([ - ("1", "Connery"), - ("10", "Miller"), - ("13", "Cobalt"), - ("17", "Emerald"), - ("19", "Jaeger"), - ("40", "SolTech"), - ("1000", "Genudine"), - ("2000", "Ceres"), - ]); -} pub struct World { - pub id: String, + filter: Filters, } impl World { - pub fn from_name(name: String) -> World { - let id = WORLD_NAME_TO_ID - .get(name.to_lowercase().as_str()) - .unwrap_or(&"-1"); - - World { id: id.to_string() } - } - - pub fn all_worlds() -> Vec { - WORLD_ID_TO_NAME - .keys() - .map(|id| World { id: id.to_string() }) - .collect() + pub fn new(filter: IdOrNameBy) -> Self { + Self { + filter: Filters { + world: Some(filter), + faction: None, + zone: None, + }, + } } } @@ -54,61 +31,121 @@ impl World { /// If World.id is not valid or known to the API, World.name will return "Unknown". #[Object] impl World { - async fn id(&self) -> &str { - &self.id + /// The ID of the world. + async fn id(&self) -> i32 { + id_or_name_to_id(&WORLD_IDS, self.filter.world.as_ref().unwrap()).unwrap() } + /// The name of the world, in official game capitalization. async fn name(&self) -> String { - WORLD_ID_TO_NAME - .get(self.id.as_str()) - .unwrap_or(&"Unknown") - .to_string() - } + let name = id_or_name_to_name(&ID_TO_WORLD, self.filter.world.as_ref().unwrap()).unwrap(); - async fn population<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - 0 - } - - async fn faction_population(&self) -> FactionPopulation { - FactionPopulation { - world_id: self.id.clone(), + // Special case for SolTech, lol. + if name == "soltech" { + return "SolTech".to_string(); } + + // Capitalize the first letter + name[0..1].to_uppercase() + &name[1..] } + /// Population filtered to this world. + async fn population(&self) -> Population { + Population::new(Some(Filters { + world: self.filter.world.clone(), + faction: None, + zone: None, + })) + } + + /// Vehicles filtered to this world. async fn vehicles(&self) -> Vehicles { - Vehicles::new(self.id.clone()) + Vehicles::new(Some(Filters { + world: self.filter.world.clone(), + faction: None, + zone: None, + })) } + /// Classes filtered to this world. async fn classes(&self) -> Classes { - Classes::new(self.id.clone()) + Classes::new(Some(Filters { + world: self.filter.world.clone(), + faction: None, + zone: None, + })) + } + + /// Get a specific zone/continent on this world. + async fn zones(&self) -> Zones { + Zones::new(Some(self.filter.clone())) } } -struct FactionPopulation { - world_id: String, -} - -impl FactionPopulation { - async fn by_faction<'ctx>(&self, ctx: &Context<'ctx>, faction: u8) -> u32 { - 0 - } -} +#[derive(Default)] +pub struct WorldQuery; #[Object] -impl FactionPopulation { - async fn vs<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_faction(ctx, 1).await +impl WorldQuery { + /// A world by ID or name. + pub async fn world(&self, by: IdOrNameBy) -> World { + World::new(by) } - async fn nc<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_faction(ctx, 2).await + /// All worlds. This is a convenience method for getting all worlds in one query. + /// If you want all of them as aggregate instead of as individual units, use `population`, `vehicles`, `classes` directly instead. + pub async fn all_worlds(&self) -> Vec { + ID_TO_WORLD + .iter() + .map(|(id, _)| World::new(IdOrNameBy::Id(*id))) + .collect() } - async fn tr<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_faction(ctx, 3).await + /// The Connery world in US West on PC + /// Shorthand for `world(by: { id: 1 }})` + pub async fn connery(&self) -> World { + World::new(IdOrNameBy::Id(1)) } - async fn ns<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { - self.by_faction(ctx, 4).await + /// The Miller world in EU on PC + /// Shorthand for `world(by: { id: 10 }})` + pub async fn miller(&self) -> World { + World::new(IdOrNameBy::Id(10)) + } + + /// The Cobalt world in EU on PC + /// Shorthand for `world(by: { id: 13 }})` + pub async fn cobalt(&self) -> World { + World::new(IdOrNameBy::Id(13)) + } + + /// The Emerald world in US East on PC + /// Shorthand for `world(by: { id: 17 }})` + pub async fn emerald(&self) -> World { + World::new(IdOrNameBy::Id(17)) + } + + /// The Jaeger world in US East on PC + /// Shorthand for `world(by: { id: 19 }})` + pub async fn jaeger(&self) -> World { + World::new(IdOrNameBy::Id(19)) + } + + /// The SolTech world in Japan on PC + /// Shorthand for `world(by: { id: 40 }})` + pub async fn soltech(&self) -> World { + World::new(IdOrNameBy::Id(40)) + } + + /// The Genudine world in US East on PS4 + /// Shorthand for `world(by: { id: 1000 }})` + pub async fn genudine(&self) -> World { + World::new(IdOrNameBy::Id(1000)) + } + + /// The Ceres world in EU on PS4 + /// Shorthand for `world(by: { id: 2000 }})` + pub async fn ceres(&self) -> World { + World::new(IdOrNameBy::Id(2000)) } } diff --git a/services/api/src/zone.rs b/services/api/src/zone.rs new file mode 100644 index 0000000..8c2be9c --- /dev/null +++ b/services/api/src/zone.rs @@ -0,0 +1,132 @@ +use crate::{ + classes::Classes, + population::Population, + utils::{id_or_name_to_id, id_or_name_to_name, Filters, IdOrNameBy, ID_TO_ZONE, ZONE_IDS}, + vehicles::Vehicles, +}; +use async_graphql::Object; + +/// An individual zone/continent. +pub struct Zone { + filters: Filters, +} + +impl Zone { + pub fn new(filters: Option) -> Self { + Self { + filters: filters.unwrap_or_default(), + } + } +} + +#[Object] +impl Zone { + /// The ID of the zone/continent. + async fn id(&self) -> i32 { + id_or_name_to_id(&ZONE_IDS, self.filters.zone.as_ref().unwrap()).unwrap() + } + + /// The name of the continent, in official game capitalization. + async fn name(&self) -> String { + let name = id_or_name_to_name(&ID_TO_ZONE, self.filters.zone.as_ref().unwrap()).unwrap(); + + // Capitalize the first letter + name[0..1].to_uppercase() + &name[1..] + } + + async fn population(&self) -> Population { + Population::new(Some(self.filters.clone())) + } + + async fn vehicles(&self) -> Vehicles { + Vehicles::new(Some(self.filters.clone())) + } + + async fn classes(&self) -> Classes { + Classes::new(Some(self.filters.clone())) + } +} + +/// Super-struct for querying zones/continents. +pub struct Zones { + filters: Filters, +} + +impl Zones { + pub fn new(filters: Option) -> Self { + Self { + filters: filters.unwrap_or_default(), + } + } +} + +#[Object] +impl Zones { + /// Every zone/continent individually. + async fn all(&self) -> Vec { + ID_TO_ZONE + .iter() + .map(|(id, _)| { + Zone::new(Some(Filters { + world: self.filters.world.clone(), + faction: self.filters.faction.clone(), + zone: Some(IdOrNameBy::Id(*id)), + })) + }) + .collect() + } + + async fn indar(&self) -> Zone { + Zone::new(Some(Filters { + world: self.filters.world.clone(), + faction: self.filters.faction.clone(), + zone: Some(IdOrNameBy::Id(2)), + })) + } + + async fn hossin(&self) -> Zone { + Zone::new(Some(Filters { + world: self.filters.world.clone(), + faction: self.filters.faction.clone(), + zone: Some(IdOrNameBy::Id(4)), + })) + } + + async fn amerish(&self) -> Zone { + Zone::new(Some(Filters { + world: self.filters.world.clone(), + faction: self.filters.faction.clone(), + zone: Some(IdOrNameBy::Id(6)), + })) + } + + async fn esamir(&self) -> Zone { + Zone::new(Some(Filters { + world: self.filters.world.clone(), + faction: self.filters.faction.clone(), + zone: Some(IdOrNameBy::Id(8)), + })) + } + + async fn oshur(&self) -> Zone { + Zone::new(Some(Filters { + world: self.filters.world.clone(), + faction: self.filters.faction.clone(), + zone: Some(IdOrNameBy::Id(344)), + })) + } +} + +#[derive(Default)] +pub struct ZoneQuery; + +#[Object] +impl ZoneQuery { + pub async fn zone(&self, filter: Option) -> Zone { + Zone::new(filter) + } + + pub async fn zones(&self, filter: Option) -> Zones { + Zones::new(filter) + } +}