1use serde_json::Value;
16use std::fmt::Write;
17
18pub fn json_to_lua(value: &Value) -> String {
19 let mut out = String::new();
20 write_value(value, &mut out);
21 out
22}
23
24fn write_value(value: &Value, out: &mut String) {
25 match value {
26 Value::Null => out.push_str("nil"),
27 Value::Bool(true) => out.push_str("true"),
28 Value::Bool(false) => out.push_str("false"),
29 Value::Number(n) => {
30 if let Some(i) = n.as_i64() {
31 let _ = write!(out, "{i}");
32 } else if let Some(u) = n.as_u64() {
33 let _ = write!(out, "{u}");
34 } else {
35 let _ = write!(out, "{n}");
36 }
37 }
38 Value::String(s) => write_string(s, out),
39 Value::Array(items) => {
40 out.push('{');
41 for (i, item) in items.iter().enumerate() {
42 if i > 0 {
43 out.push_str(", ");
44 }
45 write_value(item, out);
46 }
47 out.push('}');
48 }
49 Value::Object(map) => {
50 out.push('{');
51 let mut first = true;
52 for (k, v) in map {
53 if !first {
54 out.push_str(", ");
55 }
56 first = false;
57 write_object_key(k, out);
58 out.push_str(" = ");
59 write_value(v, out);
60 }
61 out.push('}');
62 }
63 }
64}
65
66fn write_string(s: &str, out: &mut String) {
67 out.push('"');
68 for ch in s.chars() {
69 match ch {
70 '"' => out.push_str("\\\""),
71 '\\' => out.push_str("\\\\"),
72 '\n' => out.push_str("\\n"),
73 '\r' => out.push_str("\\r"),
74 '\t' => out.push_str("\\t"),
75 '\x08' => out.push_str("\\b"),
76 '\x0c' => out.push_str("\\f"),
77 '\x07' => out.push_str("\\a"),
78 '\x0b' => out.push_str("\\v"),
79 c if (c as u32) < 0x20 => {
80 let _ = write!(out, "\\x{:02x}", c as u32);
81 }
82 c => out.push(c),
83 }
84 }
85 out.push('"');
86}
87
88fn write_object_key(k: &str, out: &mut String) {
89 if is_valid_lua_ident(k) {
90 out.push_str(k);
91 } else {
92 write_string(k, out);
93 }
94}
95
96fn is_valid_lua_ident(s: &str) -> bool {
97 let mut chars = s.chars();
98 match chars.next() {
99 Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
100 _ => return false,
101 }
102 chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use serde_json::json;
109
110 #[test]
111 fn primitives() {
112 assert_eq!(json_to_lua(&json!(null)), "nil");
113 assert_eq!(json_to_lua(&json!(true)), "true");
114 assert_eq!(json_to_lua(&json!(false)), "false");
115 assert_eq!(json_to_lua(&json!(42)), "42");
116 assert_eq!(json_to_lua(&json!(-7)), "-7");
117 assert_eq!(json_to_lua(&json!(0)), "0");
118 }
119
120 #[test]
121 fn floats_emit_as_decimal() {
122 assert_eq!(json_to_lua(&json!(3.5)), "3.5");
123 }
124
125 #[test]
126 fn large_unsigned() {
127 assert_eq!(json_to_lua(&json!(9_000_000_000_u64)), "9000000000");
128 }
129
130 #[test]
131 fn empty_collections() {
132 assert_eq!(json_to_lua(&json!([])), "{}");
133 assert_eq!(json_to_lua(&json!({})), "{}");
134 }
135
136 #[test]
137 fn array_of_ints() {
138 assert_eq!(json_to_lua(&json!([1, 2, 3])), "{1, 2, 3}");
139 }
140
141 #[test]
142 fn mixed_array() {
143 assert_eq!(
144 json_to_lua(&json!(["a", 1, true, null])),
145 "{\"a\", 1, true, nil}"
146 );
147 }
148
149 #[test]
150 fn object_simple_keys() {
151 assert_eq!(
152 json_to_lua(&json!({"topic": "rust", "n": 10})),
153 "{n = 10, topic = \"rust\"}"
154 );
155 }
156
157 #[test]
158 fn object_invalid_ident_keys_quoted() {
159 assert_eq!(
160 json_to_lua(&json!({"with-dash": 1, "9starts_with_digit": 2})),
161 "{\"9starts_with_digit\" = 2, \"with-dash\" = 1}"
162 );
163 }
164
165 #[test]
166 fn string_escaping() {
167 assert_eq!(
168 json_to_lua(&json!("hello \"world\"\n\t\\end")),
169 "\"hello \\\"world\\\"\\n\\t\\\\end\""
170 );
171 }
172
173 #[test]
174 fn string_with_control_char() {
175 assert_eq!(json_to_lua(&json!("\x01ok")), "\"\\x01ok\"");
176 }
177
178 #[test]
179 fn nested_structure() {
180 assert_eq!(
181 json_to_lua(&json!({
182 "topic": "rust",
183 "tags": ["async", "tokio"],
184 "opts": {"depth": 2, "recursive": false}
185 })),
186 "{opts = {depth = 2, recursive = false}, tags = {\"async\", \"tokio\"}, topic = \"rust\"}"
187 );
188 }
189
190 #[test]
191 fn lua_ident_edge_cases() {
192 assert!(is_valid_lua_ident("_x"));
193 assert!(is_valid_lua_ident("x9"));
194 assert!(!is_valid_lua_ident("9x"));
195 assert!(!is_valid_lua_ident("x-y"));
196 assert!(!is_valid_lua_ident(""));
197 assert!(!is_valid_lua_ident("with space"));
198 }
199
200 #[test]
201 fn roundtrip_object_count() {
202 let v = json!({"a": 1, "b": 2, "c": 3});
204 let lua = json_to_lua(&v);
205 assert_eq!(lua.matches(" = ").count(), 3);
206 }
207}