atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
9683736a2c5e4af0c861af0df5b33ec31e26cefa
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/resource.rsRust141 lines4.4 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
pub struct Verb<T: 'static> {
19
    method: &'static str,
20
    handle: Handler<T>,
21
    guard: Option<Guard<T>>,
22
}
23
24
impl<T> Verb<T> {
25
    pub const fn new(method: &'static str, handle: Handler<T>) -> Self {
26
        Self {
27
            method,
28
            handle,
29
            guard: None,
30
        }
31
    }
32
33
    /// Makes the method available only while `available` holds, refusing with `refusal` otherwise.
34
    pub const fn only_when(self, available: fn(&T) -> bool, refusal: &'static str) -> Self {
35
        Self {
36
            guard: Some((available, refusal)),
37
            ..self
38
        }
39
    }
40
41
    fn available(&self, target: &T) -> bool {
42
        self.guard.is_none_or(|(available, _)| available(target))
43
    }
44
}
45
46
/// The methods a resource supports. `Allow`, `OPTIONS` and `405` are all derived from this list.
47
pub struct Resource<T: 'static> {
48
    pub verbs: &'static [Verb<T>],
49
    pub accept_patch: bool,
50
}
51
52
impl<T> Resource<T> {
53
    pub const fn new(verbs: &'static [Verb<T>]) -> Self {
54
        Self {
55
            verbs,
56
            accept_patch: false,
57
        }
58
    }
59
60
    pub fn allow(&self, target: &T) -> String {
61
        let mut methods = Vec::new();
62
        for verb in self.verbs.iter().filter(|verb| verb.available(target)) {
63
            methods.push(verb.method);
64
            if verb.method == "GET" {
65
                methods.push("HEAD");
66
            }
67
        }
68
        methods.push("OPTIONS");
69
        methods.join(", ")
70
    }
71
72
    pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response {
73
        let requested = ctx.method.as_str();
74
        let lookup = if requested == "HEAD" {
75
            "GET"
76
        } else {
77
            requested
78
        };
79
        match self.verbs.iter().find(|verb| verb.method == lookup) {
80
            Some(verb) if verb.available(target) => (verb.handle)(target, ctx),
81
            Some(verb) => {
82
                let refusal = verb.guard.map_or("", |(_, refusal)| refusal);
83
                method_not_allowed(refusal.to_owned(), &self.allow(target))
84
            }
85
            None if requested == "OPTIONS" => options(&self.allow(target), self.accept_patch),
86
            None => unsupported(&ctx.method, &self.allow(target)),
87
        }
88
    }
89
}
90
91
fn method_not_allowed(detail: String, allow: &str) -> Response {
92
    Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail)
93
        .header(header::ALLOW, allow)
94
        .into_response()
95
}
96
97
pub fn unsupported(method: &Method, allow: &str) -> Response {
98
    let name = method.as_str();
99
    match name {
100
        "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(),
101
        "BREW" => Problem::new(
102
            StatusCode::IM_A_TEAPOT,
103
            "brew",
104
            "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.",
105
        )
106
        .into_response(),
107
        "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ" => {
108
            method_not_allowed(format!("{method} is not supported here."), allow)
109
        }
110
        _ if name.eq_ignore_ascii_case("jelq") => Problem::new(
111
            StatusCode::NOT_IMPLEMENTED,
112
            "not-implemented",
113
            format!(
114
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
115
            ),
116
        )
117
        .into_response(),
118
        _ => Problem::new(
119
            StatusCode::NOT_IMPLEMENTED,
120
            "not-implemented",
121
            format!("The server does not implement {method}."),
122
        )
123
        .into_response(),
124
    }
125
}
126
127
fn options(allow: &str, accept_patch: bool) -> Response {
128
    let mut response = (
129
        StatusCode::OK,
130
        [(header::ALLOW, allow)],
131
        Json(json!({ "detail": THERAPY })),
132
    )
133
        .into_response();
134
    if accept_patch {
135
        response.headers_mut().insert(
136
            ACCEPT_PATCH,
137
            HeaderValue::from_static("application/json-patch+json"),
138
        );
139
    }
140
    response
141
}