summaryrefslogtreecommitdiff
path: root/src/cli.rs
blob: 132baf8b7277e3f62ab6760413f36b112c7ae13a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! 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, events, expire, login, repo::pool};

/// 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,
}

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<net::TcpListener> {
        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) -> sqlx::Result<SqlitePool> {
        pool::prepare(&self.database_url).await
    }
}

fn routers() -> Router<App> {
    [channel::router(), events::router(), login::router()]
        .into_iter()
        .fold(Router::default(), Router::merge)
}

fn started_msg(listener: &net::TcpListener) -> io::Result<String> {
    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 error. See [`sqlx::Error`].
    DatabaseError(#[from] sqlx::Error),
    /// Failure due to a database migration error. See
    /// [`sqlx::migrate::MigrateError`].
    MigrateError(#[from] sqlx::migrate::MigrateError),
}