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, <script></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("&"),
42 '<' => out.push_str("<"),
43 '>' => out.push_str(">"),
44 '"' => out.push_str("""),
45 '\'' => out.push_str("'"),
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<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<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}