atagen

jelq

public

an http based roguelike rpg about jelqing

Star0
Watch0

Commit 27256acf2780

tarball

atagen <boss@atagen.co>2026-10-14 13:44 UTC

3 files changed+80-36
game: report each combo technique in plain WebDAV rather than nested JSON

responsedescription carried the whole problem document, repeating the status and title already in D:status. The detail now stands alone, the problem type goes in D:error, and any other fields become elements under the game's own namespace.
Modified file: src/game/dispatch.rs+71 additions−35 deletions
@@ -142,32 +142,13 @@ impl Run {
142142 }
143143
144144 let mut xml = String::from(
145- "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\">\n",
145+ "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<D:multistatus xmlns:D=\"DAV:\" xmlns:J=\"https://jelq.dev/\">\n",
146146 );
147147 for (training, report) in reports {
148- let _ = write!(
149- xml,
150- " <D:response>\n <D:href>{}?technique={}</D:href>\n <D:status>HTTP/1.1 {} {}</D:status>\n",
151- self.path(""),
152- training.name(),
153- report.status.as_u16(),
154- crate::problem::reason(report.status),
148+ report.write(
149+ &mut xml,
150+ &format!("{}?technique={}", self.path(""), training.name()),
155151 );
156- if let Some(location) = report.location {
157- let _ = writeln!(
158- xml,
159- " <D:location><D:href>{}</D:href></D:location>",
160- escape(&location)
161- );
162- }
163- if let Some(text) = report.text {
164- let _ = writeln!(
165- xml,
166- " <D:responsedescription>{}</D:responsedescription>",
167- escape(&text)
168- );
169- }
170- xml.push_str(" </D:response>\n");
171152 }
172153 xml.push_str(
173154 " <D:responsedescription>Each technique has been reported separately.</D:responsedescription>\n</D:multistatus>\n",
@@ -183,24 +164,79 @@ impl Run {
183164 /// What one technique of a combo would have answered on its own.
184165 struct Report {
185166 status: StatusCode,
167+ error: Option<&'static str>,
168+ detail: Option<String>,
169+ fields: Vec<(String, String)>,
186170 location: Option<String>,
187- text: Option<String>,
188171 }
189172
190173 impl Report {
191174 fn of(result: Result<Outcome, Problem>) -> Self {
192- match result {
193- Ok(outcome) => Self {
194- status: outcome.status(),
195- location: outcome.header_value(&header::LOCATION).map(str::to_owned),
196- text: outcome.body().map(Value::to_string),
197- },
198- Err(problem) => Self {
199- status: problem.status(),
200- location: None,
201- text: Some(Value::Object(problem.document()).to_string()),
202- },
175+ let (status, error, location, body) = match result {
176+ Ok(outcome) => (
177+ outcome.status(),
178+ None,
179+ outcome.header_value(&header::LOCATION).map(str::to_owned),
180+ outcome.body().and_then(Value::as_object).cloned(),
181+ ),
182+ Err(problem) => {
183+ let mut document = problem.document();
184+ for repeated in ["type", "title", "status"] {
185+ document.remove(repeated);
186+ }
187+ (problem.status(), Some(problem.slug()), None, Some(document))
188+ }
189+ };
190+ let mut body = body.unwrap_or_default();
191+ let detail = body
192+ .remove("detail")
193+ .and_then(|detail| detail.as_str().map(str::to_owned));
194+ let fields = body
195+ .into_iter()
196+ .map(|(key, value)| match value {
197+ Value::String(text) => (key, text),
198+ other => (key, other.to_string()),
199+ })
200+ .collect();
201+ Self {
202+ status,
203+ error,
204+ detail,
205+ fields,
206+ location,
207+ }
208+ }
209+
210+ /// In RFC 4918's order, with anything the game adds after it under its own namespace.
211+ fn write(&self, xml: &mut String, href: &str) {
212+ let _ = write!(
213+ xml,
214+ " <D:response>\n <D:href>{}</D:href>\n <D:status>HTTP/1.1 {} {}</D:status>\n",
215+ escape(href),
216+ self.status.as_u16(),
217+ crate::problem::reason(self.status),
218+ );
219+ if let Some(error) = self.error {
220+ let _ = writeln!(xml, " <D:error><J:{error}/></D:error>");
221+ }
222+ if let Some(detail) = &self.detail {
223+ let _ = writeln!(
224+ xml,
225+ " <D:responsedescription>{}</D:responsedescription>",
226+ escape(detail)
227+ );
228+ }
229+ if let Some(location) = &self.location {
230+ let _ = writeln!(
231+ xml,
232+ " <D:location><D:href>{}</D:href></D:location>",
233+ escape(location)
234+ );
235+ }
236+ for (key, value) in &self.fields {
237+ let _ = writeln!(xml, " <J:{key}>{}</J:{key}>", escape(value));
203238 }
239+ xml.push_str(" </D:response>\n");
204240 }
205241 }
206242
Modified file: src/problem.rs+4 additions
@@ -29,6 +29,10 @@ impl Problem {
2929 }))
3030 }
3131
32+ pub fn slug(&self) -> &'static str {
33+ self.0.slug
34+ }
35+
3236 pub fn status(&self) -> StatusCode {
3337 self.0.status
3438 }
Modified file: src/tests/body.rs+5 additions−1 deletions
@@ -411,9 +411,13 @@ async fn a_combo_tells_each_technique_in_full() {
411411 continue;
412412 }
413413 assert!(
414- report.contains("<D:responsedescription>{\"") && report.contains("\"detail\""),
414+ report.contains("<D:responsedescription>") && !report.contains("{\""),
415415 "{report}"
416416 );
417+ let failed = ["HTTP/1.1 4", "HTTP/1.1 5"]
418+ .iter()
419+ .any(|s| report.contains(s));
420+ assert_eq!(failed, report.contains("<D:error><J:"), "{report}");
417421 told += 1;
418422 }
419423 }