use crate::error::Result;
use serde_json::Value;
pub fn encode(json: &str) -> Result<String> {
let value: Value = serde_json::from_str(json)?;
let mut out = String::new();
encode_root(&value, &mut out);
Ok(out)
}
fn encode_root(value: &Value, out: &mut String) {
match value {
Value::Object(map) => {
encode_object_fields(map, 0, out);
}
Value::Array(arr) => {
encode_root_array(arr, out);
}
_ => {
encode_primitive_value(value, QuoteContext::Document, out);
}
}
}
fn encode_root_array(arr: &[Value], out: &mut String) {
let len = arr.len();
if all_primitives(arr) {
out.push_str(&format!("[{}]: ", len));
encode_inline_values(arr, out);
} else {
out.push_str(&format!("[{}]:", len));
encode_list_items(arr, 0, out);
}
}
fn encode_object_fields(map: &serde_json::Map<String, Value>, depth: usize, out: &mut String) {
let indent = make_indent(depth);
let mut first = true;
for (key, value) in map {
if !first {
out.push('\n');
}
first = false;
out.push_str(&indent);
out.push_str(&encode_key(key));
encode_field_value(key, value, depth, out);
}
}
fn encode_field_value(_key: &str, value: &Value, depth: usize, out: &mut String) {
match value {
Value::Object(map) if map.is_empty() => {
out.push(':');
}
Value::Object(map) => {
out.push(':');
out.push('\n');
encode_object_fields(map, depth + 1, out);
}
Value::Array(arr) => {
encode_array_field(arr, depth, out);
}
_ => {
out.push_str(": ");
encode_primitive_value(value, QuoteContext::Document, out);
}
}
}
fn encode_array_field(arr: &[Value], depth: usize, out: &mut String) {
let len = arr.len();
if arr.is_empty() {
out.push_str(&format!("[{}]:", len));
return;
}
if let Some(fields) = detect_tabular(arr) {
out.push_str(&format!("[{}]{{{}}}:", len, fields.join(",")));
encode_tabular_rows(arr, &fields, depth, out);
return;
}
if all_primitives(arr) {
out.push_str(&format!("[{}]: ", len));
encode_inline_values(arr, out);
return;
}
out.push_str(&format!("[{}]:", len));
encode_list_items(arr, depth, out);
}
fn encode_inline_values(arr: &[Value], out: &mut String) {
for (i, val) in arr.iter().enumerate() {
if i > 0 {
out.push(',');
}
encode_primitive_value(val, QuoteContext::InlineArray, out);
}
}
fn encode_tabular_rows(arr: &[Value], fields: &[String], depth: usize, out: &mut String) {
let row_indent = make_indent(depth + 1);
for obj_val in arr {
out.push('\n');
out.push_str(&row_indent);
if let Value::Object(map) = obj_val {
for (i, field) in fields.iter().enumerate() {
if i > 0 {
out.push(',');
}
if let Some(val) = map.get(field) {
encode_primitive_value(val, QuoteContext::TabularCell, out);
}
}
}
}
}
fn encode_list_items(arr: &[Value], depth: usize, out: &mut String) {
let item_indent = make_indent(depth + 1);
for item in arr {
out.push('\n');
out.push_str(&item_indent);
out.push_str("- ");
match item {
Value::Object(map) => {
let mut first = true;
for (key, value) in map {
if first {
first = false;
out.push_str(&encode_key(key));
encode_list_item_field_value(value, depth + 1, out);
} else {
out.push('\n');
out.push_str(&make_indent(depth + 1));
out.push_str(" ");
out.push_str(&encode_key(key));
encode_list_item_field_value(value, depth + 1, out);
}
}
}
Value::Array(inner_arr) => {
let len = inner_arr.len();
if all_primitives(inner_arr) {
out.push_str(&format!("[{}]: ", len));
encode_inline_values(inner_arr, out);
} else {
out.push_str(&format!("[{}]:", len));
encode_list_items(inner_arr, depth + 1, out);
}
}
_ => {
encode_primitive_value(item, QuoteContext::Document, out);
}
}
}
}
fn encode_list_item_field_value(value: &Value, depth: usize, out: &mut String) {
match value {
Value::Object(map) if map.is_empty() => {
out.push(':');
}
Value::Object(map) => {
out.push(':');
out.push('\n');
let nested_indent = make_indent(depth + 2);
let mut first = true;
for (key, val) in map {
if !first {
out.push('\n');
}
first = false;
out.push_str(&nested_indent);
out.push_str(&encode_key(key));
encode_field_value(key, val, depth + 2, out);
}
}
Value::Array(arr) => {
encode_array_field(arr, depth, out);
}
_ => {
out.push_str(": ");
encode_primitive_value(value, QuoteContext::Document, out);
}
}
}
#[derive(Clone, Copy, PartialEq)]
enum QuoteContext {
Document,
InlineArray,
TabularCell,
}
fn encode_primitive_value(value: &Value, ctx: QuoteContext, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
Value::Number(n) => out.push_str(&format_number(n)),
Value::String(s) => encode_string_value(s, ctx, out),
_ => out.push_str("null"), }
}
fn format_number(n: &serde_json::Number) -> String {
if let Some(i) = n.as_i64() {
return i.to_string();
}
if let Some(u) = n.as_u64() {
return u.to_string();
}
if let Some(f) = n.as_f64() {
if f.is_nan() || f.is_infinite() {
return "null".to_string();
}
let f = if f == 0.0 { 0.0 } else { f };
if f.fract() == 0.0 && f.abs() < (i64::MAX as f64) {
return (f as i64).to_string();
}
let s = format!("{}", f);
if s.contains('.') {
let trimmed = s.trim_end_matches('0');
let trimmed = trimmed.trim_end_matches('.');
trimmed.to_string()
} else {
s
}
} else {
"null".to_string()
}
}
fn encode_string_value(s: &str, ctx: QuoteContext, out: &mut String) {
if needs_quoting(s, ctx) {
out.push('"');
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(ch),
}
}
out.push('"');
} else {
out.push_str(s);
}
}
fn needs_quoting(s: &str, ctx: QuoteContext) -> bool {
if s.is_empty() {
return true;
}
if s != s.trim() {
return true;
}
if s == "true" || s == "false" || s == "null" {
return true;
}
if looks_numeric(s) {
return true;
}
if s.contains('\\') || s.contains('"') {
return true;
}
if s.contains('[') || s.contains(']') || s.contains('{') || s.contains('}') {
return true;
}
if s.contains('\n') || s.contains('\r') || s.contains('\t') {
return true;
}
if s.starts_with('-') {
return true;
}
match ctx {
QuoteContext::Document => {
if s.contains(':') {
return true;
}
}
QuoteContext::InlineArray | QuoteContext::TabularCell => {
if s.contains(',') {
return true;
}
}
}
false
}
fn looks_numeric(s: &str) -> bool {
if s.is_empty() {
return false;
}
let bytes = s.as_bytes();
let start = if bytes[0] == b'-' { 1 } else { 0 };
if start >= bytes.len() {
return false;
}
let rest = &s[start..];
if rest.is_empty() {
return false;
}
if rest.len() > 1 && rest.starts_with('0') && rest.as_bytes()[1] != b'.' {
return true; }
let mut has_dot = false;
let mut has_e = false;
for (i, &b) in rest.as_bytes().iter().enumerate() {
match b {
b'0'..=b'9' => {}
b'.' if !has_dot && !has_e => has_dot = true,
b'e' | b'E' if !has_e && i > 0 => has_e = true,
b'+' | b'-' if has_e => {}
_ => return false,
}
}
rest.as_bytes().iter().any(|b| b.is_ascii_digit())
}
fn encode_key(key: &str) -> String {
if is_valid_unquoted_key(key) {
key.to_string()
} else {
let mut out = String::with_capacity(key.len() + 2);
out.push('"');
for ch in key.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
_ => out.push(ch),
}
}
out.push('"');
out
}
}
fn is_valid_unquoted_key(key: &str) -> bool {
if key.is_empty() {
return false;
}
let mut chars = key.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.')
}
fn detect_tabular(arr: &[Value]) -> Option<Vec<String>> {
if arr.is_empty() {
return None;
}
let first = arr[0].as_object()?;
let fields: Vec<String> = first.keys().cloned().collect();
if fields.is_empty() {
return None;
}
for val in first.values() {
if val.is_object() || val.is_array() {
return None;
}
}
for item in &arr[1..] {
let obj = item.as_object()?;
if obj.len() != fields.len() {
return None;
}
for field in &fields {
let val = obj.get(field)?;
if val.is_object() || val.is_array() {
return None;
}
}
}
Some(fields)
}
fn all_primitives(arr: &[Value]) -> bool {
arr.iter().all(|v| !v.is_object() && !v.is_array())
}
fn make_indent(depth: usize) -> String {
" ".repeat(depth)
}