web_net/render.rs
1//! Rendering: a template engine behind a trait, reading typed values
2//! through a view.
3//!
4//! No JSON sits between the data and the engine. An engine resolves the
5//! names a template uses to [`Key`]s once, when it compiles the template,
6//! and reads values by key while it renders: a match or an index per value,
7//! no allocation, no parsing. Escaping for each output context (HTML text,
8//! attribute, URL, script) is the engine's job; web-net never escapes for it.
9
10use std::fmt;
11
12/// A template the engine resolved at build time. Its meaning belongs to the
13/// engine.
14#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub struct TemplateId(pub u32);
16
17/// A template engine.
18///
19/// The view type is the engine's: an engine that reads any view through
20/// web-net's [`View`] trait (an adapter for a dynamic engine) uses
21/// `type View = dyn View`; an engine that compiles templates against the
22/// application's own types uses that type, and reads it directly.
23///
24/// ```
25/// use web_net::{Output, Render, RenderError, TemplateId, Value, View};
26///
27/// /// Writes the view's `title` field and nothing else.
28/// struct TitleOnly;
29///
30/// impl Render for TitleOnly {
31/// type View<'a> = dyn View + 'a;
32/// fn template(&self, name: &str) -> Option<TemplateId> {
33/// (name == "title").then_some(TemplateId(0))
34/// }
35/// fn render(&self, _t: TemplateId, view: &dyn View, out: &mut Output<'_>) -> Result<(), RenderError> {
36/// let key = view.shape().key("title").ok_or(RenderError::new("demo.no_title"))?;
37/// match view.get(key) {
38/// Value::Text(t) => out.write(t.as_bytes()), // a real engine escapes here
39/// _ => Err(RenderError::new("demo.not_text")),
40/// }
41/// }
42/// }
43/// ```
44pub trait Render: Send + Sync + 'static {
45 /// What a handler hands the engine, for any lifetime of the data it
46 /// borrows: `dyn View + 'a` for a dynamic engine, the application's own
47 /// type (`Page`, or `Page<'a>`) for a typed one.
48 type View<'a>: ?Sized;
49
50 /// Resolves a template name. Called at build time for every template a
51 /// route names, so an unknown name stops the application from starting.
52 fn template(&self, name: &str) -> Option<TemplateId>;
53
54 /// Renders `template` against `view` into `out`.
55 ///
56 /// # Errors
57 ///
58 /// A [`RenderError`]; the request is answered with a 500 error page and
59 /// the detail goes to the events, never to the visitor.
60 fn render(
61 &self,
62 template: TemplateId,
63 view: &Self::View<'_>,
64 out: &mut Output<'_>,
65 ) -> Result<(), RenderError>;
66
67 /// Writes the page for a refusal or an error, from the facts web-net
68 /// shows a visitor. `Ok(false)` (the default) leaves it to web-net's
69 /// built-in page; so does an error, which is not reported further.
70 ///
71 /// # Errors
72 ///
73 /// A [`RenderError`]; web-net then sends its built-in page.
74 fn error_page(&self, page: &ErrorPage<'_>, out: &mut Output<'_>) -> Result<bool, RenderError> {
75 let _ = (page, out);
76 Ok(false)
77 }
78}
79
80/// The engine of an application without one: `Cx::render` answers 500
81/// `render.no_engine`, and error pages are web-net's own.
82#[derive(Clone, Copy, Debug, Default)]
83pub struct NoRender;
84
85impl Render for NoRender {
86 type View<'a> = dyn View + 'a;
87
88 fn template(&self, _name: &str) -> Option<TemplateId> {
89 None
90 }
91
92 fn render(
93 &self,
94 _template: TemplateId,
95 _view: &(dyn View + '_),
96 _out: &mut Output<'_>,
97 ) -> Result<(), RenderError> {
98 Err(RenderError::new("render.no_engine"))
99 }
100}
101
102/// What an error page shows: the status, its generic title and message, and
103/// the request's reference. Never the code or the cause.
104#[non_exhaustive]
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub struct ErrorPage<'a> {
107 /// The status (`404`).
108 pub status: u16,
109 /// Its generic title (`Not Found`).
110 pub title: &'a str,
111 /// Its generic message.
112 pub message: &'a str,
113 /// The request's reference, as the visitor quotes it.
114 pub reference: &'a str,
115}
116
117impl<'a> ErrorPage<'a> {
118 /// An error page's facts, for an engine's own tests (web-net makes the
119 /// real ones).
120 ///
121 /// ```
122 /// let p = web_net::ErrorPage::new(404, "Not Found", "The page was not found.", "1a-2");
123 /// assert_eq!((p.status, p.reference), (404, "1a-2"));
124 /// ```
125 #[must_use]
126 pub const fn new(status: u16, title: &'a str, message: &'a str, reference: &'a str) -> Self {
127 Self {
128 status,
129 title,
130 message,
131 reference,
132 }
133 }
134}
135
136/// A field or method of a view, resolved from a name by its [`Shape`].
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
138pub struct Key(pub u32);
139
140/// Identifies a shape so an engine can cache name resolutions per shape.
141/// [`ShapeId::ANONYMOUS`] says "do not cache".
142#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
143pub struct ShapeId(pub u64);
144
145impl ShapeId {
146 /// A shape whose keys may differ between instances: resolve names per
147 /// render.
148 pub const ANONYMOUS: Self = Self(0);
149}
150
151/// The names a view answers.
152pub trait Shape {
153 /// The shape's identity. Two views with the same id resolve every name
154 /// to the same key.
155 fn id(&self) -> ShapeId;
156
157 /// The key for a field or method name.
158 fn key(&self, name: &str) -> Option<Key>;
159}
160
161/// What a template reads.
162pub trait View {
163 /// The names this view answers.
164 fn shape(&self) -> &dyn Shape;
165
166 /// A field by key. The hot path: no allocation.
167 fn get(&self, key: Key) -> Value<'_>;
168
169 /// A method with arguments (`variations.options("size")`). The result is
170 /// passed to `with`, so it may borrow a value computed for the call.
171 /// The default answers [`Value::Absent`].
172 fn call(&self, key: Key, args: &[Value<'_>], with: &mut dyn FnMut(Value<'_>)) {
173 let _ = (key, args);
174 with(Value::Absent);
175 }
176}
177
178/// A sequence a template can loop over.
179pub trait List {
180 /// The number of items.
181 fn len(&self) -> usize;
182
183 /// Whether the list is empty.
184 fn is_empty(&self) -> bool {
185 self.len() == 0
186 }
187
188 /// An item, or [`Value::Absent`] past the end.
189 fn get(&self, index: usize) -> Value<'_>;
190}
191
192/// Markup that is trusted as it is: written without escaping. Making one is
193/// a visible, searchable step (`SafeHtml::trusted`), and the place to
194/// sanitize is where the markup was produced (when content is saved), not
195/// here.
196#[derive(Clone, Copy, PartialEq, Eq)]
197pub struct SafeHtml<'a>(&'a str);
198
199impl<'a> SafeHtml<'a> {
200 /// Marks `html` as trusted markup.
201 #[must_use]
202 pub const fn trusted(html: &'a str) -> Self {
203 Self(html)
204 }
205
206 /// The markup.
207 #[must_use]
208 pub const fn as_str(&self) -> &'a str {
209 self.0
210 }
211}
212
213impl fmt::Debug for SafeHtml<'_> {
214 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215 write!(f, "SafeHtml({} bytes)", self.0.len())
216 }
217}
218
219/// One value read from a view.
220#[non_exhaustive]
221#[derive(Clone, Copy)]
222pub enum Value<'a> {
223 /// Nothing here (a missing field, an empty optional).
224 Absent,
225 /// A boolean.
226 Bool(bool),
227 /// An integer.
228 Int(i64),
229 /// A float.
230 Float(f64),
231 /// Text, escaped for its context by the engine.
232 Text(&'a str),
233 /// Trusted markup, written as it is.
234 Html(SafeHtml<'a>),
235 /// A computed value (money, a date), written as text and escaped.
236 Display(&'a dyn fmt::Display),
237 /// A nested object.
238 View(&'a dyn View),
239 /// A sequence.
240 List(&'a dyn List),
241}
242
243impl Value<'_> {
244 /// Whether this is [`Value::Absent`].
245 #[must_use]
246 pub const fn is_absent(&self) -> bool {
247 matches!(self, Self::Absent)
248 }
249
250 /// The text, for [`Value::Text`].
251 #[must_use]
252 pub const fn as_text(&self) -> Option<&str> {
253 match self {
254 Self::Text(t) => Some(t),
255 _ => None,
256 }
257 }
258}
259
260impl fmt::Debug for Value<'_> {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 match self {
263 Self::Absent => f.write_str("Absent"),
264 Self::Bool(b) => write!(f, "Bool({b})"),
265 Self::Int(i) => write!(f, "Int({i})"),
266 Self::Float(x) => write!(f, "Float({x})"),
267 Self::Text(t) => write!(f, "Text({t:?})"),
268 Self::Html(h) => write!(f, "{h:?}"),
269 Self::Display(_) => f.write_str("Display(..)"),
270 Self::View(_) => f.write_str("View(..)"),
271 Self::List(l) => write!(f, "List({} items)", l.len()),
272 }
273 }
274}
275
276/// A ready-made view over a slice of named values, for small pages, error
277/// pages, and tests. Its shape is anonymous: names are looked up per render.
278///
279/// ```
280/// use web_net::{Fields, Value, View};
281///
282/// let view = Fields(&[("title", Value::Text("Hello")), ("count", Value::Int(3))]);
283/// let key = view.shape().key("count").unwrap();
284/// assert!(matches!(view.get(key), Value::Int(3)));
285/// ```
286#[derive(Clone, Copy, Debug)]
287pub struct Fields<'a>(pub &'a [(&'a str, Value<'a>)]);
288
289impl Shape for Fields<'_> {
290 fn id(&self) -> ShapeId {
291 ShapeId::ANONYMOUS
292 }
293
294 fn key(&self, name: &str) -> Option<Key> {
295 self.0
296 .iter()
297 .position(|(n, _)| *n == name)
298 .map(|i| Key(i as u32))
299 }
300}
301
302impl View for Fields<'_> {
303 fn shape(&self) -> &dyn Shape {
304 self
305 }
306
307 fn get(&self, key: Key) -> Value<'_> {
308 self.0
309 .get(key.0 as usize)
310 .map_or(Value::Absent, |(_, v)| *v)
311 }
312}
313
314/// Where an engine writes: the reply body, reused between requests, with a
315/// size budget.
316pub struct Output<'a> {
317 buf: &'a mut Vec<u8>,
318 limit: usize,
319}
320
321impl<'a> Output<'a> {
322 /// An output over `buf`, refusing to grow past `limit` bytes.
323 #[must_use]
324 pub fn new(buf: &'a mut Vec<u8>, limit: usize) -> Self {
325 Self { buf, limit }
326 }
327
328 /// Appends bytes.
329 ///
330 /// # Errors
331 ///
332 /// `render.too_large` when the page would pass the budget.
333 #[inline]
334 pub fn write(&mut self, bytes: &[u8]) -> Result<(), RenderError> {
335 if self.buf.len() + bytes.len() > self.limit {
336 return Err(RenderError::new("render.too_large"));
337 }
338 self.buf.extend_from_slice(bytes);
339 Ok(())
340 }
341
342 /// Appends a string.
343 ///
344 /// # Errors
345 ///
346 /// As [`write`](Self::write).
347 #[inline]
348 pub fn write_str(&mut self, s: &str) -> Result<(), RenderError> {
349 self.write(s.as_bytes())
350 }
351
352 /// The buffer itself, for an engine that writes into a `Vec<u8>`. The
353 /// budget is checked when the engine returns: a page past it is refused
354 /// with `render.too_large`.
355 pub fn buffer(&mut self) -> &mut Vec<u8> {
356 self.buf
357 }
358
359 /// Whether the budget was passed (checked after the engine returns).
360 pub(crate) fn over(&self) -> bool {
361 self.buf.len() > self.limit
362 }
363
364 /// Bytes written so far.
365 #[must_use]
366 pub fn len(&self) -> usize {
367 self.buf.len()
368 }
369
370 /// Whether nothing was written yet.
371 #[must_use]
372 pub fn is_empty(&self) -> bool {
373 self.buf.is_empty()
374 }
375
376 /// The bytes still allowed.
377 #[must_use]
378 pub fn remaining(&self) -> usize {
379 self.limit.saturating_sub(self.buf.len())
380 }
381}
382
383impl fmt::Debug for Output<'_> {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 f.debug_struct("Output")
386 .field("len", &self.buf.len())
387 .field("limit", &self.limit)
388 .finish()
389 }
390}
391
392/// A render failure. The code is stable; the detail is for logs only.
393pub struct RenderError {
394 code: &'static str,
395 detail: Option<Box<str>>,
396}
397
398impl RenderError {
399 /// A failure with a stable code (`"luminess.missing_block"`).
400 #[must_use]
401 pub const fn new(code: &'static str) -> Self {
402 Self { code, detail: None }
403 }
404
405 /// Adds a detail for the logs (a template name, a line number).
406 #[must_use]
407 pub fn with_detail(mut self, detail: impl Into<Box<str>>) -> Self {
408 self.detail = Some(detail.into());
409 self
410 }
411
412 /// The code.
413 #[must_use]
414 pub const fn code(&self) -> &'static str {
415 self.code
416 }
417
418 /// The detail, if any.
419 #[must_use]
420 pub fn detail(&self) -> Option<&str> {
421 self.detail.as_deref()
422 }
423}
424
425impl fmt::Debug for RenderError {
426 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427 f.debug_struct("RenderError")
428 .field("code", &self.code)
429 .field("detail", &self.detail)
430 .finish()
431 }
432}
433
434impl fmt::Display for RenderError {
435 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
436 match &self.detail {
437 Some(d) => write!(f, "render failed ({}): {d}", self.code),
438 None => write!(f, "render failed ({})", self.code),
439 }
440 }
441}
442
443impl std::error::Error for RenderError {}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448
449 #[test]
450 fn test_output_budget() {
451 let mut buf = Vec::new();
452 let mut out = Output::new(&mut buf, 4);
453 out.write(b"abc").unwrap();
454 assert_eq!(out.remaining(), 1);
455 assert_eq!(out.write(b"de").unwrap_err().code(), "render.too_large");
456 out.write(b"d").unwrap();
457 assert_eq!(buf, b"abcd");
458 }
459
460 #[test]
461 fn test_fields_view() {
462 let v = Fields(&[("a", Value::Bool(true)), ("b", Value::Text("x"))]);
463 assert_eq!(v.shape().key("b"), Some(Key(1)));
464 assert_eq!(v.shape().key("c"), None);
465 assert!(v.get(Key(9)).is_absent());
466 assert_eq!(v.get(Key(1)).as_text(), Some("x"));
467 let mut got = false;
468 v.call(Key(0), &[], &mut |x| got = x.is_absent());
469 assert!(got);
470 }
471}