Skip to main content

max / goingson

Every described screen reads the bag it means Adopts quasi-router's three-bag Request across all nine ports. A filter is read from `carried`, a path capture from `captures`, a write's own values from `payload`; every action builder that carries a view says `carrying` rather than `with`. Two workarounds retire with it. The mail screen's destination goes back to `folder` and its archive state to `archived`, having been `to` and `on` only because those names collided with its own filters. The problems inbox writes `status` again for the same reason. Both screens now say what they mean and the collision is a non-event. The mail port's finding comment is rewritten as closed rather than deleted: what it walked into is worth keeping next to the code that walked into it.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-10 19:55 UTC
Signed with PGP, not checked
Commit: 9dd377450ae91b1eb843f48e2b015d85b20e92c9
Parent: b8ea9b7
18 files changed, +459 insertions, -371 deletions
@@ -37,8 +37,8 @@
37 37 //! are the first writes in this module, and they are what proves the `Act` on a
38 38 //! row reaches a handler at all.
39 39
40 - // Handlers take their params by value because `quasi_router::Handler` is a
41 - // plain `fn(&S, Params)` pointer, so the signature is the router's and not a
40 + // Handlers take their request by value because `quasi_router::Handler` is a
41 + // plain `fn(&S, Request)` pointer, so the signature is the router's and not a
42 42 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
43 43 #![allow(clippy::needless_pass_by_value)]
44 44
@@ -175,10 +175,10 @@
175 175 fn list_action(search: Option<&str>, tag: Option<&str>) -> Action {
176 176 let mut action = Action::get("/contacts/list");
177 177 if let Some(search) = search {
178 - action = action.with("q", search);
178 + action = action.carrying("q", search);
179 179 }
180 180 if let Some(tag) = tag {
181 - action = action.with("tag", tag);
181 + action = action.carrying("tag", tag);
182 182 }
183 183 action
184 184 }
@@ -189,10 +189,11 @@
189 189 /// crate's. Same reasoning as the projects screen: not worth adding one upstream
190 190 /// for a handful of call sites.
191 191 fn id_param<T: From<uuid::Uuid>>(
192 - params: &quasi_router::Params,
192 + request: &quasi_router::Request,
193 193 name: &str,
194 194 ) -> Result<T, RouteError> {
195 - let raw = params
195 + let raw = request
196 + .captures
196 197 .get(name)
197 198 .ok_or_else(|| RouteError::not_found("no id"))?;
198 199 let uuid = uuid::Uuid::parse_str(raw).map_err(|_| RouteError::not_found("not an id"))?;
@@ -200,9 +201,9 @@
200 201 }
201 202
202 203 /// The whole screen.
203 - fn index(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
204 - let search = text(&params, "q");
205 - let tag = text(&params, "tag");
204 + fn index(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
205 + let search = text(&request.carried, "q");
206 + let tag = text(&request.carried, "tag");
206 207
207 208 let mut band = Slot::new("contacts-band", RegionKind::Band)
208 209 .with(Node::page("Contacts"))
@@ -226,8 +227,12 @@
226 227 }
227 228
228 229 /// The grid alone, which is what search and the tag filter replace.
229 - fn list(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
230 - let node = grid(state, text(&params, "q"), text(&params, "tag"))?;
230 + fn list(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
231 + let node = grid(
232 + state,
233 + text(&request.carried, "q"),
234 + text(&request.carried, "tag"),
235 + )?;
231 236 Ok(Response::fragment("contacts-grid", node))
232 237 }
233 238
@@ -412,8 +417,8 @@
412 417 }
413 418
414 419 /// One contact's detail pane.
415 - fn detail(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
416 - let contact = load(state, id_param(&params, "id")?)?;
420 + fn detail(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
421 + let contact = load(state, id_param(&request, "id")?)?;
417 422 Ok(Response::fragment("contacts-detail", detail_pane(&contact)))
418 423 }
419 424
@@ -428,41 +433,41 @@
428 433 }
429 434
430 435 /// Remove one email address.
431 - fn remove_email(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
432 - let contact: ContactId = id_param(&params, "id")?;
436 + fn remove_email(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
437 + let contact: ContactId = id_param(&request, "id")?;
433 438 state
434 439 .contacts
435 - .remove_email(id_param(&params, "sub")?, DESKTOP_USER_ID)
440 + .remove_email(id_param(&request, "sub")?, DESKTOP_USER_ID)
436 441 .map_err(|error| RouteError::internal(error.to_string()))?;
437 442 removed(state, contact)
438 443 }
439 444
440 445 /// Remove one phone number.
441 - fn remove_phone(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
442 - let contact: ContactId = id_param(&params, "id")?;
446 + fn remove_phone(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
447 + let contact: ContactId = id_param(&request, "id")?;
443 448 state
444 449 .contacts
445 - .remove_phone(id_param(&params, "sub")?, DESKTOP_USER_ID)
450 + .remove_phone(id_param(&request, "sub")?, DESKTOP_USER_ID)
446 451 .map_err(|error| RouteError::internal(error.to_string()))?;
447 452 removed(state, contact)
448 453 }
449 454
450 455 /// Remove one social handle.
451 - fn remove_social(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
452 - let contact: ContactId = id_param(&params, "id")?;
456 + fn remove_social(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
457 + let contact: ContactId = id_param(&request, "id")?;
453 458 state
454 459 .contacts
455 - .remove_social_handle(id_param(&params, "sub")?, DESKTOP_USER_ID)
460 + .remove_social_handle(id_param(&request, "sub")?, DESKTOP_USER_ID)
456 461 .map_err(|error| RouteError::internal(error.to_string()))?;
457 462 removed(state, contact)
458 463 }
459 464
460 465 /// Remove one custom field.
461 - fn remove_field(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
462 - let contact: ContactId = id_param(&params, "id")?;
466 + fn remove_field(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
467 + let contact: ContactId = id_param(&request, "id")?;
463 468 state
464 469 .contacts
465 - .remove_custom_field(id_param(&params, "sub")?, DESKTOP_USER_ID)
470 + .remove_custom_field(id_param(&request, "sub")?, DESKTOP_USER_ID)
466 471 .map_err(|error| RouteError::internal(error.to_string()))?;
467 472 removed(state, contact)
468 473 }
@@ -59,8 +59,8 @@
59 59 //! it either. Recorded rather than quietly given a button, which would make the
60 60 //! described screen offer something the screen it stands in for does not.
61 61
62 - // Handlers take their params by value because `quasi_router::Handler` is a
63 - // plain `fn(&S, Params)` pointer, so the signature is the router's and not a
62 + // Handlers take their request by value because `quasi_router::Handler` is a
63 + // plain `fn(&S, Request)` pointer, so the signature is the router's and not a
64 64 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
65 65 #![allow(clippy::needless_pass_by_value)]
66 66
@@ -123,17 +123,18 @@
123 123
124 124 impl View {
125 125 /// The view a route was addressed at.
126 - fn of(params: &quasi_router::Params) -> Self {
126 + fn of(request: &quasi_router::Request) -> Self {
127 127 Self {
128 - folder: text(params, "folder"),
129 - label: text(params, "label"),
130 - archived: matches!(params.get("archived"), Some("1" | "true")),
128 + folder: text(&request.carried, "folder"),
129 + label: text(&request.carried, "label"),
130 + archived: matches!(request.carried.get("archived"), Some("1" | "true")),
131 131 // A hand-typed `shown` is clamped rather than refused: this is an
132 132 // address, and landing on the first page is a more useful answer
133 133 // than an error page. The ceiling is the one the JS's own paging
134 134 // would reach in ten scrolls and stops a typo asking for a million
135 135 // rows.
136 - shown: params
136 + shown: request
137 + .carried
137 138 .get("shown")
138 139 .and_then(|raw| raw.parse::<i64>().ok())
139 140 .unwrap_or(PAGE)
@@ -147,48 +148,51 @@
147 148 /// That is [`super::projects::filtered_by`]'s rule, applied to four params
148 149 /// instead of two.
149 150 ///
150 - /// # The sixth finding, which this port walked into
151 + /// # The sixth finding, which this port walked into, and which is now closed
151 152 ///
152 - /// **A write's parameters and the view's parameters share one namespace, and
153 - /// nothing warns when they collide.**
153 + /// **A write's parameters and the view's parameters shared one namespace,
154 + /// and nothing warned when they collided.**
154 155 ///
155 156 /// This screen writes to `POST /emails/{id}/folder` and reads a `folder`
156 157 /// filter, and it archives through `POST /emails/{id}/archive` while reading
157 158 /// an `archived` filter. Written the obvious way โ€” the destination under
158 - /// `folder`, the desired state under `archived` โ€” both routes compile,
159 - /// answer, and are wrong in the same silent way: the write lands correctly
160 - /// and then [`View::of`] reads the write's own parameter back as the view,
161 - /// so moving a message to Archive from the INBOX answers with the Archive
162 - /// folder as though the user had navigated there. Nothing is lost and
163 - /// nothing errors; the screen just moves under them.
159 + /// `folder`, the desired state under `archived` โ€” both routes compiled,
160 + /// answered, and were wrong in the same silent way: the write landed
161 + /// correctly and then [`View::of`] read the write's own parameter back as
162 + /// the view, so moving a message to Archive from the INBOX answered with the
163 + /// Archive folder as though the user had navigated there. Nothing was lost
164 + /// and nothing errored; the screen just moved under them.
164 165 ///
165 - /// The fix here is naming โ€” the destination is `to` and the archive state is
166 - /// `on` โ€” and it is a convention held by hand, which is what makes it worth
167 - /// recording rather than just doing. Every screen that carries its view in
168 - /// the address has this hazard, it grows with the number of filters, and the
169 - /// two ports before this one had one and two filters and never met it. Filed
170 - /// against quasicoherent: the description layer knows which params are the
171 - /// view's, because the handler asked for them, and a `Params` that could say
172 - /// "these are the address and these are the payload" would make the collision
173 - /// a compile-time question instead of a convention.
166 + /// The fix was naming โ€” the destination was `to` and the archive state `on`
167 + /// โ€” a convention held by hand, which is why it was recorded rather than
168 + /// just done. The problems inbox hit the same wall the same day, on a screen
169 + /// with two filters rather than four, which killed the theory that this was
170 + /// about how many filters a screen carries.
171 + ///
172 + /// Closed 2026-08-10 in quasi-router. A request now arrives in three bags:
173 + /// `captures` from the path, `payload` from what the control sent, and
174 + /// `carried` from the address it was sent from. So this method writes the
175 + /// view with [`Action::carrying`], the writes send their values under their
176 + /// own honest names again โ€” `folder` and `archived`, not `to` and `on` โ€”
177 + /// and neither can be read as the other.
174 178 fn carry(&self, action: Action) -> Action {
175 179 let action = match &self.folder {
176 - Some(folder) => action.with("folder", folder.clone()),
180 + Some(folder) => action.carrying("folder", folder.clone()),
177 181 None => action,
178 182 };
179 183 let action = match &self.label {
180 - Some(label) => action.with("label", label.clone()),
184 + Some(label) => action.carrying("label", label.clone()),
181 185 None => action,
182 186 };
183 187 let action = if self.archived {
184 - action.with("archived", "1")
188 + action.carrying("archived", "1")
185 189 } else {
186 190 action
187 191 };
188 192 if self.shown == PAGE {
189 193 action
190 194 } else {
191 - action.with("shown", self.shown.to_string())
195 + action.carrying("shown", self.shown.to_string())
192 196 }
193 197 }
194 198
@@ -211,8 +215,9 @@
211 215 }
212 216
213 217 /// The email a route was addressed at.
214 - fn email_id(params: &quasi_router::Params) -> Result<EmailId, RouteError> {
215 - let raw = params
218 + fn email_id(request: &quasi_router::Request) -> Result<EmailId, RouteError> {
219 + let raw = request
220 + .captures
216 221 .get("id")
217 222 .ok_or_else(|| RouteError::not_found("no email id"))?;
218 223 Ok(EmailId::from(
@@ -342,9 +347,9 @@
342 347 Act::new("Mark read", at("read").with("read", "true"))
343 348 },
344 349 if email.is_archived {
345 - Act::new("Unarchive", at("archive").with("on", "false")).key("a")
350 + Act::new("Unarchive", at("archive").with("archived", "false")).key("a")
346 351 } else {
347 - Act::new("Archive", at("archive").with("on", "true")).key("a")
352 + Act::new("Archive", at("archive").with("archived", "true")).key("a")
348 353 },
349 354 Act::new("Create task", at("task")).key("t"),
350 355 Act::new("Create event", at("event")).key("e"),
@@ -807,7 +812,7 @@
807 812 // `to`, not `folder`, for the reason `View::carry` records: `folder`
808 813 // is the view's filter, and a destination under the same name
809 814 // would move the message and the screen at once.
810 - Field::new(makeover_layout::FieldKind::Text, "to", "Folder")
815 + Field::new(makeover_layout::FieldKind::Text, "folder", "Folder")
811 816 .value(latest.source_folder.clone().unwrap_or_default())
812 817 .required(),
813 818 ],
@@ -816,13 +821,13 @@
816 821 }
817 822
818 823 /// The whole screen.
819 - fn index(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
820 - Ok(screen(state, &View::of(&params), None)?.into())
824 + fn index(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
825 + Ok(screen(state, &View::of(&request), None)?.into())
821 826 }
822 827
823 828 /// The list alone, which is what a filter or another page replaces.
824 - fn list_only(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
825 - let view = View::of(&params);
829 + fn list_only(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
830 + let view = View::of(&request);
826 831 let node = list(state, &view, None)?;
827 832 Ok(Response::fragment(LIST, node))
828 833 }
@@ -838,9 +843,9 @@
838 843 /// The consequence is that the answer is the whole screen and not the pane: the
839 844 /// row behind it just lost its unread badge, and the unread figure in the band
840 845 /// changed with it.
841 - fn thread(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
842 - let id = email_id(&params)?;
843 - let view = View::of(&params);
846 + fn thread(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
847 + let id = email_id(&request)?;
848 + let view = View::of(&request);
844 849 // Marked before the read, so the screen that comes back is the one after the
845 850 // write rather than the one before it.
846 851 state
@@ -859,10 +864,10 @@
859 864 }
860 865
861 866 /// Read or unread.
862 - fn set_read(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
863 - let id = email_id(&params)?;
864 - let view = View::of(&params);
865 - let read = params.get("read") == Some("true");
867 + fn set_read(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
868 + let id = email_id(&request)?;
869 + let view = View::of(&request);
870 + let read = request.payload.get("read") == Some("true");
866 871
867 872 let found = if read {
868 873 state.emails.mark_read(id, DESKTOP_USER_ID)
@@ -903,13 +908,13 @@
903 908 /// screen that writes locally and wants to tell something else about it has
904 909 /// nowhere to say so. An outbox the app drains is one answer and a handler that
905 910 /// can return work is another, and picking between them wants a second consumer.
906 - fn set_archived(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
907 - let id = email_id(&params)?;
908 - let view = View::of(&params);
911 + fn set_archived(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
912 + let id = email_id(&request)?;
913 + let view = View::of(&request);
909 914 // `on`, not `archived`: `archived` is the view's own filter, and a write
910 915 // that reused the name would rewrite the view it answers with. See
911 916 // `View::carry`.
912 - let archived = params.get("on") == Some("true");
917 + let archived = request.payload.get("archived") == Some("true");
913 918
914 919 let found = if archived {
915 920 state.emails.archive(id, DESKTOP_USER_ID)
@@ -937,9 +942,9 @@
937 942 ///
938 943 /// A 404 for a message that is not there rather than a quiet success, which is
939 944 /// the rule the projects delete set.
940 - fn remove(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
941 - let id = email_id(&params)?;
942 - let view = View::of(&params);
945 + fn remove(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
946 + let id = email_id(&request)?;
947 + let view = View::of(&request);
943 948 let deleted = state
944 949 .emails
945 950 .delete(id, DESKTOP_USER_ID)
@@ -957,10 +962,11 @@
957 962 /// than a set of labels picked from what exists, and the JS knows it โ€” it prints
958 963 /// the existing ones under the box as a hint. Kept as it is: replacing it with a
959 964 /// multi-select is a change to the screen, and this port describes the screen.
960 - fn set_labels(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
961 - let id = email_id(&params)?;
962 - let view = View::of(&params);
963 - let labels: Vec<String> = params
965 + fn set_labels(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
966 + let id = email_id(&request)?;
967 + let view = View::of(&request);
968 + let labels: Vec<String> = request
969 + .payload
964 970 .get("labels")
965 971 .unwrap_or_default()
966 972 .split(',')
@@ -980,10 +986,15 @@
980 986 /// Move it to another folder.
981 987 ///
982 988 /// The local half only; see [`set_archived`] for why.
983 - fn set_folder(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
984 - let id = email_id(&params)?;
985 - let view = View::of(&params);
986 - let folder = params.get("to").unwrap_or_default().trim().to_owned();
989 + fn set_folder(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
990 + let id = email_id(&request)?;
991 + let view = View::of(&request);
992 + let folder = request
993 + .payload
994 + .get("folder")
995 + .unwrap_or_default()
996 + .trim()
997 + .to_owned();
987 998 if folder.is_empty() {
988 999 return Err(RouteError::not_found("no folder"));
989 1000 }
@@ -1004,11 +1015,11 @@
1004 1015 }
1005 1016
1006 1017 /// Snooze it until a time, or bring it back now.
1007 - fn set_snooze(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
1008 - let id = email_id(&params)?;
1009 - let view = View::of(&params);
1018 + fn set_snooze(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
1019 + let id = email_id(&request)?;
1020 + let view = View::of(&request);
1010 1021
1011 - if params.get("clear") == Some("true") {
1022 + if request.payload.get("clear") == Some("true") {
1012 1023 state
1013 1024 .emails
1014 1025 .unsnooze(id, DESKTOP_USER_ID)
@@ -1023,7 +1034,8 @@
1023 1034 // take it, `is_snoozed` would read false the moment it landed, and the user
1024 1035 // would be told the message was hidden when it was not. The JS enforces the
1025 1036 // same floor with the picker's `min`.
1026 - let until = params
1037 + let until = request
1038 + .payload
1027 1039 .get("until")
1028 1040 .and_then(|raw| DateTime::parse_from_rfc3339(raw).ok())
1029 1041 .map(|when| when.with_timezone(&Utc))
@@ -1070,9 +1082,9 @@
1070 1082 /// from here, and that is the whole of what a described screen has to route
1071 1083 /// around: the rules are in core, where the settings port's finding says host
1072 1084 /// facts should be, and for the same reason.
1073 - fn to_task(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
1074 - let id = email_id(&params)?;
1075 - let view = View::of(&params);
1085 + fn to_task(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
1086 + let id = email_id(&request)?;
1087 + let view = View::of(&request);
1076 1088 let email = load(state, id)?;
1077 1089
1078 1090 let contact_id = sender_contact(state, &email.from);
@@ -1106,9 +1118,9 @@
1106 1118 }
1107 1119
1108 1120 /// Make an event of it.
1109 - fn to_event(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
1110 - let id = email_id(&params)?;
1111 - let view = View::of(&params);
1121 + fn to_event(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
1122 + let id = email_id(&request)?;
1123 + let view = View::of(&request);
1112 1124 let email = load(state, id)?;
1113 1125
1114 1126 let contact_id = sender_contact(state, &email.from);
@@ -58,8 +58,8 @@
58 58 //! yet; describing a control that reaches nothing would be worse than leaving
59 59 //! it out. It returns when day planning does.
60 60
61 - // Handlers take their params by value because `quasi_router::Handler` is a
62 - // plain `fn(&S, Params)` pointer, so the signature is the router's and not a
61 + // Handlers take their request by value because `quasi_router::Handler` is a
62 + // plain `fn(&S, Request)` pointer, so the signature is the router's and not a
63 63 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
64 64 #![allow(clippy::needless_pass_by_value)]
65 65
@@ -92,8 +92,9 @@
92 92 /// there a bad value is a client bug worth reporting, and here it is a
93 93 /// hand-typed address, where landing on this month is the more useful answer
94 94 /// than an error page.
95 - fn month_of(params: &quasi_router::Params) -> NaiveDate {
96 - params
95 + fn month_of(request: &quasi_router::Request) -> NaiveDate {
96 + request
97 + .carried
97 98 .get("month")
98 99 .and_then(monthly_review::parse_month)
99 100 .unwrap_or_else(monthly_review::current_month_start)
@@ -101,7 +102,7 @@
101 102
102 103 /// The same action, still pointed at the month it was offered under.
103 104 fn in_month(action: Action, month: NaiveDate) -> Action {
104 - action.with("month", month.format("%Y-%m").to_string())
105 + action.carrying("month", month.format("%Y-%m").to_string())
105 106 }
106 107
107 108 /// The month before this one, and the month after.
@@ -514,8 +515,8 @@
514 515 }
515 516
516 517 /// The whole review.
517 - fn review(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
518 - Ok(screen(state, month_of(&params))?.into())
518 + fn review(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
519 + Ok(screen(state, month_of(&request))?.into())
519 520 }
520 521
521 522 /// The month a write names, as the repository spells it.
@@ -524,8 +525,9 @@
524 525 }
525 526
526 527 /// The goal id in a path, or a 404.
527 - fn goal_id(params: &quasi_router::Params) -> Result<goingson_core::MonthlyGoalId, RouteError> {
528 - let raw = params
528 + fn goal_id(request: &quasi_router::Request) -> Result<goingson_core::MonthlyGoalId, RouteError> {
529 + let raw = request
530 + .captures
529 531 .get("id")
530 532 .ok_or_else(|| RouteError::not_found("no goal id"))?;
531 533 Ok(goingson_core::MonthlyGoalId::from(
@@ -539,9 +541,9 @@
539 541 /// the JS: `addGoal(month, position)` is called from a specific empty slot, so
540 542 /// the position is a fact about which slot was clicked. A described form has no
541 543 /// slot, and "the next one" is what every one of those clicks meant.
542 - fn add_goal(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
543 - let month = month_of(&params);
544 - let text = params.get("text").unwrap_or_default().trim();
544 + fn add_goal(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
545 + let month = month_of(&request);
546 + let text = request.payload.get("text").unwrap_or_default().trim();
545 547 if text.is_empty() {
546 548 return Err(RouteError::conflict("A goal needs some text"));
547 549 }
@@ -570,10 +572,14 @@
570 572 }
571 573
572 574 /// Set a goal's status to the one the control named.
573 - fn set_goal_status(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
574 - let month = month_of(&params);
575 - let id = goal_id(&params)?;
576 - let status: MonthlyGoalStatus = params
575 + fn set_goal_status(
576 + state: &AppState,
577 + request: quasi_router::Request,
578 + ) -> Result<Response, RouteError> {
579 + let month = month_of(&request);
580 + let id = goal_id(&request)?;
581 + let status: MonthlyGoalStatus = request
582 + .payload
577 583 .get("status")
578 584 .ok_or_else(|| RouteError::not_found("no status"))?
579 585 .parse()
@@ -588,9 +594,9 @@
588 594 }
589 595
590 596 /// Drop a goal.
591 - fn delete_goal(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
592 - let month = month_of(&params);
593 - let id = goal_id(&params)?;
597 + fn delete_goal(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
598 + let month = month_of(&request);
599 + let id = goal_id(&request)?;
594 600
595 601 if !state
596 602 .monthly_reviews
@@ -607,10 +613,10 @@
607 613 /// Both answers empty still writes, and still marks the month reviewed: the
608 614 /// completion is the act and the writing is optional, which is the rule the
609 615 /// weekly review settled.
610 - fn complete(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
611 - let month = month_of(&params);
612 - let highlight = params.get("highlight").unwrap_or_default().trim();
613 - let change = params.get("change").unwrap_or_default().trim();
616 + fn complete(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
617 + let month = month_of(&request);
618 + let highlight = request.payload.get("highlight").unwrap_or_default().trim();
619 + let change = request.payload.get("change").unwrap_or_default().trim();
614 620
615 621 state
616 622 .monthly_reviews
@@ -34,20 +34,22 @@
34 34 //! control that derives its target from what it was drawn with races anything
35 35 //! that already moved the row.
36 36 //!
37 - //! # The sixth finding again, and this time it bit before it was read
37 + //! # This screen is why the router has two bags
38 38 //!
39 - //! The target travels as `to`, not as `status`, because `status` is already
40 - //! this screen's view filter. Written the obvious way the route works and the
41 - //! screen moves under the user: dismissing from the Open inbox answers with the
42 - //! Dismissed list, because the write's own parameter is what the next
43 - //! `status_filter` reads back. This port found it by a failing test rather than
44 - //! by reading, which is the argument for the finding the mail port filed
45 - //! against quasicoherent: the convention is held by hand, it is invisible until
46 - //! something collides, and `Params` knows enough to make it a compile-time
47 - //! question. Second screen to hit it, second one to work around it by naming.
39 + //! It filters on `status` and it writes a `status`, and for one afternoon those
40 + //! were the same name in one namespace: dismissing from the Open inbox wrote
41 + //! correctly and then answered with the Dismissed list, because the write's own
42 + //! parameter was what the next read of the filter found. The workaround was to
43 + //! call the target `to`, which is what the mail screen had already done twice
44 + //! under its own names.
45 + //!
46 + //! That convention is retired. A write's values arrive in `payload` and the
47 + //! view arrives in `carried`, so the target is called `status` here because
48 + //! that is what it is, and the filter is called `status` because that is what
49 + //! it is. Neither can reach the other.
48 50
49 - // Handlers take their params by value because `quasi_router::Handler` is a
50 - // plain `fn(&S, Params)` pointer, so the signature is the router's and not a
51 + // Handlers take their request by value because `quasi_router::Handler` is a
52 + // plain `fn(&S, Request)` pointer, so the signature is the router's and not a
51 53 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
52 54 #![allow(clippy::needless_pass_by_value)]
53 55
@@ -109,8 +111,8 @@
109 111 /// `all` means unfiltered. Same three cases as `list_problems`, and an
110 112 /// unrecognised word is a 404 rather than a silent fall back to `Open`, which
111 113 /// would answer with a different list than the one asked for.
112 - fn status_filter(params: &quasi_router::Params) -> Result<Option<ProblemStatus>, RouteError> {
113 - match text(params, "status") {
114 + fn status_filter(request: &quasi_router::Request) -> Result<Option<ProblemStatus>, RouteError> {
115 + match text(&request.carried, "status") {
114 116 None => Ok(Some(ProblemStatus::Open)),
115 117 Some(word) if word.eq_ignore_ascii_case("all") => Ok(None),
116 118 Some(word) => word
@@ -128,9 +130,9 @@
128 130 /// Carry the current filters on an action, so every control keeps the view it
129 131 /// was pressed in. The same job `in_month` does on the monthly review.
130 132 fn filtered(mut action: Action, status: Option<ProblemStatus>, source: Option<&str>) -> Action {
131 - action = action.with("status", status_word(status));
133 + action = action.carrying("status", status_word(status));
132 134 if let Some(source) = source {
133 - action = action.with("source", source);
135 + action = action.carrying("source", source);
134 136 }
135 137 action
136 138 }
@@ -150,8 +152,9 @@
150 152 }
151 153
152 154 /// Parse the path id, or answer 404.
153 - fn problem_id(params: &quasi_router::Params) -> Result<ProblemId, RouteError> {
154 - let raw = params
155 + fn problem_id(request: &quasi_router::Request) -> Result<ProblemId, RouteError> {
156 + let raw = request
157 + .captures
155 158 .get("id")
156 159 .ok_or_else(|| RouteError::not_found("no id"))?;
157 160 uuid::Uuid::parse_str(raw)
@@ -289,7 +292,7 @@
289 292 Act::new(
290 293 "Dismiss",
291 294 filtered(
292 - Action::post(format!("/problems/{id}/status")).with("to", "Dismissed"),
295 + Action::post(format!("/problems/{id}/status")).with("status", "Dismissed"),
293 296 status,
294 297 source,
295 298 ),
@@ -385,9 +388,9 @@
385 388 }
386 389
387 390 /// The whole screen.
388 - fn index(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
389 - let status = status_filter(&params)?;
390 - let source = text(&params, "source");
391 + fn index(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
392 + let status = status_filter(&request)?;
393 + let source = text(&request.carried, "source");
391 394
392 395 let mut band = Slot::new("problems-band", RegionKind::Band).with(Node::page("Problems"));
393 396
@@ -414,9 +417,9 @@
414 417 }
415 418
416 419 /// The list alone, which is what a filter chip replaces.
417 - fn list(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
418 - let status = status_filter(&params)?;
419 - let source = text(&params, "source");
420 + fn list(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
421 + let status = status_filter(&request)?;
422 + let source = text(&request.carried, "source");
420 423 Ok(Response::fragment(
421 424 "problems-list",
422 425 ranked(state, status, source)?,
@@ -430,11 +433,11 @@
430 433 /// pressed under is usually `Open` and the press is what settles it.
431 434 fn triaged(
432 435 state: &AppState,
433 - params: &quasi_router::Params,
436 + request: &quasi_router::Request,
434 437 message: &str,
435 438 ) -> Result<Response, RouteError> {
436 - let status = status_filter(params)?;
437 - let source = text(params, "source");
439 + let status = status_filter(request)?;
440 + let source = text(&request.carried, "source");
438 441 Ok(
439 442 Response::fragment("problems-list", ranked(state, status, source)?)
440 443 .toast(makeover_layout::Tone::Success, message),
@@ -447,8 +450,8 @@
447 450 /// good one: the description defaults to the problem's own text and the
448 451 /// priority to its painhours band, and both are better than anything retyped at
449 452 /// triage time. Shape the task afterwards if it needs it.
450 - fn promote_one(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
451 - let id = problem_id(&params)?;
453 + fn promote_one(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
454 + let id = problem_id(&request)?;
452 455 let outcome = promote(
453 456 state,
454 457 id,
@@ -461,7 +464,7 @@
461 464
462 465 triaged(
463 466 state,
464 - &params,
467 + &request,
465 468 if outcome.created {
466 469 "Promoted to a task."
467 470 } else {
@@ -475,10 +478,11 @@
475 478 /// The target is a param, never derived from what the row was drawn with. Two
476 479 /// windows on the same inbox therefore cannot disagree about what "the next
477 480 /// state" was.
478 - fn set_status(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
479 - let id = problem_id(&params)?;
480 - let target: ProblemStatus = params
481 - .get("to")
481 + fn set_status(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
482 + let id = problem_id(&request)?;
483 + let target: ProblemStatus = request
484 + .payload
485 + .get("status")
482 486 .ok_or_else(|| RouteError::not_found("no status"))?
483 487 .parse()
484 488 .map_err(|_| RouteError::not_found("not a triage state"))?;
@@ -493,7 +497,7 @@
493 497
494 498 triaged(
495 499 state,
496 - &params,
500 + &request,
497 501 match target {
498 502 ProblemStatus::Open => "Back in triage.",
499 503 ProblemStatus::Dismissed => "Dismissed. It stays down through the next pull.",
@@ -41,8 +41,8 @@
41 41 //! under, or acting resets the view. [`filtered`] is that, applied to the
42 42 //! detail address, the create form and both writes.
43 43
44 - // Handlers take their params by value because `quasi_router::Handler` is a
45 - // plain `fn(&S, Params)` pointer, so the signature is the router's and not a
44 + // Handlers take their request by value because `quasi_router::Handler` is a
45 + // plain `fn(&S, Request)` pointer, so the signature is the router's and not a
46 46 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
47 47 #![allow(clippy::needless_pass_by_value)]
48 48
@@ -249,8 +249,8 @@
249 249 }
250 250
251 251 /// Whether a param is on. Absent is off, which is what a URL without it means.
252 - fn flag(params: &quasi_router::Params, name: &str) -> bool {
253 - matches!(params.get(name), Some("1" | "true"))
252 + fn flag(request: &quasi_router::Request, name: &str) -> bool {
253 + matches!(request.carried.get(name), Some("1" | "true"))
254 254 }
255 255
256 256 /// The same action, carrying one flag if it is on.
@@ -259,7 +259,11 @@
259 259 /// never written. That is what keeps two addresses for the same view from
260 260 /// existing.
261 261 pub(super) fn filtered_by(action: Action, name: &str, on: bool) -> Action {
262 - if on { action.with(name, "1") } else { action }
262 + if on {
263 + action.carrying(name, "1")
264 + } else {
265 + action
266 + }
263 267 }
264 268
265 269 /// The same action, carrying the filters the screen was under.
@@ -322,13 +326,13 @@
322 326 }
323 327
324 328 /// The whole screen.
325 - fn index(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
326 - Ok(screen(state, flag(&params, "shared"), flag(&params, "retired"))?.into())
329 + fn index(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
330 + Ok(screen(state, flag(&request, "shared"), flag(&request, "retired"))?.into())
327 331 }
328 332
329 333 /// The grid alone, which is what a filter toggle replaces.
330 - fn list(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
331 - let node = grid(state, flag(&params, "shared"), flag(&params, "retired"))?;
334 + fn list(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
335 + let node = grid(state, flag(&request, "shared"), flag(&request, "retired"))?;
332 336 Ok(Response::fragment("projects-grid", node))
333 337 }
334 338
@@ -337,9 +341,10 @@
337 341 /// `ProjectId` has no `FromStr`, only `From<Uuid>`, so the parse is the uuid
338 342 /// crate's. Not worth adding one upstream for two call sites.
339 343 pub(super) fn project_id(
340 - params: &quasi_router::Params,
344 + request: &quasi_router::Request,
341 345 ) -> Result<goingson_core::ProjectId, RouteError> {
342 - let raw = params
346 + let raw = request
347 + .captures
343 348 .get("id")
344 349 .ok_or_else(|| RouteError::not_found("no project id"))?;
345 350 Ok(goingson_core::ProjectId::from(
@@ -348,10 +353,10 @@
348 353 }
349 354
350 355 /// One project's detail pane.
351 - fn detail(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
352 - let id = project_id(&params)?;
353 - let shared_only = flag(&params, "shared");
354 - let show_retired = flag(&params, "retired");
356 + fn detail(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
357 + let id = project_id(&request)?;
358 + let shared_only = flag(&request, "shared");
359 + let show_retired = flag(&request, "retired");
355 360
356 361 let project = state
357 362 .projects
@@ -491,10 +496,15 @@
491 496 }
492 497
493 498 /// The create form.
494 - fn new(_state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
499 + fn new(_state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
495 500 Ok(Response::fragment(
496 501 "projects-detail",
497 - form_pane(flag(&params, "shared"), flag(&params, "retired"), &[], None),
502 + form_pane(
503 + flag(&request, "shared"),
504 + flag(&request, "retired"),
505 + &[],
506 + None,
507 + ),
498 508 ))
499 509 }
500 510
@@ -534,12 +544,18 @@
534 544 }
535 545
536 546 /// Create a project, or answer with the form saying why not.
537 - fn create(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
538 - let shared_only = flag(&params, "shared");
539 - let show_retired = flag(&params, "retired");
547 + fn create(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
548 + let shared_only = flag(&request, "shared");
549 + let show_retired = flag(&request, "retired");
540 550
541 - let name = params.get("name").unwrap_or_default().trim().to_owned();
542 - let description = params
551 + let name = request
552 + .payload
553 + .get("name")
554 + .unwrap_or_default()
555 + .trim()
556 + .to_owned();
557 + let description = request
558 + .payload
543 559 .get("description")
544 560 .unwrap_or_default()
545 561 .trim()
@@ -550,8 +566,8 @@
550 566 // A select offers a fixed set, so an unparseable value did not come from the
551 567 // form. Refused rather than defaulted: `from_str_or_default` would file a
552 568 // typo as an `Other` project and say nothing.
553 - let project_type = parse_choice::<ProjectType>(&params, "project_type", &mut errors);
554 - let status = parse_choice::<ProjectStatus>(&params, "status", &mut errors)
569 + let project_type = parse_choice::<ProjectType>(&request.payload, "project_type", &mut errors);
570 + let status = parse_choice::<ProjectStatus>(&request.payload, "status", &mut errors)
555 571 .filter(|status| NEW_STATUSES.contains(status));
556 572 if status.is_none() && !errors.iter().any(|(field, _)| *field == "status") {
557 573 errors.push(("status", "Not a status a project starts in.".to_owned()));
@@ -562,13 +578,13 @@
562 578 let (Some(project_type), Some(status)) = (project_type, status) else {
563 579 return Ok(Response::fragment(
564 580 "projects-detail",
565 - form_pane(shared_only, show_retired, &errors, Some(&params)),
581 + form_pane(shared_only, show_retired, &errors, Some(&request.payload)),
566 582 ));
567 583 };
568 584 if !errors.is_empty() {
569 585 return Ok(Response::fragment(
570 586 "projects-detail",
571 - form_pane(shared_only, show_retired, &errors, Some(&params)),
587 + form_pane(shared_only, show_retired, &errors, Some(&request.payload)),
572 588 ));
573 589 }
574 590
@@ -608,8 +624,8 @@
608 624 /// A 404 for a project that is not there rather than a quiet success: the
609 625 /// repository answers `false`, and a delete that reports done for something it
610 626 /// never saw is how two panes end up disagreeing about what exists.
611 - fn remove(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
612 - let id = project_id(&params)?;
627 + fn remove(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
628 + let id = project_id(&request)?;
613 629 let deleted = state
614 630 .projects
615 631 .delete(id, DESKTOP_USER_ID)
@@ -617,7 +633,7 @@
617 633 if !deleted {
618 634 return Err(RouteError::not_found("no such project"));
619 635 }
620 - wrote(state, flag(&params, "shared"), flag(&params, "retired"))
636 + wrote(state, flag(&request, "shared"), flag(&request, "retired"))
621 637 }
622 638
623 639 /// The projects screen's routes.
@@ -61,8 +61,8 @@
61 61 //! no carrying: a write answers with the section it happened in, and the
62 62 //! handler knows which that is from the key.
63 63
64 - // Handlers take their params by value because `quasi_router::Handler` is a
65 - // plain `fn(&S, Params)` pointer, so the signature is the router's and not a
64 + // Handlers take their request by value because `quasi_router::Handler` is a
65 + // plain `fn(&S, Request)` pointer, so the signature is the router's and not a
66 66 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
67 67 #![allow(clippy::needless_pass_by_value)]
68 68
@@ -379,13 +379,14 @@
379 379 }
380 380
381 381 /// Appearance, which is where the JS opens.
382 - fn index(state: &AppState, _params: quasi_router::Params) -> Result<Response, RouteError> {
382 + fn index(state: &AppState, _request: quasi_router::Request) -> Result<Response, RouteError> {
383 383 Ok(screen(state, "appearance")?.into())
384 384 }
385 385
386 386 /// One section.
387 - fn section(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
388 - let slug = params
387 + fn section(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
388 + let slug = request
389 + .captures
389 390 .get("section")
390 391 .ok_or_else(|| RouteError::not_found("no section"))?;
391 392 Ok(screen(state, slug)?.into())
@@ -407,12 +408,14 @@
407 408 /// this screen, so an undeclared key or a missing value is a bug in the
408 409 /// description and not in what the user chose. `Field::error` is for the other
409 410 /// case, which this screen does not have โ€” nothing here is typed.
410 - fn set(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
411 - let key = params
411 + fn set(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
412 + let key = request
413 + .captures
412 414 .get("key")
413 415 .ok_or_else(|| RouteError::not_found("no config key"))?
414 416 .to_owned();
415 - let value = params
417 + let value = request
418 + .payload
416 419 .get(Node::SELECTED)
417 420 .ok_or_else(|| RouteError::internal("the control sent no value"))?;
418 421
@@ -29,8 +29,8 @@
29 29 //! second arrangement this screen would have to describe before it could offer
30 30 //! it. Recorded rather than faked.
31 31
32 - // Handlers take their params by value because `quasi_router::Handler` is a
33 - // plain `fn(&S, Params)` pointer, so the signature is the router's and not a
32 + // Handlers take their request by value because `quasi_router::Handler` is a
33 + // plain `fn(&S, Request)` pointer, so the signature is the router's and not a
34 34 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
35 35 #![allow(clippy::needless_pass_by_value)]
36 36
@@ -81,8 +81,9 @@
81 81 }
82 82
83 83 /// The task a route was addressed at.
84 - fn task_id(params: &quasi_router::Params) -> Result<TaskId, RouteError> {
85 - let raw = params
84 + fn task_id(request: &quasi_router::Request) -> Result<TaskId, RouteError> {
85 + let raw = request
86 + .captures
86 87 .get("id")
87 88 .ok_or_else(|| RouteError::not_found("no task id"))?;
88 89 Ok(TaskId::from(
@@ -520,8 +521,8 @@
520 521 }
521 522
522 523 /// The whole overview.
523 - fn overview(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
524 - Ok(screen(state, task_id(&params)?)?.into())
524 + fn overview(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
525 + Ok(screen(state, task_id(&request)?)?.into())
525 526 }
526 527
527 528 /// Answer a write with the screen it happened on, re-read.
@@ -540,8 +541,8 @@
540 541 /// this does not branch on recurrence. Answering with the same address then
541 542 /// shows the completed instance rather than the new one, which matches what the
542 543 /// JS does: it re-opens the task it was showing.
543 - fn complete(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
544 - let id = task_id(&params)?;
544 + fn complete(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
545 + let id = task_id(&request)?;
545 546 state
546 547 .tasks
547 548 .complete(id, DESKTOP_USER_ID)
@@ -571,8 +572,8 @@
571 572 /// the same whether a task was deleted or the user simply navigated. The two
572 573 /// were filed apart and had to land together: one enum member could not be
573 574 /// both.
574 - fn remove(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
575 - let id = task_id(&params)?;
575 + fn remove(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
576 + let id = task_id(&request)?;
576 577 let deleted = state
577 578 .tasks
578 579 .delete(id, DESKTOP_USER_ID)
@@ -588,9 +589,9 @@
588 589 }
589 590
590 591 /// Add a subtask.
591 - fn add_subtask(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
592 - let id = task_id(&params)?;
593 - let text = params.get("text").unwrap_or_default().trim();
592 + fn add_subtask(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
593 + let id = task_id(&request)?;
594 + let text = request.payload.get("text").unwrap_or_default().trim();
594 595 // An empty add is the JS's early return, not an error: the user pressed the
595 596 // button with nothing typed and the screen should simply not change.
596 597 if !text.is_empty() {
@@ -604,9 +605,13 @@
604 605 }
605 606
606 607 /// Tick or untick a subtask.
607 - fn toggle_subtask(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
608 - let id = task_id(&params)?;
609 - let raw = params
608 + fn toggle_subtask(
609 + state: &AppState,
610 + request: quasi_router::Request,
611 + ) -> Result<Response, RouteError> {
612 + let id = task_id(&request)?;
613 + let raw = request
614 + .captures
610 615 .get("sub")
611 616 .ok_or_else(|| RouteError::not_found("no subtask id"))?;
612 617 let sub = goingson_core::SubtaskId::from(
@@ -621,9 +626,9 @@
621 626 }
622 627
623 628 /// Add a note.
624 - fn add_note(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
625 - let id = task_id(&params)?;
626 - let note = params.get("note").unwrap_or_default().trim();
629 + fn add_note(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
630 + let id = task_id(&request)?;
631 + let note = request.payload.get("note").unwrap_or_default().trim();
627 632 if !note.is_empty() {
628 633 state
629 634 .tasks
@@ -41,8 +41,8 @@
41 41 //! moves the user to the current week and writes there. [`in_week`] is that,
42 42 //! applied to all five routes and to both arrows.
43 43
44 - // Handlers take their params by value because `quasi_router::Handler` is a
45 - // plain `fn(&S, Params)` pointer, so the signature is the router's and not a
44 + // Handlers take their request by value because `quasi_router::Handler` is a
45 + // plain `fn(&S, Request)` pointer, so the signature is the router's and not a
46 46 // choice made here. Same allow, for the same reason, as quasi-axum's tests.
47 47 #![allow(clippy::needless_pass_by_value)]
48 48
@@ -90,8 +90,9 @@
90 90 /// a bad value is a client bug worth reporting, and here it is a hand-typed
91 91 /// address, where landing on this week is the more useful answer than an error
92 92 /// page.
93 - fn week_of(params: &quasi_router::Params) -> NaiveDate {
94 - params
93 + fn week_of(request: &quasi_router::Request) -> NaiveDate {
94 + request
95 + .carried
95 96 .get("week")
96 97 .and_then(weekly_review::parse_week_start)
97 98 .unwrap_or_else(weekly_review::current_week_start)
@@ -99,7 +100,7 @@
99 100
100 101 /// The same action, still pointed at the week it was offered under.
101 102 fn in_week(action: Action, week: NaiveDate) -> Action {
102 - action.with("week", week.to_string())
103 + action.carrying("week", week.to_string())
103 104 }
104 105
105 106 /// Read the week.
@@ -597,8 +598,8 @@
597 598 }
598 599
599 600 /// The whole review.
600 - fn review(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
601 - Ok(screen(state, week_of(&params))?.into())
601 + fn review(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
602 + Ok(screen(state, week_of(&request))?.into())
602 603 }
603 604
604 605 /// Put a task in the week's focus, or take it out.
@@ -614,29 +615,30 @@
614 615 /// JS has the same shape and the same hole; naming it here is what the port is
615 616 /// for. Filed as a finding on the goingson task rather than on quasicoherent:
616 617 /// nothing about the description layer is involved.
617 - fn set_focus(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
618 - let raw = params
618 + fn set_focus(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
619 + let raw = request
620 + .captures
619 621 .get("id")
620 622 .ok_or_else(|| RouteError::not_found("no task id"))?;
621 623 let id = goingson_core::TaskId::from(
622 624 uuid::Uuid::parse_str(raw).map_err(|_| RouteError::not_found("not a task id"))?,
623 625 );
624 - let on = params.get("focus") == Some("true");
626 + let on = request.payload.get("focus") == Some("true");
625 627 state
626 628 .tasks
627 629 .set_focus(id, DESKTOP_USER_ID, on)
628 630 .map_err(|error| RouteError::internal(error.to_string()))?
629 631 .ok_or_else(|| RouteError::not_found("no such task"))?;
630 - wrote(state, week_of(&params))
632 + wrote(state, week_of(&request))
631 633 }
632 634
633 635 /// Take everything out of the week's focus.
634 - fn clear_focus(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
636 + fn clear_focus(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
635 637 state
636 638 .tasks
637 639 .clear_all_focus(DESKTOP_USER_ID)
638 640 .map_err(|error| RouteError::internal(error.to_string()))?;
639 - wrote(state, week_of(&params))
641 + wrote(state, week_of(&request))
640 642 }
641 643
642 644 /// Mark a weekday off, or on again.
@@ -645,9 +647,13 @@
645 647 /// write. That is the one place on this screen where two windows on the same
646 648 /// week can lose an edit, and it is the storage's shape rather than the
647 649 /// description's: `set_vacation_days` has no single-day form to call.
648 - fn toggle_vacation(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
649 - let week = week_of(&params);
650 - let day: u8 = params
650 + fn toggle_vacation(
651 + state: &AppState,
652 + request: quasi_router::Request,
653 + ) -> Result<Response, RouteError> {
654 + let week = week_of(&request);
655 + let day: u8 = request
656 + .captures
651 657 .get("day")
652 658 .and_then(|raw| raw.parse().ok())
653 659 .filter(|day| usize::from(*day) < WEEKDAYS.len())
@@ -676,10 +682,10 @@
676 682 /// is what the JS's two `if` guards do. Both empty leaves the notes empty, and
677 683 /// the review is still marked reviewed: the completion is the act, and the
678 684 /// writing is optional.
679 - fn complete(state: &AppState, params: quasi_router::Params) -> Result<Response, RouteError> {
680 - let week = week_of(&params);
681 - let went_well = params.get("went-well").unwrap_or_default().trim();
682 - let improve = params.get("improve").unwrap_or_default().trim();
685 + fn complete(state: &AppState, request: quasi_router::Request) -> Result<Response, RouteError> {
686 + let week = week_of(&request);
687 + let went_well = request.payload.get("went-well").unwrap_or_default().trim();
688 + let improve = request.payload.get("improve").unwrap_or_default().trim();
683 689
684 690 let mut notes = String::new();
685 691 if !went_well.is_empty() {
@@ -10,7 +10,7 @@
10 10 use goingson_core::{NewContact, NewContactEmail, NewSocialHandle};
11 11 use quasi_http::Render as _;
12 12 use quasi_router::Outcome;
13 - use quasi_router::{Method, Params, Response};
13 + use quasi_router::{Params, Request, Response};
14 14
15 15 use super::super::router;
16 16 use crate::state::{AppState, DESKTOP_USER_ID};
@@ -62,13 +62,13 @@
62 62
63 63 fn get(state: &AppState, path: &str, params: Params) -> Response {
64 64 router()
65 - .handle(state, Method::Get, path, params)
65 + .handle(state, Request::get(path).carrying(params))
66 66 .expect("the route answers")
67 67 }
68 68
69 69 fn post(state: &AppState, path: &str) -> Response {
70 70 router()
71 - .handle(state, Method::Post, path, Params::new())
71 + .handle(state, Request::post(path))
72 72 .expect("the route answers")
73 73 }
74 74
@@ -222,7 +222,7 @@
222 222 // The tag is a token, in the token strip, and clicking it filters the grid.
223 223 assert!(html.contains("class=\"row-tokens\""));
224 224 assert!(html.contains("friend"));
225 - assert!(html.contains("hx-vals=\"{&quot;tag&quot;:&quot;friend&quot;}\""));
225 + assert!(html.contains("hx-get=\"/contacts/list?tag=friend\""));
226 226 // And no longer joined into one string.
227 227 assert!(!html.contains("ada@example.com ยท friend"));
228 228 }
@@ -368,9 +368,7 @@
368 368 let error = router()
369 369 .handle(
370 370 &state,
371 - Method::Get,
372 - &format!("/contacts/{}", uuid::Uuid::nil()),
373 - Params::new(),
371 + Request::get(format!("/contacts/{}", uuid::Uuid::nil())),
374 372 )
375 373 .expect_err("no such contact");
376 374 assert_eq!(error.class.http_status(), 404);
@@ -380,7 +378,7 @@
380 378 async fn an_id_that_is_not_a_uuid_is_a_not_found_rather_than_a_panic() {
381 379 let state = state().await;
382 380 let error = router()
383 - .handle(&state, Method::Get, "/contacts/nonsense", Params::new())
381 + .handle(&state, Request::get("/contacts/nonsense"))
384 382 .expect_err("not an id");
385 383 assert_eq!(error.class.http_status(), 404);
386 384 }