//! What arrives: a verb, a path, and a flat bag of named values. //! //! Decision 2 on the wiki note is that an action is a route, so reads and //! mutations share one address space and the verb is what separates them. //! //! # Why there are four, when this said for months there would only ever be two //! //! The premise was right and the conclusion did not follow. A terminal has no //! opinion, and it does not need one: it reads [`Method::mutates`] and binds a //! key. But **a public HTTP server's verbs are part of its interface**, and a //! description that cannot name them cannot address it. Measured across the MNW //! server's templates: 53 write sites use `hx-delete` or `hx-put`, in 34 files, //! and 45 of the tabs waiting to be described have one. The first tab that was //! described posted its Remove to a `/delete` path invented to avoid this, and //! that path was never registered, so the button rendered correctly and //! answered 404. //! //! The objection that this leaks HTTP into a host-agnostic vocabulary is //! answered by what an [`Action`](crate::Action) already is: //! [`Destination::Route`](crate::screen::Destination::Route) carries a path, //! which is exactly as HTTP-shaped as a verb, and `Get` and `Post` were here //! from the start. The alternative considered and rejected was naming *intent* //! (`remove`, `replace`) and letting each renderer map it, which breaks on any //! route whose verb disagrees with its intent. This server has those: //! `POST /api/items/bulk/delete`. use crate::error::RouteError; /// Whether the request is asking or telling. /// /// [`Method::Get`] is the default, because the safe verb is the one a partly /// built value should have: a control that forgot to say it mutates asks /// instead of telling. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)] pub enum Method { /// Asking. Answers with a description and changes nothing. #[default] Get, /// Telling. Performs, then answers with the next description. Post, /// Telling, where what is told is that the thing at the address goes away. Delete, /// Telling, where what is told is the thing the address should hold now. Put, } impl Method { /// Whether the route is allowed to change anything. /// /// The question every non-HTTP host asks, and the only one it has to. A /// terminal binding a key, an egui frame drawing a button and a renderer /// deciding between an anchor and a button all read this rather than the /// verb, which is why adding two verbs costs those hosts nothing. #[must_use] pub const fn mutates(self) -> bool { matches!(self, Self::Post | Self::Delete | Self::Put) } /// The name an HTTP host knows it by. #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Get => "GET", Self::Post => "POST", Self::Delete => "DELETE", Self::Put => "PUT", } } } impl std::fmt::Display for Method { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) } } /// The named values a request carries. /// /// One bag, holding path captures and whatever the host handed over. Query /// string and form body are the same thing by the time they get here, which is /// what lets a terminal call a route with no notion of either. /// /// A `Vec` rather than a map, because it keeps insertion order and repeats a /// name, and both matter: a checkbox group submits one name several times, and /// dropping the repeats silently is a bug that only shows up on the screen with /// the multi-select on it. Lookup is linear over a handful of entries. /// /// # Decoding is the host's job /// /// Values arrive already decoded. quasi does not percent-decode, parse a query /// string or read a form body, because every host we target already has that /// code and ours would be a second implementation to keep correct. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Params { entries: Vec<(String, String)>, } impl Params { /// No values. #[must_use] pub const fn new() -> Self { Self { entries: Vec::new(), } } /// Add a value. Does not replace an existing one of the same name. pub fn insert(&mut self, name: impl Into, value: impl Into) { self.entries.push((name.into(), value.into())); } /// Add a value, chaining. #[must_use] pub fn with(mut self, name: impl Into, value: impl Into) -> Self { self.insert(name, value); self } /// The first value under this name. #[must_use] pub fn get(&self, name: &str) -> Option<&str> { self.entries .iter() .find(|(k, _)| k == name) .map(|(_, v)| v.as_str()) } /// Every value under this name, in the order they arrived. pub fn get_all<'a>(&'a self, name: &'a str) -> impl Iterator { self.entries .iter() .filter(move |(k, _)| k == name) .map(|(_, v)| v.as_str()) } /// Every answer to a repeating question, in slot order. /// /// The reader for the names [`Repeat::at`] writes: a question asked N /// times arrives as `name[0]`, `name[1]`, and this is what turns those /// back into a list. A handler writing `params.repeated("reminder")` gets /// what `Event.reminder_offsets_seconds` is a `Vec` of. /// /// Sorted by index rather than taken in arrival order, so a host that /// reorders a form body cannot reorder the answers. Holes are closed up /// silently: a browser that removed the middle slot may send `0` and `2`, /// and the answers are still the first and the second answer. That is the /// half the index buys over N values under one bare name, which /// [`get_all`](Self::get_all) already reads: an index survives an empty /// slot, so an error reported against the third answer comes back to the /// third box. /// /// Empty for a question nobody answered, which is a repeating question the /// reader removed every slot of and is a real answer rather than a missing /// one. /// /// [`Repeat::at`]: crate::Repeat::at #[must_use] pub fn repeated(&self, name: &str) -> Vec<&str> { let mut found: Vec<(usize, &str)> = self .entries .iter() .filter_map(|(key, value)| { let (question, at) = crate::Repeat::instance_of(key)?; (question == name).then_some((at, value.as_str())) }) .collect(); found.sort_by_key(|(at, _)| *at); found.into_iter().map(|(_, value)| value).collect() } /// Every answer to one named part of a repeating question, in slot order. /// /// [`repeated`](Self::repeated) for a slot that is several questions rather /// than one: the reader for the names [`Repeat::part_at`] writes, so /// `params.repeated_part("file", "size")` is every size in the queue, in /// the order the slots stand. /// /// Holes are closed up and the order is by index, both for /// [`repeated`](Self::repeated)'s reasons. A slot that answered some parts /// and not others is not padded here: this reads one part, and a caller /// rebuilding whole slots asks for each part and pairs them by position, /// which is only sound when the host sends every part of every slot. That /// is what the renderers emit, empty values included. /// /// [`Repeat::part_at`]: crate::Repeat::part_at #[must_use] pub fn repeated_part(&self, name: &str, part: &str) -> Vec<&str> { let mut found: Vec<(usize, &str)> = self .entries .iter() .filter_map(|(key, value)| { let (question, at, which) = crate::Repeat::part_of(key)?; (question == name && which == part).then_some((at, value.as_str())) }) .collect(); found.sort_by_key(|(at, _)| *at); found.into_iter().map(|(_, value)| value).collect() } /// The first value under this name, or a failure the host can act on. /// /// [`Class::Internal`](crate::Class), because the caller is our own emitted /// markup or our own key binding. A missing parameter means the renderer /// emitted a route it did not fill in, which is our bug rather than the /// user's, and reporting it as a bad request would file it against them. pub fn require(&self, name: &str) -> Result<&str, RouteError> { self.get(name) .ok_or_else(|| RouteError::internal(format!("route parameter `{name}` is missing"))) } /// Take everything from another bag, keeping what is already here in front. /// /// How the router merges path captures with what the host sent. Order is /// the whole content of the method: [`Params::get`] answers with the first /// match, so whatever is already here wins a collision. pub fn absorb(&mut self, other: Self) { self.entries.extend(other.entries); } /// Whether anything is under this name. #[must_use] pub fn contains(&self, name: &str) -> bool { self.get(name).is_some() } /// Every name and value, in order. pub fn iter(&self) -> impl Iterator { self.entries.iter().map(|(k, v)| (k.as_str(), v.as_str())) } /// How many values there are. Repeats count separately. #[must_use] pub fn len(&self) -> usize { self.entries.len() } /// Whether there are none. #[must_use] pub fn is_empty(&self) -> bool { self.entries.is_empty() } } impl FromIterator<(K, V)> for Params where K: Into, V: Into, { fn from_iter>(iter: I) -> Self { Self { entries: iter .into_iter() .map(|(k, v)| (k.into(), v.into())) .collect(), } } } /// Everything a handler is given: a verb, a path, and three bags of values. /// /// # Why three bags and not one /// /// Such a screen sends its filters on every control, and a write sends its own /// values; both arrived here under one namespace with nothing separating them. /// A screen filtering on `status` that also writes a `status` then had two /// meanings for one name, and the handler read whichever landed first. /// goingson's mail screen met it twice in an afternoon and its problems inbox /// met it again the same day, each time working around it by renaming the /// write's parameter — a convention held by hand, in one app, by whoever /// remembered. /// /// The split is not invented for this. HTTP already draws it and the adapters /// already had both halves in their hands before merging them: the address is /// where you are, the body is what you are telling it. /// /// - [`Self::captures`] — named pieces of the path pattern. The route's own, and /// not something anyone sent. /// - [`Self::payload`] — what this control sent. A write's values. Empty on a /// read, because a read has nothing to say: its values *are* its address. /// - [`Self::carried`] — the view the control was offered under. The query /// string. /// /// So the rule a screen needs is one sentence: **a filter is read from /// `carried`, a write's target from `payload`.** A name in both is /// well-defined rather than a collision. /// /// There is no `get` on this type on purpose. A single accessor searching all /// three would put the three bags back into one, and the compiler forcing every /// read site to name its bag is most of what the split buys. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Request { /// Asking or telling. pub method: Method, /// The path, with no scheme, host or query on it. pub path: String, /// Values captured out of the path pattern, filled in by the router. pub captures: Params, /// What the control sent. Empty on a read. pub payload: Params, /// The view the control was offered under. pub carried: Params, } impl Request { /// A read of a path, carrying nothing. pub fn get(path: impl Into) -> Self { Self { method: Method::Get, path: path.into(), ..Self::default() } } /// A write to a path, sending nothing. pub fn post(path: impl Into) -> Self { Self { method: Method::Post, path: path.into(), ..Self::default() } } /// The values this control sent, chaining. #[must_use] pub fn sending(mut self, payload: Params) -> Self { self.payload = payload; self } /// The view it was offered under, chaining. #[must_use] pub fn carrying(mut self, carried: Params) -> Self { self.carried = carried; self } }