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::borrow::Cow;
11use std::fmt;
12
13/// Text made safe to place in HTML, in element content or in a quoted
14/// attribute: `&`, `<`, `>`, `"`, and `'` become entities. Borrowed when
15/// there is nothing to escape. For a handler that writes HTML itself
16/// (`cx.html(200, &format!(..))`); a template engine escapes on its own.
17///
18/// ```
19/// use web_net::{App, escape_html, get};
20///
21/// let app = App::builder()
22///     .route(get("/hi", |cx| {
23///         let name = cx.query("name")?.unwrap_or_default();
24///         cx.html(200, &format!("<p>Hello, {}</p>", escape_html(&name)))
25///     }))
26///     .build()?;
27/// let r = serve_net::testing::respond(&app, b"GET /hi?name=%3Cscript%3E HTTP/1.1\r\nHost: localhost\r\n\r\n");
28/// assert_eq!(r.text(), "<p>Hello, &lt;script&gt;</p>");
29/// assert!(matches!(escape_html("plain"), std::borrow::Cow::Borrowed(_)));
30/// # Ok::<(), web_net::BuildError>(())
31/// ```
32#[must_use]
33pub fn escape_html(text: &str) -> Cow<'_, str> {
34    let Some(first) = text.find(['&', '<', '>', '"', '\'']) else {
35        return Cow::Borrowed(text);
36    };
37    let mut out = String::with_capacity(text.len() + 16);
38    out.push_str(&text[..first]);
39    for c in text[first..].chars() {
40        match c {
41            '&' => out.push_str("&amp;"),
42            '<' => out.push_str("&lt;"),
43            '>' => out.push_str("&gt;"),
44            '"' => out.push_str("&quot;"),
45            '\'' => out.push_str("&#39;"),
46            c => out.push(c),
47        }
48    }
49    Cow::Owned(out)
50}
51
52/// A template the engine resolved at build time. Its meaning belongs to the
53/// engine.
54///
55/// ```
56/// use web_net::TemplateId;
57///
58/// // An engine gives each template name it knows an id of its own.
59/// fn template(name: &str) -> Option<TemplateId> {
60///     let names = ["home", "post"];
61///     names.iter().position(|n| *n == name).map(|i| TemplateId(i as u32))
62/// }
63/// assert_eq!(template("post"), Some(TemplateId(1)));
64/// assert_eq!(template("missing"), None);
65/// ```
66#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
67pub struct TemplateId(pub u32);
68
69/// A template engine.
70///
71/// The view type is the engine's: an engine that reads any view through
72/// web-net's [`View`] trait (an adapter for a dynamic engine) uses
73/// `type View = dyn View`; an engine that compiles templates against the
74/// application's own types uses that type, and reads it directly.
75///
76/// ```
77/// use web_net::{Output, Render, RenderError, TemplateId, Value, View};
78///
79/// /// Writes the view's `title` field and nothing else.
80/// struct TitleOnly;
81///
82/// impl Render for TitleOnly {
83///     type View<'a> = dyn View + 'a;
84///     fn template(&self, name: &str) -> Option<TemplateId> {
85///         (name == "title").then_some(TemplateId(0))
86///     }
87///     fn render(&self, _t: TemplateId, view: &dyn View, out: &mut Output<'_>) -> Result<(), RenderError> {
88///         let key = view.shape().key("title").ok_or(RenderError::new("demo.no_title"))?;
89///         match view.get(key) {
90///             Value::Text(t) => out.write(t.as_bytes()), // a real engine escapes here
91///             _ => Err(RenderError::new("demo.not_text")),
92///         }
93///     }
94/// }
95/// ```
96pub trait Render: Send + Sync + 'static {
97    /// What a handler hands the engine, for any lifetime of the data it
98    /// borrows: `dyn View + 'a` for a dynamic engine, the application's own
99    /// type (`Page`, or `Page<'a>`) for a typed one.
100    type View<'a>: ?Sized;
101
102    /// Resolves a template name. Called at build time for every template a
103    /// route names, so an unknown name stops the application from starting.
104    fn template(&self, name: &str) -> Option<TemplateId>;
105
106    /// Renders `template` against `view` into `out`.
107    ///
108    /// # Errors
109    ///
110    /// A [`RenderError`]; the request is answered with a 500 error page and
111    /// the detail goes to the events, never to the visitor.
112    fn render(
113        &self,
114        template: TemplateId,
115        view: &Self::View<'_>,
116        out: &mut Output<'_>,
117    ) -> Result<(), RenderError>;
118
119    /// Writes the page for a refusal or an error, from the facts web-net
120    /// shows a visitor. `Ok(false)` (the default) leaves it to web-net's
121    /// built-in page; so does an error, which is not reported further.
122    ///
123    /// # Errors
124    ///
125    /// A [`RenderError`]; web-net then sends its built-in page.
126    fn error_page(&self, page: &ErrorPage<'_>, out: &mut Output<'_>) -> Result<bool, RenderError> {
127        let _ = (page, out);
128        Ok(false)
129    }
130}
131
132/// The engine of an application without one: `Context::render` answers 500
133/// `render.no_engine`, and error pages are web-net's own.
134///
135/// ```
136/// use web_net::{App, Fields, get};
137///
138/// let app = App::builder()
139///     .route(get("/", |cx| cx.render_view_named("home", &Fields(&[]))))
140///     .build()?;
141/// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
142/// assert_eq!(r.status, 500); // render.no_engine
143/// # Ok::<(), web_net::BuildError>(())
144/// ```
145#[derive(Clone, Copy, Debug, Default)]
146pub struct NoRender;
147
148impl Render for NoRender {
149    type View<'a> = dyn View + 'a;
150
151    fn template(&self, _name: &str) -> Option<TemplateId> {
152        None
153    }
154
155    fn render(
156        &self,
157        _template: TemplateId,
158        _view: &(dyn View + '_),
159        _out: &mut Output<'_>,
160    ) -> Result<(), RenderError> {
161        Err(RenderError::new("render.no_engine"))
162    }
163}
164
165/// What an error page shows: the status, its generic title and message, and
166/// the request's reference. Never the code or the cause.
167///
168/// `base` is the site's base path (`""` at the root, `/blog` for a site
169/// installed in a folder), so the page's links and stylesheets point inside
170/// the site: [`url`](Self::url) builds them as [`Context::url`](crate::Context::url)
171/// does for pages.
172///
173/// ```
174/// use web_net::{App, ErrorPage, Output, Render, RenderError, TemplateId, View, get};
175///
176/// /// No templates; its own error pages.
177/// struct Plain;
178/// impl Render for Plain {
179///     type View<'a> = dyn View + 'a;
180///     fn template(&self, _: &str) -> Option<TemplateId> { None }
181///     fn render(&self, _: TemplateId, _: &dyn View, _: &mut Output<'_>) -> Result<(), RenderError> { Ok(()) }
182///     fn error_page(&self, page: &ErrorPage<'_>, out: &mut Output<'_>) -> Result<bool, RenderError> {
183///         out.write_str(&format!("{} {}", page.status, page.title))?;
184///         Ok(true)
185///     }
186/// }
187///
188/// let app = App::builder().render(Plain).route(get("/", |cx| cx.text(200, "home"))).build()?;
189/// let r = serve_net::testing::respond(&app, b"GET /missing HTTP/1.1\r\nHost: localhost\r\n\r\n");
190/// assert_eq!((r.status, r.text()), (404, "404 Not Found"));
191/// # Ok::<(), web_net::BuildError>(())
192/// ```
193#[non_exhaustive]
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195pub struct ErrorPage<'a> {
196    /// The status (`404`).
197    pub status: u16,
198    /// Its generic title (`Not Found`).
199    pub title: &'a str,
200    /// Its generic message.
201    pub message: &'a str,
202    /// The request's reference, as the visitor quotes it.
203    pub reference: &'a str,
204    /// The site's base path: `""` at the root, `/blog` in a folder; never a
205    /// trailing slash.
206    pub base: &'a str,
207}
208
209impl<'a> ErrorPage<'a> {
210    /// An error page's facts at the site's root, for an engine's own tests
211    /// (web-net makes the real ones).
212    ///
213    /// ```
214    /// let p = web_net::ErrorPage::new(404, "Not Found", "The page was not found.", "1a-2");
215    /// assert_eq!((p.status, p.reference, p.base), (404, "1a-2", ""));
216    /// ```
217    #[must_use]
218    pub const fn new(status: u16, title: &'a str, message: &'a str, reference: &'a str) -> Self {
219        Self {
220            status,
221            title,
222            message,
223            reference,
224            base: "",
225        }
226    }
227
228    /// The same page for a site installed under `base` (`/blog`).
229    ///
230    /// ```
231    /// let p = web_net::ErrorPage::new(404, "Not Found", "-", "r").with_base("/blog");
232    /// assert_eq!(p.url("/static/site.css").unwrap(), "/blog/static/site.css");
233    /// ```
234    #[must_use]
235    pub const fn with_base(mut self, base: &'a str) -> Self {
236        self.base = base;
237        self
238    }
239
240    /// A site path as a URL path, the base path in front: what the page's
241    /// links and stylesheets use, so they work for a site in a folder.
242    ///
243    /// ```
244    /// let root = web_net::ErrorPage::new(500, "Server Error", "-", "r");
245    /// assert_eq!(root.url("/")?, "/");
246    /// let blog = root.with_base("/blog");
247    /// assert_eq!(blog.url("/static/site.css")?, "/blog/static/site.css");
248    /// assert!(blog.url("//elsewhere.example").is_err());
249    /// # Ok::<(), web_net::RenderError>(())
250    /// ```
251    ///
252    /// # Errors
253    ///
254    /// `RenderError` `url.not_local` when `path` is not a path of the site
255    /// (as for [`Context::url`](crate::Context::url)).
256    pub fn url(&self, path: &str) -> Result<String, RenderError> {
257        if !crate::cx::is_local_target(path) {
258            return Err(RenderError::new("url.not_local"));
259        }
260        Ok(crate::cx::site_url(self.base, path))
261    }
262}
263
264/// A field or method of a view, resolved from a name by its [`Shape`].
265///
266/// ```
267/// use web_net::{Fields, Key, Value, View};
268///
269/// let view = Fields(&[("title", Value::Text("Hello"))]);
270/// let key: Key = view.shape().key("title").unwrap();
271/// assert_eq!(view.get(key).as_text(), Some("Hello"));
272/// ```
273#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
274pub struct Key(pub u32);
275
276/// Identifies a shape so an engine can cache name resolutions per shape.
277/// [`ShapeId::ANONYMOUS`] says "do not cache".
278///
279/// ```
280/// use web_net::{Fields, ShapeId, View};
281///
282/// assert_eq!(Fields(&[]).shape().id(), ShapeId::ANONYMOUS);
283/// let post = ShapeId(1); // one fixed shape: an engine may cache what it resolved
284/// assert_ne!(post, ShapeId::ANONYMOUS);
285/// ```
286#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
287pub struct ShapeId(pub u64);
288
289impl ShapeId {
290    /// A shape whose keys may differ between instances: resolve names per
291    /// render.
292    ///
293    /// ```
294    /// use web_net::{Fields, ShapeId, View};
295    ///
296    /// assert_eq!(ShapeId::ANONYMOUS, ShapeId(0));
297    /// assert_eq!(Fields(&[]).shape().id(), ShapeId::ANONYMOUS);
298    /// ```
299    pub const ANONYMOUS: Self = Self(0);
300}
301
302/// The names a view answers.
303///
304/// ```
305/// use web_net::{Key, Shape, ShapeId};
306///
307/// struct PostShape;
308/// impl Shape for PostShape {
309///     fn id(&self) -> ShapeId { ShapeId(1) }
310///     fn key(&self, name: &str) -> Option<Key> {
311///         match name { "title" => Some(Key(0)), "body" => Some(Key(1)), _ => None }
312///     }
313/// }
314/// assert_eq!(PostShape.key("body"), Some(Key(1)));
315/// assert_eq!(PostShape.key("author"), None);
316/// ```
317pub trait Shape {
318    /// The shape's identity. Two views with the same id resolve every name
319    /// to the same key.
320    fn id(&self) -> ShapeId;
321
322    /// The key for a field or method name.
323    fn key(&self, name: &str) -> Option<Key>;
324}
325
326/// What a template reads.
327///
328/// ```
329/// use web_net::{Key, Shape, ShapeId, Value, View};
330///
331/// struct Post { title: String, views: i64 }
332/// impl Shape for Post {
333///     fn id(&self) -> ShapeId { ShapeId(1) }
334///     fn key(&self, name: &str) -> Option<Key> {
335///         match name { "title" => Some(Key(0)), "views" => Some(Key(1)), _ => None }
336///     }
337/// }
338/// impl View for Post {
339///     fn shape(&self) -> &dyn Shape { self }
340///     fn get(&self, key: Key) -> Value<'_> {
341///         match key.0 { 0 => Value::Text(&self.title), 1 => Value::Int(self.views), _ => Value::Absent }
342///     }
343/// }
344///
345/// let post = Post { title: "Hello".into(), views: 3 };
346/// let key = post.shape().key("title").unwrap();
347/// assert_eq!(post.get(key).as_text(), Some("Hello"));
348/// ```
349pub trait View {
350    /// The names this view answers.
351    fn shape(&self) -> &dyn Shape;
352
353    /// A field by key. The hot path: no allocation.
354    fn get(&self, key: Key) -> Value<'_>;
355
356    /// A method with arguments (`variations.options("size")`). The result is
357    /// passed to `with`, so it may borrow a value computed for the call.
358    /// The default answers [`Value::Absent`].
359    fn call(&self, key: Key, args: &[Value<'_>], with: &mut dyn FnMut(Value<'_>)) {
360        let _ = (key, args);
361        with(Value::Absent);
362    }
363}
364
365/// A sequence a template can loop over.
366///
367/// ```
368/// use web_net::{List, Value};
369///
370/// struct Tags<'a>(&'a [&'a str]);
371/// impl List for Tags<'_> {
372///     fn len(&self) -> usize { self.0.len() }
373///     fn get(&self, index: usize) -> Value<'_> {
374///         self.0.get(index).map_or(Value::Absent, |t| Value::Text(t))
375///     }
376/// }
377///
378/// let tags = Tags(&["rust", "web"]);
379/// assert_eq!(tags.get(1).as_text(), Some("web"));
380/// assert!(tags.get(2).is_absent());
381/// assert!(!tags.is_empty());
382/// ```
383pub trait List {
384    /// The number of items.
385    fn len(&self) -> usize;
386
387    /// Whether the list is empty.
388    fn is_empty(&self) -> bool {
389        self.len() == 0
390    }
391
392    /// An item, or [`Value::Absent`] past the end.
393    fn get(&self, index: usize) -> Value<'_>;
394}
395
396/// Markup that is trusted as it is: written without escaping. Making one is
397/// a visible, searchable step (`SafeHtml::trusted`), and the place to
398/// sanitize is where the markup was produced (when content is saved), not
399/// here.
400///
401/// ```
402/// use web_net::{SafeHtml, Value};
403///
404/// // Markup sanitized when it was saved, written as it is.
405/// let body = Value::Html(SafeHtml::trusted("<p>Saved <b>post</b></p>"));
406/// assert!(matches!(body, Value::Html(h) if h.as_str().starts_with("<p>")));
407/// ```
408#[derive(Clone, Copy, PartialEq, Eq)]
409pub struct SafeHtml<'a>(&'a str);
410
411impl<'a> SafeHtml<'a> {
412    /// Marks `html` as trusted markup.
413    ///
414    /// ```
415    /// use web_net::SafeHtml;
416    ///
417    /// let html = SafeHtml::trusted("<b>saved</b>");
418    /// assert_eq!(html.as_str(), "<b>saved</b>");
419    /// ```
420    #[must_use]
421    pub const fn trusted(html: &'a str) -> Self {
422        Self(html)
423    }
424
425    /// The markup.
426    ///
427    /// ```
428    /// use web_net::SafeHtml;
429    ///
430    /// let html = SafeHtml::trusted("<em>hi</em>");
431    /// assert_eq!(format!("<p>{}</p>", html.as_str()), "<p><em>hi</em></p>");
432    /// ```
433    #[must_use]
434    pub const fn as_str(&self) -> &'a str {
435        self.0
436    }
437}
438
439impl fmt::Debug for SafeHtml<'_> {
440    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
441        write!(f, "SafeHtml({} bytes)", self.0.len())
442    }
443}
444
445/// One value read from a view.
446///
447/// ```
448/// use web_net::{SafeHtml, Value, escape_html};
449///
450/// // How an engine writes a value into HTML text.
451/// fn write(v: Value<'_>) -> String {
452///     match v {
453///         Value::Text(t) => escape_html(t).into_owned(),
454///         Value::Html(h) => h.as_str().to_owned(),
455///         Value::Int(i) => i.to_string(),
456///         _ => String::new(),
457///     }
458/// }
459/// assert_eq!(write(Value::Text("a<b")), "a&lt;b");
460/// assert_eq!(write(Value::Html(SafeHtml::trusted("<b>b</b>"))), "<b>b</b>");
461/// assert_eq!(write(Value::Int(3)), "3");
462/// ```
463#[non_exhaustive]
464#[derive(Clone, Copy)]
465pub enum Value<'a> {
466    /// Nothing here (a missing field, an empty optional).
467    Absent,
468    /// A boolean.
469    Bool(bool),
470    /// An integer.
471    Int(i64),
472    /// A float.
473    Float(f64),
474    /// Text, escaped for its context by the engine.
475    Text(&'a str),
476    /// Trusted markup, written as it is.
477    Html(SafeHtml<'a>),
478    /// A computed value (money, a date), written as text and escaped.
479    Display(&'a dyn fmt::Display),
480    /// A nested object.
481    View(&'a dyn View),
482    /// A sequence.
483    List(&'a dyn List),
484}
485
486impl Value<'_> {
487    /// Whether this is [`Value::Absent`].
488    ///
489    /// ```
490    /// use web_net::Value;
491    ///
492    /// assert!(Value::Absent.is_absent());
493    /// assert!(!Value::Int(0).is_absent());
494    /// ```
495    #[must_use]
496    pub const fn is_absent(&self) -> bool {
497        matches!(self, Self::Absent)
498    }
499
500    /// The text, for [`Value::Text`].
501    ///
502    /// ```
503    /// use web_net::Value;
504    ///
505    /// assert_eq!(Value::Text("hi").as_text(), Some("hi"));
506    /// assert_eq!(Value::Int(1).as_text(), None);
507    /// ```
508    #[must_use]
509    pub const fn as_text(&self) -> Option<&str> {
510        match self {
511            Self::Text(t) => Some(t),
512            _ => None,
513        }
514    }
515}
516
517impl fmt::Debug for Value<'_> {
518    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519        match self {
520            Self::Absent => f.write_str("Absent"),
521            Self::Bool(b) => write!(f, "Bool({b})"),
522            Self::Int(i) => write!(f, "Int({i})"),
523            Self::Float(x) => write!(f, "Float({x})"),
524            Self::Text(t) => write!(f, "Text({t:?})"),
525            Self::Html(h) => write!(f, "{h:?}"),
526            Self::Display(_) => f.write_str("Display(..)"),
527            Self::View(_) => f.write_str("View(..)"),
528            Self::List(l) => write!(f, "List({} items)", l.len()),
529        }
530    }
531}
532
533/// A ready-made view over a slice of named values, for small pages, error
534/// pages, and tests. Its shape is anonymous: names are looked up per render.
535///
536/// ```
537/// use web_net::{Fields, Value, View};
538///
539/// let view = Fields(&[("title", Value::Text("Hello")), ("count", Value::Int(3))]);
540/// let key = view.shape().key("count").unwrap();
541/// assert!(matches!(view.get(key), Value::Int(3)));
542/// ```
543#[derive(Clone, Copy, Debug)]
544pub struct Fields<'a>(pub &'a [(&'a str, Value<'a>)]);
545
546impl Shape for Fields<'_> {
547    fn id(&self) -> ShapeId {
548        ShapeId::ANONYMOUS
549    }
550
551    fn key(&self, name: &str) -> Option<Key> {
552        self.0
553            .iter()
554            .position(|(n, _)| *n == name)
555            .map(|i| Key(i as u32))
556    }
557}
558
559impl View for Fields<'_> {
560    fn shape(&self) -> &dyn Shape {
561        self
562    }
563
564    fn get(&self, key: Key) -> Value<'_> {
565        self.0
566            .get(key.0 as usize)
567            .map_or(Value::Absent, |(_, v)| *v)
568    }
569}
570
571/// Where an engine writes: the reply body, reused between requests, with a
572/// size budget.
573///
574/// ```
575/// use web_net::Output;
576///
577/// let mut buf = Vec::new();
578/// let mut out = Output::new(&mut buf, 64);
579/// out.write_str("<p>hi</p>")?;
580/// assert_eq!(out.remaining(), 55);
581/// assert_eq!(buf, b"<p>hi</p>");
582/// # Ok::<(), web_net::RenderError>(())
583/// ```
584pub struct Output<'a> {
585    buf: &'a mut Vec<u8>,
586    limit: usize,
587}
588
589impl<'a> Output<'a> {
590    /// An output over `buf`, refusing to grow past `limit` bytes.
591    ///
592    /// ```
593    /// use web_net::Output;
594    ///
595    /// let mut buf = Vec::new();
596    /// let mut out = Output::new(&mut buf, 4);
597    /// assert!(out.write(b"1234").is_ok());
598    /// assert_eq!(out.write(b"5").unwrap_err().code(), "render.too_large");
599    /// ```
600    #[must_use]
601    pub fn new(buf: &'a mut Vec<u8>, limit: usize) -> Self {
602        Self { buf, limit }
603    }
604
605    /// Appends bytes.
606    ///
607    /// ```
608    /// use web_net::Output;
609    ///
610    /// let mut buf = Vec::new();
611    /// let mut out = Output::new(&mut buf, 64);
612    /// out.write(b"<p>")?;
613    /// assert_eq!(out.len(), 3);
614    /// # Ok::<(), web_net::RenderError>(())
615    /// ```
616    ///
617    /// # Errors
618    ///
619    /// `render.too_large` when the page would pass the budget.
620    #[inline]
621    pub fn write(&mut self, bytes: &[u8]) -> Result<(), RenderError> {
622        if self.buf.len() + bytes.len() > self.limit {
623            return Err(RenderError::new("render.too_large"));
624        }
625        self.buf.extend_from_slice(bytes);
626        Ok(())
627    }
628
629    /// Appends a string.
630    ///
631    /// ```
632    /// use web_net::{Output, escape_html};
633    ///
634    /// let mut buf = Vec::new();
635    /// let mut out = Output::new(&mut buf, 64);
636    /// out.write_str(&escape_html("a<b"))?;
637    /// assert_eq!(buf, b"a&lt;b");
638    /// # Ok::<(), web_net::RenderError>(())
639    /// ```
640    ///
641    /// # Errors
642    ///
643    /// As [`write`](Self::write).
644    #[inline]
645    pub fn write_str(&mut self, s: &str) -> Result<(), RenderError> {
646        self.write(s.as_bytes())
647    }
648
649    /// The buffer itself, for an engine that writes into a `Vec<u8>`. The
650    /// budget is checked when the engine returns: a page past it is refused
651    /// with `render.too_large`.
652    ///
653    /// ```
654    /// use std::io::Write;
655    /// use web_net::Output;
656    ///
657    /// let mut buf = Vec::new();
658    /// let mut out = Output::new(&mut buf, 64);
659    /// write!(out.buffer(), "{} items", 3).unwrap();
660    /// assert_eq!(out.len(), 7);
661    /// ```
662    pub fn buffer(&mut self) -> &mut Vec<u8> {
663        self.buf
664    }
665
666    /// Whether the budget was passed (checked after the engine returns).
667    pub(crate) fn over(&self) -> bool {
668        self.buf.len() > self.limit
669    }
670
671    /// Bytes written so far.
672    ///
673    /// ```
674    /// use web_net::Output;
675    ///
676    /// let mut buf = Vec::new();
677    /// let mut out = Output::new(&mut buf, 64);
678    /// out.write_str("hello")?;
679    /// assert_eq!(out.len(), 5);
680    /// # Ok::<(), web_net::RenderError>(())
681    /// ```
682    #[must_use]
683    pub fn len(&self) -> usize {
684        self.buf.len()
685    }
686
687    /// Whether nothing was written yet.
688    ///
689    /// ```
690    /// use web_net::Output;
691    ///
692    /// let mut buf = Vec::new();
693    /// let mut out = Output::new(&mut buf, 64);
694    /// assert!(out.is_empty());
695    /// out.write_str("x")?;
696    /// assert!(!out.is_empty());
697    /// # Ok::<(), web_net::RenderError>(())
698    /// ```
699    #[must_use]
700    pub fn is_empty(&self) -> bool {
701        self.buf.is_empty()
702    }
703
704    /// The bytes still allowed.
705    ///
706    /// ```
707    /// use web_net::Output;
708    ///
709    /// let mut buf = Vec::new();
710    /// let mut out = Output::new(&mut buf, 10);
711    /// out.write_str("abc")?;
712    /// assert_eq!(out.remaining(), 7);
713    /// # Ok::<(), web_net::RenderError>(())
714    /// ```
715    #[must_use]
716    pub fn remaining(&self) -> usize {
717        self.limit.saturating_sub(self.buf.len())
718    }
719}
720
721impl fmt::Debug for Output<'_> {
722    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
723        f.debug_struct("Output")
724            .field("len", &self.buf.len())
725            .field("limit", &self.limit)
726            .finish()
727    }
728}
729
730/// A render failure. The code is stable; the detail is for logs only.
731///
732/// ```
733/// use web_net::RenderError;
734///
735/// let e = RenderError::new("demo.missing_block").with_detail("page.html line 3");
736/// assert_eq!(e.code(), "demo.missing_block");
737/// assert_eq!(e.to_string(), "render failed (demo.missing_block): page.html line 3");
738/// ```
739pub struct RenderError {
740    code: &'static str,
741    detail: Option<Box<str>>,
742}
743
744impl RenderError {
745    /// A failure with a stable code (`"luminess.missing_block"`).
746    ///
747    /// ```
748    /// use web_net::RenderError;
749    ///
750    /// let e = RenderError::new("demo.missing_block");
751    /// assert_eq!((e.code(), e.detail()), ("demo.missing_block", None));
752    /// ```
753    #[must_use]
754    pub const fn new(code: &'static str) -> Self {
755        Self { code, detail: None }
756    }
757
758    /// Adds a detail for the logs (a template name, a line number).
759    ///
760    /// ```
761    /// use web_net::RenderError;
762    ///
763    /// let e = RenderError::new("demo.missing_block").with_detail("page.html line 3");
764    /// assert_eq!(e.detail(), Some("page.html line 3"));
765    /// ```
766    #[must_use]
767    pub fn with_detail(mut self, detail: impl Into<Box<str>>) -> Self {
768        self.detail = Some(detail.into());
769        self
770    }
771
772    /// The code.
773    ///
774    /// ```
775    /// use web_net::RenderError;
776    ///
777    /// assert_eq!(RenderError::new("render.too_large").code(), "render.too_large");
778    /// ```
779    #[must_use]
780    pub const fn code(&self) -> &'static str {
781        self.code
782    }
783
784    /// The detail, if any.
785    ///
786    /// ```
787    /// use web_net::RenderError;
788    ///
789    /// assert_eq!(RenderError::new("demo.bad").detail(), None);
790    /// assert_eq!(RenderError::new("demo.bad").with_detail("line 3").detail(), Some("line 3"));
791    /// ```
792    #[must_use]
793    pub fn detail(&self) -> Option<&str> {
794        self.detail.as_deref()
795    }
796}
797
798impl fmt::Debug for RenderError {
799    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
800        f.debug_struct("RenderError")
801            .field("code", &self.code)
802            .field("detail", &self.detail)
803            .finish()
804    }
805}
806
807impl fmt::Display for RenderError {
808    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
809        match &self.detail {
810            Some(d) => write!(f, "render failed ({}): {d}", self.code),
811            None => write!(f, "render failed ({})", self.code),
812        }
813    }
814}
815
816impl std::error::Error for RenderError {}
817
818#[cfg(test)]
819mod tests {
820    use super::*;
821
822    #[test]
823    fn test_output_budget() {
824        let mut buf = Vec::new();
825        let mut out = Output::new(&mut buf, 4);
826        out.write(b"abc").unwrap();
827        assert_eq!(out.remaining(), 1);
828        assert_eq!(out.write(b"de").unwrap_err().code(), "render.too_large");
829        out.write(b"d").unwrap();
830        assert_eq!(buf, b"abcd");
831    }
832
833    #[test]
834    fn test_fields_view() {
835        let v = Fields(&[("a", Value::Bool(true)), ("b", Value::Text("x"))]);
836        assert_eq!(v.shape().key("b"), Some(Key(1)));
837        assert_eq!(v.shape().key("c"), None);
838        assert!(v.get(Key(9)).is_absent());
839        assert_eq!(v.get(Key(1)).as_text(), Some("x"));
840        let mut got = false;
841        v.call(Key(0), &[], &mut |x| got = x.is_absent());
842        assert!(got);
843    }
844}