atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
3ac624339c982c4129228c05cb5332f4597fc3f7
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,
782069860236atagen2026-10-10 12:44 UTC3
    http::{HeaderValue, 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;
782069860236atagen2026-10-10 12:44 UTC9
use crate::{headers::ACCEPT_PATCH, 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>],
8b8e83329a1datagen2026-10-12 12:01 UTC65
    accept_patch: bool,
782069860236atagen2026-10-10 12:44 UTC66
}
782069860236atagen2026-10-10 12:44 UTC67
782069860236atagen2026-10-10 12:44 UTC68
impl<T> Resource<T> {
782069860236atagen2026-10-10 12:44 UTC69
    pub const fn new(verbs: &'static [Verb<T>]) -> Self {
782069860236atagen2026-10-10 12:44 UTC70
        Self {
782069860236atagen2026-10-10 12:44 UTC71
            verbs,
782069860236atagen2026-10-10 12:44 UTC72
            accept_patch: false,
782069860236atagen2026-10-10 12:44 UTC73
        }
782069860236atagen2026-10-10 12:44 UTC74
    }
782069860236atagen2026-10-10 12:44 UTC75
8b8e83329a1datagen2026-10-12 12:01 UTC76
    pub const fn patchable(verbs: &'static [Verb<T>]) -> Self {
8b8e83329a1datagen2026-10-12 12:01 UTC77
        Self {
8b8e83329a1datagen2026-10-12 12:01 UTC78
            verbs,
8b8e83329a1datagen2026-10-12 12:01 UTC79
            accept_patch: true,
8b8e83329a1datagen2026-10-12 12:01 UTC80
        }
8b8e83329a1datagen2026-10-12 12:01 UTC81
    }
8b8e83329a1datagen2026-10-12 12:01 UTC82
782069860236atagen2026-10-10 12:44 UTC83
    pub fn allow(&self, target: &T) -> String {
d604371299fbatagen2026-10-10 13:54 UTC84
        Self::listed(self.verbs.iter().filter(|verb| verb.available(target)))
d604371299fbatagen2026-10-10 13:54 UTC85
    }
d604371299fbatagen2026-10-10 13:54 UTC86
d604371299fbatagen2026-10-10 13:54 UTC87
    fn listed<'a>(verbs: impl Iterator<Item = &'a Verb<T>>) -> String {
782069860236atagen2026-10-10 12:44 UTC88
        let mut methods = Vec::new();
d604371299fbatagen2026-10-10 13:54 UTC89
        for verb in verbs {
782069860236atagen2026-10-10 12:44 UTC90
            methods.push(verb.method);
782069860236atagen2026-10-10 12:44 UTC91
            if verb.method == "GET" {
782069860236atagen2026-10-10 12:44 UTC92
                methods.push("HEAD");
782069860236atagen2026-10-10 12:44 UTC93
            }
782069860236atagen2026-10-10 12:44 UTC94
        }
782069860236atagen2026-10-10 12:44 UTC95
        methods.push("OPTIONS");
782069860236atagen2026-10-10 12:44 UTC96
        methods.join(", ")
782069860236atagen2026-10-10 12:44 UTC97
    }
782069860236atagen2026-10-10 12:44 UTC98
782069860236atagen2026-10-10 12:44 UTC99
    pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response {
8b8e83329a1datagen2026-10-12 12:01 UTC100
        let requested = ctx.method().as_str();
782069860236atagen2026-10-10 12:44 UTC101
        let lookup = if requested == "HEAD" {
782069860236atagen2026-10-10 12:44 UTC102
            "GET"
782069860236atagen2026-10-10 12:44 UTC103
        } else {
782069860236atagen2026-10-10 12:44 UTC104
            requested
782069860236atagen2026-10-10 12:44 UTC105
        };
782069860236atagen2026-10-10 12:44 UTC106
        match self.verbs.iter().find(|verb| verb.method == lookup) {
782069860236atagen2026-10-10 12:44 UTC107
            Some(verb) if verb.available(target) => (verb.handle)(target, ctx),
782069860236atagen2026-10-10 12:44 UTC108
            Some(verb) => {
782069860236atagen2026-10-10 12:44 UTC109
                let refusal = verb.guard.map_or("", |(_, refusal)| refusal);
782069860236atagen2026-10-10 12:44 UTC110
                method_not_allowed(refusal.to_owned(), &self.allow(target))
782069860236atagen2026-10-10 12:44 UTC111
            }
782069860236atagen2026-10-10 12:44 UTC112
            None if requested == "OPTIONS" => options(&self.allow(target), self.accept_patch),
8b8e83329a1datagen2026-10-12 12:01 UTC113
            None => unsupported(ctx.method(), &self.allow(target)),
782069860236atagen2026-10-10 12:44 UTC114
        }
782069860236atagen2026-10-10 12:44 UTC115
    }
