atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
8b8e83329a1daec70177de3e129964094a711509
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/resource.rsRust172 lines5.2 KB
1
use axum::{
2
    Json,
3
    http::{HeaderValue, Method, StatusCode, header},
4
    response::{IntoResponse, Response},
5
};
6
use serde_json::json;
7
8
use super::ctx::Ctx;
9
use crate::{headers::ACCEPT_PATCH, 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
    accept_patch: bool,
66
}
67
68
impl<T> Resource<T> {
69
    pub const fn new(verbs: &'static [Verb<T>]) -> Self {
70
        Self {
71
            verbs,
72
            accept_patch: false,
73
        }
74
    }
75
76
    pub const fn patchable(verbs: &'static [Verb<T>]) -> Self {
77
        Self {
78
            verbs,
79
            accept_patch: true,
80
        }
81
    }
82
83
    pub fn allow(&self, target: &T) -> String {
84
        Self::listed(self.verbs.iter().filter(|verb| verb.available(target)))
85
    }
86
87
    /// The methods with every guard assumed open, for resources that have none.
88
    pub fn allow_any(&self) -> String {
89
        Self::listed(self.verbs.iter())
90
    }
91
92
    fn listed<'a>(verbs: impl Iterator<Item = &'a Verb<T>>) -> String {
93
        let mut methods = Vec::new();
94
        for verb in verbs {
95
            methods.push(verb.method);
96
            if verb.method == "GET" {
97
                methods.push("HEAD");
98
            }
99
        }
100
        methods.push("OPTIONS");
101
        methods.join(", ")
102
    }
103
104
    pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response {
105
        let requested = ctx.method().as_str();
106
        let lookup = if requested == "HEAD" {
107
            "GET"
108
        } else {
109
            requested
110
        };
111
        match self.verbs.iter().find(|verb| verb.method == lookup) {
112
            Some(verb) if verb.available(target) => (verb.handle)(target, ctx),
113
            Some(verb) => {
114
                let refusal = verb.guard.map_or("", |(_, refusal)| refusal);
115
                method_not_allowed(refusal.to_owned(), &self.allow(target))
116
            }
117
            None if requested == "OPTIONS" => options(&self.allow(target), self.accept_patch),
118
            None => unsupported(ctx.method(), &self.allow(target)),
119
        }
120
    }
121
}
122
123
fn method_not_allowed(detail: String, allow: &str) -> Response {
124
    Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail)
125
        .header(header::ALLOW, allow)
126
        .into_response()
127
}
128
129
pub fn unsupported(method: &Method, allow: &str) -> Response {
130
    let name = method.as_str();
131
    match name {
132
        "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(),
133
        "BREW" => Problem::new(
134
            StatusCode::IM_A_TEAPOT,
135
            "brew",
136
            "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
137
        )
138
        .into_response(),
139
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ"
140
        | "HOLD" => method_not_allowed(format!("{method} is not supported here."), allow),
141
        _ if name.eq_ignore_ascii_case("jelq") => Problem::new(
142
            StatusCode::NOT_IMPLEMENTED,
143
            "not-implemented",
144
            format!(
145
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
146
            ),
147
        )
148
        .into_response(),
149
        _ => Problem::new(
150
            StatusCode::NOT_IMPLEMENTED,
151
            "not-implemented",
152
            format!("The server does not implement {method}."),
153
        )
154
        .into_response(),
155
    }
156
}
157
158
fn options(allow: &str, accept_patch: bool) -> Response {
159
    let mut response = (
160
        StatusCode::OK,
161
        [(header::ALLOW, allow)],
162
        Json(json!({ "detail": THERAPY })),
163
    )
164
        .into_response();
165
    if accept_patch {
166
        response.headers_mut().insert(
167
            ACCEPT_PATCH,
168
            HeaderValue::from_static("application/json-patch+json"),
169
        );
170
    }
171
    response
172
}