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 | fn never<T>(_: &T) -> bool { |
| 19 | false |
| 20 | } |
| 21 | |
| 22 | fn unanswered<T>(_: &mut T, _: &Ctx) -> Response { |
| 23 | StatusCode::METHOD_NOT_ALLOWED.into_response() |
| 24 | } |
| 25 | |
| 26 | pub struct Verb<T: 'static> { |
| 27 | method: &'static str, |
| 28 | handle: Handler<T>, |
| 29 | guard: Option<Guard<T>>, |
| 30 | } |
| 31 | |
| 32 | impl<T> Verb<T> { |
| 33 | pub const fn new(method: &'static str, handle: Handler<T>) -> Self { |
| 34 | Self { |
| 35 | method, |
| 36 | handle, |
| 37 | guard: None, |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | /// A method named only to be refused, so that its `405` can say why. |
| 42 | pub const fn refused(method: &'static str, refusal: &'static str) -> Self { |
| 43 | Self { |
| 44 | method, |
| 45 | handle: unanswered::<T>, |
| 46 | guard: Some((never::<T>, refusal)), |
| 47 | } |
| 48 | } |
| 49 | |
| 50 | pub const fn only_when(self, available: fn(&T) -> bool, refusal: &'static str) -> Self { |
| 51 | Self { |
| 52 | guard: Some((available, refusal)), |
| 53 | ..self |
| 54 | } |
| 55 | } |
| 56 | |
| 57 | fn available(&self, target: &T) -> bool { |
| 58 | self.guard.is_none_or(|(available, _)| available(target)) |
| 59 | } |
| 60 | } |
| 61 | |
| 62 | /// The methods a resource supports. `Allow`, `OPTIONS` and `405` are all derived from this list. |
| 63 | pub struct Resource<T: 'static> { |
| 64 | verbs: &'static [Verb<T>], |
| 65 | accept_patch: bool, |
| 66 | } |
| 67 | |
| 68 | impl<T> Resource<T> { |
| 69 | pub const fn new(verbs: &'static [Verb<T>]) -> Self { |
| 70 | Self { |
| 71 | verbs, |
| 72 | accept_patch: false, |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | pub const fn patchable(verbs: &'static [Verb<T>]) -> Self { |
| 77 | Self { |
| 78 | verbs, |
| 79 | accept_patch: true, |
| 80 | } |
| 81 | } |
| 82 | |
| 83 | pub fn allow(&self, target: &T) -> String { |
| 84 | Self::listed(self.verbs.iter().filter(|verb| verb.available(target))) |
| 85 | } |
| 86 | |
| 87 | /// The methods with every guard assumed open, for resources that have none. |
| 88 | pub fn allow_any(&self) -> String { |
| 89 | Self::listed(self.verbs.iter()) |
| 90 | } |
| 91 | |
| 92 | fn listed<'a>(verbs: impl Iterator<Item = &'a Verb<T>>) -> String { |
| 93 | let mut methods = Vec::new(); |
| 94 | for verb in verbs { |
| 95 | methods.push(verb.method); |
| 96 | if verb.method == "GET" { |
| 97 | methods.push("HEAD"); |
| 98 | } |
| 99 | } |
| 100 | methods.push("OPTIONS"); |
| 101 | methods.join(", ") |
| 102 | } |
| 103 | |
| 104 | pub fn dispatch(&self, target: &mut T, ctx: &Ctx) -> Response { |
| 105 | let requested = ctx.method().as_str(); |
| 106 | let lookup = if requested == "HEAD" { |
| 107 | "GET" |
| 108 | } else { |
| 109 | requested |
| 110 | }; |
| 111 | match self.verbs.iter().find(|verb| verb.method == lookup) { |
| 112 | Some(verb) if verb.available(target) => (verb.handle)(target, ctx), |
| 113 | Some(verb) => { |
| 114 | let refusal = verb.guard.map_or("", |(_, refusal)| refusal); |
| 115 | method_not_allowed(refusal.to_owned(), &self.allow(target)) |
| 116 | } |
| 117 | None if requested == "OPTIONS" => options(&self.allow(target), self.accept_patch), |
| 118 | None => unsupported(ctx.method(), &self.allow(target)), |
| 119 | } |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | fn method_not_allowed(detail: String, allow: &str) -> Response { |
| 124 | Problem::new(StatusCode::METHOD_NOT_ALLOWED, "method-not-allowed", detail) |
| 125 | .header(header::ALLOW, allow) |
| 126 | .into_response() |
| 127 | } |
| 128 | |
| 129 | pub fn unsupported(method: &Method, allow: &str) -> Response { |
| 130 | let name = method.as_str(); |
| 131 | match name { |
| 132 | "CONNECT" => StatusCode::NOT_IMPLEMENTED.into_response(), |
| 133 | "BREW" => Problem::new( |
| 134 | StatusCode::IM_A_TEAPOT, |
| 135 | "brew", |
| 136 | "The server is a teapot. RFC 2324 is clear on this. It is not yet short and stout.", |
| 137 | ) |
| 138 | .into_response(), |
| 139 | "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "TRACE" | "JELQ" |
| 140 | | "HOLD" => method_not_allowed(format!("{method} is not supported here."), allow), |
| 141 | _ if name.eq_ignore_ascii_case("jelq") => Problem::new( |
| 142 | StatusCode::NOT_IMPLEMENTED, |
| 143 | "not-implemented", |
| 144 | format!( |
| 145 | "The server does not implement {method}. Methods are case-sensitive. JELQ is shouted for a reason." |
| 146 | ), |
| 147 | ) |
| 148 | .into_response(), |
| 149 | _ => Problem::new( |
| 150 | StatusCode::NOT_IMPLEMENTED, |
| 151 | "not-implemented", |
| 152 | format!("The server does not implement {method}."), |
| 153 | ) |
| 154 | .into_response(), |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | fn options(allow: &str, accept_patch: bool) -> Response { |
| 159 | let mut response = ( |
| 160 | StatusCode::OK, |
| 161 | [(header::ALLOW, allow)], |
| 162 | Json(json!({ "detail": THERAPY })), |
| 163 | ) |
| 164 | .into_response(); |
| 165 | if accept_patch { |
| 166 | response.headers_mut().insert( |
| 167 | ACCEPT_PATCH, |
| 168 | HeaderValue::from_static("application/json-patch+json"), |
| 169 | ); |
| 170 | } |
| 171 | response |
| 172 | } |