atagen

jelq

public

an http based roguelike rpg about jelqing

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