use crate::button::{Button, ButtonHandlerVariant, ButtonInfo, ButtonResponse};
use crate::html_utils::create_page_html;
use axum::extract::State;
use axum::response::Html;
use axum::routing::get;
use axum::{Json, Router};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::signal;
pub mod button;
mod html_utils;
pub use tokio::sync::oneshot;
pub struct ShutdownConfig<S: Send + Sync + 'static> {
pub shutdown_rx: Option<oneshot::Receiver<()>>,
pub handler: Option<Box<dyn FnOnce(&S)>>,
}
impl<S: Send + Sync + 'static> ShutdownConfig<S> {
pub fn new(
shutdown_rx: Option<oneshot::Receiver<()>>,
handler: Option<Box<dyn FnOnce(&S)>>,
) -> ShutdownConfig<S> {
ShutdownConfig {
shutdown_rx,
handler,
}
}
fn empty() -> ShutdownConfig<S> {
ShutdownConfig {
shutdown_rx: None,
handler: None,
}
}
}
pub async fn bind_server<S: Send + Sync + 'static>(
addr: &SocketAddr,
buttons: Vec<Button<S>>,
user_state: S,
shutdown_config: Option<ShutdownConfig<S>>,
) -> hyper::Result<()> {
let page = Html(create_page_html(buttons.iter()));
let button_handlers = buttons.into_iter().map(|b| b.handler).collect();
let btnify_state = Arc::new(BtnifyState {
button_handlers,
user_state,
page,
});
let app = Router::new()
.route("/", get(get_root).post(post_root))
.with_state(Arc::clone(&btnify_state));
axum::Server::bind(addr)
.serve(app.into_make_service())
.with_graceful_shutdown(shutdown_handler(shutdown_config, btnify_state))
.await
}
async fn shutdown_handler<S: Send + Sync + 'static>(
config: Option<ShutdownConfig<S>>,
state: Arc<BtnifyState<S>>,
) {
let config = config.unwrap_or_else(ShutdownConfig::empty);
if let Some(shutdown_rx) = config.shutdown_rx {
tokio::select! {
_ = ctrl_c_signal() => {},
_ = shutdown_rx => {},
}
} else {
ctrl_c_signal().await;
}
if let Some(handler) = config.handler {
handler(&state.user_state);
}
}
async fn get_root<S: Send + Sync>(State(state): State<Arc<BtnifyState<S>>>) -> Html<String> {
state.page.clone()
}
async fn post_root<S: Send + Sync>(
State(state): State<Arc<BtnifyState<S>>>,
Json(info): Json<ButtonInfo>,
) -> Json<ButtonResponse> {
let handler = state.button_handlers.get(info.id);
let res = match handler {
Some(handler) => match handler {
ButtonHandlerVariant::Basic(handler) => handler(),
ButtonHandlerVariant::WithState(handler) => handler(&state.user_state),
ButtonHandlerVariant::WithExtraPrompts(handler, extra_prompts) => {
if info.extra_responses.len() == extra_prompts.len() {
handler(info.extra_responses)
} else {
"Error parsing extra responses (extra responses length does not match extra prompts length)".into()
}
}
ButtonHandlerVariant::WithBoth(handler, extra_prompts) => {
if info.extra_responses.len() == extra_prompts.len() {
handler(&state.user_state, info.extra_responses)
} else {
"Error parsing extra responses (extra responses length does not match extra prompts length)".into()
}
}
},
None => "Unknown button id".into(),
};
Json(res)
}
struct BtnifyState<S: Send + Sync + 'static> {
button_handlers: Vec<ButtonHandlerVariant<S>>,
user_state: S,
page: Html<String>,
}
async fn ctrl_c_signal() {
let ctrl_c = async {
signal::ctrl_c()
.await
.expect("install ctrl+c signal handler");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("install terminate signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}