rich/json.rs
1//! JSON pretty-printing.
2//!
3//! Port of upstream `rich/json.py`. [`Json`] parses a JSON string and renders it
4//! with 2-space indentation (matching Python's `json.dumps(indent=2)`) and the
5//! default JSON highlight colors.
6//!
7//! Non-ASCII strings render as UTF-8, matching upstream (`rich.json.JSON`
8//! defaults to `ensure_ascii=False`); object keys keep input order, and a
9//! repeated key keeps its first position but its last value — what both
10//! `dict` and serde_json's `preserve_order` do. The one remaining caveat is
11//! **number formatting** for exotic values — exponent notation (`1e+20`,
12//! `1e-07`) and integers beyond i64/u64 can differ from CPython's `repr`.
13//! Custom indent/sort options are deferred — see docs/DIVERGENCES.md.
14//!
15//! ## Why the parser is hand-written
16//!
17//! Upstream's parser is Python's `json`, which differs from `serde_json` in two
18//! ways that this module has to reproduce:
19//!
20//! * `json.loads` accepts (and `json.dumps(allow_nan=True)` emits) the
21//! non-finite literals `NaN`, `Infinity` and `-Infinity`. `serde_json` has no
22//! `Value` that can hold them and rejects the documents outright.
23//! * `serde_json` caps nesting at 128 levels, so a 200-deep document — which
24//! CPython parses without complaint — came back as "invalid JSON".
25//!
26//! Raising a recursion limit only moves the failure to a stack overflow, so
27//! parsing, rendering and *dropping* the tree here are all iterative: nesting
28//! depth costs heap, never stack, and no document can crash the process. Scalar
29//! *decoding* is still delegated to `serde_json`, so string escapes and number
30//! formatting stay exactly as they were.
31//!
32//! That leaves nesting *unbounded* where CPython eventually raises
33//! `RecursionError` — somewhere past 10 000 levels, at a depth that depends on
34//! the interpreter's C stack rather than on anything in the format. Reproducing
35//! a number that moves between machines would be a made-up divergence of its
36//! own, so this accepts every document CPython would and then some.
37
38use std::collections::HashMap;
39
40use crate::console::{Console, ConsoleOptions};
41use crate::errors::{Result, RichError};
42use crate::protocol::Renderable;
43use crate::segment::Segment;
44use crate::style::Style;
45
46/// A parsed JSON document, rendered with syntax highlighting. Mirrors `rich.json.JSON`.
47pub struct Json {
48 value: Node,
49 styles: JsonStyles,
50 /// See [`Json::no_wrap`].
51 no_wrap: bool,
52}
53
54/// A parsed JSON value.
55///
56/// Scalars keep the form they will be printed in: numbers are stored already
57/// formatted by `serde_json`, strings already decoded (upstream re-encodes them
58/// through `json.dumps`, so `"A"` prints as `"A"`).
59#[derive(Debug)]
60enum Node {
61 Null,
62 Bool(bool),
63 Number(String),
64 /// `NaN`, `Infinity` or `-Infinity`. Python's `json` round-trips these;
65 /// rich's `JSONHighlighter` has no rule that matches them, so they print
66 /// unstyled.
67 NonFinite(&'static str),
68 Str(String),
69 Array(Vec<Node>),
70 Object(Vec<(String, Node)>),
71}
72
73impl Drop for Node {
74 /// Dismantle the tree with an explicit stack.
75 ///
76 /// The compiler's drop glue recurses once per nesting level, so a document
77 /// deep enough to parse (parsing has no depth limit here) would overflow
78 /// the stack on the way out — a crash with no error message at all, which
79 /// is worse than the rejection this module used to hand out.
80 fn drop(&mut self) {
81 let mut pending: Vec<Node> = Vec::new();
82 take_children(self, &mut pending);
83 while let Some(mut node) = pending.pop() {
84 take_children(&mut node, &mut pending);
85 // `node` drops here with its children already moved out, so this
86 // same `drop` runs against an empty container and stops.
87 }
88 }
89}
90
91/// Move a node's children into `out`, leaving the node childless.
92fn take_children(node: &mut Node, out: &mut Vec<Node>) {
93 match node {
94 Node::Array(items) => out.append(items),
95 Node::Object(entries) => out.extend(entries.drain(..).map(|(_, value)| value)),
96 _ => {}
97 }
98}
99
100struct JsonStyles {
101 brace: Style,
102 key: Style,
103 string: Style,
104 number: Style,
105 bool_true: Style,
106 bool_false: Style,
107 null: Style,
108}
109
110impl JsonStyles {
111 fn defaults() -> Self {
112 let s = |spec: &str| Style::parse(spec).expect("valid built-in style");
113 JsonStyles {
114 brace: s("bold"),
115 key: s("bold blue"),
116 string: s("green"),
117 number: s("bold cyan"),
118 bool_true: s("italic bright_green"),
119 bool_false: s("italic bright_red"),
120 null: s("italic magenta"),
121 }
122 }
123}
124
125/// One entry of the render work list, consumed newest-first.
126enum Task<'a> {
127 /// Render this value indented `usize` levels deep.
128 Value(&'a Node, usize),
129 /// Emit a segment that has already been decided.
130 Emit(Segment),
131}
132
133impl Json {
134 /// Parse `text` as JSON. Returns an error if it is not valid JSON.
135 pub fn new(text: &str) -> Result<Self> {
136 Ok(Json {
137 value: Parser::new(text).parse_document()?,
138 styles: JsonStyles::defaults(),
139 no_wrap: false,
140 })
141 }
142
143 /// Keep `rich.json.JSON`'s `self.text.no_wrap = True`, which **crops** each
144 /// line at the available width instead of wrapping it.
145 ///
146 /// Defaults to `false`, because that is what a *top-level*
147 /// `Console.print(JSON(...))` produces and that is how this renderable is
148 /// normally reached. Upstream's flag really is set, but `Console.print`
149 /// never renders the `Text` it is set on: `_collect_renderables` sees a
150 /// `Text`, buffers it, and `check_text` hands back
151 /// `Text(sep, justify=…, end=…).join(buffered)` — and `Text.join` starts
152 /// from `self.blank_copy()`, i.e. from the *separator's* metadata. The
153 /// separator has `no_wrap=None`, so the copy that actually renders wraps
154 /// with the default `fold` overflow.
155 ///
156 /// Turn it on whenever the document is **nested** inside another
157 /// renderable — a `Panel`, `Padding`, `Styled`, `Constrain`, or rich-cli's
158 /// `ForceWidth`. Those are `ConsoleRenderable`s, so `_collect_renderables`
159 /// appends them untouched and the inner `Text` reaches
160 /// `Text.__rich_console__` with the flag intact; `Text.wrap` then skips
161 /// `divide_line` and only `truncate`s to the width.
162 ///
163 /// The two are not interchangeable. Wrapping keeps every character;
164 /// cropping discards what does not fit, which for JSON means the printed
165 /// document no longer parses — so the choice has to follow upstream's,
166 /// not taste.
167 #[must_use]
168 pub fn no_wrap(mut self, no_wrap: bool) -> Self {
169 self.no_wrap = no_wrap;
170 self
171 }
172
173 /// Flatten the document into segments, iteratively.
174 ///
175 /// A recursive walk would descend once per nesting level and overflow the
176 /// stack on the deep documents the parser now accepts.
177 fn render_value(&self) -> Vec<Segment> {
178 let brace = |text: &str| Segment::new(text.to_string(), Some(self.styles.brace.clone()));
179 let plain = |text: String| Segment::new(text, None);
180
181 let mut out = Vec::new();
182 let mut stack = vec![Task::Value(&self.value, 0)];
183 while let Some(task) = stack.pop() {
184 let (node, level) = match task {
185 Task::Emit(segment) => {
186 out.push(segment);
187 continue;
188 }
189 Task::Value(node, level) => (node, level),
190 };
191 match node {
192 Node::Null => out.push(Segment::new(
193 "null".to_string(),
194 Some(self.styles.null.clone()),
195 )),
196 Node::Bool(true) => out.push(Segment::new(
197 "true".to_string(),
198 Some(self.styles.bool_true.clone()),
199 )),
200 Node::Bool(false) => out.push(Segment::new(
201 "false".to_string(),
202 Some(self.styles.bool_false.clone()),
203 )),
204 Node::Number(number) => out.push(Segment::new(
205 number.clone(),
206 Some(self.styles.number.clone()),
207 )),
208 Node::NonFinite(literal) => out.push(plain((*literal).to_string())),
209 Node::Str(string) => out.push(Segment::new(
210 quote(string),
211 Some(self.styles.string.clone()),
212 )),
213 Node::Array(items) => {
214 out.push(brace("["));
215 if items.is_empty() {
216 out.push(brace("]"));
217 continue;
218 }
219 out.push(plain("\n".to_string()));
220 // Pushed back-to-front, so they pop in document order.
221 stack.push(Task::Emit(brace("]")));
222 stack.push(Task::Emit(plain(" ".repeat(level))));
223 let last = items.len() - 1;
224 for (index, item) in items.iter().enumerate().rev() {
225 stack.push(Task::Emit(plain("\n".to_string())));
226 if index != last {
227 stack.push(Task::Emit(plain(",".to_string())));
228 }
229 stack.push(Task::Value(item, level + 1));
230 stack.push(Task::Emit(plain(" ".repeat(level + 1))));
231 }
232 }
233 Node::Object(entries) => {
234 out.push(brace("{"));
235 if entries.is_empty() {
236 out.push(brace("}"));
237 continue;
238 }
239 out.push(plain("\n".to_string()));
240 stack.push(Task::Emit(brace("}")));
241 stack.push(Task::Emit(plain(" ".repeat(level))));
242 let last = entries.len() - 1;
243 for (index, (key, item)) in entries.iter().enumerate().rev() {
244 stack.push(Task::Emit(plain("\n".to_string())));
245 if index != last {
246 stack.push(Task::Emit(plain(",".to_string())));
247 }
248 stack.push(Task::Value(item, level + 1));
249 stack.push(Task::Emit(plain(": ".to_string())));
250 stack.push(Task::Emit(Segment::new(
251 quote(key),
252 Some(self.styles.key.clone()),
253 )));
254 stack.push(Task::Emit(plain(" ".repeat(level + 1))));
255 }
256 }
257 }
258 }
259 out
260 }
261}
262
263impl Renderable for Json {
264 fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
265 let segments = self.render_value();
266 if self.no_wrap {
267 // `Text.wrap` with `no_wrap` keeps the line whole and then calls
268 // `line.truncate(width, overflow="fold")`, which is a crop. See
269 // [`Json::no_wrap`] for when upstream gets here.
270 Segment::crop_lines(&segments, options.max_width)
271 } else {
272 // The break must land at a *word* boundary: the joined copy carries
273 // no overflow either, so upstream wraps with the default `fold`
274 // overflow and only splits mid-word when a single token is wider
275 // than the line.
276 Segment::fold_lines_words(&segments, options.max_width)
277 }
278 }
279}
280
281/// Serialize a string as a JSON string literal (quoted + escaped).
282fn quote(string: &str) -> String {
283 serde_json::to_string(string).unwrap_or_else(|_| format!("{string:?}"))
284}
285
286/// A container being filled in, held on the parser's explicit stack.
287enum Frame {
288 Array(Vec<Node>),
289 Object {
290 entries: Vec<(String, Node)>,
291 /// Key -> position in `entries`, so a repeated key overwrites in place
292 /// (`{"a": 1, "a": 2}` is one entry) without an O(n^2) rescan. Dropped
293 /// with the frame, so only the objects on the current path pay for it.
294 seen: HashMap<String, usize>,
295 /// The key whose value is currently being parsed.
296 key: String,
297 },
298}
299
300/// A non-recursive JSON reader. Structure is walked with an explicit stack;
301/// scalar tokens are handed to `serde_json` for decoding so escapes, number
302/// formats and their rejections stay identical to the rest of the workspace.
303struct Parser<'a> {
304 src: &'a str,
305 bytes: &'a [u8],
306 pos: usize,
307}
308
309impl<'a> Parser<'a> {
310 fn new(src: &'a str) -> Self {
311 Parser {
312 src,
313 bytes: src.as_bytes(),
314 pos: 0,
315 }
316 }
317
318 fn parse_document(&mut self) -> Result<Node> {
319 let value = self.parse_value()?;
320 self.skip_whitespace();
321 if self.pos != self.bytes.len() {
322 return Err(self.error("trailing characters"));
323 }
324 Ok(value)
325 }
326
327 /// Parse one value, descending into containers with an explicit stack.
328 fn parse_value(&mut self) -> Result<Node> {
329 let mut stack: Vec<Frame> = Vec::new();
330 let mut node: Node;
331
332 'descend: loop {
333 self.skip_whitespace();
334 match self.peek() {
335 Some(b'[') => {
336 self.pos += 1;
337 self.skip_whitespace();
338 if self.peek() == Some(b']') {
339 self.pos += 1;
340 node = Node::Array(Vec::new());
341 } else {
342 stack.push(Frame::Array(Vec::new()));
343 continue 'descend;
344 }
345 }
346 Some(b'{') => {
347 self.pos += 1;
348 self.skip_whitespace();
349 if self.peek() == Some(b'}') {
350 self.pos += 1;
351 node = Node::Object(Vec::new());
352 } else {
353 let key = self.parse_key()?;
354 stack.push(Frame::Object {
355 entries: Vec::new(),
356 seen: HashMap::new(),
357 key,
358 });
359 continue 'descend;
360 }
361 }
362 _ => node = self.parse_scalar()?,
363 }
364
365 // `node` is finished: hand it to its parent, then close as many
366 // containers as end here.
367 loop {
368 let Some(frame) = stack.last_mut() else {
369 return Ok(node);
370 };
371 let closing = match frame {
372 Frame::Array(items) => {
373 items.push(node);
374 b']'
375 }
376 Frame::Object { entries, seen, key } => {
377 let key = std::mem::take(key);
378 match seen.get(&key) {
379 Some(&at) => entries[at].1 = node,
380 None => {
381 seen.insert(key.clone(), entries.len());
382 entries.push((key, node));
383 }
384 }
385 b'}'
386 }
387 };
388 self.skip_whitespace();
389 match self.peek() {
390 Some(b',') => {
391 self.pos += 1;
392 if closing == b'}' {
393 let next_key = self.parse_key()?;
394 if let Some(Frame::Object { key, .. }) = stack.last_mut() {
395 *key = next_key;
396 }
397 }
398 continue 'descend;
399 }
400 Some(byte) if byte == closing => {
401 self.pos += 1;
402 node = match stack.pop() {
403 Some(Frame::Array(items)) => Node::Array(items),
404 Some(Frame::Object { entries, .. }) => Node::Object(entries),
405 None => unreachable!("the frame was just borrowed"),
406 };
407 }
408 _ if closing == b']' => return Err(self.error("expected `,` or `]`")),
409 _ => return Err(self.error("expected `,` or `}`")),
410 }
411 }
412 }
413 }
414
415 /// Parse `"key" :`, leaving the parser on the value.
416 fn parse_key(&mut self) -> Result<String> {
417 self.skip_whitespace();
418 if self.peek() != Some(b'"') {
419 return Err(self.error("key must be a string"));
420 }
421 let key = self.parse_string()?;
422 self.skip_whitespace();
423 if self.peek() != Some(b':') {
424 return Err(self.error("expected `:`"));
425 }
426 self.pos += 1;
427 Ok(key)
428 }
429
430 fn parse_scalar(&mut self) -> Result<Node> {
431 match self.peek() {
432 Some(b'"') => Ok(Node::Str(self.parse_string()?)),
433 Some(b't') => {
434 self.expect_literal("true")?;
435 Ok(Node::Bool(true))
436 }
437 Some(b'f') => {
438 self.expect_literal("false")?;
439 Ok(Node::Bool(false))
440 }
441 Some(b'n') => {
442 self.expect_literal("null")?;
443 Ok(Node::Null)
444 }
445 // Python's json emits and accepts these three (`allow_nan=True` is
446 // the default both ways), so upstream renders documents containing
447 // them instead of rejecting the file.
448 Some(b'N') => {
449 self.expect_literal("NaN")?;
450 Ok(Node::NonFinite("NaN"))
451 }
452 Some(b'I') => {
453 self.expect_literal("Infinity")?;
454 Ok(Node::NonFinite("Infinity"))
455 }
456 Some(b'-') if self.src[self.pos..].starts_with("-Infinity") => {
457 self.pos += "-Infinity".len();
458 Ok(Node::NonFinite("-Infinity"))
459 }
460 Some(b'-' | b'0'..=b'9') => self.parse_number(),
461 Some(_) => Err(self.error("expected value")),
462 None => Err(self.error("EOF while parsing a value")),
463 }
464 }
465
466 /// Read a string token and decode it with `serde_json`, so escapes, lone
467 /// surrogates and raw control characters behave exactly as before.
468 fn parse_string(&mut self) -> Result<String> {
469 let start = self.pos;
470 let mut end = self.pos + 1;
471 loop {
472 match self.bytes.get(end) {
473 None => return Err(self.error_at(self.bytes.len(), "EOF while parsing a string")),
474 Some(b'"') => {
475 end += 1;
476 break;
477 }
478 Some(b'\\') => {
479 end += 1;
480 // Step over the escaped character whole. A multi-byte
481 // character after a backslash is invalid JSON, but `end`
482 // must still land on a UTF-8 boundary or slicing panics
483 // before `serde_json` gets to reject it.
484 match self.src[end..].chars().next() {
485 Some(ch) => end += ch.len_utf8(),
486 None => {
487 return Err(
488 self.error_at(self.bytes.len(), "EOF while parsing a string")
489 )
490 }
491 }
492 }
493 // Continuation bytes are never `"` or `\`, so scanning byte by
494 // byte cannot mistake one for a delimiter.
495 Some(_) => end += 1,
496 }
497 }
498 let decoded: String = serde_json::from_str(&self.src[start..end])
499 .map_err(|error| self.error_at(start, &describe(&error)))?;
500 self.pos = end;
501 Ok(decoded)
502 }
503
504 /// Read a number token and let `serde_json` decide whether it is one — this
505 /// keeps `float_roundtrip` parsing and `Number`'s formatting.
506 fn parse_number(&mut self) -> Result<Node> {
507 let start = self.pos;
508 let mut end = self.pos;
509 while matches!(
510 self.bytes.get(end),
511 Some(b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9')
512 ) {
513 end += 1;
514 }
515 let number: serde_json::Number = serde_json::from_str(&self.src[start..end])
516 .map_err(|error| self.error_at(start, &describe(&error)))?;
517 self.pos = end;
518 Ok(Node::Number(number.to_string()))
519 }
520
521 fn expect_literal(&mut self, literal: &str) -> Result<()> {
522 if self.src[self.pos..].starts_with(literal) {
523 self.pos += literal.len();
524 Ok(())
525 } else {
526 Err(self.error("expected value"))
527 }
528 }
529
530 fn peek(&self) -> Option<u8> {
531 self.bytes.get(self.pos).copied()
532 }
533
534 fn skip_whitespace(&mut self) {
535 while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
536 self.pos += 1;
537 }
538 }
539
540 fn error(&self, message: &str) -> RichError {
541 self.error_at(self.pos, message)
542 }
543
544 fn error_at(&self, pos: usize, message: &str) -> RichError {
545 let (line, column) = self.line_column(pos);
546 RichError::Json(format!("{message} at line {line} column {column}"))
547 }
548
549 fn line_column(&self, pos: usize) -> (usize, usize) {
550 let mut pos = pos.min(self.src.len());
551 while !self.src.is_char_boundary(pos) {
552 pos -= 1;
553 }
554 let before = &self.src[..pos];
555 let line = 1 + before.matches('\n').count();
556 let column = before
557 .rsplit('\n')
558 .next()
559 .map_or(0, |tail| tail.chars().count())
560 + 1;
561 (line, column)
562 }
563}
564
565/// `serde_json`'s message without its own `at line … column …` suffix, which
566/// counts from the start of the token slice rather than the document.
567fn describe(error: &serde_json::Error) -> String {
568 let text = error.to_string();
569 match text.find(" at line ") {
570 Some(at) => text[..at].to_string(),
571 None => text,
572 }
573}
574
575#[cfg(test)]
576mod tests {
577 use super::*;
578 use crate::color::ColorSystem;
579
580 fn render(text: &str) -> String {
581 let console = Console::builder()
582 .force_terminal(true)
583 .color_system(Some(ColorSystem::Truecolor))
584 .width(40)
585 .build();
586 console.render_to_string(&Json::new(text).unwrap())
587 }
588
589 fn render_plain(text: &str, width: usize) -> String {
590 let console = Console::builder().width(width).no_color(true).build();
591 console.render_to_string(&Json::new(text).expect("valid json"))
592 }
593
594 #[test]
595 fn empty_collections_stay_inline() {
596 assert_eq!(render("{}"), "\x1b[1m{\x1b[0m\x1b[1m}\x1b[0m");
597 assert_eq!(render("[]"), "\x1b[1m[\x1b[0m\x1b[1m]\x1b[0m");
598 }
599
600 #[test]
601 fn object_with_scalars() {
602 assert_eq!(
603 render(r#"{"ok": false}"#),
604 "\x1b[1m{\x1b[0m\n \x1b[1;34m\"ok\"\x1b[0m: \x1b[3;91mfalse\x1b[0m\n\x1b[1m}\x1b[0m"
605 );
606 }
607
608 #[test]
609 fn invalid_json_errors() {
610 assert!(Json::new("{not json}").is_err());
611 }
612
613 #[test]
614 fn non_ascii_stays_utf8_in_input_order() {
615 // Upstream's JSON defaults to ensure_ascii=False, so accented/symbol
616 // characters render as UTF-8 (not \uXXXX), and keys keep input order.
617 // (Byte-parity is guaranteed by the `json_unicode` golden.)
618 let out = render("{\"name\": \"caf\u{e9}\", \"emoji\": \"\u{2764}\"}");
619 assert!(out.contains("caf\u{e9}"), "café stays UTF-8: {out:?}");
620 assert!(out.contains('\u{2764}'), "heart stays UTF-8");
621 let name_at = out.find("name").expect("name key present");
622 let emoji_at = out.find("emoji").expect("emoji key present");
623 assert!(name_at < emoji_at, "keys keep input order");
624 }
625
626 #[test]
627 fn a_long_value_is_wrapped_rather_than_cropped() {
628 // A long string value used to be cut mid-token, so the printed document
629 // was missing data -- and, for JSON, no longer parseable -- at exit 0.
630 let payload = format!("{{\"k\": \"{}\"}}", "y".repeat(120));
631 let out = render_plain(&payload, 40);
632 assert_eq!(
633 out.matches('y').count(),
634 120,
635 "characters were dropped:
636{out}"
637 );
638 }
639
640 /// Upstream wraps the JSON at word boundaries: `JSON.text` asks for
641 /// `no_wrap`, but `Console.print` re-joins it through `Text(sep).join(...)`
642 /// and the joined copy carries neither the flag nor an overflow, so the
643 /// default `fold` word wrap applies. Character folding split words
644 /// (`over t` / `he lazy`), which no upstream output ever shows.
645 ///
646 /// Captured from rich 15.0.0:
647 /// `Console(width=40).print(JSON(...))`.
648 #[test]
649 fn wrapping_breaks_at_word_boundaries() {
650 let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
651 assert_eq!(
652 render_plain(payload, 40),
653 "{\n \"k\": \"the quick brown fox jumps over \n\
654 the lazy dog and keeps running for a \n\
655 very long time indeed\"\n}"
656 );
657 }
658
659 /// Nested inside another renderable, `JSON.text.no_wrap` survives and each
660 /// line is **cropped** at the width rather than wrapped — see
661 /// [`Json::no_wrap`]. Wrapping here instead was silent content loss: with
662 /// `rich -j doc.json -w 120` on an 80-column console the document was laid
663 /// out at 120 and then cropped to 80 by `Console.print`, so whole runs
664 /// vanished and the surviving text read as if it were contiguous.
665 ///
666 /// Captured from rich-cli 1.8.1 driven by rich 15.0.0:
667 /// `COLUMNS=80 rich -j long.json -w 40`.
668 #[test]
669 fn a_nested_document_is_cropped_rather_than_wrapped() {
670 let payload = r#"{"k": "the quick brown fox jumps over the lazy dog and keeps running for a very long time indeed"}"#;
671 let console = Console::builder().width(40).no_color(true).build();
672 let json = Json::new(payload).expect("valid json").no_wrap(true);
673 assert_eq!(
674 console.render_to_string(&json),
675 "{\n \"k\": \"the quick brown fox jumps over t\n}"
676 );
677
678 // …and the wrap is still the default, because a bare
679 // `Console.print(JSON(...))` loses the flag in `Text.join`.
680 assert_eq!(
681 render_plain(payload, 40),
682 "{\n \"k\": \"the quick brown fox jumps over \n\
683 the lazy dog and keeps running for a \n\
684 very long time indeed\"\n}"
685 );
686 }
687
688 /// A crop must not cut a double-width character in half: `set_cell_size`
689 /// drops the straddling character and the line comes out one cell short,
690 /// never one cell over.
691 #[test]
692 fn cropping_never_splits_a_wide_character() {
693 let console = Console::builder().width(12).no_color(true).build();
694 let json = Json::new("{\"k\": \"\u{1f306}\u{1f306}\u{1f306}\"}")
695 .expect("valid json")
696 .no_wrap(true);
697 for line in console.render_to_string(&json).lines() {
698 assert!(
699 crate::cells::cell_len(line) <= 12,
700 "line {line:?} overflows the crop"
701 );
702 }
703 }
704
705 /// serde_json's default float parser takes a fast path that can land 1 ULP
706 /// from the value in the file, so the rendered number parsed back to a
707 /// *different* double. The `float_roundtrip` feature makes parsing exact.
708 #[test]
709 fn floats_round_trip_exactly() {
710 for literal in [
711 "-938371.9565467801",
712 "0.1",
713 "1.7976931348623157e308",
714 "5e-324",
715 "3.141592653589793",
716 ] {
717 let out = render_plain(&format!("{{\"v\": {literal}}}"), 120);
718 let rendered: String = out
719 .split(':')
720 .nth(1)
721 .expect("a value after the key")
722 .trim()
723 .trim_end_matches(['}', ' ', '\n'])
724 .to_string();
725 let want: f64 = literal.parse().expect("literal parses");
726 let got: f64 = rendered
727 .parse()
728 .unwrap_or_else(|_| panic!("rendered {rendered:?}"));
729 assert_eq!(
730 got.to_bits(),
731 want.to_bits(),
732 "{literal} rendered as {rendered} — a different double"
733 );
734 }
735 }
736
737 /// serde_json stops at 128 levels, so a 200-deep document — which CPython
738 /// parses without complaint — was reported as invalid JSON and the CLI
739 /// exited 1 on a file upstream renders.
740 #[test]
741 fn deep_nesting_is_not_rejected() {
742 for depth in [128, 129, 200, 1000] {
743 let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
744 let json = Json::new(&payload)
745 .unwrap_or_else(|error| panic!("depth {depth} rejected: {error}"));
746 let console = Console::builder()
747 .width(4 * depth + 8)
748 .no_color(true)
749 .build();
750 let out = console.render_to_string(&json);
751 assert_eq!(
752 out.matches('[').count(),
753 depth,
754 "depth {depth} did not render every level"
755 );
756 }
757 }
758
759 /// Parsing and dropping must cost heap, not stack: a recursive parser (or
760 /// the compiler's recursive drop glue) turns a deep document into a stack
761 /// overflow, which kills the process without even an error message.
762 #[test]
763 fn very_deep_nesting_does_not_overflow_the_stack() {
764 let depth = 100_000;
765 let payload = format!("{}1{}", "[".repeat(depth), "]".repeat(depth));
766 let json = Json::new(&payload).expect("deep document parses");
767 drop(json);
768 }
769
770 /// Python's json accepts and emits `NaN` / `Infinity` / `-Infinity`
771 /// (`allow_nan=True` is the default), and rich's JSONHighlighter has no
772 /// rule that matches them, so upstream prints them *unstyled*. serde_json
773 /// rejected the whole document.
774 ///
775 /// Captured from rich 15.0.0:
776 /// `Console(width=40, force_terminal=True).print(JSON(...))`.
777 #[test]
778 fn non_finite_numbers_render_unstyled() {
779 assert_eq!(
780 render(r#"{"a": NaN, "b": Infinity, "c": -Infinity, "d": 1.5}"#),
781 "\x1b[1m{\x1b[0m\n \x1b[1;34m\"a\"\x1b[0m: NaN,\n \x1b[1;34m\"b\"\x1b[0m: \
782 Infinity,\n \x1b[1;34m\"c\"\x1b[0m: -Infinity,\n \x1b[1;34m\"d\"\x1b[0m: \
783 \x1b[1;36m1.5\x1b[0m\n\x1b[1m}\x1b[0m"
784 );
785 // Python spells them with those exact capitalisations and nothing else.
786 for rejected in [r#"{"a": nan}"#, r#"{"a": inf}"#, r#"{"a": -inf}"#] {
787 assert!(Json::new(rejected).is_err(), "{rejected} should not parse");
788 }
789 }
790
791 /// The hand-written reader must accept and reject exactly what serde_json
792 /// does — apart from the three Python constants it exists to add.
793 #[test]
794 fn acceptance_matches_serde_json() {
795 let samples = [
796 "{}",
797 "[]",
798 " {\t\"a\" :\n1 } ",
799 r#"{"a": 1, "a": 2}"#,
800 r#"{"a": [1, {"b": null}], "c": "x"}"#,
801 "0",
802 "-0",
803 "0.0",
804 "1e10",
805 "1E+10",
806 "1e-7",
807 "1e999",
808 "12345678901234567890",
809 "-12345678901234567890123456789012345",
810 "01",
811 "1.",
812 ".1",
813 "+1",
814 "1e",
815 "-",
816 "--1",
817 "-i",
818 "-Inf",
819 "Infinit",
820 "NAN",
821 "1 2",
822 "",
823 " ",
824 "{",
825 "[",
826 "]",
827 "}",
828 "[,]",
829 "[1,]",
830 r#"{"a": 1,}"#,
831 r#"{a: 1}"#,
832 r#"{'a': 1}"#,
833 r#"{"a" 1}"#,
834 "[1 2]",
835 "truex",
836 "tru",
837 "nul",
838 r#""unterminated"#,
839 r#""\q""#,
840 r#""é""#,
841 r#""😀""#,
842 r#""\ud800""#,
843 "\"raw\nnewline\"",
844 r#""café ❤""#,
845 "\"\u{e9}\\\"",
846 "\u{feff}{}",
847 "[[[[1]]]]",
848 ];
849 for sample in samples {
850 let ours = Json::new(sample).is_ok();
851 let theirs = serde_json::from_str::<serde_json::Value>(sample).is_ok();
852 assert_eq!(ours, theirs, "disagreed about {sample:?}");
853 }
854 }
855
856 /// And the tree it builds must be the tree serde_json would have built.
857 /// `serde_json::to_string_pretty` happens to use the very layout upstream's
858 /// `json.dumps(indent=2)` does, so it doubles as a reference dump: key
859 /// order, repeated-key collapsing, string escaping and number formatting
860 /// all have to agree.
861 #[test]
862 fn the_parsed_tree_matches_serde_json() {
863 let samples = [
864 r#"{"name": "Alice", "age": 30, "admin": true, "tags": ["a", "b"], "meta": null}"#,
865 r#"{"a": 1, "b": 2, "a": 3}"#,
866 r#"{"a": {"b": {"c": [1, [], {}, [[2]]]}}}"#,
867 r#"{"k": "A\t\"x\"A\\\/é"}"#,
868 r#"[0, -0.5, 1e10, 1E+10, 1e-7, 12345678901234567890, 1.7976931348623157e308]"#,
869 r#"{"café": "❤", "": ""}"#,
870 "[]",
871 "{}",
872 "\"top level\"",
873 "1234",
874 ];
875 for sample in samples {
876 let reference: serde_json::Value =
877 serde_json::from_str(sample).expect("sample is valid JSON");
878 assert_eq!(
879 render_plain(sample, 10_000),
880 serde_json::to_string_pretty(&reference).expect("value re-serialises"),
881 "diverged on {sample}"
882 );
883 }
884 }
885
886 /// A repeated key collapses to one entry — first position, last value —
887 /// which is what both `dict` and serde_json's `preserve_order` produce.
888 #[test]
889 fn a_repeated_key_keeps_its_position_and_last_value() {
890 assert_eq!(
891 render_plain(r#"{"a": 1, "b": 2, "a": 3}"#, 40),
892 "{\n \"a\": 3,\n \"b\": 2\n}"
893 );
894 }
895
896 /// Escapes are decoded and re-encoded, because upstream re-serialises the
897 /// parsed data with `json.dumps`.
898 #[test]
899 fn escapes_are_re_encoded_like_dumps() {
900 assert_eq!(
901 render_plain(r#"{"k": "A\t\"x\""}"#, 60),
902 "{\n \"k\": \"A\\t\\\"x\\\"\"\n}"
903 );
904 }
905}