// Copyright 2022 Daniel Arbuckle // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Run with `cargo run --example todolist --features examples` use std::{ collections::BTreeMap, future::Future, net::TcpListener, sync::{Arc, Mutex}, }; use axum::{ extract::{Form, Path}, http::StatusCode, response::{Html, IntoResponse}, routing::{delete, get, get_service, post, put}, Extension, Router, Server, }; use sailfish::TemplateOnce; use tokio::sync::oneshot::{channel, Sender}; use toml; use ulid::Ulid; mod components; #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] #[serde(default)] pub struct Item { done: bool, label: String, } pub type ItemMap<'state> = &'state Mutex>; struct State { shutdown: Mutex>>, items: Mutex>, } impl State { fn save(self: Arc) -> impl Future { tokio::fs::write( "examples/todolist/items.toml", toml::to_string(&*self.items.lock().unwrap()).expect("serialize items"), ) } } async fn index(Extension(state): Extension>) -> impl IntoResponse { Html( components::Layout { items: &state.items, } .render_once() .unwrap_or_else(|_| "Error during page rendering".into()), ) } async fn todo_list(Extension(state): Extension>) -> impl IntoResponse { Html( components::Items { items: &state.items, } .render_once() .unwrap_or_else(|_| "Error during list rendering".into()), ) } async fn todo_add( Extension(state): Extension>, Form(item): Form, ) -> impl IntoResponse { let id = Ulid::new(); state.items.lock().unwrap().insert(id, item); state.save().await; ( [("HX-Trigger", "refresh-list")], Html( components::NewForm {} .render_once() .unwrap_or_else(|_| "Error during form rendering".into()), ), ) } async fn get_todo_item( Extension(state): Extension>, Path(id): Path, ) -> impl IntoResponse { if let Some(item) = state.items.lock().unwrap().get(&id) { Html( components::Item { id: &id, item: &item, } .render_once() .unwrap_or_else(|_| "Error during item rendering".into()), ) } else { Html(String::from("No such item")) } } async fn set_todo_item( Extension(state): Extension>, Path(id): Path, Form(item): Form, ) -> impl IntoResponse { let rendered = Html( components::Item { id: &id, item: &item, } .render_once() .unwrap_or_else(|_| "Error during new item rendering".into()), ); state.items.lock().unwrap().insert(id, item); state.save().await; rendered } async fn del_todo_item( Extension(state): Extension>, Path(id): Path, ) -> impl IntoResponse { state.items.lock().unwrap().remove(&id); state.save().await; [("HX-Trigger", "refresh-list")] } async fn shutdown(Extension(state): Extension>) -> impl IntoResponse { if let Some(shutdown) = state.shutdown.lock().unwrap().take() { let _ = shutdown.send(()); "Shutting down" } else { "Shutdown already initiated" } } fn main() { // Non-async set up. We can do any server configuration that // doesn't need an async context here, parse the command line, // etc. let (shutdown_tx, shutdown_rx) = channel::<()>(); let state = Arc::new(State { shutdown: Mutex::new(Some(shutdown_tx)), items: toml::from_str( &std::fs::read_to_string("examples/todolist/items.toml") .unwrap_or_else(|_| String::new()), ) .expect("items.toml should be valid TOML"), }); let app = Router::new() .route("/", get(index)) .route("/todo", get(todo_list)) .route("/todo", post(todo_add)) .route("/todo/:id", get(get_todo_item)) .route("/todo/:id", put(set_todo_item)) .route("/todo/:id", delete(del_todo_item)) .route("/shutdown", post(shutdown)) // We use a nested router so that request path is rewritten // for the ServeDir service. If we just routed to the service // directly, the request path received by the service would // contain the service mount point at the beginning. .nest( "/static", Router::new().route( "/*path", get_service(tower_http::services::ServeDir::new( "examples/todolist/static", )) .handle_error(|error: std::io::Error| async move { (StatusCode::INTERNAL_SERVER_ERROR, format!("{}", error)) }), ), ) .layer(Extension(state.clone())); // Pick a random available port on the loopback interface. Not // actually required, but usually a good idea. let ear = TcpListener::bind("127.0.0.1:0").expect("bind port"); let addr = ear.local_addr().expect("retrieve port"); // Here we start up the webview and actually run the server. The // async block is the asynchronous entry point for the program // (like tokio::main or async_std::main), so any server set up // code that needs to run in an async context should be there, as // well as the code to actually run the server. When the async // block finishes executing, the program will close. drteeth::launch("To Do List", addr, async move { Server::from_tcp(ear) .unwrap() .serve(app.into_make_service()) .with_graceful_shutdown(async { shutdown_rx.await.ok(); }) .await .unwrap(); }) .unwrap(); }