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