atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
c4d70621d377d45e3d418ac9197443c1b5d36399
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/mod.rsRust134 lines4.1 KB
1
mod ctx;
2
mod meta;
3
mod middleware;
4
mod registry;
5
mod resource;
6
mod response;
7
mod run;
8
mod runs;
9
mod ui;
10
11
use std::sync::Arc;
12
13
use axum::{
14
    Router,
15
    extract::State,
16
    response::{Redirect, Response},
17
    routing::{MethodRouter, any},
18
};
19
20
use tower_http::catch_panic::CatchPanicLayer;
21
22
use crate::{config::Config, records::Records};
23
use ctx::Ctx;
24
use registry::Registry;
25
use resource::Resource;
26
use run::RunResource;
27
28
pub struct AppState {
29
    runs: Registry,
30
    records: Records,
31
    config: Config,
32
}
33
34
impl AppState {
35
    pub fn new(config: Config) -> Self {
36
        let records = match &config.db {
37
            Some(path) => Records::open(path).unwrap_or_else(|e| {
38
                panic!("the records at {} could not be opened: {e}", path.display())
39
            }),
40
            None => Records::in_memory(),
41
        };
42
        Self {
43
            runs: Registry::default(),
44
            records,
45
            config,
46
        }
47
    }
48
49
    #[cfg(test)]
50
    pub(crate) fn insert(&self, run: crate::game::Run) {
51
        self.runs.insert(run);
52
    }
53
}
54
55
impl Default for AppState {
56
    fn default() -> Self {
57
        Self::new(Config::default())
58
    }
59
}
60
61
type Shared = Arc<AppState>;
62
63
pub fn app(config: Config) -> Router {
64
    router(Arc::new(AppState::new(config)))
65
}
66
67
pub fn router(state: Shared) -> Router {
68
    let api = api(state);
69
    let ui = Router::new()
70
        .route("/ui", any(|| async { Redirect::permanent("/ui/") }))
71
        .route("/ui/", any(ui::page))
72
        .route("/ui/send", any(ui::send))
73
        .with_state(api.clone());
74
    api.merge(ui)
75
        .layer(CatchPanicLayer::custom(middleware::panicked))
76
}
77
78
fn global(resource: &'static Resource<Shared>) -> MethodRouter<Shared> {
79
    any(move |State(state): State<Shared>, ctx: Ctx| async move {
80
        offload(state, move |mut state| resource.dispatch(&mut state, &ctx)).await
81
    })
82
}
83
84
fn scoped(resource: &'static RunResource) -> MethodRouter<Shared> {
85
    any(move |State(state): State<Shared>, ctx: Ctx| async move {
86
        offload(state, move |state| run::serve(&state, ctx, resource)).await
87
    })
88
}
89
90
/// The model oracle blocks on HTTP for every roll, so its turns run on the blocking pool and
91
/// leave the async workers free. Dice are quick enough to roll in place.
92
async fn offload<F>(state: Shared, work: F) -> Response
93
where
94
    F: FnOnce(Shared) -> Response + Send + 'static,
95
{
96
    if !state.config.oracle.blocks() {
97
        return work(state);
98
    }
99
    tokio::task::spawn_blocking(move || work(state))
100
        .await
101
        .unwrap_or_else(|e| std::panic::resume_unwind(e.into_panic()))
102
}
103
104
fn api(state: Shared) -> Router {
105
    Router::new()
106
        .route("/", global(&meta::INDEX))
107
        .route("/runs", global(&meta::RUNS))
108
        .route("/runs/{id}", scoped(&runs::RUN))
109
        .route("/runs/{id}/length", scoped(&runs::LENGTH))
110
        .route("/runs/{id}/foreskin", scoped(&runs::FORESKIN))
111
        .route("/runs/{id}/inventory", scoped(&runs::INVENTORY))
112
        .route("/runs/{id}/inventory/{item}", scoped(&runs::ITEM))
113
        .route("/runs/{id}/techniques", scoped(&runs::TECHNIQUES))
114
        .route("/runs/{id}/drawer", scoped(&runs::DRAWER))
115
        .route("/runs/{id}/branches/{choice}", scoped(&runs::BRANCH))
116
        .route("/runs/{id}/kitchen", scoped(&runs::KITCHEN))
117
        .route("/runs/{id}/kitchen/watermelon", scoped(&runs::MELON))
118
        .route("/runs/{id}/re-read", scoped(&runs::REREAD))
119
        .route("/runs/{id}/floor", scoped(&runs::FLOOR))
120
        .route("/runs/{id}/shop", scoped(&runs::SHOP))
121
        .route("/runs/{id}/shop/{item}", scoped(&runs::BUY))
122
        .route("/runs/{id}/portal", scoped(&runs::PORTAL))
123
        .route("/runs/{id}/elder", scoped(&runs::ELDER))
124
        .route("/runs/{id}/grip", scoped(&runs::GRIP))
125
        .route("/runs/{id}/diagnosis", scoped(&runs::DIAGNOSIS))
126
        .route("/runs/{id}/footage", scoped(&runs::FOOTAGE))
127
        .route("/forum/threads/{year}", global(&meta::THREAD))
128
        .route("/graveyard", global(&meta::GRAVEYARD))
129
        .route("/leaderboard", global(&meta::LEADERBOARD))
130
        .fallback(meta::not_found)
131
        .layer(axum::middleware::from_fn(middleware::chrome))
132
        .layer(CatchPanicLayer::custom(middleware::panicked))
133
        .with_state(state)
134
}