782069860236atagen2026-10-10 12:44 UTC116
}
782069860236atagen2026-10-10 12:44 UTC117
782069860236atagen2026-10-10 12:44 UTC118
fn method_not_allowed(detail: String, allow: &str) -> Response {
782069860236atagen2026-10-10 12:44 UTC119
    Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail)
782069860236atagen2026-10-10 12:44 UTC120
        .header(header::ALLOW, allow)
782069860236atagen2026-10-10 12:44 UTC121
        .into_response()
782069860236atagen2026-10-10 12:44 UTC122
}
782069860236atagen2026-10-10 12:44 UTC123
782069860236atagen2026-10-10 12:44 UTC124
pub fn unsupported(method: &Method, allow: &str) -> Response {
782069860236atagen2026-10-10 12:44 UTC125
    let name = method.as_str();
782069860236atagen2026-10-10 12:44 UTC126
    match name {
782069860236atagen2026-10-10 12:44 UTC127
        "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(),
782069860236atagen2026-10-10 12:44 UTC128
        "BREW" => Problem::new(
782069860236atagen2026-10-10 12:44 UTC129
            StatusCode::IM_A_TEAPOT,
782069860236atagen2026-10-10 12:44 UTC130
            "brew",
782069860236atagen2026-10-10 12:44 UTC131
            "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
782069860236atagen2026-10-10 12:44 UTC132
        )
782069860236atagen2026-10-10 12:44 UTC133
        .into_response(),
4347977b8192atagen2026-10-11 04:56 UTC134
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ"
4347977b8192atagen2026-10-11 04:56 UTC135
        | "HOLD" => method_not_allowed(format!("{method} is not supported here."), allow),
782069860236atagen2026-10-10 12:44 UTC136
        _ if name.eq_ignore_ascii_case("jelq") => Problem::new(
782069860236atagen2026-10-10 12:44 UTC137
            StatusCode::NOT_IMPLEMENTED,
782069860236atagen2026-10-10 12:44 UTC138
            "not-implemented",
782069860236atagen2026-10-10 12:44 UTC139
            format!(
782069860236atagen2026-10-10 12:44 UTC140
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
782069860236atagen2026-10-10 12:44 UTC141
            ),
782069860236atagen2026-10-10 12:44 UTC142
        )
782069860236atagen2026-10-10 12:44 UTC143
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC144
        _ => Problem::new(
782069860236atagen2026-10-10 12:44 UTC145
            StatusCode::NOT_IMPLEMENTED,
782069860236atagen2026-10-10 12:44 UTC146
            "not-implemented",
782069860236atagen2026-10-10 12:44 UTC147
            format!("The server does not implement {method}."),
782069860236atagen2026-10-10 12:44 UTC148
        )
782069860236atagen2026-10-10 12:44 UTC149
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC150
    }
782069860236atagen2026-10-10 12:44 UTC151
}
782069860236atagen2026-10-10 12:44 UTC152
782069860236atagen2026-10-10 12:44 UTC153
fn options(allow: &str, accept_patch: bool) -> Response {
782069860236atagen2026-10-10 12:44 UTC154
    let mut response = (
782069860236atagen2026-10-10 12:44 UTC155
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC156
        [(header::ALLOW, allow)],
782069860236atagen2026-10-10 12:44 UTC157
        Json(json!({ "detail": THERAPY })),
782069860236atagen2026-10-10 12:44 UTC158
    )
782069860236atagen2026-10-10 12:44 UTC159
        .into_response();
782069860236atagen2026-10-10 12:44 UTC160
    if accept_patch {
782069860236atagen2026-10-10 12:44 UTC161
        response.headers_mut().insert(
782069860236atagen2026-10-10 12:44 UTC162
            ACCEPT_PATCH,
782069860236atagen2026-10-10 12:44 UTC163
            HeaderValue::from_static("application/json-patch+json"),
782069860236atagen2026-10-10 12:44 UTC164
        );
782069860236atagen2026-10-10 12:44 UTC165
    }
782069860236atagen2026-10-10 12:44 UTC166
    response
782069860236atagen2026-10-10 12:44 UTC167
}