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