1use crate::{Widget, draw_text_span};
19use ftui_core::geometry::Rect;
20use ftui_render::frame::Frame;
21use ftui_style::Style;
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum JsonToken {
26 Key(String),
28 StringVal(String),
30 Number(String),
32 Literal(String),
34 Punctuation(String),
36 Whitespace(String),
38 Newline,
40 Error(String),
42}
43
44#[derive(Debug, Clone)]
46pub struct JsonView {
47 source: String,
48 indent: usize,
49 key_style: Style,
50 string_style: Style,
51 number_style: Style,
52 literal_style: Style,
53 punct_style: Style,
54 error_style: Style,
55}
56
57impl Default for JsonView {
58 fn default() -> Self {
59 Self::new("")
60 }
61}
62
63impl JsonView {
64 #[must_use]
66 pub fn new(source: impl Into<String>) -> Self {
67 Self {
68 source: source.into(),
69 indent: 2,
70 key_style: Style::new().bold(),
71 string_style: Style::default(),
72 number_style: Style::default(),
73 literal_style: Style::default(),
74 punct_style: Style::default(),
75 error_style: Style::default(),
76 }
77 }
78
79 #[must_use]
81 pub fn with_indent(mut self, indent: usize) -> Self {
82 self.indent = indent;
83 self
84 }
85
86 #[must_use]
88 pub fn with_key_style(mut self, style: Style) -> Self {
89 self.key_style = style;
90 self
91 }
92
93 #[must_use]
95 pub fn with_string_style(mut self, style: Style) -> Self {
96 self.string_style = style;
97 self
98 }
99
100 #[must_use]
102 pub fn with_number_style(mut self, style: Style) -> Self {
103 self.number_style = style;
104 self
105 }
106
107 #[must_use]
109 pub fn with_literal_style(mut self, style: Style) -> Self {
110 self.literal_style = style;
111 self
112 }
113
114 #[must_use]
116 pub fn with_punct_style(mut self, style: Style) -> Self {
117 self.punct_style = style;
118 self
119 }
120
121 #[must_use]
123 pub fn with_error_style(mut self, style: Style) -> Self {
124 self.error_style = style;
125 self
126 }
127
128 pub fn set_source(&mut self, source: impl Into<String>) {
130 self.source = source.into();
131 }
132
133 #[must_use]
135 pub fn source(&self) -> &str {
136 &self.source
137 }
138
139 #[must_use]
141 pub fn formatted_lines(&self) -> Vec<Vec<JsonToken>> {
142 let trimmed = self.source.trim();
143 if trimmed.is_empty() {
144 return vec![];
145 }
146
147 let mut lines: Vec<Vec<JsonToken>> = Vec::new();
148 let mut current_line: Vec<JsonToken> = Vec::new();
149 let mut depth: usize = 0;
150 let mut chars = trimmed.chars().peekable();
151
152 while let Some(&ch) = chars.peek() {
153 match ch {
154 '{' | '[' => {
155 chars.next();
156 current_line.push(JsonToken::Punctuation(ch.to_string()));
157 skip_ws(&mut chars);
159 let next = chars.peek().copied();
160 if next == Some('}') || next == Some(']') {
161 let closing = chars.next().unwrap();
163 current_line.push(JsonToken::Punctuation(closing.to_string()));
164 skip_ws(&mut chars);
166 if chars.peek() == Some(&',') {
167 chars.next();
168 current_line.push(JsonToken::Punctuation(",".to_string()));
169 }
170 } else {
171 depth += 1;
172 lines.push(current_line);
173 current_line = vec![JsonToken::Whitespace(make_indent(
174 depth.min(32),
175 self.indent,
176 ))];
177 }
178 }
179 '}' | ']' => {
180 chars.next();
181 depth = depth.saturating_sub(1);
182 lines.push(current_line);
183 current_line = vec![
184 JsonToken::Whitespace(make_indent(depth, self.indent)),
185 JsonToken::Punctuation(ch.to_string()),
186 ];
187 skip_ws(&mut chars);
189 if chars.peek() == Some(&',') {
190 chars.next();
191 current_line.push(JsonToken::Punctuation(",".to_string()));
192 }
193 }
194 '"' => {
195 let s = read_string(&mut chars);
196 skip_ws(&mut chars);
197 if chars.peek() == Some(&':') {
198 current_line.push(JsonToken::Key(s));
200 chars.next();
201 current_line.push(JsonToken::Punctuation(": ".to_string()));
202 skip_ws(&mut chars);
203 } else {
204 current_line.push(JsonToken::StringVal(s));
205 skip_ws(&mut chars);
207 if chars.peek() == Some(&',') {
208 chars.next();
209 current_line.push(JsonToken::Punctuation(",".to_string()));
210 lines.push(current_line);
211 current_line = vec![JsonToken::Whitespace(make_indent(
212 depth.min(32),
213 self.indent,
214 ))];
215 }
216 }
217 }
218 ',' => {
219 chars.next();
220 current_line.push(JsonToken::Punctuation(",".to_string()));
221 lines.push(current_line);
222 current_line = vec![JsonToken::Whitespace(make_indent(
223 depth.min(32),
224 self.indent,
225 ))];
226 }
227 ':' => {
228 chars.next();
229 current_line.push(JsonToken::Punctuation(": ".to_string()));
230 skip_ws(&mut chars);
231 }
232 ' ' | '\t' | '\r' | '\n' => {
233 chars.next();
234 }
235 _ => {
236 let literal = read_literal(&mut chars);
238 let tok = classify_literal(&literal);
239 current_line.push(tok);
240 skip_ws(&mut chars);
242 if chars.peek() == Some(&',') {
243 chars.next();
244 current_line.push(JsonToken::Punctuation(",".to_string()));
245 lines.push(current_line);
246 current_line = vec![JsonToken::Whitespace(make_indent(
247 depth.min(32),
248 self.indent,
249 ))];
250 }
251 }
252 }
253 }
254
255 if !current_line.is_empty() {
256 lines.push(current_line);
257 }
258
259 lines
260 }
261}
262
263fn make_indent(depth: usize, width: usize) -> String {
264 " ".repeat(depth * width)
265}
266
267fn skip_ws(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) {
268 while let Some(&ch) = chars.peek() {
269 if ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n' {
270 chars.next();
271 } else {
272 break;
273 }
274 }
275}
276
277fn read_string(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
278 let mut s = String::new();
279 s.push('"');
280 chars.next(); let mut escaped = false;
282 for ch in chars.by_ref() {
283 s.push(ch);
284 if escaped {
285 escaped = false;
286 } else if ch == '\\' {
287 escaped = true;
288 } else if ch == '"' {
289 break;
290 }
291 }
292 s
293}
294
295fn read_literal(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> String {
296 let mut s = String::new();
297 while let Some(&ch) = chars.peek() {
298 if ch == ','
299 || ch == '}'
300 || ch == ']'
301 || ch == ':'
302 || ch == ' '
303 || ch == '\n'
304 || ch == '\r'
305 || ch == '\t'
306 {
307 break;
308 }
309 s.push(ch);
310 chars.next();
311 }
312 s
313}
314
315fn classify_literal(s: &str) -> JsonToken {
316 match s {
317 "true" | "false" | "null" => JsonToken::Literal(s.to_string()),
318 _ => {
319 if s.bytes().all(|b| {
321 b.is_ascii_digit() || b == b'.' || b == b'-' || b == b'+' || b == b'e' || b == b'E'
322 }) && !s.is_empty()
323 {
324 JsonToken::Number(s.to_string())
325 } else {
326 JsonToken::Error(s.to_string())
327 }
328 }
329 }
330}
331
332impl Widget for JsonView {
333 fn render(&self, area: Rect, frame: &mut Frame) {
334 if area.width == 0 || area.height == 0 {
335 return;
336 }
337
338 let deg = frame.buffer.degradation;
339 frame.buffer.fill(area, ftui_render::cell::Cell::default());
340 if !deg.render_content() {
341 return;
342 }
343 let lines = self.formatted_lines();
344 let max_x = area.right();
345
346 for (row_idx, tokens) in lines.iter().enumerate() {
347 if row_idx >= area.height as usize {
348 break;
349 }
350
351 let y = area.y.saturating_add(row_idx as u16);
352 let mut x = area.x;
353
354 for token in tokens {
355 let (text, style) = match token {
356 JsonToken::Key(s) => (s.as_str(), self.key_style),
357 JsonToken::StringVal(s) => (s.as_str(), self.string_style),
358 JsonToken::Number(s) => (s.as_str(), self.number_style),
359 JsonToken::Literal(s) => (s.as_str(), self.literal_style),
360 JsonToken::Punctuation(s) => (s.as_str(), self.punct_style),
361 JsonToken::Whitespace(s) => (s.as_str(), Style::default()),
362 JsonToken::Error(s) => (s.as_str(), self.error_style),
363 JsonToken::Newline => continue,
364 };
365
366 if deg.apply_styling() {
367 x = draw_text_span(frame, x, y, text, style, max_x);
368 } else {
369 x = draw_text_span(frame, x, y, text, Style::default(), max_x);
370 }
371 }
372 }
373 }
374
375 fn is_essential(&self) -> bool {
376 false
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use ftui_render::budget::DegradationLevel;
384 use ftui_render::cell::{CellAttrs, PackedRgba};
385 use ftui_render::frame::Frame;
386 use ftui_render::grapheme_pool::GraphemePool;
387
388 #[test]
389 fn empty_source() {
390 let view = JsonView::new("");
391 assert!(view.formatted_lines().is_empty());
392 }
393
394 #[test]
395 fn simple_object() {
396 let view = JsonView::new(r#"{"a": 1}"#);
397 let lines = view.formatted_lines();
398 assert!(lines.len() >= 3); }
400
401 #[test]
402 fn nested_object() {
403 let view = JsonView::new(r#"{"a": {"b": 2}}"#);
404 let lines = view.formatted_lines();
405 assert!(lines.len() >= 3);
406 }
407
408 #[test]
409 fn array() {
410 let view = JsonView::new(r#"[1, 2, 3]"#);
411 let lines = view.formatted_lines();
412 assert!(lines.len() >= 3);
413 }
414
415 #[test]
416 fn empty_object() {
417 let view = JsonView::new(r#"{}"#);
418 let lines = view.formatted_lines();
419 assert!(!lines.is_empty());
420 }
422
423 #[test]
424 fn empty_array() {
425 let view = JsonView::new(r#"[]"#);
426 let lines = view.formatted_lines();
427 assert!(!lines.is_empty());
428 }
429
430 #[test]
431 fn string_values() {
432 let view = JsonView::new(r#"{"msg": "hello world"}"#);
433 let lines = view.formatted_lines();
434 let has_string = lines.iter().any(|line| {
436 line.iter()
437 .any(|t| matches!(t, JsonToken::StringVal(s) if s.contains("hello")))
438 });
439 assert!(has_string);
440 }
441
442 #[test]
443 fn boolean_and_null() {
444 let view = JsonView::new(r#"{"a": true, "b": false, "c": null}"#);
445 let lines = view.formatted_lines();
446 let has_literal = lines.iter().any(|line| {
447 line.iter()
448 .any(|t| matches!(t, JsonToken::Literal(s) if s == "true"))
449 });
450 assert!(has_literal);
451 }
452
453 #[test]
454 fn numbers() {
455 let view = JsonView::new(r#"{"x": 42, "y": -3.14}"#);
456 let lines = view.formatted_lines();
457 let has_number = lines.iter().any(|line| {
458 line.iter()
459 .any(|t| matches!(t, JsonToken::Number(s) if s == "42"))
460 });
461 assert!(has_number);
462 }
463
464 #[test]
465 fn escaped_string() {
466 let view = JsonView::new(r#"{"msg": "hello \"world\""}"#);
467 let lines = view.formatted_lines();
468 let has_escaped = lines.iter().any(|line| {
469 line.iter()
470 .any(|t| matches!(t, JsonToken::StringVal(s) if s.contains("\\\"")))
471 });
472 assert!(has_escaped);
473 }
474
475 #[test]
476 fn indent_width() {
477 let view = JsonView::new(r#"{"a": 1}"#).with_indent(4);
478 let lines = view.formatted_lines();
479 let has_4_indent = lines.iter().any(|line| {
480 line.iter()
481 .any(|t| matches!(t, JsonToken::Whitespace(s) if s == " "))
482 });
483 assert!(has_4_indent);
484 }
485
486 #[test]
487 fn render_basic() {
488 let view = JsonView::new(r#"{"key": "value"}"#);
489 let mut pool = GraphemePool::new();
490 let mut frame = Frame::new(40, 10, &mut pool);
491 let area = Rect::new(0, 0, 40, 10);
492 view.render(area, &mut frame);
493
494 let cell = frame.buffer.get(0, 0).unwrap();
496 assert_eq!(cell.content.as_char(), Some('{'));
497 }
498
499 #[test]
500 fn render_zero_area() {
501 let view = JsonView::new(r#"{"a": 1}"#);
502 let mut pool = GraphemePool::new();
503 let mut frame = Frame::new(40, 10, &mut pool);
504 view.render(Rect::new(0, 0, 0, 0), &mut frame); }
506
507 #[test]
508 fn render_truncated_height() {
509 let view = JsonView::new(r#"{"a": 1, "b": 2, "c": 3}"#);
510 let mut pool = GraphemePool::new();
511 let mut frame = Frame::new(40, 2, &mut pool);
512 let area = Rect::new(0, 0, 40, 2);
513 view.render(area, &mut frame); }
515
516 #[test]
517 fn is_not_essential() {
518 let view = JsonView::new("");
519 assert!(!view.is_essential());
520 }
521
522 #[test]
523 fn default_impl() {
524 let view = JsonView::default();
525 assert!(view.source().is_empty());
526 }
527
528 #[test]
529 fn set_source() {
530 let mut view = JsonView::new("");
531 view.set_source(r#"{"a": 1}"#);
532 assert!(!view.formatted_lines().is_empty());
533 }
534
535 #[test]
536 fn plain_literal() {
537 let view = JsonView::new("42");
538 let lines = view.formatted_lines();
539 assert_eq!(lines.len(), 1);
540 }
541
542 #[test]
545 fn whitespace_only_source() {
546 let view = JsonView::new(" \n\t ");
547 assert!(view.formatted_lines().is_empty());
548 }
549
550 #[test]
551 fn deeply_nested_objects() {
552 let open: String = "{\"a\": ".repeat(35);
554 let close: String = "}".repeat(35);
555 let json = format!("{open}1{close}");
556 let view = JsonView::new(json);
557 let lines = view.formatted_lines();
558 assert!(lines.len() > 10);
560 }
561
562 #[test]
563 fn scientific_notation_number() {
564 let view = JsonView::new(r#"{"x": 1.23e+10}"#);
565 let lines = view.formatted_lines();
566 let has_sci = lines.iter().any(|line| {
567 line.iter()
568 .any(|t| matches!(t, JsonToken::Number(s) if s.contains("e+")))
569 });
570 assert!(has_sci, "scientific notation should be Number: {lines:?}");
571 }
572
573 #[test]
574 fn empty_string_key_and_value() {
575 let view = JsonView::new(r#"{"": ""}"#);
576 let lines = view.formatted_lines();
577 let has_empty_key = lines.iter().any(|line| {
578 line.iter()
579 .any(|t| matches!(t, JsonToken::Key(s) if s == "\"\""))
580 });
581 assert!(has_empty_key, "empty key should be present: {lines:?}");
582 }
583
584 #[test]
585 fn unicode_in_strings() {
586 let view = JsonView::new(r#"{"emoji": "🎉🚀"}"#);
587 let lines = view.formatted_lines();
588 let has_emoji = lines.iter().any(|line| {
589 line.iter()
590 .any(|t| matches!(t, JsonToken::StringVal(s) if s.contains('🎉')))
591 });
592 assert!(has_emoji);
593 }
594
595 #[test]
596 fn unclosed_string() {
597 let view = JsonView::new(r#"{"key": "val"#);
599 let lines = view.formatted_lines();
600 assert!(!lines.is_empty());
602 }
603
604 #[test]
605 fn unclosed_object() {
606 let view = JsonView::new(r#"{"a": 1"#);
607 let lines = view.formatted_lines();
608 assert!(!lines.is_empty());
609 }
610
611 #[test]
612 fn unclosed_array() {
613 let view = JsonView::new(r#"[1, 2, 3"#);
614 let lines = view.formatted_lines();
615 assert!(!lines.is_empty());
616 }
617
618 #[test]
619 fn nested_empty_containers() {
620 let view = JsonView::new(r#"{"a": [], "b": {}}"#);
621 let lines = view.formatted_lines();
622 let flat = lines
624 .iter()
625 .map(|line| {
626 line.iter()
627 .filter_map(|t| match t {
628 JsonToken::Punctuation(s) => Some(s.as_str()),
629 _ => None,
630 })
631 .collect::<String>()
632 })
633 .collect::<String>();
634 assert!(flat.contains("[]"), "empty array should be compact: {flat}");
635 assert!(
636 flat.contains("{}"),
637 "empty object should be compact: {flat}"
638 );
639 }
640
641 #[test]
642 fn array_of_mixed_types() {
643 let view = JsonView::new(r#"[1, "two", true, null]"#);
644 let lines = view.formatted_lines();
645 let all_tokens: Vec<&JsonToken> = lines.iter().flat_map(|l| l.iter()).collect();
646 assert!(all_tokens.iter().any(|t| matches!(t, JsonToken::Number(_))));
647 assert!(
648 all_tokens
649 .iter()
650 .any(|t| matches!(t, JsonToken::StringVal(_)))
651 );
652 assert!(
653 all_tokens
654 .iter()
655 .any(|t| matches!(t, JsonToken::Literal(s) if s == "true"))
656 );
657 assert!(
658 all_tokens
659 .iter()
660 .any(|t| matches!(t, JsonToken::Literal(s) if s == "null"))
661 );
662 }
663
664 #[test]
665 fn zero_indent_width() {
666 let view = JsonView::new(r#"{"a": 1}"#).with_indent(0);
667 let lines = view.formatted_lines();
668 for line in &lines {
670 for token in line {
671 if let JsonToken::Whitespace(s) = token {
672 assert!(s.is_empty(), "zero indent should produce empty whitespace");
673 }
674 }
675 }
676 }
677
678 #[test]
679 fn bare_string_top_level() {
680 let view = JsonView::new(r#""hello""#);
681 let lines = view.formatted_lines();
682 assert_eq!(lines.len(), 1);
683 assert!(
684 lines[0]
685 .iter()
686 .any(|t| matches!(t, JsonToken::StringVal(s) if s.contains("hello")))
687 );
688 }
689
690 #[test]
691 fn error_token_for_invalid_literal() {
692 let view = JsonView::new(r#"{"a": undefined}"#);
693 let lines = view.formatted_lines();
694 let has_error = lines
695 .iter()
696 .any(|line| line.iter().any(|t| matches!(t, JsonToken::Error(_))));
697 assert!(has_error, "undefined should produce Error token");
698 }
699
700 #[test]
701 fn clone_independence() {
702 let view = JsonView::new(r#"{"a": 1}"#);
703 let cloned = view.clone();
704 assert_eq!(view.source(), cloned.source());
705 }
706
707 #[test]
708 fn debug_format() {
709 let view = JsonView::new("{}");
710 let dbg = format!("{view:?}");
711 assert!(dbg.contains("JsonView"));
712 }
713
714 #[test]
715 fn style_builders_chain() {
716 let view = JsonView::new("{}")
717 .with_indent(4)
718 .with_key_style(Style::new().bold())
719 .with_string_style(Style::default())
720 .with_number_style(Style::default())
721 .with_literal_style(Style::default())
722 .with_punct_style(Style::default())
723 .with_error_style(Style::default());
724 assert_eq!(view.indent, 4);
725 }
726
727 #[test]
728 fn render_width_one() {
729 let view = JsonView::new(r#"{"a": 1}"#);
730 let mut pool = GraphemePool::new();
731 let mut frame = Frame::new(1, 10, &mut pool);
732 view.render(Rect::new(0, 0, 1, 10), &mut frame);
733 let cell = frame.buffer.get(0, 0).unwrap();
735 assert_eq!(cell.content.as_char(), Some('{'));
736 }
737
738 #[test]
739 fn render_no_styling_drops_token_styles() {
740 let key_color = PackedRgba::rgb(1, 2, 3);
741 let number_color = PackedRgba::rgb(4, 5, 6);
742 let view = JsonView::new(r#"{"key": 1}"#)
743 .with_key_style(Style::new().fg(key_color).bold())
744 .with_number_style(Style::new().fg(number_color).italic());
745 let mut pool = GraphemePool::new();
746 let mut frame = Frame::new(40, 10, &mut pool);
747 frame.buffer.degradation = DegradationLevel::NoStyling;
748 view.render(Rect::new(0, 0, 40, 10), &mut frame);
749
750 let key_cell = frame.buffer.get(2, 1).unwrap();
751 assert_eq!(key_cell.content.as_char(), Some('"'));
752 assert_ne!(key_cell.fg, key_color);
753 assert_eq!(key_cell.attrs, CellAttrs::NONE);
754
755 let number_cell = frame.buffer.get(9, 1).unwrap();
756 assert_eq!(number_cell.content.as_char(), Some('1'));
757 assert_ne!(number_cell.fg, number_color);
758 assert_eq!(number_cell.attrs, CellAttrs::NONE);
759 }
760
761 #[test]
762 fn render_skeleton_is_noop() {
763 let view = JsonView::new(r#"{"key": "value"}"#);
764 let mut pool = GraphemePool::new();
765 let mut frame = Frame::new(40, 10, &mut pool);
766 let area = Rect::new(0, 0, 40, 10);
767 view.render(area, &mut frame);
768 frame.buffer.degradation = DegradationLevel::Skeleton;
769 view.render(area, &mut frame);
770
771 for y in 0..10 {
772 for x in 0..40 {
773 assert_eq!(
774 frame.buffer.get(x, y),
775 Some(&ftui_render::cell::Cell::default())
776 );
777 }
778 }
779 }
780
781 #[test]
782 fn render_shorter_json_clears_stale_suffix_and_rows() {
783 let long = JsonView::new(r#"{"alpha": 1000, "beta": 2000}"#);
784 let short = JsonView::new(r#"{"a": 1}"#);
785 let mut pool = GraphemePool::new();
786 let mut frame = Frame::new(40, 10, &mut pool);
787 let area = Rect::new(0, 0, 40, 10);
788
789 long.render(area, &mut frame);
790 short.render(area, &mut frame);
791
792 for y in 0..10u16 {
793 for x in 0..40u16 {
794 if y >= 3 {
795 assert_eq!(
796 frame.buffer.get(x, y),
797 Some(&ftui_render::cell::Cell::default())
798 );
799 }
800 }
801 }
802 }
803
804 #[test]
805 fn json_token_eq() {
806 assert_eq!(JsonToken::Key("a".into()), JsonToken::Key("a".into()));
807 assert_ne!(JsonToken::Key("a".into()), JsonToken::StringVal("a".into()));
808 assert_ne!(JsonToken::Newline, JsonToken::Whitespace("".into()));
809 }
810
811 #[test]
812 fn json_token_clone_and_debug() {
813 let tokens = vec![
814 JsonToken::Key("k".into()),
815 JsonToken::StringVal("s".into()),
816 JsonToken::Number("1".into()),
817 JsonToken::Literal("true".into()),
818 JsonToken::Punctuation("{".into()),
819 JsonToken::Whitespace(" ".into()),
820 JsonToken::Newline,
821 JsonToken::Error("bad".into()),
822 ];
823 for tok in &tokens {
824 let cloned = tok.clone();
825 assert_eq!(tok, &cloned);
826 let _ = format!("{tok:?}");
827 }
828 }
829
830 #[test]
831 fn classify_literal_empty_string() {
832 let result = classify_literal("");
834 assert!(matches!(result, JsonToken::Error(s) if s.is_empty()));
835 }
836
837 #[test]
838 fn negative_number() {
839 assert_eq!(
840 classify_literal("-42"),
841 JsonToken::Number("-42".to_string())
842 );
843 }
844
845 #[test]
846 fn number_with_exponent() {
847 assert_eq!(
848 classify_literal("5E-3"),
849 JsonToken::Number("5E-3".to_string())
850 );
851 }
852
853 #[test]
856 fn classify_literal_types() {
857 assert_eq!(
858 classify_literal("true"),
859 JsonToken::Literal("true".to_string())
860 );
861 assert_eq!(
862 classify_literal("false"),
863 JsonToken::Literal("false".to_string())
864 );
865 assert_eq!(
866 classify_literal("null"),
867 JsonToken::Literal("null".to_string())
868 );
869 assert_eq!(classify_literal("42"), JsonToken::Number("42".to_string()));
870 assert_eq!(
871 classify_literal("-3.14"),
872 JsonToken::Number("-3.14".to_string())
873 );
874 assert!(matches!(classify_literal("invalid!"), JsonToken::Error(_)));
875 }
876}