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 { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 61 | let mut methods = Vec::new(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 62 | for verb in self.verbs.iter().filter(|verb| verb.available(target)) { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 63 | methods.push(verb.method); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 64 | if verb.method == "GET" { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 65 | methods.push("HEAD"); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 66 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 67 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 68 | methods.push("OPTIONS"); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 69 | methods.join(", ") |
782069860236 | atagen | 2026-10-10 12:44 UTC | 70 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 71 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 72 | pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 73 | let requested = ctx.method.as_str(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 74 | let lookup = if requested == "HEAD" { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 75 | "GET" |
782069860236 | atagen | 2026-10-10 12:44 UTC | 76 | } else { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 77 | requested |
782069860236 | atagen | 2026-10-10 12:44 UTC | 78 | }; |
782069860236 | atagen | 2026-10-10 12:44 UTC | 79 | match self.verbs.iter().find(|verb| verb.method == lookup) { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 80 | Some(verb) if verb.available(target) => (verb.handle)(target, ctx), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 81 | Some(verb) => { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 82 | let refusal = verb.guard.map_or("", |(_, refusal)| refusal); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 83 | method_not_allowed(refusal.to_owned(), &self.allow(target)) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 84 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 85 | None if requested == "OPTIONS" => options(&self.allow(target), self.accept_patch), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 86 | None => unsupported(&ctx.method, &self.allow(target)), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 87 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 88 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 89 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 90 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 91 | fn method_not_allowed(detail: String, allow: &str) -> Response { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 92 | Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 93 | .header(header::ALLOW, allow) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 94 | .into_response() |
782069860236 | atagen | 2026-10-10 12:44 UTC | 95 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 96 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 97 | pub fn unsupported(method: &Method, allow: &str) -> Response { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 98 | let name = method.as_str(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 99 | match name { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 100 | "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 101 | "BREW" => Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 102 | StatusCode::IM_A_TEAPOT, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 103 | "brew", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 104 | "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 | 105 | ) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 106 | .into_response(), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 107 | "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ" => { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 108 | method_not_allowed(format!("{method} is not supported here."), allow) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 109 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 110 | _ if name.eq_ignore_ascii_case("jelq") => Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 111 | StatusCode::NOT_IMPLEMENTED, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 112 | "not-implemented", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 113 | format!( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 114 | "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason." |
782069860236 | atagen | 2026-10-10 12:44 UTC | 115 | ), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 116 | ) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 117 | .into_response(), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 118 | _ => Problem::new( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 119 | StatusCode::NOT_IMPLEMENTED, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 120 | "not-implemented", |
782069860236 | atagen | 2026-10-10 12:44 UTC | 121 | format!("The server does not implement {method}."), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 122 | ) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 123 | .into_response(), |
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 | |
782069860236 | atagen | 2026-10-10 12:44 UTC | 127 | fn options(allow: &str, accept_patch: bool) -> Response { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 128 | let mut response = ( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 129 | StatusCode::OK, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 130 | [(header::ALLOW, allow)], |
782069860236 | atagen | 2026-10-10 12:44 UTC | 131 | Json(json!({ "detail": THERAPY })), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 132 | ) |
782069860236 | atagen | 2026-10-10 12:44 UTC | 133 | .into_response(); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 134 | if accept_patch { |
782069860236 | atagen | 2026-10-10 12:44 UTC | 135 | response.headers_mut().insert( |
782069860236 | atagen | 2026-10-10 12:44 UTC | 136 | ACCEPT_PATCH, |
782069860236 | atagen | 2026-10-10 12:44 UTC | 137 | HeaderValue::from_static("application/json-patch+json"), |
782069860236 | atagen | 2026-10-10 12:44 UTC | 138 | ); |
782069860236 | atagen | 2026-10-10 12:44 UTC | 139 | } |
782069860236 | atagen | 2026-10-10 12:44 UTC | 140 | response |
782069860236 | atagen | 2026-10-10 12:44 UTC | 141 | } |