use eyre::{Context, Result};
use serde_json::Value;
use std::io::Write;
use std::path::Path;
pub fn to_string_with_multiline(value: &Value) -> Result<String> {
let mut buffer = Vec::new();
write_json_with_multiline(value, &mut buffer, 0)?;
Ok(String::from_utf8(buffer)?)
}
pub fn write_json_with_multiline(
value: &Value,
writer: &mut impl Write,
indent: usize,
) -> Result<()> {
match value {
Value::Null => write!(writer, "null")?,
Value::Bool(b) => write!(writer, "{}", b)?,
Value::Number(n) => write!(writer, "{}", n)?,
Value::String(s) => write_string(s, writer, indent)?,
Value::Array(arr) => write_array(arr, writer, indent)?,
Value::Object(obj) => write_object(obj, writer, indent)?,
}
Ok(())
}
fn write_string(s: &str, writer: &mut impl Write, indent: usize) -> Result<()> {
if s.contains('\n') && !s.contains(r#"""""#) {
write_triple_quoted_string(s, writer, indent)?;
} else {
write!(writer, "\"{}\"", escape_json_string(s))?;
}
Ok(())
}
fn write_triple_quoted_string(s: &str, writer: &mut impl Write, _indent: usize) -> Result<()> {
write!(writer, "\"\"\"")?;
write!(writer, "{}", s)?;
write!(writer, "\"\"\"")?;
Ok(())
}
fn escape_json_string(s: &str) -> String {
let mut escaped = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'"' => escaped.push_str(r#"\""#),
'\\' => escaped.push_str(r"\\"),
'\n' => escaped.push_str(r"\n"),
'\r' => escaped.push_str(r"\r"),
'\t' => escaped.push_str(r"\t"),
'\x08' => escaped.push_str(r"\b"),
'\x0C' => escaped.push_str(r"\f"),
c if c.is_control() => {
escaped.push_str(&format!("\\u{:04x}", c as u32));
}
c => escaped.push(c),
}
}
escaped
}
fn write_array(arr: &[Value], writer: &mut impl Write, indent: usize) -> Result<()> {
if arr.is_empty() {
write!(writer, "[]")?;
return Ok(());
}
writeln!(writer, "[")?;
for (i, item) in arr.iter().enumerate() {
write!(writer, "{:indent$}", "", indent = indent + 2)?;
write_json_with_multiline(item, writer, indent + 2)?;
if i < arr.len() - 1 {
writeln!(writer, ",")?;
} else {
writeln!(writer)?;
}
}
write!(writer, "{:indent$}]", "", indent = indent)?;
Ok(())
}
fn write_object(
obj: &serde_json::Map<String, Value>,
writer: &mut impl Write,
indent: usize,
) -> Result<()> {
if obj.is_empty() {
write!(writer, "{{}}")?;
return Ok(());
}
writeln!(writer, "{{")?;
let keys: Vec<_> = obj.keys().collect();
for (i, key) in keys.iter().enumerate() {
write!(writer, "{:indent$}\"{}\":", "", key, indent = indent + 2)?;
let value = &obj[*key];
match value {
Value::Object(_) | Value::Array(_) => {
write!(writer, " ")?;
write_json_with_multiline(value, writer, indent + 2)?;
}
_ => {
write!(writer, " ")?;
write_json_with_multiline(value, writer, indent + 2)?;
}
}
if i < keys.len() - 1 {
writeln!(writer, ",")?;
} else {
writeln!(writer)?;
}
}
write!(writer, "{:indent$}}}", "", indent = indent)?;
Ok(())
}
pub fn read_json5_file(path: &Path) -> Result<Value> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read file: {}", path.display()))?;
from_json5_str(&content)
.with_context(|| format!("Failed to parse JSON5 from file: {}", path.display()))
}
pub fn from_json5_str(s: &str) -> Result<Value> {
Ok(kibana_sync::json5::from_json5_str(s)?)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_simple_string() {
let value = json!("hello");
let output = to_string_with_multiline(&value).unwrap();
assert_eq!(output, "\"hello\"");
}
#[test]
fn test_simple_multiline_string() {
let value = json!("line1\nline2\nline3");
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("\"\"\""));
assert!(output.contains("line1"));
assert!(output.contains("line2"));
assert!(output.contains("line3"));
}
#[test]
fn test_string_with_triple_quotes_falls_back() {
let value = json!("content with \"\"\" in it");
let output = to_string_with_multiline(&value).unwrap();
assert!(!output.contains("\"\"\"\"\"\""));
assert!(output.contains("\\\"\\\"\\\""));
}
#[test]
fn test_simple_object() {
let value = json!({
"name": "test",
"count": 42
});
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("\"name\""));
assert!(output.contains("\"test\""));
assert!(output.contains("\"count\""));
assert!(output.contains("42"));
}
#[test]
fn test_object_with_multiline_field() {
let value = json!({
"id": "wf-123",
"yaml": "line1\nline2\nline3"
});
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("\"id\":"));
assert!(output.contains("\"yaml\":"));
assert!(output.contains("\"\"\""));
assert!(output.contains("line1"));
assert!(output.contains("line2"));
}
#[test]
fn test_mixed_fields() {
let value = json!({
"id": "wf-123",
"yaml": "multi\nline",
"name": "single line"
});
let output = to_string_with_multiline(&value).unwrap();
let yaml_section = output.split("\"yaml\":").nth(1).unwrap();
assert!(yaml_section.contains("\"\"\""));
assert!(output.contains("\"name\": \"single line\""));
}
#[test]
fn test_array() {
let value = json!([1, 2, 3]);
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("["));
assert!(output.contains("]"));
assert!(output.contains("1"));
assert!(output.contains("2"));
assert!(output.contains("3"));
}
#[test]
fn test_empty_array() {
let value = json!([]);
let output = to_string_with_multiline(&value).unwrap();
assert_eq!(output, "[]");
}
#[test]
fn test_empty_object() {
let value = json!({});
let output = to_string_with_multiline(&value).unwrap();
assert_eq!(output, "{}");
}
#[test]
fn test_nested_objects() {
let value = json!({
"definition": {
"steps": [
{
"with": {
"message": "line1\nline2"
}
}
]
}
});
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("\"\"\""));
assert!(output.contains("line1"));
assert!(output.contains("line2"));
}
#[test]
fn test_escape_special_chars() {
let value = json!("test\"quote\\backslash\ttab");
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains(r#"\""#));
assert!(output.contains(r"\\"));
assert!(output.contains(r"\t"));
}
#[test]
fn test_null_value() {
let value = json!(null);
let output = to_string_with_multiline(&value).unwrap();
assert_eq!(output, "null");
}
#[test]
fn test_boolean_values() {
let value = json!({"flag1": true, "flag2": false});
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("true"));
assert!(output.contains("false"));
}
#[test]
fn test_number_values() {
let value = json!({"int": 42, "float": std::f64::consts::PI});
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("42"));
assert!(output.contains("3.14"));
}
#[test]
fn test_realistic_workflow() {
let value = json!({
"id": "workflow-123",
"name": "Test workflow",
"yaml": "name: Test workflow\nversion: 1.0\nsteps:\n - name: deploy\n action: run",
"definition": {
"version": "1",
"steps": [
{
"name": "log",
"type": "console",
"with": {
"message": "Multi-line\nlog message\nhere"
}
}
]
}
});
let output = to_string_with_multiline(&value).unwrap();
let triple_quote_count = output.matches(r#"""""#).count();
assert!(triple_quote_count >= 4);
assert!(output.contains("\"id\":"));
assert!(output.contains("\"yaml\":"));
assert!(output.contains("\"definition\":"));
}
#[test]
fn test_empty_string() {
let value = json!("");
let output = to_string_with_multiline(&value).unwrap();
assert_eq!(output, "\"\"");
}
#[test]
fn test_string_with_only_newlines() {
let value = json!("\n\n");
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("\"\"\""));
}
#[test]
fn test_array_with_multiline_strings() {
let value = json!(["single line", "multi\nline", "another single"]);
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("\"\"\""));
assert!(output.contains("multi"));
}
#[test]
fn test_unicode_characters() {
let value = json!({"emoji": "Hello 🌍", "chinese": "你好"});
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains("🌍"));
assert!(output.contains("你好"));
}
#[test]
fn test_control_characters() {
let value = json!("test\x08backspace\x0Cformfeed");
let output = to_string_with_multiline(&value).unwrap();
assert!(output.contains(r"\b"));
assert!(output.contains(r"\f"));
}
#[test]
fn test_json5_parse_triple_quotes() {
let json5 = r#"{"yaml": """line1
line2
line3"""}"#;
let value = from_json5_str(json5).unwrap();
let yaml = value["yaml"].as_str().unwrap();
assert_eq!(yaml, "line1\nline2\nline3");
}
#[test]
fn test_json5_parse_with_comments() {
let json5 = r#"{
// This is a comment
"id": "test-123",
/* Multi-line
comment */
"name": "Test"
}"#;
let value = from_json5_str(json5).unwrap();
assert_eq!(value["id"].as_str().unwrap(), "test-123");
assert_eq!(value["name"].as_str().unwrap(), "Test");
}
#[test]
fn test_json5_parse_trailing_commas() {
let json5 = r#"{
"id": "test-123",
"name": "Test",
}"#;
let value = from_json5_str(json5).unwrap();
assert_eq!(value["id"].as_str().unwrap(), "test-123");
assert_eq!(value["name"].as_str().unwrap(), "Test");
}
#[test]
fn test_roundtrip_multiline() {
let original = json!({
"id": "wf-123",
"yaml": "name: Test\nversion: 1.0\nsteps:\n - deploy"
});
let output = to_string_with_multiline(&original).unwrap();
assert!(output.contains("\"\"\""));
let parsed = from_json5_str(&output).unwrap();
assert_eq!(parsed["id"], original["id"]);
assert_eq!(parsed["yaml"], original["yaml"]);
}
#[test]
fn test_json5_parse_triple_quotes_ending_with_quote() {
let json5 = r#"{"text": """I'm sorry, you can't do that."""}"#;
let value = from_json5_str(json5).unwrap();
let text = value["text"].as_str().unwrap();
assert_eq!(text, r#"I'm sorry, you can't do that."#);
}
#[test]
fn test_json5_parse_triple_quotes_with_embedded_quotes() {
let json5 = r#"{"text": """He said "hello" and then left."""}"#;
let value = from_json5_str(json5).unwrap();
let text = value["text"].as_str().unwrap();
assert_eq!(text, r#"He said "hello" and then left."#);
}
#[test]
fn test_roundtrip_complex_workflow() {
let original = json!({
"id": "workflow-123",
"name": "Test workflow",
"yaml": "name: Test\nversion: 1.0",
"definition": {
"steps": [{
"with": {
"message": "Line1\nLine2\nLine3"
}
}]
}
});
let output = to_string_with_multiline(&original).unwrap();
let parsed = from_json5_str(&output).unwrap();
assert_eq!(parsed["id"], original["id"]);
assert_eq!(parsed["name"], original["name"]);
assert_eq!(parsed["yaml"], original["yaml"]);
assert_eq!(
parsed["definition"]["steps"][0]["with"]["message"],
original["definition"]["steps"][0]["with"]["message"]
);
}
}