use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt::Write;
use crate::{Value, ValueType};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PathSegment {
Key(String),
Index(usize),
}
pub type Path = Vec<PathSegment>;
pub type Span = (usize, usize);
#[derive(Debug)]
pub struct FormattedValue {
pub text: String,
pub spans: BTreeMap<Path, Span>,
}
pub fn format_value_with_spans(value: &Value) -> FormattedValue {
let mut ctx = FormatContext::new();
format_value_into(&mut ctx, value, &[]);
FormattedValue {
text: ctx.output,
spans: ctx.spans,
}
}
pub fn format_value(value: &Value) -> String {
let mut ctx = FormatContext::new();
format_value_into(&mut ctx, value, &[]);
ctx.output
}
struct FormatContext {
output: String,
spans: BTreeMap<Path, Span>,
indent: usize,
}
impl FormatContext {
const fn new() -> Self {
Self {
output: String::new(),
spans: BTreeMap::new(),
indent: 0,
}
}
const fn len(&self) -> usize {
self.output.len()
}
fn write_indent(&mut self) {
for _ in 0..self.indent {
self.output.push_str(" ");
}
}
fn record_span(&mut self, path: &[PathSegment], start: usize, end: usize) {
self.spans.insert(path.to_vec(), (start, end));
}
}
fn format_value_into(ctx: &mut FormatContext, value: &Value, current_path: &[PathSegment]) {
let start = ctx.len();
match value.value_type() {
ValueType::Null => {
ctx.output.push_str("null");
}
ValueType::Bool => {
if value.is_true() {
ctx.output.push_str("true");
} else {
ctx.output.push_str("false");
}
}
ValueType::Number => {
let num = value.as_number().unwrap();
if let Some(i) = num.to_i64() {
let _ = write!(ctx.output, "{i}");
} else if let Some(u) = num.to_u64() {
let _ = write!(ctx.output, "{u}");
} else if let Some(f) = num.to_f64() {
let _ = write!(ctx.output, "{f}");
}
}
ValueType::String => {
let s = value.as_string().unwrap();
ctx.output.push('"');
for c in s.as_str().chars() {
match c {
'"' => ctx.output.push_str("\\\""),
'\\' => ctx.output.push_str("\\\\"),
'\n' => ctx.output.push_str("\\n"),
'\r' => ctx.output.push_str("\\r"),
'\t' => ctx.output.push_str("\\t"),
c if c.is_control() => {
let _ = write!(ctx.output, "\\u{:04x}", c as u32);
}
c => ctx.output.push(c),
}
}
ctx.output.push('"');
}
ValueType::Bytes => {
let bytes = value.as_bytes().unwrap();
ctx.output.push_str("<bytes:");
let _ = write!(ctx.output, "{}", bytes.len());
ctx.output.push('>');
}
ValueType::Array => {
let arr = value.as_array().unwrap();
if arr.is_empty() {
ctx.output.push_str("[]");
} else {
ctx.output.push_str("[\n");
ctx.indent += 1;
for (i, item) in arr.iter().enumerate() {
ctx.write_indent();
let mut item_path = current_path.to_vec();
item_path.push(PathSegment::Index(i));
format_value_into(ctx, item, &item_path);
if i < arr.len() - 1 {
ctx.output.push(',');
}
ctx.output.push('\n');
}
ctx.indent -= 1;
ctx.write_indent();
ctx.output.push(']');
}
}
ValueType::Object => {
let obj = value.as_object().unwrap();
if obj.is_empty() {
ctx.output.push_str("{}");
} else {
ctx.output.push_str("{\n");
ctx.indent += 1;
let entries: Vec<_> = obj.iter().collect();
for (i, (key, val)) in entries.iter().enumerate() {
ctx.write_indent();
ctx.output.push('"');
ctx.output.push_str(key.as_str());
ctx.output.push_str("\": ");
let mut item_path = current_path.to_vec();
item_path.push(PathSegment::Key(key.as_str().into()));
format_value_into(ctx, val, &item_path);
if i < entries.len() - 1 {
ctx.output.push(',');
}
ctx.output.push('\n');
}
ctx.indent -= 1;
ctx.write_indent();
ctx.output.push('}');
}
}
ValueType::DateTime => {
let dt = value.as_datetime().unwrap();
let _ = write!(ctx.output, "{dt:?}");
}
ValueType::QName => {
let qname = value.as_qname().unwrap();
let _ = write!(ctx.output, "{qname:?}");
}
ValueType::Uuid => {
let uuid = value.as_uuid().unwrap();
let _ = write!(ctx.output, "{uuid:?}");
}
}
let end = ctx.len();
ctx.record_span(current_path, start, end);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{VArray, VObject, VString};
#[test]
fn test_format_primitives() {
assert_eq!(format_value(&Value::NULL), "null");
assert_eq!(format_value(&Value::TRUE), "true");
assert_eq!(format_value(&Value::FALSE), "false");
assert_eq!(format_value(&Value::from(42i64)), "42");
assert_eq!(
format_value(&Value::from(VString::new("hello"))),
"\"hello\""
);
}
#[test]
fn test_format_array() {
let mut arr = VArray::new();
arr.push(Value::from(1i64));
arr.push(Value::from(2i64));
arr.push(Value::from(3i64));
let value: Value = arr.into();
let result = format_value_with_spans(&value);
assert!(result.text.contains("1"));
assert!(result.text.contains("2"));
assert!(result.text.contains("3"));
let path_0 = vec![PathSegment::Index(0)];
assert!(result.spans.contains_key(&path_0));
}
#[test]
fn test_format_object() {
let mut obj = VObject::new();
obj.insert("name", Value::from(VString::new("Alice")));
obj.insert("age", Value::from(30i64));
let value: Value = obj.into();
let result = format_value_with_spans(&value);
assert!(result.text.contains("\"name\""));
assert!(result.text.contains("\"Alice\""));
assert!(result.text.contains("\"age\""));
assert!(result.text.contains("30"));
let name_path = vec![PathSegment::Key("name".into())];
let age_path = vec![PathSegment::Key("age".into())];
assert!(
result.spans.contains_key(&name_path),
"Missing span for 'name'"
);
assert!(
result.spans.contains_key(&age_path),
"Missing span for 'age'"
);
let age_span = result.spans.get(&age_path).unwrap();
let age_text = &result.text[age_span.0..age_span.1];
assert_eq!(age_text, "30");
}
#[test]
fn test_format_nested() {
let mut inner = VObject::new();
inner.insert("x", Value::from(10i64));
let mut outer = VObject::new();
outer.insert("point", Value::from(inner));
let value: Value = outer.into();
let result = format_value_with_spans(&value);
let nested_path = vec![
PathSegment::Key("point".into()),
PathSegment::Key("x".into()),
];
assert!(
result.spans.contains_key(&nested_path),
"Missing span for nested path. Spans: {:?}",
result.spans
);
let span = result.spans.get(&nested_path).unwrap();
let text = &result.text[span.0..span.1];
assert_eq!(text, "10");
}
}