Skip to main content

max / quasi

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