atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
e807266c323bcc1fd00f59aa160b3b94d8168beb
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/mod.rsRust120 lines3.6 KB
1
mod ctx;
2
mod meta;
3
mod middleware;
4
mod resource;
5
mod response;
6
mod run;
7
mod runs;
8
mod ui;
9
10
use std::{
11
    collections::HashMap,
12
    sync::{Arc, Mutex, MutexGuard, PoisonError},
13
};
14
15
use axum::{
16
    Router,
17
    extract::State,
18
    response::Redirect,
19
    routing::{MethodRouter, any},
20
};
21
22
use tower_http::catch_panic::CatchPanicLayer;
23
24
use crate::{config::Config, game::Run};
25
use ctx::Ctx;
26
use resource::Resource;
27
use run::RunResource;
28
29
pub struct AppState {
30
    runs: Mutex<HashMap<String, Run>>,
31
    config: Config,
32
}
33
34
impl AppState {
35
    pub fn new(config: Config) -> Self {
36
        Self {
37
            runs: Mutex::default(),
38
            config,
39
        }
40
    }
41
42
    /// A panic mid-request must not take every other run down with it, so a poisoned lock is recovered.
43
    fn runs(&self) -> MutexGuard<'_, HashMap<String, Run>> {
44
        self.runs.lock().unwrap_or_else(PoisonError::into_inner)
45
    }
46
47
    #[cfg(test)]
48
    pub(crate) fn insert(&self, run: Run) {
49
        self.runs().insert(run.id.clone(), run);
50
    }
51
}
52
53
impl Default for AppState {
54
    fn default() -> Self {
55
        Self::new(Config::default())
56
    }
57
}
58
59
type Shared = Arc<AppState>;
60
61
pub fn app(config: Config) -> Router {
62
    router(Arc::new(AppState::new(config)))
63
}
64
65
pub fn router(state: Shared) -> Router {
66
    let api = api(state);
67
    let ui = Router::new()
68
        .route("/ui", any(|| async { Redirect::permanent("/ui/") }))
69
        .route("/ui/", any(ui::page))
70
        .route("/ui/send", any(ui::send))
71
        .with_state(api.clone());
72
    api.merge(ui)
73
        .layer(CatchPanicLayer::custom(middleware::panicked))
74
}
75
76
fn global(resource: &'static Resource<Shared>) -> MethodRouter<Shared> {
77
    any(
78
        move |State(mut state): State<Shared>, ctx: Ctx| async move {
79
            resource.dispatch(&mut state, &ctx)
80
        },
81
    )
82
}
83
84
fn scoped(resource: &'static RunResource) -> MethodRouter<Shared> {
85
    any(
86
        move |State(state): State<Shared>, ctx: Ctx| async move { run::serve(&state, ctx, resource) },
87
    )
88
}
89
90
fn api(state: Shared) -> Router {
91
    Router::new()
92
        .route("/", global(&meta::INDEX))
93
        .route("/runs", global(&meta::RUNS))
94
        .route("/runs/{id}", scoped(&runs::RUN))
95
        .route("/runs/{id}/length", scoped(&runs::LENGTH))
96
        .route("/runs/{id}/foreskin", scoped(&runs::FORESKIN))
97
        .route("/runs/{id}/inventory", scoped(&runs::INVENTORY))
98
        .route("/runs/{id}/inventory/{item}", scoped(&runs::ITEM))
99
        .route("/runs/{id}/techniques", scoped(&runs::TECHNIQUES))
100
        .route("/runs/{id}/drawer", scoped(&runs::DRAWER))
101
        .route("/runs/{id}/branches/{choice}", scoped(&runs::BRANCH))
102
        .route("/runs/{id}/kitchen", scoped(&runs::KITCHEN))
103
        .route("/runs/{id}/kitchen/watermelon", scoped(&runs::MELON))
104
        .route("/runs/{id}/re-read", scoped(&runs::REREAD))
105
        .route("/runs/{id}/floor", scoped(&runs::FLOOR))
106
        .route("/runs/{id}/shop", scoped(&runs::SHOP))
107
        .route("/runs/{id}/shop/{item}", scoped(&runs::BUY))
108
        .route("/runs/{id}/portal", scoped(&runs::PORTAL))
109
        .route("/runs/{id}/elder", scoped(&runs::ELDER))
110
        .route("/runs/{id}/grip", scoped(&runs::GRIP))
111
        .route("/runs/{id}/diagnosis", scoped(&runs::DIAGNOSIS))
112
        .route("/runs/{id}/footage", scoped(&runs::FOOTAGE))
113
        .route("/forum/threads/{year}", global(&meta::THREAD))
114
        .route("/graveyard", global(&meta::GRAVEYARD))
115
        .route("/leaderboard", global(&meta::LEADERBOARD))
116
        .fallback(meta::not_found)
117
        .layer(axum::middleware::from_fn(middleware::chrome))
118
        .layer(CatchPanicLayer::custom(middleware::panicked))
119
        .with_state(state)
120
}