implement webserver backend

This commit is contained in:
Ilion Beyst 2021-12-25 20:30:09 +01:00
parent 003c551e73
commit e681eb91cd
3 changed files with 138 additions and 0 deletions

View file

@ -7,3 +7,4 @@ Tools for developping planetwars bots locally.
1. Initialize your project directory: `pwcli init-project my_project` 1. Initialize your project directory: `pwcli init-project my_project`
2. Enter your fresh project: `cd my_project` 2. Enter your fresh project: `cd my_project`
3. Run an example match: `pwcli run-match hex simplebot simplebot` 3. Run an example match: `pwcli run-match hex simplebot simplebot`
4. View your match in the web UI: `pwcli serve`

View file

@ -26,6 +26,8 @@ enum Commands {
InitProject(InitProjectCommand), InitProject(InitProjectCommand),
/// Run a match /// Run a match
RunMatch(RunMatchCommand), RunMatch(RunMatchCommand),
/// Host local webserver
Serve(ServeCommand),
} }
#[derive(Parser)] #[derive(Parser)]
@ -42,6 +44,9 @@ struct InitProjectCommand {
path: String, path: String,
} }
#[derive(Parser)]
struct ServeCommand;
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
struct ProjectConfig { struct ProjectConfig {
bots: HashMap<String, BotConfig>, bots: HashMap<String, BotConfig>,
@ -58,6 +63,7 @@ pub async fn run() {
let res = match matches.command { let res = match matches.command {
Commands::RunMatch(command) => run_match(command).await, Commands::RunMatch(command) => run_match(command).await,
Commands::InitProject(command) => init_project(command), Commands::InitProject(command) => init_project(command),
Commands::Serve(_) => run_webserver().await,
}; };
if let Err(err) = res { if let Err(err) = res {
eprintln!("{}", err); eprintln!("{}", err);
@ -139,3 +145,11 @@ fn init_project(command: InitProjectCommand) -> io::Result<()> {
Ok(()) Ok(())
} }
mod web;
async fn run_webserver() -> io::Result<()> {
let project_dir = env::current_dir().unwrap();
web::run(project_dir).await;
Ok(())
}

View file

@ -0,0 +1,123 @@
use axum::{
body::{boxed, Full},
extract::{Extension, Path},
handler::Handler,
http::{header, StatusCode, Uri},
response::{IntoResponse, Response},
routing::{get, Router},
AddExtensionLayer, Json,
};
use mime_guess;
use rust_embed::RustEmbed;
use serde::{Deserialize, Serialize};
use std::{
net::SocketAddr,
path::{self, PathBuf},
sync::Arc,
};
struct State {
project_root: PathBuf,
}
impl State {
fn new(project_root: PathBuf) -> Self {
Self { project_root }
}
}
pub async fn run(project_root: PathBuf) {
let shared_state = Arc::new(State::new(project_root));
// build our application with a route
let app = Router::new()
.route("/", get(index_handler))
.route("/api/matches", get(list_matches))
.route("/api/matches/:match_id", get(get_match))
.fallback(static_handler.into_service())
.layer(AddExtensionLayer::new(shared_state));
// run it
let addr = SocketAddr::from(([127, 0, 0, 1], 5000));
println!("serving at http://{}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
#[derive(Serialize, Deserialize)]
struct Match {
name: String,
}
async fn list_matches(Extension(state): Extension<Arc<State>>) -> Json<Vec<Match>> {
let matches = state
.project_root
.join("matches")
.read_dir()
.unwrap()
.filter_map(|entry| {
let entry = entry.unwrap();
extract_match_name(entry).map(|name| Match { name })
})
.collect::<Vec<_>>();
Json(matches)
}
// extracts 'filename' if the entry matches'$filename.log'.
fn extract_match_name(entry: std::fs::DirEntry) -> Option<String> {
let file_name = entry.file_name();
let path = path::Path::new(&file_name);
if path.extension() == Some("log".as_ref()) {
path.file_stem()
.and_then(|name| name.to_str())
.map(|name| name.to_string())
} else {
None
}
}
async fn get_match(Extension(state): Extension<Arc<State>>, Path(id): Path<String>) -> String {
let mut match_path = state.project_root.join("matches").join(id);
match_path.set_extension("log");
std::fs::read_to_string(match_path).unwrap()
}
async fn index_handler() -> impl IntoResponse {
static_handler("/index.html".parse::<Uri>().unwrap()).await
}
// static_handler is a handler that serves static files from the
async fn static_handler(uri: Uri) -> impl IntoResponse {
let path = uri.path().trim_start_matches('/').to_string();
StaticFile(path)
}
#[derive(RustEmbed)]
#[folder = "../web/pw-frontend/dist/"]
struct Asset;
pub struct StaticFile<T>(pub T);
impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
fn into_response(self) -> Response {
let path = self.0.into();
match Asset::get(path.as_str()) {
Some(content) => {
let body = boxed(Full::from(content.data));
let mime = mime_guess::from_path(path).first_or_octet_stream();
Response::builder()
.header(header::CONTENT_TYPE, mime.as_ref())
.body(body)
.unwrap()
}
None => Response::builder()
.status(StatusCode::NOT_FOUND)
.body(boxed(Full::from("404")))
.unwrap(),
}
}
}