use crate::{RsonValue, RsonError, RsonResult};
use core::fmt::Write;
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
#[derive(Debug, Clone)]
pub struct FormatOptions {
pub indent_size: usize,
pub compact: bool,
pub trailing_commas: bool,
pub max_line_length: usize,
pub sort_keys: bool,
}
impl Default for FormatOptions {
fn default() -> Self {
Self {
indent_size: 2,
compact: false,
trailing_commas: true,
max_line_length: 80,
sort_keys: false,
}
}
}
impl FormatOptions {
pub fn compact() -> Self {
Self {
compact: true,
trailing_commas: false,
..Default::default()
}
}
pub fn pretty() -> Self {
Self {
compact: false,
trailing_commas: true,
indent_size: 2,
..Default::default()
}
}
}
pub struct Formatter<'a> {
options: &'a FormatOptions,
output: String,
indent_level: usize,
}
impl<'a> Formatter<'a> {
pub fn new(options: &'a FormatOptions) -> Self {
Self {
options,
output: String::new(),
indent_level: 0,
}
}
pub fn format(mut self, value: &RsonValue) -> RsonResult<String> {
self.format_value(value)?;
Ok(self.output)
}
fn format_value(&mut self, value: &RsonValue) -> RsonResult<()> {
match value {
RsonValue::Null => self.write_str("null"),
RsonValue::Bool(b) => self.write_str(&b.to_string()),
RsonValue::Int(i) => self.write_str(&i.to_string()),
RsonValue::Float(f) => self.write_str(&f.to_string()),
RsonValue::String(s) => self.format_string(s),
RsonValue::Char(c) => self.format_char(*c),
RsonValue::Array(arr) => self.format_array(arr),
RsonValue::Map(map) => self.format_map(map),
RsonValue::Struct { name, fields } => self.format_struct(name, fields),
RsonValue::Tuple(values) => self.format_tuple(values),
RsonValue::Enum { name, variant, value } => self.format_enum(name, variant, value.as_ref().map(|v| &**v)),
RsonValue::Option(opt) => self.format_option(opt.as_ref().map(|v| &**v)),
}
}
fn write_str(&mut self, s: &str) -> RsonResult<()> {
self.output.push_str(s);
Ok(())
}
fn write_char(&mut self, c: char) -> RsonResult<()> {
self.output.push(c);
Ok(())
}
fn write_indent(&mut self) -> RsonResult<()> {
if !self.options.compact {
for _ in 0..(self.indent_level * self.options.indent_size) {
self.write_char(' ')?;
}
}
Ok(())
}
fn write_newline(&mut self) -> RsonResult<()> {
if !self.options.compact {
self.write_char('\n')?;
}
Ok(())
}
fn write_space(&mut self) -> RsonResult<()> {
if !self.options.compact {
self.write_char(' ')?;
}
Ok(())
}
fn format_string(&mut self, s: &str) -> RsonResult<()> {
self.write_char('"')?;
for c in s.chars() {
match c {
'"' => self.write_str("\\\"")?,
'\\' => self.write_str("\\\\")?,
'\n' => self.write_str("\\n")?,
'\r' => self.write_str("\\r")?,
'\t' => self.write_str("\\t")?,
'\0' => self.write_str("\\0")?,
c if c.is_control() => {
write!(self.output, "\\u{:04x}", c as u32)
.map_err(|_| RsonError::custom("Failed to write Unicode escape"))?;
}
c => self.write_char(c)?,
}
}
self.write_char('"')?;
Ok(())
}
fn format_char(&mut self, c: char) -> RsonResult<()> {
self.write_char('\'')?;
match c {
'\'' => self.write_str("\\'")?,
'\\' => self.write_str("\\\\")?,
'\n' => self.write_str("\\n")?,
'\r' => self.write_str("\\r")?,
'\t' => self.write_str("\\t")?,
'\0' => self.write_str("\\0")?,
c if c.is_control() => {
write!(self.output, "\\u{:04x}", c as u32)
.map_err(|_| RsonError::custom("Failed to write Unicode escape"))?;
}
c => self.write_char(c)?,
}
self.write_char('\'')?;
Ok(())
}
fn format_array(&mut self, arr: &[RsonValue]) -> RsonResult<()> {
self.write_char('[')?;
if arr.is_empty() {
self.write_char(']')?;
return Ok(());
}
let multiline = !self.options.compact && self.should_be_multiline_array(arr);
if multiline {
self.write_newline()?;
self.indent_level += 1;
}
for (i, item) in arr.iter().enumerate() {
if i > 0 {
self.write_char(',')?;
if multiline {
self.write_newline()?;
} else {
self.write_space()?;
}
}
if multiline {
self.write_indent()?;
}
self.format_value(item)?;
}
if self.options.trailing_commas && !arr.is_empty() {
self.write_char(',')?;
}
if multiline {
self.write_newline()?;
self.indent_level -= 1;
self.write_indent()?;
}
self.write_char(']')?;
Ok(())
}
fn format_map(&mut self, map: &indexmap::IndexMap<String, RsonValue>) -> RsonResult<()> {
self.write_char('{')?;
if map.is_empty() {
self.write_char('}')?;
return Ok(());
}
let multiline = !self.options.compact && self.should_be_multiline_map(map);
if multiline {
self.write_newline()?;
self.indent_level += 1;
}
let mut entries: Vec<_> = map.iter().collect();
if self.options.sort_keys {
entries.sort_by_key(|(key, _)| *key);
}
for (i, (key, value)) in entries.iter().enumerate() {
if i > 0 {
self.write_char(',')?;
if multiline {
self.write_newline()?;
} else {
self.write_space()?;
}
}
if multiline {
self.write_indent()?;
}
self.format_map_key(key)?;
self.write_char(':')?;
self.write_space()?;
self.format_value(value)?;
}
if self.options.trailing_commas && !map.is_empty() {
self.write_char(',')?;
}
if multiline {
self.write_newline()?;
self.indent_level -= 1;
self.write_indent()?;
}
self.write_char('}')?;
Ok(())
}
fn format_map_key(&mut self, key: &str) -> RsonResult<()> {
if is_valid_identifier(key) {
self.write_str(key)?;
} else {
self.format_string(key)?;
}
Ok(())
}
fn format_struct(&mut self, name: &str, fields: &indexmap::IndexMap<String, RsonValue>) -> RsonResult<()> {
self.write_str(name)?;
self.write_char('(')?;
if fields.is_empty() {
self.write_char(')')?;
return Ok(());
}
let multiline = !self.options.compact && self.should_be_multiline_struct(fields);
if multiline {
self.write_newline()?;
self.indent_level += 1;
}
for (i, (field_name, value)) in fields.iter().enumerate() {
if i > 0 {
self.write_char(',')?;
if multiline {
self.write_newline()?;
} else {
self.write_space()?;
}
}
if multiline {
self.write_indent()?;
}
self.write_str(field_name)?;
self.write_char(':')?;
self.write_space()?;
self.format_value(value)?;
}
if self.options.trailing_commas && !fields.is_empty() {
self.write_char(',')?;
}
if multiline {
self.write_newline()?;
self.indent_level -= 1;
self.write_indent()?;
}
self.write_char(')')?;
Ok(())
}
fn format_tuple(&mut self, values: &[RsonValue]) -> RsonResult<()> {
self.write_char('(')?;
for (i, value) in values.iter().enumerate() {
if i > 0 {
self.write_char(',')?;
self.write_space()?;
}
self.format_value(value)?;
}
if values.len() == 1 || (self.options.trailing_commas && !values.is_empty()) {
self.write_char(',')?;
}
self.write_char(')')?;
Ok(())
}
fn format_enum(&mut self, name: &str, variant: &str, value: Option<&RsonValue>) -> RsonResult<()> {
self.write_str(name)?;
self.write_str("::")?;
self.write_str(variant)?;
if let Some(val) = value {
self.write_char('(')?;
self.format_value(val)?;
self.write_char(')')?;
}
Ok(())
}
fn format_option(&mut self, value: Option<&RsonValue>) -> RsonResult<()> {
match value {
Some(val) => {
self.write_str("Some(")?;
self.format_value(val)?;
self.write_char(')')?;
}
None => self.write_str("None")?,
}
Ok(())
}
fn should_be_multiline_array(&self, arr: &[RsonValue]) -> bool {
if self.options.compact {
return false;
}
arr.len() > 3 || arr.iter().any(|v| matches!(v,
RsonValue::Array(_) |
RsonValue::Map(_) |
RsonValue::Struct { .. }
))
}
fn should_be_multiline_map(&self, map: &indexmap::IndexMap<String, RsonValue>) -> bool {
if self.options.compact {
return false;
}
map.len() > 1 || map.values().any(|v| matches!(v,
RsonValue::Array(_) |
RsonValue::Map(_) |
RsonValue::Struct { .. }
))
}
fn should_be_multiline_struct(&self, fields: &indexmap::IndexMap<String, RsonValue>) -> bool {
if self.options.compact {
return false;
}
fields.len() > 2 || fields.values().any(|v| matches!(v,
RsonValue::Array(_) |
RsonValue::Map(_) |
RsonValue::Struct { .. }
))
}
}
fn is_valid_identifier(s: &str) -> bool {
if s.is_empty() {
return false;
}
match s {
"true" | "false" | "null" | "Some" | "None" => return false,
_ => {}
}
let mut chars = s.chars();
let first = chars.next().unwrap();
if !first.is_alphabetic() && first != '_' {
return false;
}
chars.all(|c| c.is_alphanumeric() || c == '_')
}
pub fn format_rson(value: &RsonValue, options: &FormatOptions) -> RsonResult<String> {
let formatter = Formatter::new(options);
formatter.format(value)
}
pub fn format_pretty(value: &RsonValue) -> RsonResult<String> {
format_rson(value, &FormatOptions::pretty())
}
pub fn format_compact(value: &RsonValue) -> RsonResult<String> {
format_rson(value, &FormatOptions::compact())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::RsonValue;
use indexmap::IndexMap;
#[test]
fn test_format_primitives() {
assert_eq!(format_compact(&RsonValue::Null).unwrap(), "null");
assert_eq!(format_compact(&RsonValue::Bool(true)).unwrap(), "true");
assert_eq!(format_compact(&RsonValue::Bool(false)).unwrap(), "false");
assert_eq!(format_compact(&RsonValue::Int(42)).unwrap(), "42");
assert_eq!(format_compact(&RsonValue::Float(3.14)).unwrap(), "3.14");
assert_eq!(format_compact(&RsonValue::String("hello".to_string())).unwrap(), r#""hello""#);
assert_eq!(format_compact(&RsonValue::Char('a')).unwrap(), "'a'");
}
#[test]
fn test_format_array() {
let arr = RsonValue::Array(vec![
RsonValue::Int(1),
RsonValue::Int(2),
RsonValue::Int(3),
]);
assert_eq!(format_compact(&arr).unwrap(), "[1,2,3]");
let pretty = format_pretty(&arr).unwrap();
assert!(pretty.contains("["));
assert!(pretty.contains("1,"));
assert!(pretty.contains("2,"));
assert!(pretty.contains("3,"));
assert!(pretty.contains("]"));
}
#[test]
fn test_format_map() {
let mut map = IndexMap::new();
map.insert("name".to_string(), RsonValue::String("Alice".to_string()));
map.insert("age".to_string(), RsonValue::Int(30));
let value = RsonValue::Map(map);
let formatted = format_compact(&value).unwrap();
assert!(formatted.contains("name:\"Alice\""));
assert!(formatted.contains("age:30"));
}
#[test]
fn test_format_struct() {
let mut fields = IndexMap::new();
fields.insert("x".to_string(), RsonValue::Int(10));
fields.insert("y".to_string(), RsonValue::Int(20));
let value = RsonValue::Struct {
name: "Point".to_string(),
fields,
};
let formatted = format_compact(&value).unwrap();
assert_eq!(formatted, "Point(x:10,y:20)");
}
#[test]
fn test_format_enum() {
let enum_val = RsonValue::Enum {
name: "Color".to_string(),
variant: "Red".to_string(),
value: None,
};
assert_eq!(format_compact(&enum_val).unwrap(), "Color::Red");
let enum_with_value = RsonValue::Enum {
name: "Result".to_string(),
variant: "Ok".to_string(),
value: Some(Box::new(RsonValue::String("success".to_string()))),
};
assert_eq!(format_compact(&enum_with_value).unwrap(), r#"Result::Ok("success")"#);
}
#[test]
fn test_format_option() {
assert_eq!(format_compact(&RsonValue::Option(None)).unwrap(), "None");
assert_eq!(
format_compact(&RsonValue::Option(Some(Box::new(RsonValue::Int(42))))).unwrap(),
"Some(42)"
);
}
#[test]
fn test_string_escaping() {
let value = RsonValue::String("hello\nworld\"test".to_string());
let formatted = format_compact(&value).unwrap();
assert_eq!(formatted, r#""hello\nworld\"test""#);
}
#[test]
fn test_identifier_validation() {
assert!(is_valid_identifier("hello"));
assert!(is_valid_identifier("_private"));
assert!(is_valid_identifier("field1"));
assert!(!is_valid_identifier("123invalid"));
assert!(!is_valid_identifier("true"));
assert!(!is_valid_identifier(""));
assert!(!is_valid_identifier("hello-world"));
}
}