atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
8599b930cebf9a1fb90b7538c44eab5649929cf9
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git

Blame: src/http/resource.rs

782069860236atagen2026-10-10 12:44 UTC1
use axum::{
782069860236atagen2026-10-10 12:44 UTC2
    Json,
6619c458c23eatagen2026-10-14 08:08 UTC3
    http::{Method, StatusCode, header},
782069860236atagen2026-10-10 12:44 UTC4
    response::{IntoResponse, Response},
782069860236atagen2026-10-10 12:44 UTC5
};
782069860236atagen2026-10-10 12:44 UTC6
use serde_json::json;
782069860236atagen2026-10-10 12:44 UTC7
782069860236atagen2026-10-10 12:44 UTC8
use super::ctx::Ctx;
6619c458c23eatagen2026-10-14 08:08 UTC9
use crate::problem::Problem;
782069860236atagen2026-10-10 12:44 UTC10
782069860236atagen2026-10-10 12:44 UTC11
const THERAPY: &str = "Your options are listed in the Allow header. There are more of them than you think. You could also try being honest. It is not in the Allow header because it is not a method.";
782069860236atagen2026-10-10 12:44 UTC12
782069860236atagen2026-10-10 12:44 UTC13
pub type Handler<T> = fn(&mut T, &Ctx) -> Response;
782069860236atagen2026-10-10 12:44 UTC14
782069860236atagen2026-10-10 12:44 UTC15
/// A predicate for when a method is available, and the refusal when it is not.
782069860236atagen2026-10-10 12:44 UTC16
type Guard<T> = (fn(&T) -> bool, &'static str);
782069860236atagen2026-10-10 12:44 UTC17
8b8e83329a1datagen2026-10-12 12:01 UTC18
fn never<T>(_: &T) -> bool {
8b8e83329a1datagen2026-10-12 12:01 UTC19
    false
8b8e83329a1datagen2026-10-12 12:01 UTC20
}
8b8e83329a1datagen2026-10-12 12:01 UTC21
8b8e83329a1datagen2026-10-12 12:01 UTC22
fn unanswered<T>(_: &mut T, _: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC23
    StatusCode::METHOD_NOT_ALLOWED.into_response()
8b8e83329a1datagen2026-10-12 12:01 UTC24
}
8b8e83329a1datagen2026-10-12 12:01 UTC25
782069860236atagen2026-10-10 12:44 UTC26
pub struct Verb<T: 'static> {
782069860236atagen2026-10-10 12:44 UTC27
    method: &'static str,
782069860236atagen2026-10-10 12:44 UTC28
    handle: Handler<T>,
782069860236atagen2026-10-10 12:44 UTC29
    guard: Option<Guard<T>>,
782069860236atagen2026-10-10 12:44 UTC30
}
782069860236atagen2026-10-10 12:44 UTC31
782069860236atagen2026-10-10 12:44 UTC32
impl<T> Verb<T> {
782069860236atagen2026-10-10 12:44 UTC33
    pub const fn new(method: &'static str, handle: Handler<T>) -> Self {
782069860236atagen2026-10-10 12:44 UTC34
        Self {
782069860236atagen2026-10-10 12:44 UTC35
            method,
782069860236atagen2026-10-10 12:44 UTC36
            handle,
782069860236atagen2026-10-10 12:44 UTC37
            guard: None,
782069860236atagen2026-10-10 12:44 UTC38
        }
782069860236atagen2026-10-10 12:44 UTC39
    }
782069860236atagen2026-10-10 12:44 UTC40
8b8e83329a1datagen2026-10-12 12:01 UTC41
    /// A method named only to be refused, so that its `405` can say why.
8b8e83329a1datagen2026-10-12 12:01 UTC42
    pub const fn refused(method: &'static str, refusal: &'static str) -> Self {
8b8e83329a1datagen2026-10-12 12:01 UTC43
        Self {
8b8e83329a1datagen2026-10-12 12:01 UTC44
            method,
8b8e83329a1datagen2026-10-12 12:01 UTC45
            handle: unanswered::<T>,
8b8e83329a1datagen2026-10-12 12:01 UTC46
            guard: Some((never::<T>, refusal)),
8b8e83329a1datagen2026-10-12 12:01 UTC47
        }
8b8e83329a1datagen2026-10-12 12:01 UTC48
    }
8b8e83329a1datagen2026-10-12 12:01 UTC49
782069860236atagen2026-10-10 12:44 UTC50
    pub const fn only_when(self, available: fn(&T) -> bool, refusal: &'static str) -> Self {
782069860236atagen2026-10-10 12:44 UTC51
        Self {
782069860236atagen2026-10-10 12:44 UTC52
            guard: Some((available, refusal)),
782069860236atagen2026-10-10 12:44 UTC53
            ..self
782069860236atagen2026-10-10 12:44 UTC54
        }
782069860236atagen2026-10-10 12:44 UTC55
    }
782069860236atagen2026-10-10 12:44 UTC56
782069860236atagen2026-10-10 12:44 UTC57
    fn available(&self, target: &T) -> bool {
782069860236atagen2026-10-10 12:44 UTC58
        self.guard.is_none_or(|(available, _)| available(target))
782069860236atagen2026-10-10 12:44 UTC59
    }
782069860236atagen2026-10-10 12:44 UTC60
}
782069860236atagen2026-10-10 12:44 UTC61
782069860236atagen2026-10-10 12:44 UTC62
/// The methods a resource supports. `Allow`, `OPTIONS` and `405` are all derived from this list.
782069860236atagen2026-10-10 12:44 UTC63
pub struct Resource<T: 'static> {
8b8e83329a1datagen2026-10-12 12:01 UTC64
    verbs: &'static [Verb<T>],
