add graphql support

This commit is contained in:
41666 2022-11-25 18:36:33 -05:00
parent d1ab9ae173
commit ce66c16185
9 changed files with 919 additions and 77 deletions

View file

@ -9,4 +9,7 @@ edition = "2021"
rocket = { version = "0.5.0-rc.2", features = ["json"] }
rocket_db_pools = { version = "0.1.0-rc.2", features = [ "deadpool_redis" ] }
serde_json = "1.0.88"
serde = "1.0.147"
serde = "1.0.147"
juniper = "0.15.10"
juniper_rocket = "0.8.2"
once_cell = "1.16.0"

View file

@ -1,3 +1,4 @@
use crate::redispool::RedisPool;
use core::time;
use rocket_db_pools::deadpool_redis::redis::{pipe, AsyncCommands};
use rocket_db_pools::Connection;
@ -5,8 +6,6 @@ use serde::{Deserialize, Serialize};
use std::ops::Sub;
use std::time::SystemTime;
use crate::redispool::RedisPool;
#[derive(Serialize, Deserialize, Debug)]
struct ClassCounts {
world_id: String,
@ -14,13 +13,13 @@ struct ClassCounts {
}
#[derive(Serialize, Deserialize, Debug)]
struct Classes {
light_assault: u32,
engineer: u32,
combat_medic: u32,
heavy_assault: u32,
infiltrator: u32,
max: u32,
pub struct Classes {
light_assault: i32,
engineer: i32,
combat_medic: i32,
heavy_assault: i32,
infiltrator: i32,
max: i32,
}
#[get("/w/<world_id>/classes")]
@ -34,21 +33,36 @@ pub async fn get_classes(world_id: String, mut con: Connection<RedisPool>) -> se
Err(_) => {}
}
let classes = fetch_classes(world_id.clone(), &mut con).await;
// I hate this but it's fast???
// The type only allows 12 at a time.
let response = ClassCounts { world_id, classes };
let out = json!(response);
con.set_ex::<String, String, ()>(cache_key, out.to_string(), 5)
.await
.unwrap();
out
}
pub async fn fetch_classes(world_id: String, con: &mut Connection<RedisPool>) -> Classes {
let filter_timestamp = SystemTime::now()
.sub(time::Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
// I hate this but it's fast???
// The type only allows 12 at a time.
let (light_assault, engineer, combat_medic, heavy_assault, infiltrator, max): (
u32,
u32,
u32,
u32,
u32,
u32,
i32,
i32,
i32,
i32,
i32,
i32,
) = pipe()
.zcount(
format!("c:{}/{}", world_id, "light_assault"),
@ -80,27 +94,16 @@ pub async fn get_classes(world_id: String, mut con: Connection<RedisPool>) -> se
filter_timestamp,
"+inf",
)
.query_async(&mut *con)
.query_async(&mut **con)
.await
.unwrap();
let response = ClassCounts {
world_id,
classes: Classes {
light_assault,
engineer,
combat_medic,
heavy_assault,
infiltrator,
max,
},
};
let out = json!(response);
con.set_ex::<String, String, ()>(cache_key, out.to_string(), 5)
.await
.unwrap();
out
Classes {
light_assault,
engineer,
combat_medic,
heavy_assault,
infiltrator,
max,
}
}

View file

@ -15,7 +15,7 @@ impl Fairing for CORS {
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
response.set_header(Header::new("Access-Control-Allow-Methods", "GET"));
response.set_header(Header::new("Access-Control-Allow-Methods", "GET, POST"));
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
response.set_header(Header::new("Access-Control-Allow-Credentials", "false"));
}

View file

@ -0,0 +1,65 @@
use crate::redispool::RedisPool;
use self::types::World;
use juniper::{graphql_object, FieldResult, ID};
use rocket::response::content::RawHtml;
pub mod types;
pub struct Context {
con: RedisPool,
}
impl juniper::Context for Context {}
#[get("/graphiql")]
pub fn graphiql() -> RawHtml<String> {
juniper_rocket::graphiql_source("/graphql", None)
}
#[get("/")]
pub fn playground() -> RawHtml<String> {
juniper_rocket::playground_source("/graphql", None)
}
#[post("/", data = "<query>")]
pub async fn post_graphql(
query: juniper_rocket::GraphQLRequest,
schema: &rocket::State<Schema>,
con: &RedisPool,
) -> juniper_rocket::GraphQLResponse {
query.execute(&*schema, &Context { con: con.clone() }).await
}
#[get("/?<query..>")]
pub async fn get_graphql(
query: juniper_rocket::GraphQLRequest,
schema: &rocket::State<Schema>,
con: &RedisPool,
) -> juniper_rocket::GraphQLResponse {
query.execute(&*schema, &Context { con: con.clone() }).await
}
pub struct Query;
#[graphql_object(context = Context)]
impl Query {
fn world(id: ID) -> FieldResult<Option<World>> {
Ok(Some(World { id }))
}
}
pub type Schema = juniper::RootNode<
'static,
Query,
juniper::EmptyMutation<Context>,
juniper::EmptySubscription<Context>,
>;
pub fn schema() -> Schema {
Schema::new(
Query,
juniper::EmptyMutation::<Context>::new(),
juniper::EmptySubscription::<Context>::new(),
)
}

View file

@ -0,0 +1,271 @@
use juniper::graphql_object;
use once_cell::sync::Lazy;
use rocket_db_pools::deadpool_redis::redis::{cmd, pipe};
use std::{
collections::HashMap,
ops::Sub,
time::{Duration, SystemTime},
};
static WORLD_ID_TO_NAME: Lazy<HashMap<&str, &str>> = Lazy::new(|| {
HashMap::from([
("1", "Connery"),
("10", "Miller"),
("13", "Cobalt"),
("17", "Emerald"),
("19", "Jaeger"),
("40", "SolTech"),
("1000", "Genudine"),
("2000", "Ceres"),
])
});
#[derive(Clone, Debug)]
pub struct World {
pub id: juniper::ID,
}
#[graphql_object(context = super::Context)]
impl World {
pub fn name(&self) -> String {
WORLD_ID_TO_NAME
.get(&self.id.to_string().as_str())
.unwrap_or(&"Unknown")
.to_string()
}
pub async fn population(&self, context: &mut super::Context) -> i32 {
let mut con = (*context).con.get().await.unwrap();
let id = self.id.to_string();
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let (vs, nc, tr, ns): (i32, i32, i32, i32) = pipe()
.zcount(format!("wp:{}/{}", id, 1), filter_timestamp, "+inf")
.zcount(format!("wp:{}/{}", id, 2), filter_timestamp, "+inf")
.zcount(format!("wp:{}/{}", id, 3), filter_timestamp, "+inf")
.zcount(format!("wp:{}/{}", id, 4), filter_timestamp, "+inf")
.query_async(&mut con)
.await
.unwrap();
tr + vs + nc + ns
}
pub async fn faction_population(&self) -> FactionPopulation {
FactionPopulation {
world_id: self.id.clone(),
}
}
pub async fn vehicles(&self) -> Vehicles {
Vehicles {
world_id: self.id.clone(),
}
}
}
pub struct FactionPopulation {
world_id: juniper::ID,
}
impl FactionPopulation {
async fn by_faction(&self, context: &super::Context, world_id: String, faction: i32) -> i32 {
let mut con = (*context).con.get().await.unwrap();
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
cmd("ZCOUNT")
.arg(format!("wp:{}/{}", world_id, faction))
.arg(filter_timestamp)
.arg("+inf")
.query_async(&mut con)
.await
.unwrap()
}
}
#[graphql_object(context = super::Context)]
#[graphql(description = "The population of each faction on a world")]
impl FactionPopulation {
async fn vs(&self, context: &super::Context) -> i32 {
self.by_faction(context, self.world_id.to_string(), 1).await
}
async fn nc(&self, context: &super::Context) -> i32 {
self.by_faction(context, self.world_id.to_string(), 2).await
}
async fn tr(&self, context: &super::Context) -> i32 {
self.by_faction(context, self.world_id.to_string(), 3).await
}
async fn ns(&self, context: &super::Context) -> i32 {
self.by_faction(context, self.world_id.to_string(), 4).await
}
}
pub struct Vehicles {
world_id: juniper::ID,
}
impl Vehicles {
async fn get_vehicle(
&self,
context: &super::Context,
world_id: String,
vehicle_name: &str,
) -> i32 {
let mut con = (*context).con.get().await.unwrap();
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
cmd("ZCOUNT")
.arg(format!("v:{}/{}", world_id, vehicle_name))
.arg(filter_timestamp)
.arg("+inf")
.query_async(&mut con)
.await
.unwrap()
}
}
#[graphql_object(context = super::Context)]
#[graphql(description = "The count of active vehicles on a world")]
impl Vehicles {
// Transporters
async fn flash(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "flash")
.await
}
async fn sunderer(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "sunderer")
.await
}
async fn ant(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "ant")
.await
}
async fn harasser(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "harasser")
.await
}
async fn javelin(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "javelin")
.await
}
// Tanks
async fn lightning(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "lightning")
.await
}
async fn prowler(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "prowler")
.await
}
async fn vanguard(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "vanguard")
.await
}
async fn magrider(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "magrider")
.await
}
async fn chimera(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "chimera")
.await
}
// Air
async fn mosquito(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "mosquito")
.await
}
async fn liberator(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "liberator")
.await
}
async fn galaxy(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "galaxy")
.await
}
async fn valkyrie(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "valkyrie")
.await
}
async fn reaver(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "reaver")
.await
}
async fn scythe(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "scythe")
.await
}
async fn dervish(&self, context: &super::Context) -> i32 {
self.get_vehicle(context, self.world_id.to_string(), "dervish")
.await
}
}
pub struct Classes {
pub world_id: juniper::ID,
}
impl Classes {
async fn get_class(&self, context: &super::Context, world_id: String, class_name: &str) -> i32 {
let mut con = (*context).con.get().await.unwrap();
let filter_timestamp = SystemTime::now()
.sub(Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
cmd("ZCOUNT")
.arg(format!("c:{}/{}", world_id, class_name))
.arg(filter_timestamp)
.arg("+inf")
.query_async(&mut con)
.await
.unwrap()
}
}
#[graphql_object(context = super::Context)]
#[graphql(description = "The count of active classes on a world")]
impl Classes {
async fn infiltrator(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "infiltrator")
.await
}
async fn light_assault(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "light_assault")
.await
}
async fn combat_medic(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "combat_medic")
.await
}
async fn engineer(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "engineer")
.await
}
async fn heavy_assault(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "heavy_assault")
.await
}
async fn max(&self, context: &super::Context) -> i32 {
self.get_class(context, self.world_id.to_string(), "max")
.await
}
}

