an http based roguelike rpg about jelqing
Commit 98432b6057aa
tarball4 files changed+115-21
@@ -4,7 +4,7 @@ use super::{
44
};55
use crate::problem::Problem;66
use axum::http::{StatusCode, header};7
-use serde_json::json;7
+use serde_json::{Value, json};88
use std::{fmt::Write as _, ops::ControlFlow};99
1010
impl Run {@@ -133,31 +133,45 @@ impl Run {
133133
return Ok(interrupted);134134
}135135
136
- let mut statuses = Vec::new();136
+ let mut reports = Vec::new();137137
for training in [a, b] {138138
let result = self.roll(training, intensity, lube);139139
if self.ended.is_some() {140140
return result;141141
}142
- let status = match result {143
- Ok(outcome) => self.gossip(outcome).status(),144
- Err(problem) => problem.status(),145
- };146
- statuses.push((training, status));142
+ reports.push((143
+ training,144
+ Report::of(result.map(|outcome| self.gossip(outcome))),145
+ ));147146
}148147
149148
let mut xml = String::from(150149
"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n",151150
);152
- for (training, status) in statuses {151
+ for (training, report) in reports {153152
let _ = write!(154153
xml,155
- " <D:response>\n <D:href>{}?technique={}</D:href>\n <D:status>HTTP/1.1 {} {}</D:status>\n </D:response>\n",154
+ " <D:response>\n <D:href>{}?technique={}</D:href>\n <D:status>HTTP/1.1 {} {}</D:status>\n",156155
self.path(""),157156
training.name(),158
- status.as_u16(),159
- crate::problem::reason(status),157
+ report.status.as_u16(),158
+ crate::problem::reason(report.status),160159
);160
+ if let Some(location) = report.location {161
+ let _ = writeln!(162
+ xml,163
+ " <D:location><D:href>{}</D:href></D:location>",164
+ escape(&location)165
+ );166
+ }167
+ if let Some(text) = report.text {168
+ let _ = writeln!(169
+ xml,170
+ " <D:responsedescription>{}</D:responsedescription>",171
+ escape(&text)172
+ );173
+ }174
+ xml.push_str(" </D:response>\n");161175
}162176
xml.push_str(163177
" <D:responsedescription>Each technique has been reported separately.</D:responsedescription>\n</D:multistatus>\n",@@ -169,3 +183,33 @@ impl Run {
169183
))170184
}171185
}186
+187
+/// What one technique of a combo would have answered on its own.188
+struct Report {189
+ status: StatusCode,190
+ location: Option<String>,191
+ text: Option<String>,192
+}193
+194
+impl Report {195
+ fn of(result: Result<Outcome, Problem>) -> Self {196
+ match result {197
+ Ok(outcome) => Self {198
+ status: outcome.status(),199
+ location: outcome.header_value(&header::LOCATION).map(str::to_owned),200
+ text: outcome.body().map(Value::to_string),201
+ },202
+ Err(problem) => Self {203
+ status: problem.status(),204
+ location: None,205
+ text: Some(Value::Object(problem.document()).to_string()),206
+ },207
+ }208
+ }209
+}210
+211
+fn escape(text: &str) -> String {212
+ text.replace('&', "&")213
+ .replace('<', "<")214
+ .replace('>', ">")215
+}@@ -46,6 +46,20 @@ impl Outcome {
4646
self.status4747
}4848
49
+ pub fn body(&self) -> Option<&Value> {50
+ match &self.payload {51
+ Payload::Json(body) => Some(body),52
+ _ => None,53
+ }54
+ }55
+56
+ pub fn header_value(&self, name: &HeaderName) -> Option<&str> {57
+ self.headers58
+ .iter()59
+ .find(|(n, _)| n == name)60
+ .map(|(_, v)| v.as_str())61
+ }62
+4963
pub(super) fn retold(mut self, status: StatusCode, detail: &str) -> Self {5064
self.status = status;5165
if let Payload::Json(body) = &mut self.payload {@@ -38,6 +38,20 @@ impl Problem {
3838
self3939
}4040
41
+ pub fn document(&self) -> Map<String, Value> {42
+ let problem = &self.0;43
+ let mut body = Map::new();44
+ body.insert(45
+ "type".into(),46
+ json!(format!("{PROBLEM_BASE}{}", problem.slug)),47
+ );48
+ body.insert("title".into(), json!(reason(problem.status)));49
+ body.insert("status".into(), json!(problem.status.as_u16()));50
+ body.insert("detail".into(), json!(problem.detail));51
+ body.extend(problem.extensions.clone());52
+ body53
+ }54
+4155
pub fn header(mut self, name: HeaderName, value: impl AsRef<str>) -> Self {4256
if let Ok(value) = HeaderValue::from_str(value.as_ref()) {4357
self.0.headers.append(name, value);@@ -57,17 +71,8 @@ pub fn reason(status: StatusCode) -> &'static str {
5771
5872
impl IntoResponse for Problem {5973
fn into_response(self) -> Response {74
+ let body = self.document();6075
let problem = *self.0;61
- let mut body = Map::new();62
- body.insert(63
- "type".into(),64
- json!(format!("{PROBLEM_BASE}{}", problem.slug)),65
- );66
- body.insert("title".into(), json!(reason(problem.status)));67
- body.insert("status".into(), json!(problem.status.as_u16()));68
- body.insert("detail".into(), json!(problem.detail));69
- body.extend(problem.extensions);70
-7176
let mut response = (problem.status, Json(body)).into_response();7277
let headers = response.headers_mut();7378
headers.insert(@@ -377,6 +377,37 @@ async fn a_combo_chafes_twice_and_costs_two_stamina() {
377377
assert_eq!(header(&combo, "x-stamina"), "8/10");378378
}379379
380
+#[tokio::test]381
+async fn a_combo_tells_each_technique_in_full() {382
+ let mut told = 0;383
+ for seed in 0..40 {384
+ let app = app_with(Floor::Bedroom, seed, |run| run.body.eq = 60);385
+ let lube = [("content-type", "application/baby-oil")];386
+ let combo = send(387
+ &app,388
+ "JELQ",389
+ "/runs/cafebabe?technique=wet,stretch",390
+ &lube,391
+ "",392
+ )393
+ .await;394
+ if combo.status() != StatusCode::MULTI_STATUS {395
+ continue;396
+ }397
+ for report in text(combo).await.split("<D:response>").skip(1) {398
+ if report.contains("HTTP/1.1 204") {399
+ continue;400
+ }401
+ assert!(402
+ report.contains("<D:responsedescription>{\"") && report.contains("\"detail\""),403
+ "{report}"404
+ );405
+ told += 1;406
+ }407
+ }408
+ assert!(told > 0, "no combo told anything in forty runs");409
+}410
+380411
#[test]381412
fn the_drawer_starts_with_at_most_one_bottle() {382413
let mut stocked = 0;