api rebuild

This commit is contained in:
41666 2022-12-09 07:34:45 -05:00
parent 50c4ac387a
commit cecaecb92e
11 changed files with 782 additions and 175 deletions

21
Cargo.lock generated
View file

@ -49,6 +49,7 @@ dependencies = [
"async-graphql-axum", "async-graphql-axum",
"axum", "axum",
"lazy_static", "lazy_static",
"reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sqlx", "sqlx",
@ -64,9 +65,9 @@ checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
[[package]] [[package]]
name = "async-graphql" name = "async-graphql"
version = "5.0.2" version = "5.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5005cfd364b44d9cb55486b44184fe41a57b97339e17ce10db05f6b6093571d9" checksum = "42bb92ffef089e5b61e90bcc004c9689554dfb5a150d88e81c7f6fef9e76eeae"
dependencies = [ dependencies = [
"async-graphql-derive", "async-graphql-derive",
"async-graphql-parser", "async-graphql-parser",
@ -96,9 +97,9 @@ dependencies = [
[[package]] [[package]]
name = "async-graphql-axum" name = "async-graphql-axum"
version = "5.0.2" version = "5.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5554f6f1578ba1c65942ff7103c4c5421a47890a64666e1863d38bb9194ab091" checksum = "077bf197d54397cff2324b79d86a7a21b2a83260e62e33eccae33009427897c9"
dependencies = [ dependencies = [
"async-graphql", "async-graphql",
"async-trait", "async-trait",
@ -113,9 +114,9 @@ dependencies = [
[[package]] [[package]]
name = "async-graphql-derive" name = "async-graphql-derive"
version = "5.0.2" version = "5.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ff995b9d89198740d3701f1e5f7101e2822d5f4f1f4db19e9a1a9314cb14364" checksum = "4fa579c7cea32030600994d579554b257e10d5ad87705f3d150b49ee08bd629d"
dependencies = [ dependencies = [
"Inflector", "Inflector",
"async-graphql-parser", "async-graphql-parser",
@ -129,9 +130,9 @@ dependencies = [
[[package]] [[package]]
name = "async-graphql-parser" name = "async-graphql-parser"
version = "5.0.2" version = "5.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e73570e2270b9921a183df47760bea67a65afb145eaaa1b82a9c34b0c6209ff" checksum = "3b67a5bea60997ca72908854655ae87f7970dc7d786d9a42fd1d17069fa42ebc"
dependencies = [ dependencies = [
"async-graphql-value", "async-graphql-value",
"pest", "pest",
@ -141,9 +142,9 @@ dependencies = [
[[package]] [[package]]
name = "async-graphql-value" name = "async-graphql-value"
version = "5.0.2" version = "5.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97013c726c11f29262f52e9487025a72bae58262bad3c26389936ce3bf143b11" checksum = "79c2721eb88245ca055e148a3f03cb11a88535c206ac5a7c59e9edb22816320a"
dependencies = [ dependencies = [
"bytes", "bytes",
"indexmap", "indexmap",

View file

@ -8,10 +8,11 @@ edition = "2021"
[dependencies] [dependencies]
serde_json = "1.0.89" serde_json = "1.0.89"
serde = "1.0.149" serde = "1.0.149"
async-graphql = { version = "5.0.2" } async-graphql = { version = "5.0.3" }
async-graphql-axum = "5.0.2" async-graphql-axum = "5.0.3"
axum = "0.6.1" axum = "0.6.1"
sqlx = { version = "0.6.2", features = [ "runtime-tokio-native-tls", "postgres" ] } sqlx = { version = "0.6.2", features = [ "runtime-tokio-native-tls", "postgres" ] }
tokio = { version = "1.23.0", features = [ "full" ] } tokio = { version = "1.23.0", features = [ "full" ] }
tower-http = { version = "0.3.5", features = ["cors"] } tower-http = { version = "0.3.5", features = ["cors"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
reqwest = "0.11.13"

View file

@ -1,36 +1,140 @@
use crate::utils::{Filters, IdOrNameBy};
use async_graphql::{Context, Object}; 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::<Pool<Postgres>>().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 { pub struct Classes {
world_id: String, filters: Filters,
} }
impl Classes { impl Classes {
pub fn new(world_id: String) -> Self { pub fn new(filters: Option<Filters>) -> Self {
Self { world_id } Self {
} filters: filters.unwrap_or_default(),
async fn by_class<'ctx>(&self, ctx: &Context<'ctx>, class_name: &str) -> u32 { }
0
} }
} }
#[Object] #[Object]
impl Classes { impl Classes {
async fn infiltrator<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn infiltrator(&self) -> Class {
self.by_class(ctx, "infiltrator").await Class {
filters: self.filters.clone(),
class_name: "infiltrator".to_string(),
}
} }
async fn light_assault<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn light_assault(&self) -> Class {
self.by_class(ctx, "light_assault").await Class {
filters: self.filters.clone(),
class_name: "light_assault".to_string(),
}
} }
async fn combat_medic<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn combat_medic(&self) -> Class {
self.by_class(ctx, "combat_medic").await Class {
filters: self.filters.clone(),
class_name: "combat_medic".to_string(),
}
} }
async fn engineer<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn engineer(&self) -> Class {
self.by_class(ctx, "engineer").await Class {
filters: self.filters.clone(),
class_name: "engineer".to_string(),
}
} }
async fn heavy_assault<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn heavy_assault(&self) -> Class {
self.by_class(ctx, "heavy_assault").await Class {
filters: self.filters.clone(),
class_name: "heavy_assault".to_string(),
}
} }
async fn max<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn max(&self) -> Class {
self.by_class(ctx, "max").await 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<Filters>) -> Classes {
Classes::new(filter)
}
/// Get a specific class
pub async fn class(&self, filter: Option<Filters>, class_name: String) -> Class {
Class {
filters: filter.unwrap_or_default(),
class_name,
}
} }
} }

View file

@ -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 {}
}
}

View file

@ -1,8 +1,11 @@
mod classes; mod classes;
mod health; mod health;
mod population;
mod query; mod query;
mod utils;
mod vehicles; mod vehicles;
mod world; mod world;
mod zone;
use async_graphql::{ use async_graphql::{
http::GraphiQLSource, EmptyMutation, EmptySubscription, Request, Response, Schema, http::GraphiQLSource, EmptyMutation, EmptySubscription, Request, Response, Schema,
@ -61,7 +64,7 @@ async fn main() {
.unwrap_or("postgres://saerrouser:saerro321@localhost:5432/data".to_string()); .unwrap_or("postgres://saerrouser:saerro321@localhost:5432/data".to_string());
let db = sqlx::PgPool::connect(&db_url).await.unwrap(); 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()) .data(db.clone())
.finish(); .finish();

View file

@ -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<Filters>) -> 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::<Pool<Postgres>>().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::<Pool<Postgres>>().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<Filters>) -> Population {
Population::new(filter)
}
}

View file

@ -1,30 +1,15 @@
use crate::health::Health; use crate::{
use crate::world::World; classes::ClassesQuery, health::HealthQuery, population::PopulationQuery,
use async_graphql::Object; vehicles::VehicleQuery, world::WorldQuery, zone::ZoneQuery,
};
use async_graphql::MergedObject;
pub struct Query; #[derive(MergedObject, Default)]
pub struct Query(
#[Object] PopulationQuery,
impl Query { VehicleQuery,
/// Returns a graph for the world with the given ID. ClassesQuery,
/// If the world does not exist, this will not fail. WorldQuery,
async fn world(&self, id: String) -> World { ZoneQuery,
World { id: id.clone() } HealthQuery,
} );
/// 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> {
World::all_worlds()
}
/// Reports on the health of Saerro Listening Post
async fn health(&self) -> Health {
Health {}
}
}

100
services/api/src/utils.rs Normal file
View file

@ -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<String, i32> = 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<i32, String> = WORLD_IDS
.iter()
.map(|(name, id)| (id.to_owned(), name.to_owned()))
.collect();
pub static ref FACTION_IDS: HashMap<String, i32> = 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<i32, String> = FACTION_IDS
.iter()
.map(|(name, id)| (id.to_owned(), name.to_owned()))
.collect();
pub static ref ZONE_IDS: HashMap<String, i32> = 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<i32, String> = 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<String, i32>, by: &IdOrNameBy) -> Option<i32> {
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<i32, String>, id: &IdOrNameBy) -> Option<String> {
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<IdOrNameBy>,
/// The faction to filter by, like VS, NC, TR, or NS
pub faction: Option<IdOrNameBy>,
/// The zone or continent to filter by, like Indar, Amerish, etc.
pub zone: Option<IdOrNameBy>,
}
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
}
}

View file

@ -1,73 +1,221 @@
use crate::utils::{Filters, IdOrNameBy};
use async_graphql::{Context, Object}; 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::<Pool<Postgres>>().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 { pub struct Vehicles {
world_id: String, filters: Filters,
} }
impl Vehicles { impl Vehicles {
pub fn new(world_id: String) -> Self { pub fn new(filters: Option<Filters>) -> Self {
Self { world_id } Self {
} filters: filters.unwrap_or_default(),
async fn by_vehicle<'ctx>(&self, ctx: &Context<'ctx>, vehicle_name: &str) -> u32 { }
0
} }
} }
#[Object] #[Object]
impl Vehicles { impl Vehicles {
async fn flash<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn total<'ctx>(&self, ctx: &Context<'ctx>) -> i64 {
self.by_vehicle(ctx, "flash").await let pool = ctx.data::<Pool<Postgres>>().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 { async fn sunderer(&self) -> Vehicle {
self.by_vehicle(ctx, "ant").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "sunderer".to_string(),
}
} }
async fn harasser<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn ant(&self) -> Vehicle {
self.by_vehicle(ctx, "harasser").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "ant".to_string(),
}
} }
async fn javelin<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn harasser(&self) -> Vehicle {
self.by_vehicle(ctx, "javelin").await 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 // Tanks
async fn lightning<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn lightning(&self) -> Vehicle {
self.by_vehicle(ctx, "lightning").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "javelin".to_string(),
}
} }
async fn prowler<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn prowler(&self) -> Vehicle {
self.by_vehicle(ctx, "prowler").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "prowler".to_string(),
}
} }
async fn vanguard<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn vanguard(&self) -> Vehicle {
self.by_vehicle(ctx, "vanguard").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "vanguard".to_string(),
}
} }
async fn magrider<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn magrider(&self) -> Vehicle {
self.by_vehicle(ctx, "magrider").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "magrider".to_string(),
}
} }
async fn chimera<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn chimera(&self) -> Vehicle {
self.by_vehicle(ctx, "chimera").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "chimera".to_string(),
}
} }
// Air // Air
async fn mosquito<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn mosquito(&self) -> Vehicle {
self.by_vehicle(ctx, "mosquito").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "mosquito".to_string(),
}
} }
async fn liberator<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn liberator(&self) -> Vehicle {
self.by_vehicle(ctx, "liberator").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "liberator".to_string(),
}
} }
async fn galaxy<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn galaxy(&self) -> Vehicle {
self.by_vehicle(ctx, "galaxy").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "galaxy".to_string(),
}
} }
async fn valkyrie<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn valkyrie(&self) -> Vehicle {
self.by_vehicle(ctx, "valkyrie").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "valkyrie".to_string(),
}
} }
async fn reaver<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn reaver(&self) -> Vehicle {
self.by_vehicle(ctx, "reaver").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "reaver".to_string(),
}
} }
async fn scythe<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn scythe(&self) -> Vehicle {
self.by_vehicle(ctx, "scythe").await Vehicle {
filters: self.filters.clone(),
vehicle_name: "scythe".to_string(),
}
} }
async fn dervish<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { async fn dervish(&self) -> Vehicle {
self.by_vehicle(ctx, "dervish").await 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<Filters>) -> Vehicles {
Vehicles::new(filter)
} }
} }