View file

@ -1,5 +1,6 @@
pub mod classes;
pub mod cors;
pub mod graphql;
pub mod population;
pub mod redispool;
pub mod vehicles;
@ -10,18 +11,11 @@ use rocket::{error, Build, Rocket};
use rocket_db_pools::deadpool_redis::redis::{cmd, pipe};
use rocket_db_pools::{Connection, Database};
use serde::{Deserialize, Serialize};
#[macro_use]
extern crate rocket;
#[macro_use]
extern crate serde_json;
#[derive(Serialize, Deserialize)]
struct IncomingHeaders {
host: String,
}
fn hello_world(host: String, world_id: &str) -> serde_json::Value {
json!({
"population": format!("https://{}/w/{}", host, world_id),
@ -99,6 +93,7 @@ fn rocket() -> Rocket<Build> {
}
rocket
}))
.manage(graphql::schema())
.mount(
"/",
routes![
@ -109,4 +104,13 @@ fn rocket() -> Rocket<Build> {
classes::get_classes,
],
)
.mount(
"/graphql",
routes![
graphql::graphiql,
graphql::playground,
graphql::get_graphql,
graphql::post_graphql
],
)
}

View file

@ -8,17 +8,17 @@ use crate::redispool::RedisPool;
#[derive(Serialize, Deserialize, Debug)]
pub struct Factions {
tr: u32,
nc: u32,
vs: u32,
ns: u32,
pub tr: i32,
pub nc: i32,
pub vs: i32,
pub ns: i32,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct WorldPopulation {
world_id: u32,
total: u32,
factions: Factions,
world_id: i32,
pub total: i32,
pub factions: Factions,
}
#[derive(Serialize, Deserialize, Debug)]
@ -28,28 +28,30 @@ pub struct MultipleWorldPopulation {
#[get("/w/<world_id>")]
pub async fn get_world_pop(world_id: String, mut con: Connection<RedisPool>) -> serde_json::Value {
json!(fetch_world_pop(world_id, &mut con).await)
}
pub async fn fetch_world_pop(world_id: String, con: &mut Connection<RedisPool>) -> WorldPopulation {
let filter_timestamp = SystemTime::now()
.sub(time::Duration::from_secs(60 * 15))
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let (vs, nc, tr, ns): (u32, u32, u32, u32) = pipe()
let (vs, nc, tr, ns): (i32, i32, i32, i32) = pipe()
.zcount(format!("wp:{}/{}", world_id, 1), filter_timestamp, "+inf")
.zcount(format!("wp:{}/{}", world_id, 2), filter_timestamp, "+inf")
.zcount(format!("wp:{}/{}", world_id, 3), filter_timestamp, "+inf")
.zcount(format!("wp:{}/{}", world_id, 4), filter_timestamp, "+inf")
.query_async(&mut *con)
.query_async(&mut **con)
.await
.unwrap();
let total = tr + vs + nc;
let response = WorldPopulation {
WorldPopulation {
world_id: world_id.parse().unwrap(),
total,
factions: Factions { tr, nc, vs, ns },
};
json!(response)
}
}

View file

@ -1,5 +1,5 @@
use rocket_db_pools::{deadpool_redis, Database};
#[derive(Database)]
#[derive(Database, Clone)]
#[database("redis")]
pub struct RedisPool(deadpool_redis::Pool);