atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
27256acf27800b63fd950b95ecc55b2a2486c1c5
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/resource.rsRust149 lines4.5 KB
1
use axum::{
2
    Json,
3
    http::{Method, StatusCode, header},
4
    response::{IntoResponse, Response},
5
};
6
use serde_json::json;
7
8
use super::ctx::Ctx;
9
use crate::problem::Problem;
10
11
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.";
12
13
pub type Handler<T> = fn(&mut T, &Ctx) -> Response;
14
15
/// A predicate for when a method is available, and the refusal when it is not.
16
type Guard<T> = (fn(&T) -> bool, &'static str);
17
18
fn never<T>(_: &T) -> bool {
19
    false
20
}
21
22
fn unanswered<T>(_: &mut T, _: &Ctx) -> Response {
23
    StatusCode::METHOD_NOT_ALLOWED.into_response()
24
}
25
26
pub struct Verb<T: 'static> {
27
    method: &'static str,
28
    handle: Handler<T>,
29
    guard: Option<Guard<T>>,
30
}
31
32
impl<T> Verb<T> {
33
    pub const fn new(method: &'static str, handle: Handler<T>) -> Self {
34
        Self {
35
            method,
36
            handle,
37
            guard: None,
38
        }
39
    }
40
41
    /// A method named only to be refused, so that its `405` can say why.
42
    pub const fn refused(method: &'static str, refusal: &'static str) -> Self {
43
        Self {
44
            method,
45
            handle: unanswered::<T>,
46
            guard: Some((never::<T>, refusal)),
47
        }
48
    }
49
50
    pub const fn only_when(self, available: fn(&T) -> bool, refusal: &'static str) -> Self {
51
        Self {
52
            guard: Some((available, refusal)),
53
            ..self
54
        }
55
    }
56
57
    fn available(&self, target: &T) -> bool {
58
        self.guard.is_none_or(|(available, _)| available(target))
59
    }
60
}
61
62
/// The methods a resource supports. `Allow`, `OPTIONS` and `405` are all derived from this list.
63
pub struct Resource<T: 'static> {
64
    verbs: &'static [Verb<T>],
65
}
66
67
impl<T> Resource<T> {
68
    pub const fn new(verbs: &'static [Verb<T>]) -> Self {
69
        Self { verbs }
70
    }
71
72
    pub fn allow(&self, target: &T) -> String {
73
        Self::listed(self.verbs.iter().filter(|verb| verb.available(target)))
74
    }
75
76
    fn listed<'a>(verbs: impl Iterator<Item = &'a Verb<T>>) -> String {
77
        let mut methods = Vec::new();
78
        for verb in verbs {
79
            methods.push(verb.method);
80
            if verb.method == "GET" {
81
                methods.push("HEAD");
82
            }
83
        }
84
        methods.push("OPTIONS");
85
        methods.join(", ")
86
    }
87
88
    pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response {
89
        let requested = ctx.method().as_str();
90
        let lookup = if requested == "HEAD" {
91
            "GET"
92
        } else {
93
            requested
94
        };
95
        match self.verbs.iter().find(|verb| verb.method == lookup) {
96
            Some(verb) if verb.available(target) => (verb.handle)(target, ctx),
97
            Some(verb) => {
98
                let refusal = verb.guard.map_or("", |(_, refusal)| refusal);
99
                method_not_allowed(refusal.to_owned(), &self.allow(target))
100
            }
101
            None if requested == "OPTIONS" => options(&self.allow(target)),
102
            None => unsupported(ctx.method(), &self.allow(target)),
103
        }
104
    }
105
}
106
107
fn method_not_allowed(detail: String, allow: &str) -> Response {
108
    Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail)
109
        .header(header::ALLOW, allow)
110
        .into_response()
111
}
112
113
pub fn unsupported(method: &Method, allow: &str) -> Response {
114
    let name = method.as_str();
115
    match name {
116
        "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(),
117
        "BREW" => Problem::new(
118
            StatusCode::IM_A_TEAPOT,
119
            "brew",
120
            "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
121
        )
122
        .into_response(),
123
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ"
124
        | "HOLD" => method_not_allowed(format!("{method} is not supported here."), allow),
125
        _ if name.eq_ignore_ascii_case("jelq") => Problem::new(
126
            StatusCode::NOT_IMPLEMENTED,
127
            "not-implemented",
128
            format!(
129
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
130
            ),
131
        )
132
        .into_response(),
133
        _ => Problem::new(
134
            StatusCode::NOT_IMPLEMENTED,
135
            "not-implemented",
136
            format!("The server does not implement {method}."),
137
        )
138
        .into_response(),
139
    }
140
}
141
142
fn options(allow: &str) -> Response {
143
    (
144
        StatusCode::OK,
145
        [(header::ALLOW, allow)],
146
        Json(json!({ "detail": THERAPY })),
147
    )
148
        .into_response()
149
}