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/// ```
20/// use web_net::{Output, Render, RenderError, TemplateId, Value, View};
21///
22/// /// Writes the view's `title` field and nothing else.
23/// struct TitleOnly;
24///
25/// impl Render for TitleOnly {
26///     fn template(&self, name: &str) -> Option<TemplateId> {
27///         (name == "title").then_some(TemplateId(0))
28///     }
29///     fn render(&self, _t: TemplateId, view: &dyn View, out: &mut Output<'_>) -> Result<(), RenderError> {
30///         let key = view.shape().key("title").ok_or(RenderError::new("demo.no_title"))?;
31///         match view.get(key) {
32///             Value::Text(t) => out.write(t.as_bytes()), // a real engine escapes here
33///             _ => Err(RenderError::new("demo.not_text")),
34///         }
35///     }
36/// }
37/// ```
38pub trait Render: Send + Sync + 'static {
39    /// Resolves a template name. Called at build time for every template a
40    /// route names, so an unknown name stops the application from starting.
41    fn template(&self, name: &str) -> Option<TemplateId>;
42
43    /// Renders `template` against `view` into `out`.
44    ///
45    /// # Errors
46    ///
47    /// A [`RenderError`]; the request is answered with a 500 error page and
48    /// the detail goes to the events, never to the visitor.
49    fn render(
50        &self,
51        template: TemplateId,
52        view: &dyn View,
53        out: &mut Output<'_>,
54    ) -> Result<(), RenderError>;
55}
56
57/// A field or method of a view, resolved from a name by its [`Shape`].
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
59pub struct Key(pub u32);
60
61/// Identifies a shape so an engine can cache name resolutions per shape.
62/// [`ShapeId::ANONYMOUS`] says "do not cache".
63#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub struct ShapeId(pub u64);
65
66impl ShapeId {
67    /// A shape whose keys may differ between instances: resolve names per
68    /// render.
69    pub const ANONYMOUS: Self = Self(0);
70}
71
72/// The names a view answers.
73pub trait Shape {
74    /// The shape's identity. Two views with the same id resolve every name
75    /// to the same key.
76    fn id(&self) -> ShapeId;
77
78    /// The key for a field or method name.
79    fn key(&self, name: &str) -> Option<Key>;
80}
81
82/// What a template reads.
83pub trait View {
84    /// The names this view answers.
85    fn shape(&self) -> &dyn Shape;
86
87    /// A field by key. The hot path: no allocation.
88    fn get(&self, key: Key) -> Value<'_>;
89
90    /// A method with arguments (`variations.options("size")`). The result is
91    /// passed to `with`, so it may borrow a value computed for the call.
92    /// The default answers [`Value::Absent`].
93    fn call(&self, key: Key, args: &[Value<'_>], with: &mut dyn FnMut(Value<'_>)) {
94        let _ = (key, args);
95        with(Value::Absent);
96    }
97}
98
99/// A sequence a template can loop over.
100pub trait List {
101    /// The number of items.
102    fn len(&self) -> usize;
103
104    /// Whether the list is empty.
105    fn is_empty(&self) -> bool {
106        self.len() == 0
107    }
108
109    /// An item, or [`Value::Absent`] past the end.
110    fn get(&self, index: usize) -> Value<'_>;
111}
112
113/// Markup that is trusted as it is: written without escaping. Making one is
114/// a visible, searchable step (`SafeHtml::trusted`), and the place to
115/// sanitize is where the markup was produced (when content is saved), not
116/// here.
117#[derive(Clone, Copy, PartialEq, Eq)]
118pub struct SafeHtml<'a>(&'a str);
119
120impl<'a> SafeHtml<'a> {
121    /// Marks `html` as trusted markup.
122    #[must_use]
123    pub const fn trusted(html: &'a str) -> Self {
124        Self(html)
125    }
126
127    /// The markup.
128    #[must_use]
129    pub const fn as_str(&self) -> &'a str {
130        self.0
131    }
132}
133
134impl fmt::Debug for SafeHtml<'_> {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(f, "SafeHtml({} bytes)", self.0.len())
137    }
138}
139
140/// One value read from a view.
141#[non_exhaustive]
142#[derive(Clone, Copy)]
143pub enum Value<'a> {
144    /// Nothing here (a missing field, an empty optional).
145    Absent,
146    /// A boolean.
147    Bool(bool),
148    /// An integer.
149    Int(i64),
150    /// A float.
151    Float(f64),
152    /// Text, escaped for its context by the engine.
153    Text(&'a str),
154    /// Trusted markup, written as it is.
155    Html(SafeHtml<'a>),
156    /// A computed value (money, a date), written as text and escaped.
157    Display(&'a dyn fmt::Display),
158    /// A nested object.
159    View(&'a dyn View),
160    /// A sequence.
161    List(&'a dyn List),
162}
163
164impl Value<'_> {
165    /// Whether this is [`Value::Absent`].
166    #[must_use]
167    pub const fn is_absent(&self) -> bool {
168        matches!(self, Self::Absent)
169    }
170
171    /// The text, for [`Value::Text`].
172    #[must_use]
173    pub const fn as_text(&self) -> Option<&str> {
174        match self {
175            Self::Text(t) => Some(t),
176            _ => None,
177        }
178    }
179}
180
181impl fmt::Debug for Value<'_> {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        match self {
184            Self::Absent => f.write_str("Absent"),
185            Self::Bool(b) => write!(f, "Bool({b})"),
186            Self::Int(i) => write!(f, "Int({i})"),
187            Self::Float(x) => write!(f, "Float({x})"),
188            Self::Text(t) => write!(f, "Text({t:?})"),
189            Self::Html(h) => write!(f, "{h:?}"),
190            Self::Display(_) => f.write_str("Display(..)"),
191            Self::View(_) => f.write_str("View(..)"),
192            Self::List(l) => write!(f, "List({} items)", l.len()),
193        }
194    }
195}
196
197/// A ready-made view over a slice of named values, for small pages, error
198/// pages, and tests. Its shape is anonymous: names are looked up per render.
199///
200/// ```
201/// use web_net::{Fields, Value, View};
202///
203/// let view = Fields(&[("title", Value::Text("Hello")), ("count", Value::Int(3))]);
204/// let key = view.shape().key("count").unwrap();
205/// assert!(matches!(view.get(key), Value::Int(3)));
206/// ```
207#[derive(Clone, Copy, Debug)]
208pub struct Fields<'a>(pub &'a [(&'a str, Value<'a>)]);
209
210impl Shape for Fields<'_> {
211    fn id(&self) -> ShapeId {
212        ShapeId::ANONYMOUS
213    }
214
215    fn key(&self, name: &str) -> Option<Key> {
216        self.0
217            .iter()
218            .position(|(n, _)| *n == name)
219            .map(|i| Key(i as u32))
220    }
221}
222
223impl View for Fields<'_> {
224    fn shape(&self) -> &dyn Shape {
225        self
226    }
227
228    fn get(&self, key: Key) -> Value<'_> {
229        self.0
230            .get(key.0 as usize)
231            .map_or(Value::Absent, |(_, v)| *v)
232    }
233}
234
235/// Where an engine writes: the reply body, reused between requests, with a
236/// size budget.
237pub struct Output<'a> {
238    buf: &'a mut Vec<u8>,
239    limit: usize,
240}
241
242impl<'a> Output<'a> {
243    /// An output over `buf`, refusing to grow past `limit` bytes.
244    #[must_use]
245    pub fn new(buf: &'a mut Vec<u8>, limit: usize) -> Self {
246        Self { buf, limit }
247    }
248
249    /// Appends bytes.
250    ///
251    /// # Errors
252    ///
253    /// `render.too_large` when the page would pass the budget.
254    #[inline]
255    pub fn write(&mut self, bytes: &[u8]) -> Result<(), RenderError> {
256        if self.buf.len() + bytes.len() > self.limit {
257            return Err(RenderError::new("render.too_large"));
258        }
259        self.buf.extend_from_slice(bytes);
260        Ok(())
261    }
262
263    /// Appends a string.
264    ///
265    /// # Errors
266    ///
267    /// As [`write`](Self::write).
268    #[inline]
269    pub fn write_str(&mut self, s: &str) -> Result<(), RenderError> {
270        self.write(s.as_bytes())
271    }
272
273    /// Bytes written so far.
274    #[must_use]
275    pub fn len(&self) -> usize {
276        self.buf.len()
277    }
278
279    /// Whether nothing was written yet.
280    #[must_use]
281    pub fn is_empty(&self) -> bool {
282        self.buf.is_empty()
283    }
284
285    /// The bytes still allowed.
286    #[must_use]
287    pub fn remaining(&self) -> usize {
288        self.limit.saturating_sub(self.buf.len())
289    }
290}
291
292impl fmt::Debug for Output<'_> {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        f.debug_struct("Output")
295            .field("len", &self.buf.len())
296            .field("limit", &self.limit)
297            .finish()
298    }
299}
300
301/// A render failure. The code is stable; the detail is for logs only.
302pub struct RenderError {
303    code: &'static str,
304    detail: Option<Box<str>>,
305}
306
307impl RenderError {
308    /// A failure with a stable code (`"luminess.missing_block"`).
309    #[must_use]
310    pub const fn new(code: &'static str) -> Self {
311        Self { code, detail: None }
312    }
313
314    /// Adds a detail for the logs (a template name, a line number).
315    #[must_use]
316    pub fn with_detail(mut self, detail: impl Into<Box<str>>) -> Self {
317        self.detail = Some(detail.into());
318        self
319    }
320
321    /// The code.
322    #[must_use]
323    pub const fn code(&self) -> &'static str {
324        self.code
325    }
326
327    /// The detail, if any.
328    #[must_use]
329    pub fn detail(&self) -> Option<&str> {
330        self.detail.as_deref()
331    }
332}
333
334impl fmt::Debug for RenderError {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        f.debug_struct("RenderError")
337            .field("code", &self.code)
338            .field("detail", &self.detail)
339            .finish()
340    }
341}
342
343impl fmt::Display for RenderError {
344    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345        match &self.detail {
346            Some(d) => write!(f, "render failed ({}): {d}", self.code),
347            None => write!(f, "render failed ({})", self.code),
348        }
349    }
350}
351
352impl std::error::Error for RenderError {}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn test_output_budget() {
360        let mut buf = Vec::new();
361        let mut out = Output::new(&mut buf, 4);
362        out.write(b"abc").unwrap();
363        assert_eq!(out.remaining(), 1);
364        assert_eq!(out.write(b"de").unwrap_err().code(), "render.too_large");
365        out.write(b"d").unwrap();
366        assert_eq!(buf, b"abcd");
367    }
368
369    #[test]
370    fn test_fields_view() {
371        let v = Fields(&[("a", Value::Bool(true)), ("b", Value::Text("x"))]);
372        assert_eq!(v.shape().key("b"), Some(Key(1)));
373        assert_eq!(v.shape().key("c"), None);
374        assert!(v.get(Key(9)).is_absent());
375        assert_eq!(v.get(Key(1)).as_text(), Some("x"));
376        let mut got = false;
377        v.call(Key(0), &[], &mut |x| got = x.is_absent());
378        assert!(got);
379    }
380}