use actix_cors::Cors; use actix_files as fs; use actix_web::{web, App, Error, HttpResponse, HttpServer}; use futures::Future; use lootalot_db::{DbApi, Pool, QueryResult}; use lootalot_db::{Item, LootManager}; use lootalot_db as db; use serde::{Deserialize, Serialize}; use std::env; use std::default; type AppPool = web::Data; /// Wraps call to the DbApi and process its result as a async HttpResponse /// /// Provides a convenient way to call the api inside a route definition. Given a connection pool, /// access to the api is granted in a closure. The closure is called in a blocking way and should /// return a QueryResult. /// If the query succeeds, it's result is returned as JSON data. Otherwise, an InternalServerError /// is returned. /// /// # Usage /// ``` /// (...) /// .route("path/to/", /// move |pool: web::Data| { /// // user data can be processed here /// // ... /// db_call(pool, move |api| { /// // ...do what you want with the api /// } /// } /// ) /// ``` pub fn db_call(pool: AppPool, query: Q) -> impl Future where J: serde::ser::Serialize + Send + 'static, Q: Fn(DbConnection) -> ApiResponse + Send + 'static, { let conn = pool.get().unwrap(); web::block(move || query(conn)).then(|res| match res { Ok(r) => HttpResponse::Ok().json(r), Err(e) => { dbg!(&e); HttpResponse::InternalServerError().finish() } }) } mod endpoints { use super::*; #[derive(Serialize, Deserialize, Debug)] pub struct PlayerClaim { player_id: i32, item_id: i32, } #[derive(Serialize, Deserialize, Debug)] pub struct WealthUpdate { player_id: i32, value_in_gp: f32, } #[derive(Serialize, Deserialize, Debug)] pub struct NewPlayer { pub name: String, pub wealth: f32, } #[derive(Serialize, Deserialize, Debug)] pub struct LootUpdate { player_id: i32, items: Vec<(i32, Option)>, } pub fn players_list(pool: AppPool) -> impl Future { db_call(pool, move |conn| { let (value, errors) = match db::Players(conn).all() { Ok(v) => (Some(v), None), Err(e) => (None, Some(e.to_string())), }; ApiResponse { value, errors, ..Default::default() } }) } pub fn player_loot( pool: AppPool, player_id: web::Path, ) -> impl Future { db_call(pool, move |conn| { let (value, errors) = { match db::LootManager(&conn, *player_id).all() { Ok(v) => (Some(v), None), Err(e) => (None, Some(e.to_string())), } }; ApiResponse { value, errors, ..Default::default() } }) } pub fn update_wealth( pool: AppPool, (player, data): (web::Path, web::Json), ) -> impl Future { db_call(pool, move |conn| { let (updates, errors) = match db::AsPlayer(conn, player) .update_wealth(data.value_in_gp) { Ok(w) => (Some(vec![w.as_tuple(),]), None), Err(e) => (None, Some(e.to_string())), }; ApiResponse { updates, errors, ..Default::default() } }) } pub fn buy_item( pool: AppPool, data: web::Json, ) -> impl Future { db_call(pool, move |api| { api.as_player(data.player_id).buy(&data.items) }) } pub fn sell_item( pool: AppPool, data: web::Json, ) -> impl Future { db_call(pool, move |api| { api.as_player(data.player_id).sell(&data.items) }) } pub fn player_claims(pool: AppPool) -> impl Future { db_call(pool, move |api| api.fetch_claims()) } pub fn put_claim( pool: AppPool, (player, loot): (web::Path, web::Json), ) -> impl Future { db_call(pool, move |api| api.as_player(*player).claim(loot.item_id)) } pub fn delete_claim( pool: AppPool, (player, data): (web::Path, web::Json), ) -> impl Future { db_call(pool, move |api| { api.as_player(*player).unclaim(data.item_id) }) } } pub(crate) fn serve() -> std::io::Result<()> { let www_root: String = env::var("WWW_ROOT").expect("WWW_ROOT must be set"); dbg!(&www_root); let pool = lootalot_db::create_pool(); HttpServer::new(move || { App::new() .data(pool.clone()) .wrap( Cors::new() .allowed_origin("http://localhost:8080") .allowed_methods(vec!["GET", "POST", "PUT", "DELETE", "OPTIONS"]) .max_age(3600), ) .service( web::scope("/api") .service( web::scope("/players") .service( web::resource("/") .route(web::get().to_async(endpoints::players_list)), ) // List of players //.route(web::put().to_async(endpoints::new_player)) // Create/Update player .service( web::scope("/{player_id}") //.route(web::get().to_async(...)) // Details of player .service( web::resource("/claims") //.route(web::get().to_async(endpoints::player_claims)) .route(web::put().to_async(endpoints::put_claim)) .route(web::delete().to_async(endpoints::delete_claim)), ) .service( web::resource("/wealth") //.route(web::get().to_async(...)) .route(web::put().to_async(endpoints::update_wealth)), ) .service( web::resource("/loot") .route(web::get().to_async(endpoints::player_loot)) .route(web::put().to_async(endpoints::buy_item)) .route(web::delete().to_async(endpoints::sell_item)), ), ), ) .route("/claims", web::get().to_async(endpoints::player_claims)) .route( "/items", web::get().to_async(move |pool: AppPool| { db_call(pool, move |api| api.fetch_inventory()) }), ) .service( web::scope("/admin") .route( "/resolve-claims", web::get().to_async(move |pool: AppPool| { db_call(pool, move |api| api.as_admin().resolve_claims()) }), ) .route( "/add-loot", web::post().to_async( move |pool: AppPool, data: web::Json>| { db_call(pool, move |api| { api.as_admin().add_loot(data.to_vec()) }) }, ), ) .route( "/add-player", web::get().to_async( move |pool: AppPool, data: web::Json| { db_call(pool, move |api| { api.as_admin().add_player(&data.name, data.wealth) }) }, ), ), ), ) .service(fs::Files::new("/", www_root.clone()).index_file("index.html")) }) .bind("127.0.0.1:8088")? .run() }