atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
585e6b7ce08636105da83cdbed5ff24679d0db4d
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
782069860236atagen2026-10-10 12:44 UTC18
pub struct Verb<T: 'static> {
782069860236atagen2026-10-10 12:44 UTC19
    method: &'static str,
782069860236atagen2026-10-10 12:44 UTC20
    handle: Handler<T>,
782069860236atagen2026-10-10 12:44 UTC21
    guard: Option<Guard<T>>,
782069860236atagen2026-10-10 12:44 UTC22
}
782069860236atagen2026-10-10 12:44 UTC23
782069860236atagen2026-10-10 12:44 UTC24
impl<T> Verb<T> {
782069860236atagen2026-10-10 12:44 UTC25
    pub const fn new(method: &'static str, handle: Handler<T>) -> Self {
782069860236atagen2026-10-10 12:44 UTC26
        Self {
782069860236atagen2026-10-10 12:44 UTC27
            method,
782069860236atagen2026-10-10 12:44 UTC28
            handle,
782069860236atagen2026-10-10 12:44 UTC29
            guard: None,
782069860236atagen2026-10-10 12:44 UTC30
        }
782069860236atagen2026-10-10 12:44 UTC31
    }
782069860236atagen2026-10-10 12:44 UTC32
782069860236atagen2026-10-10 12:44 UTC33
    /// Makes the method available only while `available` holds, refusing with `refusal` otherwise.
782069860236atagen2026-10-10 12:44 UTC34
    pub const fn only_when(self, available: fn(&T) -> bool, refusal: &'static str) -> Self {
782069860236atagen2026-10-10 12:44 UTC35
        Self {
782069860236atagen2026-10-10 12:44 UTC36
            guard: Some((available, refusal)),
782069860236atagen2026-10-10 12:44 UTC37
            ..self
782069860236atagen2026-10-10 12:44 UTC38
        }
782069860236atagen2026-10-10 12:44 UTC39
    }
782069860236atagen2026-10-10 12:44 UTC40
782069860236atagen2026-10-10 12:44 UTC41
    fn available(&self, target: &T) -> bool {
782069860236atagen2026-10-10 12:44 UTC42
        self.guard.is_none_or(|(available, _)| available(target))
782069860236atagen2026-10-10 12:44 UTC43
    }
782069860236atagen2026-10-10 12:44 UTC44
}
782069860236atagen2026-10-10 12:44 UTC45
782069860236atagen2026-10-10 12:44 UTC46
/// The methods a resource supports. `Allow`, `OPTIONS` and `405` are all derived from this list.
782069860236atagen2026-10-10 12:44 UTC47
pub struct Resource<T: 'static> {
782069860236atagen2026-10-10 12:44 UTC48
    pub verbs: &'static [Verb<T>],
782069860236atagen2026-10-10 12:44 UTC49
    pub accept_patch: bool,
782069860236atagen2026-10-10 12:44 UTC50
}
782069860236atagen2026-10-10 12:44 UTC51
782069860236atagen2026-10-10 12:44 UTC52
impl<T> Resource<T> {
782069860236atagen2026-10-10 12:44 UTC53
    pub const fn new(verbs: &'static [Verb<T>]) -> Self {
782069860236atagen2026-10-10 12:44 UTC54
        Self {
782069860236atagen2026-10-10 12:44 UTC55
            verbs,
782069860236atagen2026-10-10 12:44 UTC56
            accept_patch: false,
782069860236atagen2026-10-10 12:44 UTC57
        }
782069860236atagen2026-10-10 12:44 UTC58
    }
782069860236atagen2026-10-10 12:44 UTC59
782069860236atagen2026-10-10 12:44 UTC60
    pub fn allow(&self, target: &T) -> String {
782069860236atagen2026-10-10 12:44 UTC61
        let mut methods = Vec::new();
782069860236atagen2026-10-10 12:44 UTC62
        for verb in self.verbs.iter().filter(|verb| verb.available(target)) {
782069860236atagen2026-10-10 12:44 UTC63
            methods.push(verb.method);
782069860236atagen2026-10-10 12:44 UTC64
            if verb.method == "GET" {
782069860236atagen2026-10-10 12:44 UTC65
                methods.push("HEAD");
782069860236atagen2026-10-10 12:44 UTC66
            }
782069860236atagen2026-10-10 12:44 UTC67
        }
782069860236atagen2026-10-10 12:44 UTC68
        methods.push("OPTIONS");
782069860236atagen2026-10-10 12:44 UTC69
        methods.join(", ")
782069860236atagen2026-10-10 12:44 UTC70
    }
782069860236atagen2026-10-10 12:44 UTC71
782069860236atagen2026-10-10 12:44 UTC72
    pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response {
782069860236atagen2026-10-10 12:44 UTC73
        let requested = ctx.method.as_str();
782069860236atagen2026-10-10 12:44 UTC74
        let lookup = if requested == "HEAD" {
782069860236atagen2026-10-10 12:44 UTC75
            "GET"
782069860236atagen2026-10-10 12:44 UTC76
        } else {
782069860236atagen2026-10-10 12:44 UTC77
            requested
782069860236atagen2026-10-10 12:44 UTC78
        };
782069860236atagen2026-10-10 12:44 UTC79
        match self.verbs.iter().find(|verb| verb.method == lookup) {
782069860236atagen2026-10-10 12:44 UTC80
            Some(verb) if verb.available(target) => (verb.handle)(target, ctx),
782069860236atagen2026-10-10 12:44 UTC81
            Some(verb) => {
782069860236atagen2026-10-10 12:44 UTC82
                let refusal = verb.guard.map_or("", |(_, refusal)| refusal);
782069860236atagen2026-10-10 12:44 UTC83
                method_not_allowed(refusal.to_owned(), &self.allow(target))
782069860236atagen2026-10-10 12:44 UTC84
            }
782069860236atagen2026-10-10 12:44 UTC85
            None if requested == "OPTIONS" => options(&self.allow(target), self.accept_patch),
782069860236atagen2026-10-10 12:44 UTC86
            None => unsupported(&ctx.method, &self.allow(target)),
782069860236atagen2026-10-10 12:44 UTC87
        }
782069860236atagen2026-10-10 12:44 UTC88
    }
782069860236atagen2026-10-10 12:44 UTC89
}
782069860236atagen2026-10-10 12:44 UTC90
782069860236atagen2026-10-10 12:44 UTC91
fn method_not_allowed(detail: String, allow: &str) -> Response {
782069860236atagen2026-10-10 12:44 UTC92
    Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail)
