Skip to main content

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}