an http based roguelike rpg about jelqing
Commit 6619c458c23e
tarball11 files changed+20-220
@@ -23,7 +23,6 @@ pub const HOLD_EQ: i32 = 15;
2323
pub const ARRIVAL_BONUS: i32 = 1;2424
pub const GREED_WARM: f64 = 1.5;2525
pub const GREED_HOT: f64 = 2.0;26
-pub const PATCH_EQ: i32 = 3;2726
pub const STROKE_EQ: i32 = 2;2827
pub const STROKE_EQ_COST: i32 = 6;2928
pub const SOFT_INTENSITY: i32 = 2;@@ -1,10 +1,9 @@
11
use axum::http::StatusCode;2
-use serde::Deserialize;3
-use serde_json::{Value, json};2
+use serde_json::json;43
54
use super::{65
Outcome, Run,7
- balance::{EXAGGERATION, HONEST_TOLERANCE, PATCH_EQ, PUT_GAIN},6
+ balance::{HONEST_TOLERANCE, PUT_GAIN},87
};98
use crate::problem::Problem;109
@@ -37,61 +36,6 @@ pub(super) fn exaggeration() -> Problem {
3736
)3837
}3938
40
-/// One JSON Patch operation as sent, before the server decides what it will honour.41
-#[derive(Deserialize)]42
-pub struct PatchOp {43
- op: String,44
- path: String,45
- #[serde(default)]46
- value: Value,47
-}48
-49
-#[derive(Clone, Copy, PartialEq, Eq)]50
-enum Measurement {51
- Length,52
- Girth,53
- Foreskin,54
-}55
-56
-struct Claim {57
- measurement: Measurement,58
- value: f64,59
-}60
-61
-impl PatchOp {62
- fn claim(&self) -> Result<Claim, Problem> {63
- if self.op != "test" {64
- return Err(Problem::new(65
- StatusCode::UNPROCESSABLE_ENTITY,66
- "wishful-thinking",67
- "The server accepts only the truth.",68
- )69
- .with("op", self.op.clone()));70
- }71
- let measurement = match self.path.as_str() {72
- "/length" => Measurement::Length,73
- "/girth" => Measurement::Girth,74
- "/foreskin" => Measurement::Foreskin,75
- _ => {76
- return Err(Problem::new(77
- StatusCode::UNPROCESSABLE_ENTITY,78
- "no-such-measurement",79
- "There is no such measurement.",80
- )81
- .with("path", self.path.clone()));82
- }83
- };84
- let value = self.value.as_f64().ok_or_else(|| {85
- Problem::new(86
- StatusCode::BAD_REQUEST,87
- "malformed-patch",88
- "A test requires a numeric value.",89
- )90
- })?;91
- Ok(Claim { measurement, value })92
- }93
-}94
-9539
impl Run {9640
pub fn put(&mut self) -> Outcome {9741
if self.body.put_applied {@@ -102,33 +46,4 @@ impl Run {
10246
self.touch();10347
Outcome::json(StatusCode::OK, json!({ "detail": "Applied." }))10448
}105
-106
- /// Being measured is not arousing, so every PATCH costs a little EQ whatever it finds.107
- pub fn patch(&mut self, ops: &[PatchOp]) -> Result<Outcome, Problem> {108
- self.reset_eq(self.body.eq - PATCH_EQ);109
- self.touch();110
- for op in ops {111
- let claim = op.claim()?;112
- let actual = match claim.measurement {113
- Measurement::Length => self.current_length(),114
- Measurement::Girth => self.body.girth,115
- Measurement::Foreskin => self.body.foreskin.unwrap_or(0.0),116
- };117
- if claim.measurement == Measurement::Length && claim.value >= actual + EXAGGERATION {118
- self.standing.shame += 2;119
- return Err(exaggeration());120
- }121
- if !honest(claim.value, actual) {122
- self.standing.shame += 1;123
- return Err(124
- Problem::new(StatusCode::CONFLICT, "test-failed", "The test failed.")125
- .with("path", op.path.clone()),126
- );127
- }128
- }129
- Ok(Outcome::json(130
- StatusCode::OK,131
- json!({ "detail": "Correct." }),132
- ))133
- }13449
}@@ -43,7 +43,7 @@ pub use household::{Excuse, Household, MumVisit, vocabulary as excuse_vocabulary
4343
pub use inventory::Items;4444
pub use items::{Item, SHOP};4545
pub use lube::{LUBRICANTS, Lube};46
-pub use measure::{PatchOp, shaft, volume};46
+pub use measure::{shaft, volume};4747
pub use notice::Notice;4848
pub use outcome::{Outcome, Payload};4949
pub use run::Run;@@ -1,6 +1,5 @@
11
use axum::http::HeaderName;22
3
-pub const ACCEPT_PATCH: HeaderName = HeaderName::from_static("accept-patch");43
pub const SERVER_TIMING: HeaderName = HeaderName::from_static("server-timing");54
pub const X_CASH: HeaderName = HeaderName::from_static("x-cash");65
pub const X_DIAGNOSIS: HeaderName = HeaderName::from_static("x-diagnosis");@@ -1,17 +1,12 @@
11
use std::{collections::HashMap, fmt::Write as _, time::Instant};22
33
use axum::{4
- body::Bytes,54
extract::{FromRequest, FromRequestParts, Query, RawPathParams, Request},6
- http::{HeaderMap, HeaderName, Method, StatusCode, Uri, Version, header},7
- response::{IntoResponse, Response},5
+ http::{HeaderMap, HeaderName, Method, Uri, Version, header},6
+ response::Response,87
};98
use hyper::upgrade::OnUpgrade;109
11
-use crate::problem::Problem;12
-13
-pub const BODY_LIMIT: usize = 64 * 1024;14
-1510
/// Everything a handler may look at, gathered once per request.1611
pub struct Ctx {1712
method: Method,@@ -20,7 +15,6 @@ pub struct Ctx {
2015
headers: HeaderMap,2116
query: HashMap<String, String>,2217
params: HashMap<String, String>,23
- body: Bytes,2418
now: Instant,2519
upgrade: Option<OnUpgrade>,2620
}@@ -53,10 +47,6 @@ impl Ctx {
5347
&self.method5448
}5549
56
- pub fn body(&self) -> &[u8] {57
- &self.body58
- }59
-6050
pub fn now(&self) -> Instant {6151
self.now6252
}@@ -85,7 +75,7 @@ impl<S: Send + Sync> FromRequest<S> for Ctx {
8575
8676
async fn from_request(mut request: Request, state: &S) -> Result<Self, Self::Rejection> {8777
let upgrade = request.extensions_mut().remove::<OnUpgrade>();88
- let (mut parts, body) = request.into_parts();78
+ let (mut parts, _) = request.into_parts();8979
let params = RawPathParams::from_request_parts(&mut parts, state)9080
.await9181
.map(|raw| {@@ -97,14 +87,6 @@ impl<S: Send + Sync> FromRequest<S> for Ctx {
9787
let query = Query::<HashMap<String, String>>::try_from_uri(&parts.uri)9888
.map(|q| q.0)9989
.unwrap_or_default();100
- let body = axum::body::to_bytes(body, BODY_LIMIT).await.map_err(|_| {101
- Problem::new(102
- StatusCode::PAYLOAD_TOO_LARGE,103
- "body-too-large",104
- "The request body is larger than anything the server is prepared to receive.",105
- )106
- .into_response()107
- })?;10890
Ok(Self {10991
method: parts.method,11092
uri: parts.uri,@@ -112,7 +94,6 @@ impl<S: Send + Sync> FromRequest<S> for Ctx {
11294
headers: parts.headers,11395
query,11496
params,115
- body,11697
now: Instant::now(),11798
upgrade,11899
})@@ -1,12 +1,12 @@
11
use axum::{22
Json,3
- http::{HeaderValue, Method, StatusCode, header},3
+ http::{Method, StatusCode, header},44
response::{IntoResponse, Response},55
};66
use serde_json::json;77
88
use super::ctx::Ctx;9
-use crate::{headers::ACCEPT_PATCH, problem::Problem};9
+use crate::problem::Problem;1010
1111
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.";1212
@@ -62,22 +62,11 @@ impl<T> Verb<T> {
6262
/// The methods a resource supports. `Allow`, `OPTIONS` and `405` are all derived from this list.6363
pub struct Resource<T: 'static> {6464
verbs: &'static [Verb<T>],65
- accept_patch: bool,6665
}6766
6867
impl<T> Resource<T> {6968
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
- }69
+ Self { verbs }8170
}8271
8372
pub fn allow(&self, target: &T) -> String {@@ -109,7 +98,7 @@ impl<T> Resource<T> {
10998
let refusal = verb.guard.map_or("", |(_, refusal)| refusal);11099
method_not_allowed(refusal.to_owned(), &self.allow(target))111100
}112
- None if requested == "OPTIONS" => options(&self.allow(target), self.accept_patch),101
+ None if requested == "OPTIONS" => options(&self.allow(target)),113102
None => unsupported(ctx.method(), &self.allow(target)),114103
}115104
}@@ -150,18 +139,11 @@ pub fn unsupported(method: &Method, allow: &str) -> Response {
150139
}151140
}152141
153
-fn options(allow: &str, accept_patch: bool) -> Response {154
- let mut response = (142
+fn options(allow: &str) -> Response {143
+ (155144
StatusCode::OK,156145
[(header::ALLOW, allow)],157146
Json(json!({ "detail": THERAPY })),158147
)159
- .into_response();160
- if accept_patch {161
- response.headers_mut().insert(162
- ACCEPT_PATCH,163
- HeaderValue::from_static("application/json-patch+json"),164
- );165
- }166
- response148
+ .into_response()167149
}@@ -18,10 +18,9 @@ use super::{
1818
};1919
use crate::{2020
game::{21
- Branch, Cause, Floor, Grip, Item, LUBRICANTS, Lube, PatchOp, Run, Technique,21
+ Branch, Cause, Floor, Grip, Item, LUBRICANTS, Lube, Run, Technique,2222
balance::{EQ_BAND, VIEWS_PER_SECOND},2323
},24
- headers::ACCEPT_PATCH,2524
problem::Problem,2625
};2726
@@ -45,7 +44,7 @@ fn not_in_clinic(run: &Run) -> bool {
4544
4645
pub static RUN: RunResource = RunResource {4746
scope: Scope::Home,48
- resource: Resource::patchable(&[47
+ resource: Resource::new(&[4948
Verb::new("GET", get_run),5049
Verb::new("JELQ", jelq).only_when(not_in_clinic, "JELQ is not performed in the clinic."),5150
Verb::new("HOLD", hold_run).only_when(@@ -53,7 +52,6 @@ pub static RUN: RunResource = RunResource {
5352
"HOLD is only available above 70 EQ or at 2 stamina or less.",5453
),5554
Verb::new("PUT", put_run),56
- Verb::new("PATCH", patch_run),5755
Verb::new("DELETE", delete_run),5856
Verb::new("TRACE", trace_run),5957
]),@@ -131,30 +129,6 @@ fn hold_run(run: &mut Run, _: &Ctx) -> Response {
131129
respond(run.hold())132130
}133131
134
-fn patch_run(run: &mut Run, ctx: &Ctx) -> Response {135
- let is_patch = ctx136
- .header(header::CONTENT_TYPE)137
- .is_some_and(|ct| ct.starts_with("application/json-patch+json"));138
- if !is_patch {139
- return Problem::new(140
- StatusCode::UNSUPPORTED_MEDIA_TYPE,141
- "patch-format",142
- "Patches are accepted as application/json-patch+json.",143
- )144
- .header(ACCEPT_PATCH, "application/json-patch+json")145
- .into_response();146
- }147
- match serde_json::from_slice::<Vec<PatchOp>>(ctx.body()) {148
- Ok(ops) => respond(run.patch(&ops)),149
- Err(_) => Problem::new(150
- StatusCode::BAD_REQUEST,151
- "malformed-patch",152
- "The patch could not be parsed.",153
- )154
- .into_response(),155
- }156
-}157
-158132
fn delete_run(run: &mut Run, _: &Ctx) -> Response {159133
run.end(Cause::Abandoned);160134
StatusCode::NO_CONTENT.into_response()@@ -12,9 +12,10 @@ use serde::Deserialize;
1212
use serde_json::json;1313
use tower::ServiceExt;1414
15
-use super::{ctx::BODY_LIMIT, resource::unsupported};15
+use super::resource::unsupported;1616
use crate::problem::{Problem, reason};1717
18
+const BODY_LIMIT: usize = 64 * 1024;1819
const RESPONSE_LIMIT: usize = 1 << 20;1920
2021
#[derive(Deserialize)]@@ -23,8 +24,6 @@ struct Envelope {
2324
path: String,2425
#[serde(default)]2526
headers: Vec<(String, String)>,26
- #[serde(default)]27
- body: String,2827
}2928
3029
pub async fn page(method: Method) -> Response {@@ -73,7 +72,7 @@ pub async fn send(State(api): State<Router>, headers: HeaderMap, body: Bytes) ->
7372
for cookie in headers.get_all(header::COOKIE) {7473
request = request.header(header::COOKIE, cookie);7574
}76
- let Ok(request) = request.body(Body::from(envelope.body)) else {75
+ let Ok(request) = request.body(Body::empty()) else {7776
return bad_envelope("The request could not be built. A header may be malformed.");7877
};7978
let Ok(response) = api.oneshot(request).await;@@ -357,21 +357,6 @@ async fn lubes_run_out_but_spit_never_does() {
357357
}358358
359359
#[tokio::test]360
-async fn patching_costs_a_little_eq() {361
- let app = seeded_with(Floor::Bedroom, |run| run.body.eq = 50);362
- let patch = send(363
- &app,364
- "PATCH",365
- "/runs/cafebabe",366
- &[("content-type", "application/json-patch+json")],367
- r#"[{"op": "test", "path": "/girth", "value": 1.0}]"#,368
- )369
- .await;370
- assert_eq!(patch.status(), StatusCode::CONFLICT);371
- assert_eq!(header(&patch, "x-eq"), "47");372
-}373
-374
-#[tokio::test]375360
async fn a_combo_chafes_twice_and_costs_two_stamina() {376361
let app = seeded_with(Floor::Hotel, |run| {377362
run.here.wifi_accepted = true;@@ -83,33 +83,6 @@ async fn the_clinic_does_not_advertise_jelq() {
8383
}8484
8585
#[tokio::test]86
-async fn patch_only_accepts_the_truth() {87
- let Harness { app, .. } = harness(Floor::Bedroom, SEED, |_| {});88
- let patch = [("content-type", "application/json-patch+json")];89
- let replace = send(90
- &app,91
- "PATCH",92
- "/runs/cafebabe",93
- &patch,94
- r#"[{"op":"replace","path":"/length","value":12}]"#,95
- )96
- .await;97
- assert_eq!(replace.status(), StatusCode::UNPROCESSABLE_ENTITY);98
- let lie = send(99
- &app,100
- "PATCH",101
- "/runs/cafebabe",102
- &patch,103
- r#"[{"op":"test","path":"/length","value":40}]"#,104
- )105
- .await;106
- assert_eq!(107
- json(lie).await["type"],108
- "https://jelq.dev/problems/exaggeration"109
- );110
-}111
-112
-#[tokio::test]11386
async fn http_1_0_gets_the_pdf_treatment() {11487
let request = Request::builder()11588
.uri("/graveyard")@@ -51,7 +51,7 @@ pre, li, p, td, label { overflow-wrap: anywhere; }
5151
5252
<script>5353
const REL = "https://jelq.dev/rels/";54
-const METHODS = ["JELQ", "HOLD", "GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE"];54
+const METHODS = ["JELQ", "HOLD", "GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS", "TRACE"];5555
const ALL_LUBES = ["", "application/baby-oil", "application/coconut-oil", "application/lotion", "application/vaseline", "text/plain; charset=spit", "image/jpeg"];5656
const DEATHS = ["vascular-event", "caught", "opened", "banned", "degloved", "zip", "watermelon", "overstayed", "run-ended"];5757
let stocked = null;@@ -79,7 +79,6 @@ const FIELDS = {
7979
also: { label: "combo with", kind: "select" },8080
intensity: { label: "intensity, 1 to 10", kind: "number", value: "5" },8181
lube: { label: "Content-Type", header: "Content-Type", kind: "select" },82
- patch: { label: "body", kind: "textarea", value: '[{"op": "test", "path": "/length", "value": 6}]' },8382
range: { label: "Range", header: "Range", kind: "text", placeholder: "inches=0-2" },8483
ifMatch: { label: "If-Match", header: "If-Match", kind: "text" },8584
ifNoneMatch: { label: "If-None-Match", header: "If-None-Match", kind: "text" },@@ -91,7 +90,6 @@ const FIELDS = {
9190
};9291
const BY_TARGET = {9392
"runs POST": ["seed"],94
- "run PATCH": ["patch"],9593
"length GET": ["range"],9694
"length HEAD": ["range"],9795
"foreskin DELETE": ["ifMatch"],@@ -443,7 +441,6 @@ async function go(event) {
443441
const value = name => fieldValue(name).trim();444442
const headers = [];445443
let path = t.path;446
- let body = "";447444
for (const name of fieldNames()) {448445
const f = FIELDS[name];449446
if (f.header && value(name)) headers.push([f.header, value(name)]);@@ -460,10 +457,6 @@ async function go(event) {
460457
if (names.includes("intensity")) path += "&intensity=" + encodeURIComponent(value("intensity"));461458
}462459
if (key === "runs POST" && value("seed")) path += "?seed=" + encodeURIComponent(value("seed"));463
- if (method === "PATCH") {464
- headers.push(["Content-Type", "application/json-patch+json"]);465
- body = value("patch");466
- }467460
468461
const sent = ["> " + method + " " + short(path)].concat(headers.map(([name, v]) => "> " + name + ": " + v)).join("\n");469462
delete values.authorization;@@ -473,7 +466,7 @@ async function go(event) {
473466
const response = await fetch("/ui/send", {474467
method: "POST",475468
headers: { "Content-Type": "application/json" },476
- body: JSON.stringify({ method, path, headers, body }),469
+ body: JSON.stringify({ method, path, headers }),477470
});478471
r = await response.json();479472
} catch (error) {