an http based roguelike rpg about jelqing
| 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 | } |