//! The `hi` command-line interface. //! //! This module supports running `hi` as a freestanding program, via the //! [`Args`] struct. use std::io; use axum::{middleware, Router}; use clap::Parser; use sqlx::sqlite::SqlitePool; use tokio::net; use crate::{app::App, channel, clock, db, event, expire, login, message, ui}; /// Command-line entry point for running the `hi` server. /// /// This is intended to be used as a Clap [Parser], to capture command-line /// arguments for the `hi` server: /// /// ```no_run /// # use hi::cli::Error; /// # /// # #[tokio::main] /// # async fn main() -> Result<(), Error> { /// use clap::Parser; /// use hi::cli::Args; /// /// let args = Args::parse(); /// args.run().await?; /// # Ok(()) /// # } /// ``` #[derive(Parser)] #[command( about = "Run the `hi` server.", long_about = r#"Run the `hi` server. The database at `--database-url` will be created, or upgraded, automatically."# )] pub struct Args { /// The network address `hi` should listen on #[arg(short, long, env, default_value = "localhost")] address: String, /// The network port `hi` should listen on #[arg(short, long, env, default_value_t = 64209)] port: u16, /// Sqlite URL or path for the `hi` database #[arg(short, long, env, default_value = "sqlite://.hi")] database_url: String, /// Sqlite URL or path for a backup of the `hi` database during upgrades #[arg(short = 'D', long, env, default_value = "sqlite://.hi.backup")] backup_database_url: String, } impl Args { /// Runs the `hi` server, using the parsed configuation in `self`. /// /// This will perform the following tasks: /// /// * Migrate the `hi` database (at `--database-url`). /// * Start an HTTP server (on the interface and port controlled by /// `--address` and `--port`). /// * Print a status message. /// * Wait for that server to shut down. /// /// # Errors /// /// Will return `Err` if the server is unable to start, or terminates /// prematurely. The specific [`Error`] variant will expose the cause /// of the failure. pub async fn run(self) -> Result<(), Error> { let pool = self.pool().await?; let app = App::from(pool); let app = routers() .route_layer(middleware::from_fn_with_state( app.clone(), expire::middleware, )) .route_layer(middleware::from_fn(clock::middleware)) .with_state(app); let listener = self.listener().await?; let started_msg = started_msg(&listener)?; let serve = axum::serve(listener, app); println!("{started_msg}"); serve.await?; Ok(()) } async fn listener(&self) -> io::Result { let listen_addr = self.listen_addr(); let listener = tokio::net::TcpListener::bind(listen_addr).await?; Ok(listener) } fn listen_addr(&self) -> impl net::ToSocketAddrs + '_ { (self.address.as_str(), self.port) } async fn pool(&self) -> Result { db::prepare(&self.database_url, &self.backup_database_url).await } } fn routers() -> Router { [ channel::router(), event::router(), login::router(), message::router(), ui::router(), ] .into_iter() .fold(Router::default(), Router::merge) } fn started_msg(listener: &net::TcpListener) -> io::Result { let local_addr = listener.local_addr()?; Ok(format!("listening on http://{local_addr}/")) } /// Errors that can be raised by [`Args::run`]. #[derive(Debug, thiserror::Error)] #[error(transparent)] pub enum Error { /// Failure due to `io::Error`. See [`io::Error`]. IoError(#[from] io::Error), /// Failure due to a database initialization error. See [`db::Error`]. Database(#[from] db::Error), }