1use chrono::SecondsFormat;
12
13use itertools::Itertools;
14use pretty;
15
16use std::borrow::Cow;
17use std::io;
18
19use crate::types::Value;
20
21fn 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 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 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 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 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 #[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 #[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 #[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}