782069860236atagen2026-10-10 12:44 UTC65
}
782069860236atagen2026-10-10 12:44 UTC66
782069860236atagen2026-10-10 12:44 UTC67
impl<T> Resource<T> {
782069860236atagen2026-10-10 12:44 UTC68
    pub const fn new(verbs: &'static [Verb<T>]) -> Self {
6619c458c23eatagen2026-10-14 08:08 UTC69
        Self { verbs }
8b8e83329a1datagen2026-10-12 12:01 UTC70
    }
8b8e83329a1datagen2026-10-12 12:01 UTC71
782069860236atagen2026-10-10 12:44 UTC72
    pub fn allow(&self, target: &T) -> String {
d604371299fbatagen2026-10-10 13:54 UTC73
        Self::listed(self.verbs.iter().filter(|verb| verb.available(target)))
d604371299fbatagen2026-10-10 13:54 UTC74
    }
d604371299fbatagen2026-10-10 13:54 UTC75
d604371299fbatagen2026-10-10 13:54 UTC76
    fn listed<'a>(verbs: impl Iterator<Item = &'a Verb<T>>) -> String {
782069860236atagen2026-10-10 12:44 UTC77
        let mut methods = Vec::new();
d604371299fbatagen2026-10-10 13:54 UTC78
        for verb in verbs {
782069860236atagen2026-10-10 12:44 UTC79
            methods.push(verb.method);
782069860236atagen2026-10-10 12:44 UTC80
            if verb.method == "GET" {
782069860236atagen2026-10-10 12:44 UTC81
                methods.push("HEAD");
782069860236atagen2026-10-10 12:44 UTC82
            }
782069860236atagen2026-10-10 12:44 UTC83
        }
782069860236atagen2026-10-10 12:44 UTC84
        methods.push("OPTIONS");
782069860236atagen2026-10-10 12:44 UTC85
        methods.join(", ")
782069860236atagen2026-10-10 12:44 UTC86
    }
782069860236atagen2026-10-10 12:44 UTC87
782069860236atagen2026-10-10 12:44 UTC88
    pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC89
        let requested = ctx.method().as_str();
782069860236atagen2026-10-10 12:44 UTC90
        let lookup = if requested == "HEAD" {
782069860236atagen2026-10-10 12:44 UTC91
            "GET"
782069860236atagen2026-10-10 12:44 UTC92
        } else {
782069860236atagen2026-10-10 12:44 UTC93
            requested
782069860236atagen2026-10-10 12:44 UTC94
        };
