an http based roguelike rpg about jelqing
| 1 | use axum::http::StatusCode; |
| 2 | use serde::Deserialize; |
| 3 | use serde_json::{Value, json}; |
| 4 | |
| 5 | use super::{ |
| 6 | Grip, Outcome, Run, |
| 7 | balance::{EXAGGERATION, PUT_GAIN}, |
| 8 | etag_matches, honest, |
| 9 | }; |
| 10 | use crate::problem::Problem; |
| 11 | |
| 12 | #[derive(Deserialize)] |
| 13 | pub struct PatchOp { |
| 14 | pub op: String, |
| 15 | pub path: String, |
| 16 | #[serde(default)] |
| 17 | pub value: Value, |
| 18 | } |
| 19 | |
| 20 | pub(super) fn exaggeration() -> Problem { |
| 21 | Problem::new( |
| 22 | StatusCode::BAD_REQUEST, |
| 23 | "exaggeration", |
| 24 | "The server has measured. Lying is not a technique.", |
| 25 | ) |
| 26 | } |
| 27 | |
| 28 | impl Run { |
| 29 | pub fn put(&mut self) -> Outcome { |
| 30 | if self.put_applied { |
| 31 | return Outcome::empty(StatusCode::NO_CONTENT); |
| 32 | } |
| 33 | self.put_applied = true; |
| 34 | self.length += PUT_GAIN; |
| 35 | self.touch(); |
| 36 | Outcome::json(StatusCode::OK, json!({ "detail": "Applied." })) |
| 37 | } |
| 38 | |
| 39 | pub fn patch(&mut self, ops: &[PatchOp]) -> Result<Outcome, Problem> { |
| 40 | for op in ops { |
| 41 | if op.op != "test" { |
| 42 | return Err(Problem::new( |
| 43 | StatusCode::UNPROCESSABLE_ENTITY, |
| 44 | "wishful-thinking", |
| 45 | "The server accepts only the truth.", |
| 46 | ) |
| 47 | .with("op", op.op.clone())); |
| 48 | } |
| 49 | let actual = match op.path.as_str() { |
| 50 | "/length" => self.current_length(), |
| 51 | "/girth" => self.girth, |
| 52 | "/foreskin" => self.foreskin.unwrap_or(0.0), |
| 53 | _ => { |
| 54 | return Err(Problem::new( |
| 55 | StatusCode::UNPROCESSABLE_ENTITY, |
| 56 | "no-such-measurement", |
| 57 | "There is no such measurement.", |
| 58 | ) |
| 59 | .with("path", op.path.clone())); |
| 60 | } |
| 61 | }; |
| 62 | let Some(claimed) = op.value.as_f64() else { |
| 63 | return Err(Problem::new( |
| 64 | StatusCode::BAD_REQUEST, |
| 65 | "malformed-patch", |
| 66 | "A test requires a numeric value.", |
| 67 | )); |
| 68 | }; |
| 69 | if op.path == "/length" && claimed >= actual + EXAGGERATION { |
| 70 | self.shame += 2; |
| 71 | self.touch(); |
| 72 | return Err(exaggeration()); |
| 73 | } |
| 74 | if !honest(claimed, actual) { |
| 75 | self.shame += 1; |
| 76 | self.touch(); |
| 77 | return Err( |
| 78 | Problem::new(StatusCode::CONFLICT, "test-failed", "The test failed.") |
| 79 | .with("path", op.path.clone()), |
| 80 | ); |
| 81 | } |
| 82 | } |
| 83 | Ok(Outcome::json( |
| 84 | StatusCode::OK, |
| 85 | json!({ "detail": "Correct." }), |
| 86 | )) |
| 87 | } |
| 88 | |
| 89 | pub fn delete_foreskin(&mut self, if_match: Option<&str>) -> Result<Outcome, Problem> { |
| 90 | if self.foreskin.is_none() { |
| 91 | return Err(Problem::new( |
| 92 | StatusCode::GONE, |
| 93 | "foreskin-gone", |
| 94 | "The foreskin is gone. It will not be returning.", |
| 95 | )); |
| 96 | } |
| 97 | let Some(if_match) = if_match else { |
| 98 | return Err(Problem::new( |
| 99 | StatusCode::PRECONDITION_REQUIRED, |
| 100 | "confirmation-required", |
| 101 | "The server requires you to be sure. Send If-Match.", |
| 102 | )); |
| 103 | }; |
| 104 | if !etag_matches(if_match, &self.etag()) { |
| 105 | return Err(Problem::new( |
| 106 | StatusCode::PRECONDITION_FAILED, |
| 107 | "stale-foreskin", |
| 108 | "The foreskin has changed since you last looked at it.", |
| 109 | )); |
| 110 | } |
| 111 | self.foreskin = None; |
| 112 | self.shame += 20; |
| 113 | self.touch(); |
| 114 | Ok(Outcome::empty(StatusCode::NO_CONTENT)) |
| 115 | } |
| 116 | |
| 117 | pub fn set_grip(&mut self, grip: Grip) { |
| 118 | self.grip = grip; |
| 119 | self.touch(); |
| 120 | } |
| 121 | } |