use serde_json::Value;
use std::fmt::Write;
pub fn json_to_lua(value: &Value) -> String {
let mut out = String::new();
write_value(value, &mut out);
out
}
fn write_value(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("nil"),
Value::Bool(true) => out.push_str("true"),
Value::Bool(false) => out.push_str("false"),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
let _ = write!(out, "{i}");
} else if let Some(u) = n.as_u64() {
let _ = write!(out, "{u}");
} else {
let _ = write!(out, "{n}");
}
}
Value::String(s) => write_string(s, out),
Value::Array(items) => {
out.push('{');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push_str(", ");
}
write_value(item, out);
}
out.push('}');
}
Value::Object(map) => {
out.push('{');
let mut first = true;
for (k, v) in map {
if !first {
out.push_str(", ");
}
first = false;
write_object_key(k, out);
out.push_str(" = ");
write_value(v, out);
}
out.push('}');
}
}
}
fn write_string(s: &str, out: &mut String) {
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"),
'\x08' => out.push_str("\\b"),
'\x0c' => out.push_str("\\f"),
'\x07' => out.push_str("\\a"),
'\x0b' => out.push_str("\\v"),
c if (c as u32) < 0x20 => {
let _ = write!(out, "\\x{:02x}", c as u32);
}
c => out.push(c),
}
}
out.push('"');
}
fn write_object_key(k: &str, out: &mut String) {
if is_valid_lua_ident(k) {
out.push_str(k);
} else {
write_string(k, out);
}
}
fn is_valid_lua_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn primitives() {
assert_eq!(json_to_lua(&json!(null)), "nil");
assert_eq!(json_to_lua(&json!(true)), "true");
assert_eq!(json_to_lua(&json!(false)), "false");
assert_eq!(json_to_lua(&json!(42)), "42");
assert_eq!(json_to_lua(&json!(-7)), "-7");
assert_eq!(json_to_lua(&json!(0)), "0");
}
#[test]
fn floats_emit_as_decimal() {
assert_eq!(json_to_lua(&json!(3.5)), "3.5");
}
#[test]
fn large_unsigned() {
assert_eq!(json_to_lua(&json!(9_000_000_000_u64)), "9000000000");
}
#[test]
fn empty_collections() {
assert_eq!(json_to_lua(&json!([])), "{}");
assert_eq!(json_to_lua(&json!({})), "{}");
}
#[test]
fn array_of_ints() {
assert_eq!(json_to_lua(&json!([1, 2, 3])), "{1, 2, 3}");
}
#[test]
fn mixed_array() {
assert_eq!(
json_to_lua(&json!(["a", 1, true, null])),
"{\"a\", 1, true, nil}"
);
}
#[test]
fn object_simple_keys() {
assert_eq!(
json_to_lua(&json!({"topic": "rust", "n": 10})),
"{n = 10, topic = \"rust\"}"
);
}
#[test]
fn object_invalid_ident_keys_quoted() {
assert_eq!(
json_to_lua(&json!({"with-dash": 1, "9starts_with_digit": 2})),
"{\"9starts_with_digit\" = 2, \"with-dash\" = 1}"
);
}
#[test]
fn string_escaping() {
assert_eq!(
json_to_lua(&json!("hello \"world\"\n\t\\end")),
"\"hello \\\"world\\\"\\n\\t\\\\end\""
);
}
#[test]
fn string_with_control_char() {
assert_eq!(json_to_lua(&json!("\x01ok")), "\"\\x01ok\"");
}
#[test]
fn nested_structure() {
assert_eq!(
json_to_lua(&json!({
"topic": "rust",
"tags": ["async", "tokio"],
"opts": {"depth": 2, "recursive": false}
})),
"{opts = {depth = 2, recursive = false}, tags = {\"async\", \"tokio\"}, topic = \"rust\"}"
);
}
#[test]
fn lua_ident_edge_cases() {
assert!(is_valid_lua_ident("_x"));
assert!(is_valid_lua_ident("x9"));
assert!(!is_valid_lua_ident("9x"));
assert!(!is_valid_lua_ident("x-y"));
assert!(!is_valid_lua_ident(""));
assert!(!is_valid_lua_ident("with space"));
}
#[test]
fn roundtrip_object_count() {
let v = json!({"a": 1, "b": 2, "c": 3});
let lua = json_to_lua(&v);
assert_eq!(lua.matches(" = ").count(), 3);
}
}