782069860236atagen2026-10-10 12:44 UTC95
        match self.verbs.iter().find(|verb| verb.method == lookup) {
782069860236atagen2026-10-10 12:44 UTC96
            Some(verb) if verb.available(target) => (verb.handle)(target, ctx),
782069860236atagen2026-10-10 12:44 UTC97
            Some(verb) => {
782069860236atagen2026-10-10 12:44 UTC98
                let refusal = verb.guard.map_or("", |(_, refusal)| refusal);
782069860236atagen2026-10-10 12:44 UTC99
                method_not_allowed(refusal.to_owned(), &self.allow(target))
782069860236atagen2026-10-10 12:44 UTC100
            }
6619c458c23eatagen2026-10-14 08:08 UTC101
            None if requested == "OPTIONS" => options(&self.allow(target)),
8b8e83329a1datagen2026-10-12 12:01 UTC102
            None => unsupported(ctx.method(), &self.allow(target)),
782069860236atagen2026-10-10 12:44 UTC103
        }
782069860236atagen2026-10-10 12:44 UTC104
    }
782069860236atagen2026-10-10 12:44 UTC105
}
782069860236atagen2026-10-10 12:44 UTC106
782069860236atagen2026-10-10 12:44 UTC107
fn method_not_allowed(detail: String, allow: &str) -> Response {
782069860236atagen2026-10-10 12:44 UTC108
    Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail)
782069860236atagen2026-10-10 12:44 UTC109
        .header(header::ALLOW, allow)
782069860236atagen2026-10-10 12:44 UTC110
        .into_response()
782069860236atagen2026-10-10 12:44 UTC111
}
782069860236atagen2026-10-10 12:44 UTC112
782069860236atagen2026-10-10 12:44 UTC113
pub fn unsupported(method: &Method, allow: &str) -> Response {
782069860236atagen2026-10-10 12:44 UTC114
    let name = method.as_str();
782069860236atagen2026-10-10 12:44 UTC115
    match name {
782069860236atagen2026-10-10 12:44 UTC116
        "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(),
782069860236atagen2026-10-10 12:44 UTC117
        "BREW" => Problem::new(
782069860236atagen2026-10-10 12:44 UTC118
            StatusCode::IM_A_TEAPOT,
782069860236atagen2026-10-10 12:44 UTC119
            "brew",
782069860236atagen2026-10-10 12:44 UTC120
            "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
782069860236atagen2026-10-10 12:44 UTC121
        )
782069860236atagen2026-10-10 12:44 UTC122
        .into_response(),
4347977b8192atagen2026-10-11 04:56 UTC123
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ"
4347977b8192atagen2026-10-11 04:56 UTC124
        | "HOLD" => method_not_allowed(format!("{method} is not supported here."), allow),
782069860236atagen2026-10-10 12:44 UTC125
        _ if name.eq_ignore_ascii_case("jelq") => Problem::new(
782069860236atagen2026-10-10 12:44 UTC126
            StatusCode::NOT_IMPLEMENTED,
782069860236atagen2026-10-10 12:44 UTC127
            "not-implemented",
782069860236atagen2026-10-10 12:44 UTC128
            format!(
782069860236atagen2026-10-10 12:44 UTC129
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
782069860236atagen2026-10-10 12:44 UTC130
            ),
782069860236atagen2026-10-10 12:44 UTC131
        )
782069860236atagen2026-10-10 12:44 UTC132
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC133
        _ => Problem::new(
782069860236atagen2026-10-10 12:44 UTC134
            StatusCode::NOT_IMPLEMENTED,
782069860236atagen2026-10-10 12:44 UTC135
            "not-implemented",
782069860236atagen2026-10-10 12:44 UTC136
            format!("The server does not implement {method}."),
782069860236atagen2026-10-10 12:44 UTC137
        )
782069860236atagen2026-10-10 12:44 UTC138
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC139
    }
782069860236atagen2026-10-10 12:44 UTC140
}
782069860236atagen2026-10-10 12:44 UTC141
6619c458c23eatagen2026-10-14 08:08 UTC142
fn options(allow: &str) -> Response {
6619c458c23eatagen2026-10-14 08:08 UTC143
    (
782069860236atagen2026-10-10 12:44 UTC144
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC145
        [(header::ALLOW, allow)],
782069860236atagen2026-10-10 12:44 UTC146
        Json(json!({ "detail": THERAPY })),
782069860236atagen2026-10-10 12:44 UTC147
    )
6619c458c23eatagen2026-10-14 08:08 UTC148
        .into_response()
782069860236atagen2026-10-10 12:44 UTC149
}