View file

@ -1,48 +1,25 @@
use crate::{classes::Classes, vehicles::Vehicles}; use crate::{
use async_graphql::{Context, Object}; classes::Classes,
use lazy_static::lazy_static; population::Population,
use std::collections::HashMap; 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 struct World {
pub id: String, filter: Filters,
} }
impl World { impl World {
pub fn from_name(name: String) -> World { pub fn new(filter: IdOrNameBy) -> Self {
let id = WORLD_NAME_TO_ID Self {
.get(name.to_lowercase().as_str()) filter: Filters {
.unwrap_or(&"-1"); world: Some(filter),
faction: None,
World { id: id.to_string() } zone: None,
} },
}
pub fn all_worlds() -> Vec<World> {
WORLD_ID_TO_NAME
.keys()
.map(|id| World { id: id.to_string() })
.collect()
} }
} }
@ -54,61 +31,121 @@ impl World {
/// If World.id is not valid or known to the API, World.name will return "Unknown". /// If World.id is not valid or known to the API, World.name will return "Unknown".
#[Object] #[Object]
impl World { impl World {
async fn id(&self) -> &str { /// The ID of the world.
&self.id 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 { async fn name(&self) -> String {
WORLD_ID_TO_NAME let name = id_or_name_to_name(&ID_TO_WORLD, self.filter.world.as_ref().unwrap()).unwrap();
.get(self.id.as_str())
.unwrap_or(&"Unknown")
.to_string()
}
async fn population<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { // Special case for SolTech, lol.
0 if name == "soltech" {
} return "SolTech".to_string();
async fn faction_population(&self) -> FactionPopulation {
FactionPopulation {
world_id: self.id.clone(),
} }
// 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 { 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 { 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 { #[derive(Default)]
world_id: String, pub struct WorldQuery;
}
impl FactionPopulation {
async fn by_faction<'ctx>(&self, ctx: &Context<'ctx>, faction: u8) -> u32 {
0
}
}
#[Object] #[Object]
impl FactionPopulation { impl WorldQuery {
async fn vs<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { /// A world by ID or name.
self.by_faction(ctx, 1).await pub async fn world(&self, by: IdOrNameBy) -> World {
World::new(by)
} }
async fn nc<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { /// All worlds. This is a convenience method for getting all worlds in one query.
self.by_faction(ctx, 2).await /// 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<World> {
ID_TO_WORLD
.iter()
.map(|(id, _)| World::new(IdOrNameBy::Id(*id)))
.collect()
} }
async fn tr<'ctx>(&self, ctx: &Context<'ctx>) -> u32 { /// The Connery world in US West on PC
self.by_faction(ctx, 3).await /// 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 { /// The Miller world in EU on PC
self.by_faction(ctx, 4).await /// 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))
} }
} }

132
services/api/src/zone.rs Normal file
View file

@ -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<Filters>) -> 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<Filters>) -> Self {
Self {
filters: filters.unwrap_or_default(),
}
}
}
#[Object]
impl Zones {
/// Every zone/continent individually.
async fn all(&self) -> Vec<Zone> {
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<Filters>) -> Zone {
Zone::new(filter)
}
pub async fn zones(&self, filter: Option<Filters>) -> Zones {
Zones::new(filter)
}
}