atagen

jelq

public

an http based roguelike rpg about jelqing

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