Skip to main content

max / quasi

10.3 KB · 280 lines History Blame Raw
1 //! What arrives: a verb, a path, and a flat bag of named values.
2 //!
3 //! Decision 2 on the wiki note is that an action is a route, so reads and
4 //! mutations share one address space and the verb is what separates them.
5 //!
6 //! # Why there are four, when this said for months there would only ever be two
7 //!
8 //! `61e1b069`, decided 2026-08-10. The old text here read: "There is no third
9 //! member and there is not going to be one: `PUT`, `PATCH` and `DELETE` are
10 //! HTTP's vocabulary, and a terminal binding a key to a route has no opinion
11 //! about which of them a deletion is."
12 //!
13 //! The premise was right and the conclusion did not follow. A terminal has no
14 //! opinion, and it does not need one: it reads [`Method::mutates`] and binds a
15 //! key. But **a public HTTP server's verbs are part of its interface**, and a
16 //! description that cannot name them cannot address it. Measured across the MNW
17 //! server's templates: 53 write sites use `hx-delete` or `hx-put`, in 34 files,
18 //! and 45 of the tabs waiting to be described have one. The first tab that was
19 //! described posted its Remove to a `/delete` path invented to avoid this, and
20 //! that path was never registered, so the button rendered correctly and
21 //! answered 404.
22 //!
23 //! The objection that this leaks HTTP into a host-agnostic vocabulary is
24 //! answered by what an [`Action`](crate::Action) already is:
25 //! [`Destination::Route`](crate::screen::Destination::Route) carries a path,
26 //! which is exactly as HTTP-shaped as a verb, and `Get` and `Post` were here
27 //! from the start. The alternative considered and rejected was naming *intent*
28 //! (`remove`, `replace`) and letting each renderer map it, which breaks on any
29 //! route whose verb disagrees with its intent. This server has those:
30 //! `POST /api/items/bulk/delete`.
31
32 use crate::error::RouteError;
33
34 /// Whether the request is asking or telling.
35 ///
36 /// [`Method::Get`] is the default, because the safe verb is the one a partly
37 /// built value should have: a control that forgot to say it mutates asks
38 /// instead of telling.
39 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
40 pub enum Method {
41 /// Asking. Answers with a description and changes nothing.
42 #[default]
43 Get,
44 /// Telling. Performs, then answers with the next description.
45 Post,
46 /// Telling, where what is told is that the thing at the address goes away.
47 Delete,
48 /// Telling, where what is told is the thing the address should hold now.
49 Put,
50 }
51
52 impl Method {
53 /// Whether the route is allowed to change anything.
54 ///
55 /// The question every non-HTTP host asks, and the only one it has to. A
56 /// terminal binding a key, an egui frame drawing a button and a renderer
57 /// deciding between an anchor and a button all read this rather than the
58 /// verb, which is why adding two verbs costs those hosts nothing.
59 #[must_use]
60 pub const fn mutates(self) -> bool {
61 matches!(self, Self::Post | Self::Delete | Self::Put)
62 }
63
64 /// The name an HTTP host knows it by.
65 #[must_use]
66 pub const fn as_str(self) -> &'static str {
67 match self {
68 Self::Get => "GET",
69 Self::Post => "POST",
70 Self::Delete => "DELETE",
71 Self::Put => "PUT",
72 }
73 }
74 }
75
76 impl std::fmt::Display for Method {
77 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78 f.write_str(self.as_str())
79 }
80 }
81
82 /// The named values a request carries.
83 ///
84 /// One bag, holding path captures and whatever the host handed over. Query
85 /// string and form body are the same thing by the time they get here, which is
86 /// what lets a terminal call a route with no notion of either.
87 ///
88 /// A `Vec` rather than a map, because it keeps insertion order and repeats a
89 /// name, and both matter: a checkbox group submits one name several times, and
90 /// dropping the repeats silently is a bug that only shows up on the screen with
91 /// the multi-select on it. Lookup is linear over a handful of entries.
92 ///
93 /// # Decoding is the host's job
94 ///
95 /// Values arrive already decoded. quasi does not percent-decode, parse a query
96 /// string or read a form body, because every host we target already has that
97 /// code and ours would be a second implementation to keep correct.
98 #[derive(Debug, Clone, Default, PartialEq, Eq)]
99 pub struct Params {
100 entries: Vec<(String, String)>,
101 }
102
103 impl Params {
104 /// No values.
105 #[must_use]
106 pub const fn new() -> Self {
107 Self {
108 entries: Vec::new(),
109 }
110 }
111
112 /// Add a value. Does not replace an existing one of the same name.
113 pub fn insert(&mut self, name: impl Into<String>, value: impl Into<String>) {
114 self.entries.push((name.into(), value.into()));
115 }
116
117 /// Add a value, chaining.
118 #[must_use]
119 pub fn with(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
120 self.insert(name, value);
121 self
122 }
123
124 /// The first value under this name.
125 #[must_use]
126 pub fn get(&self, name: &str) -> Option<&str> {
127 self.entries
128 .iter()
129 .find(|(k, _)| k == name)
130 .map(|(_, v)| v.as_str())
131 }
132
133 /// Every value under this name, in the order they arrived.
134 pub fn get_all<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> {
135 self.entries
136 .iter()
137 .filter(move |(k, _)| k == name)
138 .map(|(_, v)| v.as_str())
139 }
140
141 /// The first value under this name, or a failure the host can act on.
142 ///
143 /// [`Class::Internal`](crate::Class), because the caller is our own emitted
144 /// markup or our own key binding. A missing parameter means the renderer
145 /// emitted a route it did not fill in, which is our bug rather than the
146 /// user's, and reporting it as a bad request would file it against them.
147 pub fn require(&self, name: &str) -> Result<&str, RouteError> {
148 self.get(name)
149 .ok_or_else(|| RouteError::internal(format!("route parameter `{name}` is missing")))
150 }
151
152 /// Take everything from another bag, keeping what is already here in front.
153 ///
154 /// How the router merges path captures with what the host sent. Order is
155 /// the whole content of the method: [`Params::get`] answers with the first
156 /// match, so whatever is already here wins a collision.
157 pub fn absorb(&mut self, other: Self) {
158 self.entries.extend(other.entries);
159 }
160
161 /// Whether anything is under this name.
162 #[must_use]
163 pub fn contains(&self, name: &str) -> bool {
164 self.get(name).is_some()
165 }
166
167 /// Every name and value, in order.
168 pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
169 self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str()))
170 }
171
172 /// How many values there are. Repeats count separately.
173 #[must_use]
174 pub fn len(&self) -> usize {
175 self.entries.len()
176 }
177
178 /// Whether there are none.
179 #[must_use]
180 pub fn is_empty(&self) -> bool {
181 self.entries.is_empty()
182 }
183 }
184
185 impl<K, V> FromIterator<(K, V)> for Params
186 where
187 K: Into<String>,
188 V: Into<String>,
189 {
190 fn from_iter<I: IntoIterator<Item = (K, V)>>(iter: I) -> Self {
191 Self {
192 entries: iter
193 .into_iter()
194 .map(|(k, v)| (k.into(), v.into()))
195 .collect(),
196 }
197 }
198 }
199
200 /// Everything a handler is given: a verb, a path, and three bags of values.
201 ///
202 /// # Why three bags and not one
203 ///
204 /// One bag was the shape until 2026-08-10, and it could not answer the question
205 /// every screen that carries its view in the address ends up asking. Such a
206 /// screen sends its filters on every control, and a write sends its own values;
207 /// both arrived here under one namespace with nothing separating them. A screen
208 /// filtering on `status` that also writes a `status` then had two meanings for
209 /// one name, and the handler read whichever landed first. goingson's mail screen
210 /// met it twice in an afternoon and its problems inbox met it again the same
211 /// day, each time working around it by renaming the write's parameter — a
212 /// convention held by hand, in one app, by whoever remembered.
213 ///
214 /// The split is not invented for this. HTTP already draws it and the adapters
215 /// already had both halves in their hands before merging them: the address is
216 /// where you are, the body is what you are telling it.
217 ///
218 /// - [`Self::captures`] — named pieces of the path pattern. The route's own, and
219 /// not something anyone sent.
220 /// - [`Self::payload`] — what this control sent. A write's values. Empty on a
221 /// read, because a read has nothing to say: its values *are* its address.
222 /// - [`Self::carried`] — the view the control was offered under. The query
223 /// string.
224 ///
225 /// So the rule a screen needs is one sentence: **a filter is read from
226 /// `carried`, a write's target from `payload`.** A name in both is now
227 /// well-defined rather than a collision, which is the property that retires the
228 /// naming convention.
229 ///
230 /// There is no `get` on this type on purpose. A single accessor that searched
231 /// all three would be the old bag again with more steps, and the compiler
232 /// forcing every read site to name its bag is most of what this change buys.
233 #[derive(Debug, Clone, Default, PartialEq, Eq)]
234 pub struct Request {
235 /// Asking or telling.
236 pub method: Method,
237 /// The path, with no scheme, host or query on it.
238 pub path: String,
239 /// Values captured out of the path pattern, filled in by the router.
240 pub captures: Params,
241 /// What the control sent. Empty on a read.
242 pub payload: Params,
243 /// The view the control was offered under.
244 pub carried: Params,
245 }
246
247 impl Request {
248 /// A read of a path, carrying nothing.
249 pub fn get(path: impl Into<String>) -> Self {
250 Self {
251 method: Method::Get,
252 path: path.into(),
253 ..Self::default()
254 }
255 }
256
257 /// A write to a path, sending nothing.
258 pub fn post(path: impl Into<String>) -> Self {
259 Self {
260 method: Method::Post,
261 path: path.into(),
262 ..Self::default()
263 }
264 }
265
266 /// The values this control sent, chaining.
267 #[must_use]
268 pub fn sending(mut self, payload: Params) -> Self {
269 self.payload = payload;
270 self
271 }
272
273 /// The view it was offered under, chaining.
274 #[must_use]
275 pub fn carrying(mut self, carried: Params) -> Self {
276 self.carried = carried;
277 self
278 }
279 }
280