|
| 1 | +use axum::body::Body; |
| 2 | +use axum::middleware::{from_fn, Next}; |
| 3 | +use axum::response::{IntoResponse, Response}; |
| 4 | +use axum::{routing::get, serve, Router}; |
| 5 | +use hyper::header::ACCEPT; |
| 6 | +use hyper::header::CONTENT_TYPE; |
| 7 | +use hyper::Request; |
| 8 | +use hyper::StatusCode; |
| 9 | +use redb::{Database, ReadableTable, TableDefinition}; |
| 10 | +use serde::{Deserialize, Serialize}; |
| 11 | +use std::fs; |
| 12 | +use std::net::SocketAddr; |
| 13 | +use std::path::Path; |
| 14 | +use std::sync::Arc; |
| 15 | +use tokio::signal::unix::{signal, SignalKind}; |
| 16 | +use tokio::{net::TcpListener, sync::mpsc}; |
| 17 | +use tower::ServiceBuilder; |
| 18 | + |
| 19 | +static GLOBALS: TableDefinition<u64, u64> = TableDefinition::new("globals"); |
| 20 | + |
| 21 | +#[derive(Clone)] |
| 22 | +struct BrowserFriendlyJson { |
| 23 | + data: String, |
| 24 | +} |
| 25 | + |
| 26 | +struct JsonHtmlTemplate<'a>(&'a str, &'a str); |
| 27 | + |
| 28 | +impl IntoResponse for BrowserFriendlyJson { |
| 29 | + fn into_response(self) -> Response { |
| 30 | + let mut response = StatusCode::NOT_IMPLEMENTED.into_response(); |
| 31 | + response.extensions_mut().insert(self); |
| 32 | + response |
| 33 | + } |
| 34 | +} |
| 35 | + |
| 36 | +const fn find_split_position(bytes: &[u8]) -> usize { |
| 37 | + let mut i = 0; |
| 38 | + while i < bytes.len() && (bytes[i] != b'{' || bytes[i + 1] != b'}') { |
| 39 | + i += 1; |
| 40 | + } |
| 41 | + i |
| 42 | + // TODO(dkorolev): Panic if did not find `{}` or if found more than one `{}`. |
| 43 | + // NOTE(dkorolev): Why not create the split `str` slice at compile time, huh? |
| 44 | +} |
| 45 | + |
| 46 | +static JSON_TEMPLATE_HTML: &[u8] = include_bytes!("jsontemplate.html"); |
| 47 | +static JSON_TEMPLATE_HTML_SPLIT_IDX: usize = find_split_position(&JSON_TEMPLATE_HTML); |
| 48 | + |
| 49 | +#[derive(Serialize, Deserialize, Debug)] |
| 50 | +#[serde(tag = "type")] |
| 51 | +enum JSONResponse { |
| 52 | + Point { x: i32, y: i32 }, |
| 53 | + Message { text: String }, |
| 54 | + Counters { counter_runs: u64, counter_requests: u64 }, |
| 55 | +} |
| 56 | + |
| 57 | +fn create_response<S: Into<String>>(content_type: &str, body: S) -> Response<Body> { |
| 58 | + Response::builder().status(StatusCode::OK).header(CONTENT_TYPE, content_type).body(Body::from(body.into())).unwrap() |
| 59 | +} |
| 60 | + |
| 61 | +async fn browser_json_renderer(request: Request<Body>, next: Next, tmpl: Arc<JsonHtmlTemplate<'_>>) -> Response { |
| 62 | + // TODO(dkorolev): Can this be more Rusty? |
| 63 | + let mut accept_html = false; |
| 64 | + request.headers().get(&ACCEPT).map(|value| { |
| 65 | + let s = std::str::from_utf8(value.as_ref()).unwrap(); |
| 66 | + s.split(',').for_each(|value| { |
| 67 | + if value == "text/html" || value == "html" { |
| 68 | + accept_html = true; |
| 69 | + } |
| 70 | + }) |
| 71 | + }); |
| 72 | + |
| 73 | + // NOTE(dkorolev): I could not put the above logic to inside after `if let`, although, clearly it should be there. |
| 74 | + let mut response = next.run(request).await; |
| 75 | + if let Some(my_data) = response.extensions_mut().remove::<BrowserFriendlyJson>() { |
| 76 | + if accept_html { |
| 77 | + return create_response("text/html", format!("{}{}{}", tmpl.0, my_data.data, tmpl.1)); |
| 78 | + } else { |
| 79 | + return create_response("application/json", my_data.data); |
| 80 | + } |
| 81 | + } |
| 82 | + |
| 83 | + response |
| 84 | +} |
| 85 | + |
| 86 | +#[tokio::main] |
| 87 | +async fn main() -> Result<(), Box<dyn std::error::Error>> { |
| 88 | + fs::create_dir_all(&Path::new("./.db"))?; |
| 89 | + let redb = Database::create("./.db/demo.redb")?; |
| 90 | + run_main(&redb, inc_counter(&redb).await?).await; |
| 91 | + Ok(()) |
| 92 | +} |
| 93 | + |
| 94 | +async fn inc_counter(redb: &Database) -> Result<u64, Box<dyn std::error::Error>> { |
| 95 | + let mut counter_runs: u64 = 0; |
| 96 | + let txn = redb.begin_write()?; |
| 97 | + { |
| 98 | + let mut table = txn.open_table(GLOBALS)?; |
| 99 | + if let Some(value) = table.get(&1)? { |
| 100 | + counter_runs = value.value(); |
| 101 | + } |
| 102 | + counter_runs += 1; |
| 103 | + println!("Run counter in the DB: {}", counter_runs); |
| 104 | + table.insert(&1, &counter_runs)?; |
| 105 | + } |
| 106 | + txn.commit()?; |
| 107 | + Ok(counter_runs) |
| 108 | +} |
| 109 | + |
| 110 | +async fn run_main(_redb: &Database, counter_runs: u64) { |
| 111 | + // NOTE(dkorolev): Can this be done at compile time? |
| 112 | + let html_template = Arc::new(JsonHtmlTemplate( |
| 113 | + std::str::from_utf8(&JSON_TEMPLATE_HTML[0..JSON_TEMPLATE_HTML_SPLIT_IDX]).expect("NON-UTF8 TEMPLATE"), |
| 114 | + std::str::from_utf8(&JSON_TEMPLATE_HTML[(JSON_TEMPLATE_HTML_SPLIT_IDX + 2)..]).expect("NON-UTF8 TEMPLATE"), |
| 115 | + )); |
| 116 | + |
| 117 | + let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1); |
| 118 | + |
| 119 | + let counter_runs = Arc::new(counter_runs); |
| 120 | + |
| 121 | + let app = Router::new() |
| 122 | + .route("/healthz", get(|| async { "OK\n" })) |
| 123 | + .route("/", get(|| async { "hello this is a rust http server\n" })) |
| 124 | + .route( |
| 125 | + "/quit", |
| 126 | + get({ |
| 127 | + let shutdown_tx = shutdown_tx.clone(); |
| 128 | + || async move { |
| 129 | + let _ = shutdown_tx.send(()).await; |
| 130 | + "yes i am shutting down\n" |
| 131 | + } |
| 132 | + }), |
| 133 | + ) |
| 134 | + .route( |
| 135 | + "/json", |
| 136 | + get({ |
| 137 | + let counter_runs = Box::new(*counter_runs); |
| 138 | + let cnt_requests = Box::new(42); // NOT IMPLEMENTED YET |
| 139 | + || async move { |
| 140 | + let response = JSONResponse::Counters { counter_runs: *counter_runs, counter_requests: *cnt_requests }; |
| 141 | + BrowserFriendlyJson { data: serde_json::to_string(&response).unwrap() } |
| 142 | + } |
| 143 | + }), |
| 144 | + ) |
| 145 | + .layer(ServiceBuilder::new().layer(from_fn({ |
| 146 | + // TODO(dkorolev): Can I just move the `html_template` into `browser_json_renderer`? |
| 147 | + let html_template = Arc::clone(&html_template); |
| 148 | + move |req, next| browser_json_renderer(req, next, Arc::clone(&html_template)) |
| 149 | + }))); |
| 150 | + |
| 151 | + let addr = SocketAddr::from(([0, 0, 0, 0], 3000)); |
| 152 | + let listener = TcpListener::bind(addr).await.unwrap(); |
| 153 | + |
| 154 | + println!("rust http server ready on {}", addr); |
| 155 | + |
| 156 | + let server = serve(listener, app); |
| 157 | + |
| 158 | + let mut term_signal = signal(SignalKind::terminate()).expect("failed to register SIGTERM handler"); |
| 159 | + let mut int_signal = signal(SignalKind::interrupt()).expect("failed to register SIGINT handler"); |
| 160 | + |
| 161 | + tokio::select! { |
| 162 | + _ = server.with_graceful_shutdown(async move { shutdown_rx.recv().await; }) => { println! ("done"); } |
| 163 | + _ = tokio::signal::ctrl_c() => { println!("terminating due to Ctrl+C"); } |
| 164 | + _ = term_signal.recv() => { println!("terminating due to SIGTERM"); } |
| 165 | + _ = int_signal.recv() => { println!("terminating due to SIGINT"); } |
| 166 | + } |
| 167 | + |
| 168 | + println!("rust http server down"); |
| 169 | +} |
0 commit comments