Skip to main content

edn/
pretty_print.rs

1// Copyright 2016-2018 Mozilla
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
4// this file except in compliance with the License. You may obtain a copy of the
5// License at http://www.apache.org/licenses/LICENSE-2.0
6// Unless required by applicable law or agreed to in writing, software distributed
7// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
8// CONDITIONS OF ANY KIND, either express or implied. See the License for the
9// specific language governing permissions and limitations under the License.
10
11use chrono::SecondsFormat;
12
13use itertools::Itertools;
14use pretty;
15
16use std::borrow::Cow;
17use std::io;
18
19use crate::types::Value;
20
21/// Render a string as an EDN text literal, escaping what `raw_text` cannot read
22/// back literally.
23///
24/// The printer used to emit `"` + the raw string + `"`, which is not
25/// round-trippable: an embedded `"` closed the literal early and an embedded `\`
26/// began an escape the parser then rejected, so printing a value containing
27/// either produced EDN that `parse::value` refused. Newlines and tabs are legal
28/// raw inside a literal, so they are left alone rather than escaped.
29fn escape_text(s: &str) -> String {
30    let mut out = String::with_capacity(s.len() + 2);
31    out.push('"');
32    for c in s.chars() {
33        match c {
34            '"' => out.push_str("\\\""),
35            '\\' => out.push_str("\\\\"),
36            _ => out.push(c),
37        }
38    }
39    out.push('"');
40    out
41}
42
43impl Value {
44    /// Return a pretty string representation of this `Value`.
45    pub fn to_pretty(&self, width: usize) -> Result<String, io::Error> {
46        let mut out = Vec::new();
47        self.write_pretty(width, &mut out)?;
48        Ok(String::from_utf8_lossy(&out).into_owned())
49    }
50
51    /// Write a pretty representation of this `Value` to the given writer.
52    fn write_pretty<W>(&self, width: usize, out: &mut W) -> Result<(), io::Error>
53    where
54        W: io::Write,
55    {
56        self.as_doc(&pretty::BoxAllocator).1.render(width, out)
57    }
58
59    /// Bracket a collection of values.
60    ///
61    /// We aim for
62    /// [1 2 3]
63    /// and fall back if necessary to
64    /// [1,
65    ///  2,
66    ///  3].
67    fn bracket<'a, A, T, I>(
68        &'a self,
69        allocator: &'a A,
70        open: T,
71        vs: I,
72        close: T,
73    ) -> pretty::DocBuilder<'a, A>
74    where
75        A: pretty::DocAllocator<'a>,
76        <A as pretty::DocAllocator<'a>>::Doc: std::clone::Clone,
77        T: Into<Cow<'a, str>>,
78        I: IntoIterator<Item = &'a Value>,
79    {
80        let open = open.into();
81        let n = open.len() as isize;
82        let i = {
83            let this = vs.into_iter().map(|v| v.as_doc(allocator));
84            let element = allocator.line();
85            Itertools::intersperse(this, element)
86        };
87        allocator
88            .text(open)
89            .append(allocator.concat(i).nest(n))
90            .append(allocator.text(close))
91            .group()
92    }
93
94    /// Recursively traverses this value and creates a pretty.rs document.
95    /// A pretty printing implementation for edn queries optimized for
96    /// readability and limited whitespace expansion.
97    fn as_doc<'a, A>(&'a self, pp: &'a A) -> pretty::DocBuilder<'a, A>
98    where
99        A: pretty::DocAllocator<'a>,
100        <A as pretty::DocAllocator<'a>>::Doc: std::clone::Clone,
101    {
102        match *self {
103            Value::Vector(ref vs) => self.bracket(pp, "[", vs, "]"),
104            Value::List(ref vs) => self.bracket(pp, "(", vs, ")"),
105            Value::Set(ref vs) => self.bracket(pp, "#{", vs, "}"),
106            Value::Map(ref vs) => {
107                let xs = {
108                    let this = vs
109                        .iter()
110                        .rev()
111                        .map(|(k, v)| k.as_doc(pp).append(pp.line()).append(v.as_doc(pp)).group());
112                    let element = pp.line();
113                    Itertools::intersperse(this, element)
114                };
115                pp.text("{")
116                    .append(pp.concat(xs).nest(1))
117                    .append(pp.text("}"))
118                    .group()
119            }
120            Value::NamespacedSymbol(ref v) => pp.text(v.namespace()).append("/").append(v.name()),
121            Value::PlainSymbol(ref v) => pp.text(v.to_string()),
122            Value::Keyword(ref v) => pp.text(v.to_string()),
123            Value::Text(ref v) => pp.text(escape_text(v)),
124            Value::Uuid(ref u) => pp
125                .text("#uuid \"")
126                .append(u.hyphenated().to_string())
127                .append("\""),
128            Value::Instant(ref v) => pp
129                .text("#inst \"")
130                .append(v.to_rfc3339_opts(SecondsFormat::AutoSi, true))
131                .append("\""),
132            _ => pp.text(self.to_string()),
133        }
134    }
135}
136
137#[cfg(test)]
138mod test {
139    use crate::parse;
140
141    #[test]
142    fn test_pp_io() {
143        let string = "$";
144        let data = parse::value(string).unwrap().without_spans();
145
146        assert!(data.write_pretty(40, &mut Vec::new()).is_ok());
147    }
148
149    #[test]
150    fn test_pp_types_empty() {
151        let string = "[ [ ] ( ) #{ } { }, \"\" ]";
152        let data = parse::value(string).unwrap().without_spans();
153
154        assert_eq!(data.to_pretty(40).unwrap(), "[[] () #{} {} \"\"]");
155    }
156
157    #[test]
158    fn test_vector() {
159        let string = "[1 2 3 4 5 6]";
160        let data = parse::value(string).unwrap().without_spans();
161
162        assert_eq!(data.to_pretty(20).unwrap(), "[1 2 3 4 5 6]");
163        assert_eq!(
164            data.to_pretty(10).unwrap(),
165            "\
166[1
167 2
168 3
169 4
170 5
171 6]"
172        );
173    }
174
175    #[test]
176    fn test_map() {
177        let string = "{:a 1 :b 2 :c 3}";
178        let data = parse::value(string).unwrap().without_spans();
179
180        assert_eq!(data.to_pretty(20).unwrap(), "{:a 1 :b 2 :c 3}");
181        assert_eq!(
182            data.to_pretty(10).unwrap(),
183            "\
184{:a 1
185 :b 2
186 :c 3}"
187        );
188    }
189
190    #[test]
191    fn test_pp_types() {
192        let string = "[ 1 2 ( 3.14 ) #{ 4N } { foo/bar 42 :baz/boz 43 } [ ] :five :six/seven eight nine/ten true false nil #f NaN #f -Infinity #f +Infinity ]";
193        let data = parse::value(string).unwrap().without_spans();
194
195        assert_eq!(
196            data.to_pretty(40).unwrap(),
197            "\
198[1
199 2
200 (3.14)
201 #{4N}
202 {:baz/boz 43 foo/bar 42}
203 []
204 :five
205 :six/seven
206 eight
207 nine/ten
208 true
209 false
210 nil
211 #f NaN
212 #f -Infinity
213 #f +Infinity]"
214        );
215    }
216
217    #[test]
218    fn test_pp_query1() {
219        let string = "[:find ?id ?bar ?baz :in $ :where [?id :session/keyword-foo ?symbol1 ?symbol2 \"some string\"] [?tx :db/tx ?ts]]";
220        let data = parse::value(string).unwrap().without_spans();
221
222        assert_eq!(
223            data.to_pretty(40).unwrap(),
224            "\
225[:find
226 ?id
227 ?bar
228 ?baz
229 :in
230 $
231 :where
232 [?id
233  :session/keyword-foo
234  ?symbol1
235  ?symbol2
236  \"some string\"]
237 [?tx :db/tx ?ts]]"
238        );
239    }
240
241    #[test]
242    fn test_pp_query2() {
243        let string = "[:find [?id ?bar ?baz] :in [$] :where [?id :session/keyword-foo ?symbol1 ?symbol2 \"some string\"] [?tx :db/tx ?ts] (not-join [?id] [?id :session/keyword-bar _])]";
244        let data = parse::value(string).unwrap().without_spans();
245
246        assert_eq!(
247            data.to_pretty(40).unwrap(),
248            "\
249[:find
250 [?id ?bar ?baz]
251 :in
252 [$]
253 :where
254 [?id
255  :session/keyword-foo
256  ?symbol1
257  ?symbol2
258  \"some string\"]
259 [?tx :db/tx ?ts]
260 (not-join
261  [?id]
262  [?id :session/keyword-bar _])]"
263        );
264    }
265    /// `\n`/`\t`/`\r` must unescape to the control characters they denote.
266    /// The rule previously echoed the character following the backslash, so
267    /// these yielded the letters `n`/`t`/`r` and `"a\nb"` silently became
268    /// `"anb"` -- data loss for any text carrying a newline or tab.
269    #[test]
270    fn test_string_escapes_unescape_to_control_characters() {
271        let data = parse::value(r#""a\nb\tc\rd""#).unwrap().without_spans();
272        assert_eq!(data, crate::Value::Text("a\nb\tc\rd".into()));
273    }
274
275    /// The passthrough arm: `\\` and `\"` are their own translation.
276    #[test]
277    fn test_backslash_and_quote_escapes_are_preserved() {
278        let data = parse::value(r#""a\\b\"c""#).unwrap().without_spans();
279        assert_eq!(data, crate::Value::Text("a\\b\"c".into()));
280    }
281
282    /// Printing then reparsing must be the identity. The printer emitted the
283    /// raw string, so a value containing `"` closed its literal early and one
284    /// containing `\` began an escape the parser rejected -- EDN this crate
285    /// wrote, this crate could not read.
286    #[test]
287    fn test_text_round_trips_through_the_printer() {
288        for original in [
289            "plain",
290            "has\"a quote",
291            "has\\a backslash",
292            "a\nnewline",
293            "a\ttab",
294            "all: \" \\ \n \t",
295            "",
296        ] {
297            let value = crate::Value::Text(original.to_string());
298            let printed = value.to_pretty(200).unwrap();
299            let reparsed = parse::value(&printed)
300                .unwrap_or_else(|e| panic!("printed {printed:?} is unparsable: {e}"))
301                .without_spans();
302            assert_eq!(reparsed, value, "round-trip changed {original:?}");
303        }
304    }
305}