atagen

jelq

public

an http based roguelike rpg about jelqing

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