atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0
159f0998263bdbc45fc96bfc40453947b3a44cc3
1
ssh://brewery@pls.lobotomise.me:33/atagen/jelq.git
src/http/resource.rsRust149 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
        | "HOLD" => method_not_allowed(format!("{method} is not supported here."), allow),
118
        _ if name.eq_ignore_ascii_case("jelq") => Problem::new(
119
            StatusCode::NOT_IMPLEMENTED,
120
            "not-implemented",
121
            format!(
122
                "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason."
123
            ),
124
        )
125
        .into_response(),
126
        _ => Problem::new(
127
            StatusCode::NOT_IMPLEMENTED,
128
            "not-implemented",
129
            format!("The server does not implement {method}."),
130
        )
131
        .into_response(),
132
    }
133
}
134
135
fn options(allow: &str, accept_patch: bool) -> Response {
136
    let mut response = (
137
        StatusCode::OK,
138
        [(header::ALLOW, allow)],
139
        Json(json!({ "detail": THERAPY })),
140
    )
141
        .into_response();
142
    if accept_patch {
143
        response.headers_mut().insert(
144
            ACCEPT_PATCH,
145
            HeaderValue::from_static("application/json-patch+json"),
146
        );
147
    }
148
    response
149
}