782069860236atagen2026-10-10 12:44 UTC93
        .header(header::ALLOW, allow)
782069860236atagen2026-10-10 12:44 UTC94
        .into_response()
782069860236atagen2026-10-10 12:44 UTC95
}
782069860236atagen2026-10-10 12:44 UTC96
782069860236atagen2026-10-10 12:44 UTC97
pub fn unsupported(method: &Method, allow: &str) -> Response {
782069860236atagen2026-10-10 12:44 UTC98
    let name = method.as_str();
782069860236atagen2026-10-10 12:44 UTC99
    match name {
782069860236atagen2026-10-10 12:44 UTC100
        "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(),
782069860236atagen2026-10-10 12:44 UTC101
        "BREW" => Problem::new(
782069860236atagen2026-10-10 12:44 UTC102
            StatusCode::IM_A_TEAPOT,
782069860236atagen2026-10-10 12:44 UTC103
            "brew",
782069860236atagen2026-10-10 12:44 UTC104
            "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
782069860236atagen2026-10-10 12:44 UTC105
        )
782069860236atagen2026-10-10 12:44 UTC106
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC107
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ" => {
782069860236atagen2026-10-10 12:44 UTC108
            method_not_allowed(format!("{method} is not supported here."), allow)
782069860236atagen2026-10-10 12:44 UTC109
        }
782069860236atagen2026-10-10 12:44 UTC110
        _ if name.eq_ignore_ascii_case("jelq") => Problem::new(
782069860236atagen2026-10-10 12:44 UTC111
            StatusCode::NOT_IMPLEMENTED,
782069860236atagen2026-10-10 12:44 UTC112
            "not-implemented",
782069860236atagen2026-10-10 12:44 UTC113
            format!(
782069860236atagen2026-10-10 12:44 UTC114
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
782069860236atagen2026-10-10 12:44 UTC115
            ),
782069860236atagen2026-10-10 12:44 UTC116
        )
782069860236atagen2026-10-10 12:44 UTC117
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC118
        _ => Problem::new(
782069860236atagen2026-10-10 12:44 UTC119
            StatusCode::NOT_IMPLEMENTED,
782069860236atagen2026-10-10 12:44 UTC120
            "not-implemented",
782069860236atagen2026-10-10 12:44 UTC121
            format!("The server does not implement {method}."),
782069860236atagen2026-10-10 12:44 UTC122
        )
782069860236atagen2026-10-10 12:44 UTC123
        .into_response(),
782069860236atagen2026-10-10 12:44 UTC124
    }
782069860236atagen2026-10-10 12:44 UTC125
}
782069860236atagen2026-10-10 12:44 UTC126
782069860236atagen2026-10-10 12:44 UTC127
fn options(allow: &str, accept_patch: bool) -> Response {
782069860236atagen2026-10-10 12:44 UTC128
    let mut response = (
782069860236atagen2026-10-10 12:44 UTC129
        StatusCode::OK,
782069860236atagen2026-10-10 12:44 UTC130
        [(header::ALLOW, allow)],
782069860236atagen2026-10-10 12:44 UTC131
        Json(json!({ "detail": THERAPY })),
782069860236atagen2026-10-10 12:44 UTC132
    )
782069860236atagen2026-10-10 12:44 UTC133
        .into_response();
782069860236atagen2026-10-10 12:44 UTC134
    if accept_patch {
782069860236atagen2026-10-10 12:44 UTC135
        response.headers_mut().insert(
782069860236atagen2026-10-10 12:44 UTC136
            ACCEPT_PATCH,
782069860236atagen2026-10-10 12:44 UTC137
            HeaderValue::from_static("application/json-patch+json"),
782069860236atagen2026-10-10 12:44 UTC138
        );
782069860236atagen2026-10-10 12:44 UTC139
    }
782069860236atagen2026-10-10 12:44 UTC140
    response
782069860236atagen2026-10-10 12:44 UTC141
}