max / quasi
| 1 | //! Failure, classified once so every host renders it the same way. |
| 2 | //! |
| 3 | //! Decision 9 on the wiki note. A handler that cannot do what was asked returns |
| 4 | //! a [`RouteError`] rather than a [`Response`](crate::Response) describing the |
| 5 | //! problem. Two things fall out of that, and both are the reason the type |
| 6 | //! exists. |
| 7 | //! |
| 8 | //! A class is not a message. The class is what the *host* acts on: axum turns |
| 9 | //! it into a status code, the terminal and egui turn it into a banner. Handing |
| 10 | //! back a plain response instead would have hosted axum answer 200 to |
| 11 | //! everything, so caches, logs and monitoring could not tell a denied action |
| 12 | //! from a completed one. |
| 13 | //! |
| 14 | //! The message is already UI. It carries a `makeover-layout` [`Notice`], so |
| 15 | //! errors become something on screen in one place rather than once per handler. |
| 16 | |
| 17 | use ; |
| 18 | |
| 19 | use crateMethod; |
| 20 | |
| 21 | /// What kind of failure it was. |
| 22 | /// |
| 23 | /// Five, and each one is a different thing for a host to do. `NotFound` and |
| 24 | /// `Denied` are separate because a host that conflates them cannot answer the |
| 25 | /// question its own access log asks. `Conflict` is separate because it is the |
| 26 | /// one failure the user can fix by looking at the screen again, which is a |
| 27 | /// banner rather than a toast. `Unsupported` is separate because the address |
| 28 | /// is real and only the verb is wrong, which is a different sentence and a |
| 29 | /// different status code. |
| 30 | /// |
| 31 | /// `#[non_exhaustive]` for the reason `makeover-layout` puts it on the |
| 32 | /// vocabularies renderers match against: growth must not be a lockstep event |
| 33 | /// across every host adapter. |
| 34 | |
| 35 | |
| 36 | |
| 37 | /// The thing addressed is not there. |
| 38 | NotFound, |
| 39 | /// The thing is there and this caller may not have it. |
| 40 | Denied, |
| 41 | /// The request was answerable and the current state refuses it. |
| 42 | Conflict, |
| 43 | /// The address is there and does not take this verb. |
| 44 | /// |
| 45 | /// Measured against MNW's `/pricing`. Distinct from |
| 46 | /// [`NotFound`](Self::NotFound), which says the address does not exist: |
| 47 | /// answering that for a page a host serves at that URL tells a crawler the |
| 48 | /// page is gone. What does not exist is the verb, and the verbs that do is |
| 49 | /// information the caller can act on -- see [`RouteError::allow`]. |
| 50 | Unsupported, |
| 51 | /// We are broken. |
| 52 | Internal, |
| 53 | |
| 54 | |
| 55 | |
| 56 | /// What the failure is saying, in `makeover-layout`'s vocabulary. |
| 57 | /// |
| 58 | /// Only `Internal` is [`Tone::Danger`]. The other three are conditions the |
| 59 | /// user can understand and often act on, and spending the loudest tone on |
| 60 | /// all of them is how a `Danger` stops meaning anything. |
| 61 | |
| 62 | pub const |
| 63 | match self |
| 64 | SelfNotFound | SelfDenied | SelfConflict | SelfUnsupported => Warning, |
| 65 | SelfInternal => Danger, |
| 66 | |
| 67 | |
| 68 | |
| 69 | /// The status code an HTTP host answers with. |
| 70 | /// |
| 71 | /// A convenience rather than a leak: this is a `u16`, no host crate is |
| 72 | /// imported to produce it, and every HTTP adapter we will write would |
| 73 | /// otherwise hand-roll the same four-arm match. Hosts with no notion of a |
| 74 | /// status code ignore it and read [`Class`] directly. |
| 75 | |
| 76 | pub const |
| 77 | match self |
| 78 | SelfNotFound => 404, |
| 79 | SelfDenied => 403, |
| 80 | SelfUnsupported => 405, |
| 81 | SelfConflict => 409, |
| 82 | SelfInternal => 500, |
| 83 | |
| 84 | |
| 85 | |
| 86 | /// Whether the failure is ours rather than the caller's. |
| 87 | /// |
| 88 | /// The line a host logs on. A `NotFound` at volume is a broken link |
| 89 | /// somewhere; an `Internal` at any volume is a page. |
| 90 | |
| 91 | pub const |
| 92 | matches! |
| 93 | |
| 94 | |
| 95 | |
| 96 | /// A route that could not answer. |
| 97 | /// |
| 98 | /// Carries a class for the host and a notice for the screen. The notice |
| 99 | /// defaults to [`Notice::Banner`], because a failure is persistent by nature: |
| 100 | /// it is dismissed by fixing the condition that caused it, which is exactly |
| 101 | /// what `makeover-layout` says a banner is for. Call [`RouteError::as_toast`] |
| 102 | /// where the failure really is transient. |
| 103 | |
| 104 | |
| 105 | /// What kind of failure it was. |
| 106 | pub class: Class, |
| 107 | /// How the message sits on screen. |
| 108 | pub notice: Notice, |
| 109 | /// What to tell the user. Already user-facing text, not a debug string. |
| 110 | pub message: String, |
| 111 | /// The verbs the address does take, for a |
| 112 | /// [`Class::Unsupported`](Class::Unsupported). |
| 113 | /// |
| 114 | /// Empty for every other class, and a caller should read it as "no claim |
| 115 | /// is being made" rather than "no verbs". It rides on the error rather |
| 116 | /// than being asked of the router afterwards because the two adapters that |
| 117 | /// need it are not both holding one: `quasi_http::respond` is handed a |
| 118 | /// `Result` and no table. |
| 119 | /// |
| 120 | /// Sorted and deduplicated by [`unsupported`](Self::unsupported), so |
| 121 | /// `Allow` reads the same however the routes were registered. |
| 122 | pub allow: , |
| 123 | |
| 124 | |
| 125 | |
| 126 | /// A failure of the given class, shown as a banner. |
| 127 | |
| 128 | Self |
| 129 | class, |
| 130 | notice: Banner, |
| 131 | message: message.into, |
| 132 | allow: Vecnew, |
| 133 | |
| 134 | |
| 135 | |
| 136 | /// The address is there and does not take this verb. |
| 137 | /// |
| 138 | /// `allow` is what it does take, and is what an HTTP host puts in the |
| 139 | /// header of the same name. Sorted and deduplicated here so that the |
| 140 | /// answer does not depend on registration order. |
| 141 | |
| 142 | message: impl , |
| 143 | allow: impl , |
| 144 | |
| 145 | let mut allow: = allow.into_iter.collect; |
| 146 | allow.sort_unstable; |
| 147 | allow.dedup; |
| 148 | Self |
| 149 | allow, |
| 150 | ..Selfnew |
| 151 | |
| 152 | |
| 153 | |
| 154 | /// The `Allow` header's value, or `None` when nothing is being claimed. |
| 155 | /// |
| 156 | /// Here rather than in each adapter because both HTTP hosts want the same |
| 157 | /// string, and because the comma-and-space spelling is the one RFC 9110 |
| 158 | /// gives. A host with no notion of a header ignores it and reads |
| 159 | /// [`allow`](Self::allow). |
| 160 | |
| 161 | |
| 162 | if self.allow.is_empty |
| 163 | return None; |
| 164 | |
| 165 | Some |
| 166 | self.allow |
| 167 | .iter |
| 168 | .map |
| 169 | . |
| 170 | .join, |
| 171 | |
| 172 | |
| 173 | |
| 174 | /// The thing addressed is not there. |
| 175 | |
| 176 | Selfnew |
| 177 | |
| 178 | |
| 179 | /// The caller may not have it. |
| 180 | |
| 181 | Selfnew |
| 182 | |
| 183 | |
| 184 | /// The current state refuses the request. |
| 185 | |
| 186 | Selfnew |
| 187 | |
| 188 | |
| 189 | /// We are broken. |
| 190 | |
| 191 | Selfnew |
| 192 | |
| 193 | |
| 194 | /// The same failure, shown as a toast instead of a banner. |
| 195 | |
| 196 | |
| 197 | self.notice = Toast; |
| 198 | self |
| 199 | |
| 200 | |
| 201 | /// What the failure is saying. Delegates to the class. |
| 202 | |
| 203 | pub const |
| 204 | self.class.tone |
| 205 | |
| 206 | |
| 207 | |
| 208 | |
| 209 | |
| 210 | write! |
| 211 | |
| 212 | |
| 213 | |
| 214 | |
| 215 |