use std::fmt::Write as _;
use serde::Serialize;
use serde_json::Value;
use crate::segment::Segment;
use crate::style::Style;
#[derive(Debug, Clone)]
pub struct JsonTheme {
pub key: Style,
pub string: Style,
pub number: Style,
pub bool_true: Style,
pub bool_false: Style,
pub null: Style,
pub bracket: Style,
pub punctuation: Style,
}
impl Default for JsonTheme {
fn default() -> Self {
Self {
key: Style::new().color_str("blue").unwrap_or_default().bold(),
string: Style::new().color_str("green").unwrap_or_default(),
number: Style::new().color_str("cyan").unwrap_or_default().bold(),
null: Style::new()
.color_str("magenta")
.unwrap_or_default()
.italic(),
bool_true: Style::new()
.color_str("bright_green")
.unwrap_or_default()
.italic(),
bool_false: Style::new()
.color_str("bright_red")
.unwrap_or_default()
.italic(),
bracket: Style::new().bold(),
punctuation: Style::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum JsonIndent {
None,
Spaces(usize),
String(String),
}
impl Default for JsonIndent {
fn default() -> Self {
Self::Spaces(2)
}
}
#[derive(Debug, Clone)]
pub struct JsonOptions {
pub indent: JsonIndent,
pub highlight: bool,
pub sort_keys: bool,
pub ensure_ascii: bool,
}
impl Default for JsonOptions {
fn default() -> Self {
Self {
indent: JsonIndent::default(),
highlight: true,
sort_keys: false,
ensure_ascii: false,
}
}
}
#[derive(Debug, Clone)]
pub struct Json {
value: Value,
indent: JsonIndent,
sort_keys: bool,
ensure_ascii: bool,
highlight: bool,
theme: JsonTheme,
}
impl Json {
#[must_use]
pub fn new(value: Value) -> Self {
Self {
value,
indent: JsonIndent::default(),
sort_keys: false,
ensure_ascii: false,
highlight: true,
theme: JsonTheme::default(),
}
}
#[must_use]
pub fn with_options(value: Value, options: JsonOptions) -> Self {
Self {
value,
indent: options.indent,
sort_keys: options.sort_keys,
ensure_ascii: options.ensure_ascii,
highlight: options.highlight,
theme: JsonTheme::default(),
}
}
#[expect(
clippy::should_implement_trait,
reason = "returns Result with custom error, not FromStr pattern"
)]
pub fn from_str(s: &str) -> Result<Self, JsonError> {
let value: Value = serde_json::from_str(s).map_err(JsonError::Parse)?;
Ok(Self::new(value))
}
pub fn from_str_with_options(s: &str, options: JsonOptions) -> Result<Self, JsonError> {
let value: Value = serde_json::from_str(s).map_err(JsonError::Parse)?;
Ok(Self::with_options(value, options))
}
pub fn from_data<T: Serialize>(data: &T) -> Result<Self, JsonError> {
let value = serde_json::to_value(data).map_err(JsonError::Serialize)?;
Ok(Self::new(value))
}
#[must_use]
pub fn indent(mut self, spaces: usize) -> Self {
self.indent = JsonIndent::Spaces(spaces);
self
}
#[must_use]
pub fn indent_str(mut self, unit: impl Into<String>) -> Self {
self.indent = JsonIndent::String(unit.into());
self
}
#[must_use]
pub fn compact(mut self) -> Self {
self.indent = JsonIndent::None;
self
}
#[must_use]
pub fn sort_keys(mut self, sort: bool) -> Self {
self.sort_keys = sort;
self
}
#[must_use]
pub fn ensure_ascii(mut self, ensure_ascii: bool) -> Self {
self.ensure_ascii = ensure_ascii;
self
}
#[must_use]
pub fn highlight(mut self, highlight: bool) -> Self {
self.highlight = highlight;
self
}
#[must_use]
pub fn theme(mut self, theme: JsonTheme) -> Self {
self.theme = theme;
self
}
fn style(&self, style: &Style) -> Option<Style> {
if self.highlight {
Some(style.clone())
} else {
None
}
}
fn is_compact(&self) -> bool {
matches!(self.indent, JsonIndent::None)
}
fn indent_prefix(&self, depth: usize, tab_size: usize) -> String {
match &self.indent {
JsonIndent::None => String::new(),
JsonIndent::Spaces(n) => " ".repeat(n.saturating_mul(depth)),
JsonIndent::String(unit) => expand_tabs_at_col0(&unit.repeat(depth), tab_size),
}
}
fn render_value(&self, value: &Value, depth: usize, tab_size: usize) -> Vec<Segment<'_>> {
match value {
Value::Null => vec![Segment::new("null", self.style(&self.theme.null))],
Value::Bool(b) => {
let text = if *b { "true" } else { "false" };
let style = if *b {
self.style(&self.theme.bool_true)
} else {
self.style(&self.theme.bool_false)
};
vec![Segment::new(text, style)]
}
Value::Number(n) => {
vec![Segment::new(n.to_string(), self.style(&self.theme.number))]
}
Value::String(s) => {
let escaped = escape_json_string(s, self.ensure_ascii);
vec![Segment::new(
format!("\"{escaped}\""),
self.style(&self.theme.string),
)]
}
Value::Array(arr) => self.render_array(arr, depth, tab_size),
Value::Object(obj) => self.render_object(obj, depth, tab_size),
}
}
fn render_array(&self, arr: &[Value], depth: usize, tab_size: usize) -> Vec<Segment<'_>> {
const MAX_DEPTH: usize = 20;
if depth > MAX_DEPTH {
return vec![Segment::new("[...]", self.style(&self.theme.bracket))];
}
if arr.is_empty() {
return vec![Segment::new("[]", self.style(&self.theme.bracket))];
}
let mut segments = Vec::new();
segments.push(Segment::new("[", self.style(&self.theme.bracket)));
if self.is_compact() {
for (i, item) in arr.iter().enumerate() {
segments.extend(self.render_value(item, depth + 1, tab_size));
if i < arr.len() - 1 {
segments.push(Segment::new(", ", self.style(&self.theme.punctuation)));
}
}
segments.push(Segment::new("]", self.style(&self.theme.bracket)));
} else {
let indent_str = self.indent_prefix(depth + 1, tab_size);
let close_indent = self.indent_prefix(depth, tab_size);
segments.push(Segment::new("\n", None));
for (i, item) in arr.iter().enumerate() {
segments.push(Segment::new(indent_str.clone(), None));
segments.extend(self.render_value(item, depth + 1, tab_size));
if i < arr.len() - 1 {
segments.push(Segment::new(",", self.style(&self.theme.punctuation)));
}
segments.push(Segment::new("\n", None));
}
segments.push(Segment::new(close_indent, None));
segments.push(Segment::new("]", self.style(&self.theme.bracket)));
}
segments
}
fn render_object(
&self,
obj: &serde_json::Map<String, Value>,
depth: usize,
tab_size: usize,
) -> Vec<Segment<'_>> {
const MAX_DEPTH: usize = 20;
if depth > MAX_DEPTH {
return vec![Segment::new("{...}", self.style(&self.theme.bracket))];
}
if obj.is_empty() {
return vec![Segment::new("{}", self.style(&self.theme.bracket))];
}
let mut segments = Vec::new();
let keys: Vec<&String> = if self.sort_keys {
let mut k: Vec<_> = obj.keys().collect();
k.sort();
k
} else {
obj.keys().collect()
};
segments.push(Segment::new("{", self.style(&self.theme.bracket)));
if self.is_compact() {
for (i, key) in keys.iter().enumerate() {
let value = &obj[*key];
let escaped_key = escape_json_string(key, self.ensure_ascii);
segments.push(Segment::new(
format!("\"{escaped_key}\""),
self.style(&self.theme.key),
));
segments.push(Segment::new(": ", self.style(&self.theme.punctuation)));
segments.extend(self.render_value(value, depth + 1, tab_size));
if i < keys.len() - 1 {
segments.push(Segment::new(", ", self.style(&self.theme.punctuation)));
}
}
segments.push(Segment::new("}", self.style(&self.theme.bracket)));
} else {
let indent_str = self.indent_prefix(depth + 1, tab_size);
let close_indent = self.indent_prefix(depth, tab_size);
segments.push(Segment::new("\n", None));
for (i, key) in keys.iter().enumerate() {
let value = &obj[*key];
segments.push(Segment::new(indent_str.clone(), None));
let escaped_key = escape_json_string(key, self.ensure_ascii);
segments.push(Segment::new(
format!("\"{escaped_key}\""),
self.style(&self.theme.key),
));
segments.push(Segment::new(": ", self.style(&self.theme.punctuation)));
segments.extend(self.render_value(value, depth + 1, tab_size));
if i < keys.len() - 1 {
segments.push(Segment::new(",", self.style(&self.theme.punctuation)));
}
segments.push(Segment::new("\n", None));
}
segments.push(Segment::new(close_indent, None));
segments.push(Segment::new("}", self.style(&self.theme.bracket)));
}
segments
}
#[must_use]
pub fn render_with_tab_size(&self, tab_size: usize) -> Vec<Segment<'_>> {
self.render_value(&self.value, 0, tab_size)
}
#[must_use]
pub fn render(&self) -> Vec<Segment<'_>> {
self.render_with_tab_size(8)
}
#[must_use]
pub fn to_plain_string(&self) -> String {
self.render().iter().map(|s| s.text.as_ref()).collect()
}
}
fn escape_json_string(s: &str, ensure_ascii: bool) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\u{0008}' => result.push_str("\\b"),
'\u{000c}' => result.push_str("\\f"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
c if ensure_ascii && !c.is_ascii() => {
let code = c as u32;
if code <= 0xFFFF {
let _ = write!(result, "\\u{code:04x}");
} else {
let n = code - 0x1_0000;
let high_bits = u16::try_from((n >> 10) & 0x03FF).unwrap_or_default();
let low_bits = u16::try_from(n & 0x03FF).unwrap_or_default();
let high = 0xD800u16 | high_bits;
let low = 0xDC00u16 | low_bits;
let _ = write!(result, "\\u{high:04x}\\u{low:04x}");
}
}
c if c.is_control() => {
let _ = write!(result, "\\u{:04x}", c as u32);
}
c => result.push(c),
}
}
result
}
fn expand_tabs_at_col0(s: &str, tab_size: usize) -> String {
if tab_size == 0 || !s.contains('\t') {
return s.to_string();
}
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
if ch == '\t' {
out.push_str(&" ".repeat(tab_size));
} else {
out.push(ch);
}
}
out
}
#[derive(Debug)]
pub enum JsonError {
Parse(serde_json::Error),
Serialize(serde_json::Error),
}
impl std::fmt::Display for JsonError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parse(e) => write!(f, "JSON parse error: {e}"),
Self::Serialize(e) => write!(f, "JSON serialize error: {e}"),
}
}
}
impl std::error::Error for JsonError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Parse(e) => Some(e),
Self::Serialize(e) => Some(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_json_null() {
let json = Json::new(Value::Null);
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "null");
}
#[test]
fn test_json_bool_true() {
let json = Json::new(Value::Bool(true));
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "true");
}
#[test]
fn test_json_bool_false() {
let json = Json::new(Value::Bool(false));
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "false");
}
#[test]
fn test_json_number_int() {
let json = Json::new(serde_json::json!(42));
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "42");
}
#[test]
fn test_json_number_float() {
let json = Json::new(serde_json::json!(1.23));
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "1.23");
}
#[test]
fn test_json_string() {
let json = Json::new(serde_json::json!("hello"));
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "\"hello\"");
}
#[test]
fn test_json_string_escaped() {
let json = Json::new(serde_json::json!("line1\nline2"));
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "\"line1\\nline2\"");
}
#[test]
fn test_json_string_ensure_ascii_surrogate_pair() {
let json = Json::new(serde_json::json!("😀")).ensure_ascii(true);
let text = json.to_plain_string();
assert_eq!(text, "\"\\ud83d\\ude00\"");
}
#[test]
fn test_json_empty_array() {
let json = Json::new(serde_json::json!([]));
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "[]");
}
#[test]
fn test_json_simple_array() {
let json = Json::new(serde_json::json!([1, 2, 3])).indent(2);
let text = json.to_plain_string();
assert!(text.contains("[\n"));
assert!(text.contains(" 1"));
assert!(text.contains(" 2"));
assert!(text.contains(" 3"));
assert!(text.contains(']'));
}
#[test]
fn test_json_empty_object() {
let json = Json::new(serde_json::json!({}));
let segments = json.render();
let text: String = segments.iter().map(|s| s.text.as_ref()).collect();
assert_eq!(text, "{}");
}
#[test]
fn test_json_simple_object() {
let json = Json::new(serde_json::json!({"name": "Alice"})).indent(2);
let text = json.to_plain_string();
assert!(text.contains("{\n"));
assert!(text.contains("\"name\""));
assert!(text.contains(": \"Alice\""));
assert!(text.contains('}'));
}
#[test]
fn test_json_compact_object_has_spaces() {
let json = Json::new(serde_json::json!({"age": 30, "name": "Alice"})).compact();
let text = json.to_plain_string();
assert!(text.starts_with('{'));
assert!(text.ends_with('}'));
assert!(text.contains("\"age\": 30"));
assert!(text.contains(", \"name\""));
assert!(text.contains(": "));
assert!(text.contains(", "));
assert!(!text.contains('\n'));
}
#[test]
fn test_json_nested_object() {
let json = Json::new(serde_json::json!({
"person": {
"name": "Alice",
"age": 30
}
}))
.indent(2);
let text = json.to_plain_string();
assert!(text.contains("\"person\""));
assert!(text.contains("\"name\""));
assert!(text.contains("\"Alice\""));
assert!(text.contains("\"age\""));
assert!(text.contains("30"));
}
#[test]
fn test_json_from_str() {
let json = Json::from_str(r#"{"key": "value"}"#).unwrap();
let text = json.to_plain_string();
assert!(text.contains("\"key\""));
assert!(text.contains("\"value\""));
}
#[test]
fn test_json_from_str_invalid() {
let result = Json::from_str("not valid json");
assert!(result.is_err());
}
#[test]
fn test_json_from_data_round_trip() {
#[derive(Serialize)]
struct X {
a: i32,
}
let json = Json::from_data(&X { a: 1 }).unwrap();
assert!(json.to_plain_string().contains("\"a\""));
}
#[test]
fn test_json_sort_keys() {
let json = Json::new(serde_json::json!({"z": 1, "a": 2, "m": 3})).sort_keys(true);
let text = json.to_plain_string();
let pos_a = text.find("\"a\"").unwrap();
let pos_m = text.find("\"m\"").unwrap();
let pos_z = text.find("\"z\"").unwrap();
assert!(pos_a < pos_m);
assert!(pos_m < pos_z);
}
#[test]
fn test_json_no_highlight() {
let json = Json::new(serde_json::json!("test")).highlight(false);
let segments = json.render();
assert!(segments.iter().all(|s| s.style.is_none()));
}
#[test]
fn test_json_with_highlight() {
let json = Json::new(serde_json::json!("test")).highlight(true);
let segments = json.render();
assert!(segments.iter().any(|s| s.style.is_some()));
}
#[test]
fn test_json_custom_indent() {
let json = Json::new(serde_json::json!([1])).indent(4);
let text = json.to_plain_string();
assert!(text.contains(" 1"));
}
#[test]
fn test_json_mixed_array() {
let json = Json::new(serde_json::json!([1, "two", true, null]));
let text = json.to_plain_string();
assert!(text.contains('1'));
assert!(text.contains("\"two\""));
assert!(text.contains("true"));
assert!(text.contains("null"));
}
#[test]
fn test_json_complex() {
let json = Json::new(serde_json::json!({
"users": [
{"name": "Alice", "active": true},
{"name": "Bob", "active": false}
],
"count": 2,
"meta": null
}))
.sort_keys(true);
let text = json.to_plain_string();
assert!(text.contains("\"users\""));
assert!(text.contains("\"count\""));
assert!(text.contains("\"meta\""));
assert!(text.contains("\"Alice\""));
assert!(text.contains("\"Bob\""));
assert!(text.contains("true"));
assert!(text.contains("false"));
assert!(text.contains("null"));
assert!(text.contains('2'));
}
#[test]
fn test_escape_json_string() {
assert_eq!(escape_json_string("hello", false), "hello");
assert_eq!(escape_json_string("say \"hi\"", false), "say \\\"hi\\\"");
assert_eq!(escape_json_string("a\\b", false), "a\\\\b");
assert_eq!(escape_json_string("line1\nline2", false), "line1\\nline2");
assert_eq!(escape_json_string("tab\there", false), "tab\\there");
assert_eq!(escape_json_string("\u{0008}", false), "\\b");
assert_eq!(escape_json_string("\u{000c}", false), "\\f");
}
#[test]
fn test_json_custom_theme() {
let theme = JsonTheme {
key: Style::new().color_str("red").unwrap_or_default(),
string: Style::new().color_str("blue").unwrap_or_default(),
number: Style::new().color_str("green").unwrap_or_default(),
bool_true: Style::new().color_str("yellow").unwrap_or_default(),
bool_false: Style::new().color_str("yellow").unwrap_or_default(),
null: Style::new().color_str("white").unwrap_or_default(),
bracket: Style::new().color_str("cyan").unwrap_or_default(),
punctuation: Style::new().color_str("magenta").unwrap_or_default(),
};
let json = Json::new(serde_json::json!({"key": "value"})).theme(theme);
let segments = json.render();
assert!(segments.iter().any(|s| s.style.is_some()));
}
}