use crate::error::Result;
use crate::value::Value;
use indexmap::IndexMap;
use std::io::Write;
#[derive(Debug, Clone)]
pub struct SerializeOptions {
pub indent_size: usize,
pub delimiter: Delimiter,
pub use_tabular: bool,
pub max_inline_items: usize,
}
impl Default for SerializeOptions {
fn default() -> Self {
SerializeOptions {
indent_size: 2,
delimiter: Delimiter::Comma,
use_tabular: true,
max_inline_items: 10,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Delimiter {
Comma,
Tab,
Pipe,
}
impl Delimiter {
pub fn as_char(&self) -> char {
match self {
Delimiter::Comma => ',',
Delimiter::Tab => '\t',
Delimiter::Pipe => '|',
}
}
pub fn as_str(&self) -> &'static str {
match self {
Delimiter::Comma => ",",
Delimiter::Tab => "\t",
Delimiter::Pipe => "|",
}
}
}
pub struct Serializer<'a> {
options: &'a SerializeOptions,
output: Vec<u8>,
}
impl<'a> Serializer<'a> {
pub fn new(options: &'a SerializeOptions) -> Self {
Serializer {
options,
output: Vec::new(),
}
}
pub fn serialize(mut self, value: &Value) -> Result<String> {
self.write_value(value, 0)?;
String::from_utf8(self.output).map_err(|_| {
crate::error::Error::InvalidUtf8
})
}
fn write_value(&mut self, value: &Value, depth: usize) -> Result<()> {
match value {
Value::Null => {
write!(self.output, "null")?;
}
Value::Bool(true) => {
write!(self.output, "true")?;
}
Value::Bool(false) => {
write!(self.output, "false")?;
}
Value::Number(n) => {
write!(self.output, "{}", n)?;
}
Value::String(s) => {
self.write_string(s)?;
}
Value::Array(arr) => {
self.write_array(arr, depth)?;
}
Value::Object(obj) => {
self.write_object(obj, depth)?;
}
}
Ok(())
}
fn write_string(&mut self, s: &str) -> Result<()> {
if self.needs_quoting(s) {
write!(self.output, "\"{}\"", self.escape_string(s))?;
} else {
write!(self.output, "{}", s)?;
}
Ok(())
}
fn needs_quoting(&self, s: &str) -> bool {
if s.is_empty() {
return true;
}
if s == "true" || s == "false" || s == "null" {
return true;
}
if self.is_numeric(s) {
return true;
}
let delimiter = self.options.delimiter.as_char();
let chars_to_quote = [':', ',', '[', ']', '{', '}', '\n', '\r', '\t', '"', '\'', delimiter];
s.starts_with(' ')
|| s.ends_with(' ')
|| s.starts_with('\t')
|| s.ends_with('\t')
|| s.chars().any(|c| chars_to_quote.contains(&c))
}
fn is_numeric(&self, s: &str) -> bool {
if s.is_empty() {
return false;
}
let mut chars = s.chars().peekable();
if let Some('-') = chars.peek() {
chars.next();
}
let mut has_digits = false;
while let Some(c) = chars.peek() {
if c.is_ascii_digit() {
has_digits = true;
chars.next();
} else {
break;
}
}
if !has_digits {
return false;
}
if let Some('.') = chars.peek() {
chars.next();
let mut frac_digits = false;
while let Some(c) = chars.peek() {
if c.is_ascii_digit() {
frac_digits = true;
chars.next();
} else {
break;
}
}
if !frac_digits {
return false;
}
}
if let Some('e') | Some('E') = chars.peek() {
chars.next();
if let Some('+') | Some('-') = chars.peek() {
chars.next();
}
let mut exp_digits = false;
while let Some(c) = chars.peek() {
if c.is_ascii_digit() {
exp_digits = true;
chars.next();
} else {
break;
}
}
if !exp_digits {
return false;
}
}
chars.peek().is_none()
}
fn escape_string(&self, s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
.replace('\r', "\\r")
.replace('\t', "\\t")
}
fn write_array(&mut self, arr: &[Value], depth: usize) -> Result<()> {
if arr.is_empty() {
write!(self.output, "[]")?;
return Ok(());
}
if self.options.use_tabular && self.is_uniform_object_array(arr) {
self.write_tabular_array(arr, depth)?;
} else if arr.len() <= self.options.max_inline_items
&& arr.iter().all(|v| self.is_primitive(v))
{
self.write_inline_primitive_array(arr)?;
} else {
self.write_expanded_array(arr, depth)?;
}
Ok(())
}
fn is_uniform_object_array(&self, arr: &[Value]) -> bool {
if arr.len() < 2 {
return false;
}
let first = match &arr[0] {
Value::Object(obj) => obj,
_ => return false,
};
let keys: Vec<_> = first.keys().collect();
for item in arr.iter().skip(1) {
match item {
Value::Object(obj) => {
if obj.len() != keys.len() {
return false;
}
for (i, key) in keys.iter().enumerate() {
if obj.keys().nth(i) != Some(*key) {
return false;
}
}
}
_ => return false,
}
}
true
}
fn is_primitive(&self, value: &Value) -> bool {
!matches!(value, Value::Object(_) | Value::Array(_))
}
fn write_tabular_array(&mut self, arr: &[Value], depth: usize) -> Result<()> {
self.write_expanded_array(arr, depth)
}
fn write_expanded_array(&mut self, arr: &[Value], depth: usize) -> Result<()> {
let indent = self.make_indent(depth);
for item in arr {
write!(self.output, "\n{}", indent)?;
write!(self.output, "- ")?;
self.write_value_inline(item)?;
}
Ok(())
}
fn write_inline_primitive_array(&mut self, arr: &[Value]) -> Result<()> {
let delim = self.options.delimiter.as_str();
for (i, item) in arr.iter().enumerate() {
if i > 0 {
write!(self.output, "{}", delim)?;
}
self.write_value_inline(item)?;
}
Ok(())
}
fn write_value_inline(&mut self, value: &Value) -> Result<()> {
match value {
Value::Null => {
write!(self.output, "null")?;
}
Value::Bool(true) => {
write!(self.output, "true")?;
}
Value::Bool(false) => {
write!(self.output, "false")?;
}
Value::Number(n) => {
write!(self.output, "{}", n)?;
}
Value::String(s) => {
self.write_string(s)?;
}
Value::Array(arr) => {
write!(self.output, "[")?;
for (i, item) in arr.iter().enumerate() {
if i > 0 {
write!(self.output, ", ")?;
}
self.write_value_inline(item)?;
}
write!(self.output, "]")?;
}
Value::Object(obj) => {
write!(self.output, "{{")?;
for (i, (k, v)) in obj.iter().enumerate() {
if i > 0 {
write!(self.output, ", ")?;
}
self.write_string(k)?;
write!(self.output, ": ")?;
self.write_value_inline(v)?;
}
write!(self.output, "}}")?;
}
}
Ok(())
}
fn write_object(&mut self, obj: &IndexMap<String, Value>, depth: usize) -> Result<()> {
if obj.is_empty() {
return Ok(());
}
let indent = self.make_indent(depth);
for (i, (key, value)) in obj.iter().enumerate() {
if i > 0 || depth > 0 {
write!(self.output, "\n")?;
}
write!(self.output, "{}", indent)?;
let is_tabular = if let Value::Array(arr) = value {
self.options.use_tabular && self.is_uniform_object_array(arr)
} else {
false
};
self.write_string(key)?;
if is_tabular {
if let Value::Array(arr) = value {
self.write_tabular_array_header(arr, depth)?;
}
} else {
write!(self.output, ":")?;
if let Value::Array(arr) = value {
if arr.is_empty() {
write!(self.output, " []")?;
} else if arr.len() <= self.options.max_inline_items
&& arr.iter().all(|v| self.is_primitive(v))
{
write!(self.output, " [{}]: ", arr.len())?;
self.write_inline_primitive_array(arr)?;
} else {
self.write_expanded_array_value(arr, depth + 1)?;
}
} else if value.is_object() {
self.write_nested_object(value, depth + 1)?;
} else {
write!(self.output, " ")?;
self.write_value_inline(value)?;
}
}
}
Ok(())
}
fn write_tabular_array_header(&mut self, arr: &[Value], depth: usize) -> Result<()> {
let first = arr[0].as_object().unwrap();
let keys: Vec<_> = first.keys().cloned().collect();
let length = arr.len();
write!(self.output, "[{}]{{", length)?;
for (i, key) in keys.iter().enumerate() {
if i > 0 {
write!(self.output, ",")?;
}
write!(self.output, "{}", key)?;
}
write!(self.output, "}}:")?;
let row_indent = self.make_indent(depth + 1);
let delim = self.options.delimiter.as_str();
for row in arr {
write!(self.output, "\n{}", row_indent)?;
let obj = row.as_object().unwrap();
for (i, key) in keys.iter().enumerate() {
if i > 0 {
write!(self.output, "{}", delim)?;
}
let val = obj.get(key).unwrap();
self.write_value_inline(val)?;
}
}
Ok(())
}
fn write_expanded_array_value(&mut self, arr: &[Value], depth: usize) -> Result<()> {
let indent = self.make_indent(depth);
for item in arr {
write!(self.output, "\n{}", indent)?;
write!(self.output, "- ")?;
self.write_value_inline(item)?;
}
Ok(())
}
fn write_nested_object(&mut self, value: &Value, depth: usize) -> Result<()> {
if let Value::Object(obj) = value {
let indent = self.make_indent(depth);
for (_i, (key, val)) in obj.iter().enumerate() {
write!(self.output, "\n{}", indent)?;
self.write_string(key)?;
write!(self.output, ":")?;
if val.is_object() {
self.write_nested_object(val, depth + 1)?;
} else if let Value::Array(arr) = val {
if arr.is_empty() {
write!(self.output, " []")?;
} else if arr.len() <= self.options.max_inline_items
&& arr.iter().all(|v| self.is_primitive(v))
{
write!(self.output, " [{}]: ", arr.len())?;
self.write_inline_primitive_array(arr)?;
} else {
self.write_expanded_array_value(arr, depth + 1)?;
}
} else {
write!(self.output, " ")?;
self.write_value_inline(val)?;
}
}
}
Ok(())
}
fn make_indent(&self, depth: usize) -> String {
" ".repeat(depth * self.options.indent_size)
}
}
pub fn to_string(value: &Value) -> Result<String> {
let options = SerializeOptions::default();
let serializer = Serializer::new(&options);
serializer.serialize(value)
}
pub fn to_string_pretty(value: &Value, options: &SerializeOptions) -> Result<String> {
let serializer = Serializer::new(options);
serializer.serialize(value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::value::Value;
use indexmap::IndexMap;
#[test]
fn test_serialize_simple_object() {
let mut obj = IndexMap::new();
obj.insert("id".to_string(), Value::integer(123));
obj.insert("name".to_string(), Value::string("Alice"));
obj.insert("active".to_string(), Value::Bool(true));
let value = Value::Object(obj);
let result = to_string(&value).unwrap();
assert!(result.contains("id: 123"));
assert!(result.contains("name: Alice"));
assert!(result.contains("active: true"));
}
#[test]
fn test_serialize_array_inline() {
let value = Value::Array(vec![
Value::string("foo"),
Value::string("bar"),
Value::string("baz"),
]);
let result = to_string(&value).unwrap();
assert_eq!(result, "foo,bar,baz");
}
#[test]
fn test_serialize_tabular_array() {
let mut row1 = IndexMap::new();
row1.insert("id".to_string(), Value::integer(1));
row1.insert("name".to_string(), Value::string("Alice"));
row1.insert("role".to_string(), Value::string("admin"));
let mut row2 = IndexMap::new();
row2.insert("id".to_string(), Value::integer(2));
row2.insert("name".to_string(), Value::string("Bob"));
row2.insert("role".to_string(), Value::string("user"));
let mut obj = IndexMap::new();
obj.insert(
"users".to_string(),
Value::Array(vec![Value::Object(row1), Value::Object(row2)]),
);
let value = Value::Object(obj);
let result = to_string(&value).unwrap();
assert!(result.contains("users[2]{id,name,role}:"));
assert!(result.contains("1,Alice,admin"));
assert!(result.contains("2,Bob,user"));
}
#[test]
fn test_string_quoting() {
let mut obj = IndexMap::new();
obj.insert("msg".to_string(), Value::string("hello, world"));
obj.insert("path".to_string(), Value::string("/home/user"));
let value = Value::Object(obj);
let result = to_string(&value).unwrap();
assert!(result.contains("\"hello, world\""));
assert!(result.contains("path: /home/user"));
}
#[test]
fn test_serialize_nested_object() {
let mut inner = IndexMap::new();
inner.insert("id".to_string(), Value::integer(1));
inner.insert("name".to_string(), Value::string("Alice"));
let mut obj = IndexMap::new();
obj.insert("user".to_string(), Value::Object(inner));
let value = Value::Object(obj);
let result = to_string(&value).unwrap();
assert!(result.contains("user:"));
assert!(result.contains(" id: 1"));
assert!(result.contains(" name: Alice"));
}
}