use crate::console::RenderContext;
use crate::highlighter::{Highlighter, JsonHighlighter};
use crate::renderable::{Renderable, Segment};
#[cfg(test)]
use crate::text::Overflow;
use crate::text::Text;
use serde::Serialize;
use serde_json::{self, Value};
use std::fmt;
use std::fs;
use std::path::Path;
#[derive(Debug)]
pub enum JsonError {
Parse(serde_json::Error),
Serialize(serde_json::Error),
Io(std::io::Error),
}
impl fmt::Display for JsonError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JsonError::Parse(e) => write!(f, "JSON parse error: {}", e),
JsonError::Serialize(e) => write!(f, "JSON serialize error: {}", e),
JsonError::Io(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for JsonError {}
impl From<std::io::Error> for JsonError {
fn from(e: std::io::Error) -> Self {
JsonError::Io(e)
}
}
#[derive(Debug, Clone)]
pub enum JsonIndent {
Compact,
Spaces(usize),
Custom(String),
}
impl Default for JsonIndent {
fn default() -> Self {
JsonIndent::Spaces(2)
}
}
impl From<usize> for JsonIndent {
fn from(n: usize) -> Self {
if n == 0 {
JsonIndent::Compact
} else {
JsonIndent::Spaces(n)
}
}
}
impl From<&str> for JsonIndent {
fn from(s: &str) -> Self {
JsonIndent::Custom(s.to_string())
}
}
#[derive(Debug, Clone)]
pub struct JsonOptions {
pub indent: JsonIndent,
pub highlight: bool,
pub sort_keys: bool,
pub ensure_ascii: bool,
pub no_wrap: bool,
}
impl Default for JsonOptions {
fn default() -> Self {
JsonOptions {
indent: JsonIndent::Spaces(2),
highlight: true,
sort_keys: false,
ensure_ascii: false,
no_wrap: true, }
}
}
#[derive(Debug, Clone)]
pub struct Json {
text: Text,
value: Value,
options: JsonOptions,
}
impl Json {
#[allow(clippy::should_implement_trait)]
pub fn from_str(json: &str) -> Result<Self, JsonError> {
Self::from_str_with_options(json, JsonOptions::default())
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, JsonError> {
let content = fs::read_to_string(path)?;
Self::from_str(&content)
}
pub fn from_data<T: Serialize>(data: &T) -> Result<Self, JsonError> {
let value = serde_json::to_value(data).map_err(JsonError::Serialize)?;
Self::from_value(value, JsonOptions::default())
}
pub fn from_str_with_options(json: &str, options: JsonOptions) -> Result<Self, JsonError> {
let value: Value = serde_json::from_str(json).map_err(JsonError::Parse)?;
Self::from_value(value, options)
}
fn from_value(value: Value, options: JsonOptions) -> Result<Self, JsonError> {
let text = Self::render_value(&value, &options);
Ok(Json {
text,
value,
options,
})
}
fn render_value(value: &Value, options: &JsonOptions) -> Text {
let value_to_render = if options.sort_keys {
sort_json_keys(value)
} else {
value.clone()
};
let formatted = format_json(&value_to_render, &options.indent, options.ensure_ascii);
let mut text = if options.highlight {
let highlighter = JsonHighlighter::new();
let spans = highlighter.highlight(&formatted);
Text::from_spans(spans)
} else {
Text::plain(formatted)
};
if options.no_wrap {
text = text.no_wrap();
}
text
}
fn rerender(&self) -> Self {
let text = Self::render_value(&self.value, &self.options);
Json {
text,
value: self.value.clone(),
options: self.options.clone(),
}
}
pub fn indent(mut self, spaces: usize) -> Self {
self.options.indent = JsonIndent::from(spaces);
self.rerender()
}
pub fn indent_with(mut self, indent_str: &str) -> Self {
self.options.indent = JsonIndent::Custom(indent_str.to_string());
self.rerender()
}
pub fn compact(mut self) -> Self {
self.options.indent = JsonIndent::Compact;
self.rerender()
}
pub fn sort_keys(mut self) -> Self {
self.options.sort_keys = true;
self.rerender()
}
pub fn ensure_ascii(mut self) -> Self {
self.options.ensure_ascii = true;
self.rerender()
}
pub fn no_highlight(mut self) -> Self {
self.options.highlight = false;
self.rerender()
}
pub fn wrap(mut self) -> Self {
self.options.no_wrap = false;
self.rerender()
}
pub fn plain_text(&self) -> String {
self.text.plain_text()
}
}
impl Renderable for Json {
fn render(&self, context: &RenderContext) -> Vec<Segment> {
self.text.render(context)
}
}
const ERR_SERIALIZE_COMPACT: &str = "Failed to serialize JSON value to string";
const ERR_SERIALIZE_FORMATTED: &str = "Failed to serialize JSON value with custom formatter";
const ERR_INVALID_UTF8: &str = "JSON serialization produced invalid UTF-8";
fn format_json(value: &Value, indent: &JsonIndent, ensure_ascii: bool) -> String {
match indent {
JsonIndent::Compact => {
let result = serde_json::to_string(value).expect(ERR_SERIALIZE_COMPACT);
if ensure_ascii {
escape_non_ascii(&result)
} else {
result
}
}
JsonIndent::Spaces(n) => {
let indent_bytes = " ".repeat(*n).into_bytes();
let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
let mut buf = Vec::new();
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
value.serialize(&mut ser).expect(ERR_SERIALIZE_FORMATTED);
let result = String::from_utf8(buf).expect(ERR_INVALID_UTF8);
if ensure_ascii {
escape_non_ascii(&result)
} else {
result
}
}
JsonIndent::Custom(s) => {
let indent_bytes = s.as_bytes().to_vec();
let formatter = serde_json::ser::PrettyFormatter::with_indent(&indent_bytes);
let mut buf = Vec::new();
let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
value.serialize(&mut ser).expect(ERR_SERIALIZE_FORMATTED);
let result = String::from_utf8(buf).expect(ERR_INVALID_UTF8);
if ensure_ascii {
escape_non_ascii(&result)
} else {
result
}
}
}
}
fn escape_non_ascii(s: &str) -> String {
let mut result = String::with_capacity(s.len());
for c in s.chars() {
if c.is_ascii() {
result.push(c);
} else {
let code = c as u32;
if code <= 0xFFFF {
result.push_str(&format!("\\u{:04x}", code));
} else {
let code = code - 0x10000;
let high = 0xD800 + (code >> 10);
let low = 0xDC00 + (code & 0x3FF);
result.push_str(&format!("\\u{:04x}\\u{:04x}", high, low));
}
}
}
result
}
fn sort_json_keys(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut sorted: Vec<_> = map.iter().collect();
sorted.sort_by(|a, b| a.0.cmp(b.0));
let sorted_map: serde_json::Map<String, Value> = sorted
.into_iter()
.map(|(k, v)| (k.clone(), sort_json_keys(v)))
.collect();
Value::Object(sorted_map)
}
Value::Array(arr) => Value::Array(arr.iter().map(sort_json_keys).collect()),
_ => value.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_json_from_str() {
let json = Json::from_str(r#"{"name": "Alice"}"#);
assert!(json.is_ok());
}
#[test]
fn test_json_invalid() {
let json = Json::from_str("not valid json");
assert!(json.is_err());
}
#[test]
fn test_json_from_data() {
#[derive(Serialize)]
struct User {
name: String,
age: u32,
}
let user = User {
name: "Bob".to_string(),
age: 25,
};
let json = Json::from_data(&user);
assert!(json.is_ok());
let text = json.unwrap().plain_text();
assert!(text.contains("Bob"));
assert!(text.contains("25"));
}
#[test]
fn test_json_sort_keys() {
let json = Json::from_str(r#"{"z": 1, "a": 2, "m": 3}"#)
.unwrap()
.sort_keys();
let text = json.plain_text();
let a_pos = text.find("\"a\"").unwrap();
let m_pos = text.find("\"m\"").unwrap();
let z_pos = text.find("\"z\"").unwrap();
assert!(a_pos < m_pos);
assert!(m_pos < z_pos);
}
#[test]
fn test_json_nested() {
let json = Json::from_str(r#"{"user": {"name": "Alice", "scores": [1, 2, 3]}}"#);
assert!(json.is_ok());
}
#[test]
fn test_json_types() {
let json = Json::from_str(
r#"{"str": "hello", "num": 42, "float": 3.14, "bool": true, "null": null}"#,
);
assert!(json.is_ok());
}
#[test]
fn test_json_indent_spaces() {
let json = Json::from_str(r#"{"a": 1}"#).unwrap().indent(4);
let text = json.plain_text();
assert!(text.contains(" \"a\""));
}
#[test]
fn test_json_indent_tab() {
let json = Json::from_str(r#"{"a": 1}"#).unwrap().indent_with("\t");
let text = json.plain_text();
assert!(text.contains("\t\"a\""));
}
#[test]
fn test_json_compact() {
let json = Json::from_str(r#"{"a": 1, "b": 2}"#).unwrap().compact();
let text = json.plain_text();
assert!(!text.contains('\n'));
assert_eq!(text, r#"{"a":1,"b":2}"#);
}
#[test]
fn test_json_ensure_ascii() {
let json = Json::from_str(r#"{"emoji": "😀"}"#).unwrap().ensure_ascii();
let text = json.plain_text();
assert!(text.contains("\\u"));
assert!(!text.contains("😀"));
}
#[test]
fn test_json_no_highlight() {
let json = Json::from_str(r#"{"a": 1}"#).unwrap().no_highlight();
assert!(json.plain_text().contains("\"a\""));
}
#[test]
fn test_json_no_wrap_default() {
let json = Json::from_str(r#"{"a": 1}"#).unwrap();
assert_eq!(json.text.overflow, Overflow::Visible);
}
#[test]
fn test_json_wrap() {
let json = Json::from_str(r#"{"a": 1}"#).unwrap().wrap();
assert_eq!(json.text.overflow, Overflow::Wrap);
}
#[test]
fn test_json_from_file() {
let mut temp_file = NamedTempFile::new().unwrap();
writeln!(temp_file, r#"{{"name": "test", "value": 42}}"#).unwrap();
let json = Json::from_file(temp_file.path());
assert!(json.is_ok());
let text = json.unwrap().plain_text();
assert!(text.contains("test"));
assert!(text.contains("42"));
}
#[test]
fn test_json_from_file_not_found() {
let json = Json::from_file("/nonexistent/path/to/file.json");
assert!(json.is_err());
}
}