summaryrefslogtreecommitdiff
path: root/src/cli.rs
blob: d88916a8c8a9e933c3cbbdea6fd92510c14e1341 (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
130
131
//! 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};

/// 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) -> Result<SqlitePool, db::Error> {
        db::prepare(&self.database_url).await
    }
}

fn routers() -> Router<App> {
    [
        channel::router(),
        event::router(),
        login::router(),
        message::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 initialization error. See [`db::Error`].
    Database(#[from] db::Error),
}