//! 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 //! //! `61e1b069`, decided 2026-08-10. The old text here read: "There is no third //! member and there is not going to be one: `PUT`, `PATCH` and `DELETE` are //! HTTP's vocabulary, and a terminal binding a key to a route has no opinion //! about which of them a deletion is." //! //! 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()) } /// 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 /// /// One bag was the shape until 2026-08-10, and it could not answer the question /// every screen that carries its view in the address ends up asking. 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 now /// well-defined rather than a collision, which is the property that retires the /// naming convention. /// /// There is no `get` on this type on purpose. A single accessor that searched /// all three would be the old bag again with more steps, and the compiler /// forcing every read site to name its bag is most of what this change 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 } }