Skip to main content

topcoat_view/
format.rs

1#[cfg(feature = "http")]
2use http::{HeaderMap, StatusCode};
3
4/// A plain string writer that render output accumulates into.
5///
6/// `Formatter` is escaping-agnostic: [`write_str`](Self::write_str) and
7/// [`write_char`](Self::write_char) append exactly what they are given. Text
8/// that needs to be made safe for an HTML position is written through an
9/// [`HtmlWriter`](crate::HtmlWriter) created for the matching
10/// [`HtmlContext`](crate::HtmlContext) instead.
11pub struct Formatter<'a> {
12    buf: &'a mut String,
13    #[cfg(feature = "http")]
14    status_code: Option<StatusCode>,
15    #[cfg(feature = "http")]
16    headers: HeaderMap,
17}
18
19impl<'a> Formatter<'a> {
20    /// Creates a new `Formatter` that writes into the given destination.
21    #[inline]
22    pub fn new(buf: &'a mut String) -> Self {
23        Self {
24            buf,
25            #[cfg(feature = "http")]
26            status_code: None,
27            #[cfg(feature = "http")]
28            headers: HeaderMap::new(),
29        }
30    }
31
32    /// Writes a string verbatim.
33    #[inline]
34    pub fn write_str(&mut self, s: &str) {
35        self.buf.push_str(s);
36    }
37
38    /// Writes a single character verbatim.
39    #[inline]
40    pub fn write_char(&mut self, c: char) {
41        self.buf.push(c);
42    }
43
44    /// Records a response status code, keeping an earlier one if already
45    /// recorded: the first status code rendered wins.
46    #[cfg(feature = "http")]
47    #[inline]
48    pub(crate) fn record_status_code(&mut self, status_code: StatusCode) {
49        self.status_code.get_or_insert(status_code);
50    }
51
52    /// Records response headers, keeping earlier values for names already
53    /// recorded: the first render part that mentions a header name provides
54    /// all of that name's values.
55    #[cfg(feature = "http")]
56    pub(crate) fn record_headers(&mut self, headers: &HeaderMap) {
57        if self.headers.is_empty() {
58            self.headers = headers.clone();
59            return;
60        }
61        for name in headers.keys() {
62            if !self.headers.contains_key(name) {
63                for value in headers.get_all(name) {
64                    self.headers.append(name.clone(), value.clone());
65                }
66            }
67        }
68    }
69
70    /// Consumes the formatter, returning the recorded status code and
71    /// headers.
72    #[cfg(feature = "http")]
73    pub(crate) fn into_recorded(self) -> (Option<StatusCode>, HeaderMap) {
74        (self.status_code, self.headers)
75    }
76}
77
78impl std::fmt::Write for Formatter<'_> {
79    fn write_str(&mut self, s: &str) -> std::fmt::Result {
80        Formatter::write_str(self, s);
81        Ok(())
82    }
83
84    fn write_char(&mut self, c: char) -> std::fmt::Result {
85        Formatter::write_char(self, c);
86        Ok(())
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn write_str_is_verbatim() {
96        let mut buf = String::new();
97        let mut f = Formatter::new(&mut buf);
98        f.write_str("<b>&\"'</b>");
99        assert_eq!(buf, "<b>&\"'</b>");
100    }
101
102    #[test]
103    fn write_char_is_verbatim() {
104        let mut buf = String::new();
105        let mut f = Formatter::new(&mut buf);
106        f.write_char('<');
107        f.write_char('é');
108        assert_eq!(buf, "<é");
109    }
110
111    #[test]
112    fn fmt_write_is_verbatim() {
113        use std::fmt::Write;
114
115        let (one, two) = (1, "two");
116        let mut buf = String::new();
117        let mut f = Formatter::new(&mut buf);
118        write!(f, "{one} < {two}").unwrap();
119        assert_eq!(buf, "1 < two");
120    }
121}