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`
|
||||
2. Enter your fresh project: `cd my_project`
|
||||
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),
|
||||
/// Run a match
|
||||
RunMatch(RunMatchCommand),
|
||||
/// Host local webserver
|
||||
Serve(ServeCommand),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
|
@ -42,6 +44,9 @@ struct InitProjectCommand {
|
|||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
struct ServeCommand;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct ProjectConfig {
|
||||
bots: HashMap<String, BotConfig>,
|
||||
|
@ -58,6 +63,7 @@ pub async fn run() {
|
|||
let res = match matches.command {
|
||||
Commands::RunMatch(command) => run_match(command).await,
|
||||
Commands::InitProject(command) => init_project(command),
|
||||
Commands::Serve(_) => run_webserver().await,
|
||||
};
|
||||
if let Err(err) = res {
|
||||
eprintln!("{}", err);
|
||||
|
@ -139,3 +145,11 @@ fn init_project(command: InitProjectCommand) -> io::Result<()> {
|
|||
|
||||
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