implement webserver backend
This commit is contained in:
parent
003c551e73
commit
e681eb91cd
3 changed files with 138 additions and 0 deletions
|
@ -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`
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
123
planetwars-localdev/src/web/mod.rs
Normal file
123
planetwars-localdev/src/web/mod.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue