atagen

jelq

public

an http based roguelike rpg about jelqing

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