use crate::conversion::ConversionConfig;
use crate::error::{FormattingError, FormattingResult};
use serde_json::{Map, Value};
const MAX_NESTING_DEPTH: usize = 100;
pub struct NestedFormatter<'a> {
config: &'a ConversionConfig,
current_depth: usize,
}
impl<'a> NestedFormatter<'a> {
pub fn new(config: &'a ConversionConfig) -> Self {
Self {
config,
current_depth: 0,
}
}
pub fn format_nested_object(
&mut self,
object: &Map<String, Value>,
) -> FormattingResult<String> {
self.check_depth()?;
if object.is_empty() {
return Ok("{}".to_string());
}
let mut result = String::new();
result.push('{');
let keys: Vec<&str> = object.keys().map(|k| k.as_str()).collect();
for (i, key) in keys.iter().enumerate() {
if i > 0 {
result.push(',');
}
result.push('\n');
result.push_str(&self.get_indent());
let formatted_key = self.format_key(key)?;
result.push_str(&formatted_key);
result.push_str(": ");
let value = object.get(*key).unwrap();
self.current_depth += 1;
let formatted_value = self.format_nested_value(value)?;
self.current_depth -= 1;
result.push_str(&formatted_value);
}
result.push('\n');
result.push_str(&self.get_indent());
result.push('}');
Ok(result)
}
pub fn format_nested_value(&mut self, value: &Value) -> FormattingResult<String> {
self.check_depth()?;
match value {
Value::Null => Ok("null".to_string()),
Value::Bool(b) => Ok(b.to_string()),
Value::Number(n) => self.format_number(n),
Value::String(s) => self.format_string(s),
Value::Array(a) => self.format_nested_array(a),
Value::Object(o) => self.format_nested_object(o),
}
}
fn format_nested_array(&mut self, array: &[Value]) -> FormattingResult<String> {
self.check_depth()?;
if array.is_empty() {
return Ok("[]".to_string());
}
let mut result = String::new();
result.push('[');
for (i, value) in array.iter().enumerate() {
if i > 0 {
result.push_str(", ");
}
self.current_depth += 1;
let formatted = self.format_nested_value(value)?;
self.current_depth -= 1;
result.push_str(&formatted);
}
result.push(']');
Ok(result)
}
fn format_number(&self, value: &serde_json::Number) -> FormattingResult<String> {
if let Some(f) = value.as_f64() {
if f.is_infinite() {
return Err(FormattingError::invalid_structure(
"Infinite numbers are not supported in TOON".to_string(),
));
}
if f.is_nan() {
return Err(FormattingError::invalid_structure(
"NaN values are not supported in TOON".to_string(),
));
}
}
Ok(value.to_string())
}
fn format_string(&self, value: &str) -> FormattingResult<String> {
use crate::conversion::QuoteStrategy;
match self.config.quote_strings {
QuoteStrategy::Always => self.quote_string(value),
QuoteStrategy::Never => Ok(value.to_string()),
QuoteStrategy::Smart => {
if self.should_quote_string(value) {
self.quote_string(value)
} else {
Ok(value.to_string())
}
}
}
}
fn format_key(&self, key: &str) -> FormattingResult<String> {
if self.should_quote_string(key) {
self.quote_string(key)
} else {
Ok(key.to_string())
}
}
fn should_quote_string(&self, value: &str) -> bool {
if value.is_empty() {
return true;
}
if value == "null" || value == "true" || value == "false" {
return true;
}
if value.parse::<f64>().is_ok() {
return true;
}
if value.trim() != value {
return true;
}
let control_chars = ":[]{}\n\r";
if value.chars().any(|c| control_chars.contains(c)) {
return true;
}
if value.contains(self.config.delimiter.as_str()) {
return true;
}
if value.chars().any(|c| c.is_control()) {
return true;
}
false
}
fn quote_string(&self, value: &str) -> FormattingResult<String> {
if value.is_empty() {
return Ok("\"\"".to_string());
}
let mut quoted = String::with_capacity(value.len() + 2);
quoted.push('"');
for ch in value.chars() {
match ch {
'"' => quoted.push_str("\\\""),
'\\' => quoted.push_str("\\\\"),
'\n' => quoted.push_str("\\n"),
'\r' => quoted.push_str("\\r"),
'\t' => quoted.push_str("\\t"),
_ => quoted.push(ch),
}
}
quoted.push('"');
Ok(quoted)
}
fn get_indent(&self) -> String {
" ".repeat(self.current_depth * self.config.indent_size as usize)
}
fn check_depth(&self) -> FormattingResult<()> {
if self.current_depth >= MAX_NESTING_DEPTH {
return Err(FormattingError::invalid_structure(format!(
"Maximum nesting depth exceeded ({})",
MAX_NESTING_DEPTH
)));
}
Ok(())
}
pub fn current_depth(&self) -> usize {
self.current_depth
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_simple_nested_object() {
let config = ConversionConfig::default();
let mut formatter = NestedFormatter::new(&config);
let obj = json!({
"user": {
"name": "Alice",
"age": 30
}
});
let result = formatter
.format_nested_object(obj.as_object().unwrap())
.unwrap();
assert!(result.contains("user:"));
assert!(result.contains("name:"));
assert!(result.contains("Alice"));
}
#[test]
fn test_deeply_nested_object() {
let config = ConversionConfig::default();
let mut formatter = NestedFormatter::new(&config);
let obj = json!({
"level1": {
"level2": {
"level3": {
"value": "deep"
}
}
}
});
let result = formatter
.format_nested_object(obj.as_object().unwrap())
.unwrap();
assert!(result.contains("level1:"));
assert!(result.contains("level2:"));
assert!(result.contains("level3:"));
assert!(result.contains("value:"));
assert!(result.contains("deep"));
}
#[test]
fn test_max_depth_exceeded() {
let config = ConversionConfig::default();
let mut formatter = NestedFormatter::new(&config);
formatter.current_depth = MAX_NESTING_DEPTH;
let obj = json!({"key": "value"});
let result = formatter.format_nested_object(obj.as_object().unwrap());
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Maximum nesting depth"));
}
#[test]
fn test_nested_arrays_in_objects() {
let config = ConversionConfig::default();
let mut formatter = NestedFormatter::new(&config);
let obj = json!({
"data": {
"items": [1, 2, 3],
"nested": {
"values": [4, 5, 6]
}
}
});
let result = formatter
.format_nested_object(obj.as_object().unwrap())
.unwrap();
assert!(result.contains("data:"));
assert!(result.contains("items:"));
assert!(result.contains("[1, 2, 3]"));
assert!(result.contains("nested:"));
assert!(result.contains("values:"));
}
#[test]
fn test_empty_nested_objects() {
let config = ConversionConfig::default();
let mut formatter = NestedFormatter::new(&config);
let obj = json!({
"empty": {},
"other": "value"
});
let result = formatter
.format_nested_object(obj.as_object().unwrap())
.unwrap();
assert!(result.contains("empty: {}"));
assert!(result.contains("other:"));
}
#[test]
fn test_string_quoting_in_nested() {
let config = ConversionConfig::default();
let mut formatter = NestedFormatter::new(&config);
let obj = json!({
"data": {
"keyword": "true",
"normal": "hello",
"with:colon": "value"
}
});
let result = formatter
.format_nested_object(obj.as_object().unwrap())
.unwrap();
assert!(result.contains("\"true\"")); assert!(result.contains("hello")); assert!(result.contains("\"with:colon\"")); }
}