atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
cddf59e37d5f7b2a7e0f8b644252196caf5a7e97
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/resource.rsRust167 lines5.0 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
    fn listed<'a>(verbs: impl Iterator<Item = &'a Verb<T>>) -> String {
88
        let mut methods = Vec::new();
89
        for verb in verbs {
90
            methods.push(verb.method);
91
            if verb.method == "GET" {
92
                methods.push("HEAD");
93
            }
94
        }
95
        methods.push("OPTIONS");
96
        methods.join(", ")
97
    }
98
99
    pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response {
100
        let requested = ctx.method().as_str();
101
        let lookup = if requested == "HEAD" {
102
            "GET"
103
        } else {
104
            requested
105
        };
106
        match self.verbs.iter().find(|verb| verb.method == lookup) {
107
            Some(verb) if verb.available(target) => (verb.handle)(target, ctx),
108
            Some(verb) => {
109
                let refusal = verb.guard.map_or("", |(_, refusal)| refusal);
110
                method_not_allowed(refusal.to_owned(), &self.allow(target))
111
            }
112
            None if requested == "OPTIONS" => options(&self.allow(target), self.accept_patch),
113
            None => unsupported(ctx.method(), &self.allow(target)),
114
        }
115
    }
116
}
117
118
fn method_not_allowed(detail: String, allow: &str) -> Response {
119
    Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail)
120
        .header(header::ALLOW, allow)
121
        .into_response()
122
}
123
124
pub fn unsupported(method: &Method, allow: &str) -> Response {
125
    let name = method.as_str();
126
    match name {
127
        "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(),
128
        "BREW" => Problem::new(
129
            StatusCode::IM_A_TEAPOT,
130
            "brew",
131
            "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
132
        )
133
        .into_response(),
134
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ"
135
        | "HOLD" => method_not_allowed(format!("{method} is not supported here."), allow),
136
        _ if name.eq_ignore_ascii_case("jelq") => Problem::new(
137
            StatusCode::NOT_IMPLEMENTED,
138
            "not-implemented",
139
            format!(
140
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
141
            ),
142
        )
143
        .into_response(),
144
        _ => Problem::new(
145
            StatusCode::NOT_IMPLEMENTED,
146
            "not-implemented",
147
            format!("The server does not implement {method}."),
148
        )
149
        .into_response(),
150
    }
151
}
152
153
fn options(allow: &str, accept_patch: bool) -> Response {
154
    let mut response = (
155
        StatusCode::OK,
156
        [(header::ALLOW, allow)],
157
        Json(json!({ "detail": THERAPY })),
158
    )
159
        .into_response();
160
    if accept_patch {
161
        response.headers_mut().insert(
162
            ACCEPT_PATCH,
163
            HeaderValue::from_static("application/json-patch+json"),
164
        );
165
    }
166
    response
167
}