1use crate::error::{ConvertError, ParseError};
2use crate::parser::parse;
3use crate::value::{Object, Value};
4
5const INLINE_MAX_LEN: usize = 120;
6const INLINE_MAX_FIELDS: usize = 4;
7
8#[derive(Debug, Clone)]
12pub struct ToJsonOptions {
13 pub indent: String,
15 pub strip_version: bool,
17}
18
19impl Default for ToJsonOptions {
20 fn default() -> Self {
21 Self { indent: " ".into(), strip_version: true }
22 }
23}
24
25#[derive(Debug, Clone)]
27pub struct FromJsonOptions {
28 pub version_header: bool,
30 pub indent: String,
32}
33
34impl Default for FromJsonOptions {
35 fn default() -> Self {
36 Self { version_header: true, indent: " ".into() }
37 }
38}
39
40pub fn to_json(source: &str, opts: ToJsonOptions) -> Result<String, ParseError> {
44 let mut val = parse(source)?;
45
46 if opts.strip_version {
47 if let Value::Object(ref mut obj) = val {
48 obj.keys.retain(|k| k != "__sas_version__");
49 obj.values.remove("__sas_version__");
50 }
51 }
52
53 Ok(marshal_value(&val, &opts.indent, ""))
54}
55
56fn marshal_value(val: &Value, indent: &str, prefix: &str) -> String {
57 match val {
58 Value::Null => "null".into(),
59 Value::Bool(b) => b.to_string(),
60 Value::Int(n) => n.to_string(),
61 Value::Float(f) => {
62 if f.fract() == 0.0 && f.abs() < 1e15 {
64 format!("{:.1}", f)
65 } else {
66 format!("{}", f)
67 }
68 }
69 Value::String(s) => json_escape_string(s),
70 Value::Array(arr) => marshal_array(arr, indent, prefix),
71 Value::Object(obj) => marshal_object(obj, indent, prefix),
72 }
73}
74
75fn marshal_object(obj: &Object, indent: &str, prefix: &str) -> String {
76 if obj.is_empty() { return "{}".into(); }
77 let new_prefix = format!("{}{}", prefix, indent);
78 let mut s = String::from("{\n");
79 for (i, k) in obj.keys.iter().enumerate() {
80 let v = &obj.values[k];
81 s.push_str(&new_prefix);
82 s.push_str(&json_escape_string(k));
83 s.push_str(": ");
84 s.push_str(&marshal_value(v, indent, &new_prefix));
85 if i < obj.keys.len() - 1 { s.push(','); }
86 s.push('\n');
87 }
88 s.push_str(prefix);
89 s.push('}');
90 s
91}
92
93fn marshal_array(arr: &[Value], indent: &str, prefix: &str) -> String {
94 if arr.is_empty() { return "[]".into(); }
95 let new_prefix = format!("{}{}", prefix, indent);
96 let mut s = String::from("[\n");
97 for (i, v) in arr.iter().enumerate() {
98 s.push_str(&new_prefix);
99 s.push_str(&marshal_value(v, indent, &new_prefix));
100 if i < arr.len() - 1 { s.push(','); }
101 s.push('\n');
102 }
103 s.push_str(prefix);
104 s.push(']');
105 s
106}
107
108fn json_escape_string(s: &str) -> String {
109 let mut out = String::with_capacity(s.len() + 2);
110 out.push('"');
111 for ch in s.chars() {
112 match ch {
113 '"' => out.push_str("\\\""),
114 '\\' => out.push_str("\\\\"),
115 '\n' => out.push_str("\\n"),
116 '\r' => out.push_str("\\r"),
117 '\t' => out.push_str("\\t"),
118 c if (c as u32) < 0x20 => {
119 out.push_str(&format!("\\u{:04X}", c as u32));
120 }
121 c => out.push(c),
122 }
123 }
124 out.push('"');
125 out
126}
127
128pub fn from_json(json_src: &str, opts: FromJsonOptions) -> Result<String, ConvertError> {
132 let value = parse_json_value(json_src.trim())
135 .map_err(|e| ConvertError::new(format!("JSON parse error: {}", e)))?;
136
137 match value {
138 JsonValue::Object(map) => from_map_inner(&map, &opts),
139 _ => Err(ConvertError::new("Top-level JSON value must be an object")),
140 }
141}
142
143fn from_map_inner(map: &[(String, JsonValue)], opts: &FromJsonOptions) -> Result<String, ConvertError> {
144 let mut lines: Vec<String> = Vec::new();
145 if opts.version_header {
146 lines.push(r#"__sas_version__ -> "1.1""#.into());
147 lines.push(String::new());
148 }
149 serialize_map_body(map, &mut lines, "", &opts.indent, "__root__")?;
150 while lines.last().map(|l: &String| l.is_empty()).unwrap_or(false) {
151 lines.pop();
152 }
153 Ok(lines.join("\n") + "\n")
154}
155
156fn serialize_map_body(
157 map: &[(String, JsonValue)],
158 lines: &mut Vec<String>,
159 cur: &str,
160 unit: &str,
161 path: &str,
162) -> Result<(), ConvertError> {
163 for (raw_key, val) in map {
164 let key = sanitize_key(raw_key);
165 let full_path = format!("{}.{}", path, key);
166 serialize_kv(&key, val, lines, cur, unit, &full_path)?;
167 }
168 Ok(())
169}
170
171fn serialize_kv(
172 key: &str, val: &JsonValue,
173 lines: &mut Vec<String>,
174 ind: &str, unit: &str, path: &str,
175) -> Result<(), ConvertError> {
176 match val {
177 JsonValue::Null => lines.push(format!("{}{} -> null", ind, key)),
178 JsonValue::Bool(b) => lines.push(format!("{}{} -> {}", ind, key, b)),
179 JsonValue::Number(n) => lines.push(format!("{}{} -> {}", ind, key, n)),
180 JsonValue::String(s) => {
181 if s.contains('\n') && !s.contains("\"\"\"") {
182 lines.push(format!("{}{} -> \"\"\"", ind, key));
183 let content = if s.ends_with('\n') { &s[..s.len()-1] } else { s.as_str() };
184 for l in content.split('\n') { lines.push(l.to_string()); }
185 lines.push("\"\"\"".into());
186 } else {
187 lines.push(format!("{}{} -> {}", ind, key, sas_escape_string(s)));
188 }
189 }
190 JsonValue::Array(arr) => serialize_array(key, arr, lines, ind, unit, path)?,
191 JsonValue::Object(map) => serialize_object(key, map, lines, ind, unit, path)?,
192 }
193 Ok(())
194}
195
196fn serialize_object(
197 key: &str, map: &[(String, JsonValue)],
198 lines: &mut Vec<String>,
199 ind: &str, unit: &str, path: &str,
200) -> Result<(), ConvertError> {
201 if !map.is_empty() && map.len() <= INLINE_MAX_FIELDS && map.iter().all(|(_, v)| v.is_scalar()) {
203 let fields: Vec<String> = map.iter()
204 .map(|(k, v)| format!("{} -> {}", sanitize_key(k), v.to_sas_scalar()))
205 .collect();
206 let candidate = format!("{}{} -> {{ {} }}", ind, key, fields.join(" | "));
207 if candidate.len() <= INLINE_MAX_LEN {
208 lines.push(candidate);
209 return Ok(());
210 }
211 }
212 lines.push(format!("{}{} ::", ind, key));
213 serialize_map_body(map, lines, &format!("{}{}", ind, unit), unit, path)?;
214 lines.push(format!("{}:: {}", ind, key));
215 lines.push(String::new());
216 Ok(())
217}
218
219fn serialize_array(
220 key: &str, arr: &[JsonValue],
221 lines: &mut Vec<String>,
222 ind: &str, unit: &str, path: &str,
223) -> Result<(), ConvertError> {
224 if arr.is_empty() {
225 lines.push(format!("{}{} -> []", ind, key));
226 return Ok(());
227 }
228 if arr.iter().all(|v| v.is_scalar()) {
229 let parts: Vec<String> = arr.iter().map(|v| v.to_sas_scalar()).collect();
230 let candidate = format!("{}{} -> [{}]", ind, key, parts.join(" | "));
231 if candidate.len() <= INLINE_MAX_LEN {
232 lines.push(candidate);
233 return Ok(());
234 }
235 }
236 lines.push(format!("{}{} ::", ind, key));
237 let inner_ind = format!("{}{}", ind, unit);
238 for (i, item) in arr.iter().enumerate() {
239 let item_path = format!("{}[{}]", path, i);
240 match item {
241 JsonValue::Null | JsonValue::Bool(_) | JsonValue::Number(_) | JsonValue::String(_) => {
242 lines.push(format!("{}- {}", inner_ind, item.to_sas_scalar()));
243 }
244 JsonValue::Array(sub) => {
245 lines.push(format!("{}- ::", inner_ind));
246 serialize_array("items", sub, lines, &format!("{}{}", inner_ind, unit), unit, &item_path)?;
247 lines.push(format!("{}:: -", inner_ind));
248 }
249 JsonValue::Object(map) => {
250 lines.push(format!("{}- ::", inner_ind));
251 serialize_map_body(map, lines, &format!("{}{}", inner_ind, unit), unit, &item_path)?;
252 lines.push(format!("{}:: -", inner_ind));
253 }
254 }
255 }
256 lines.push(format!("{}:: {}", ind, key));
257 lines.push(String::new());
258 Ok(())
259}
260
261fn sas_escape_string(s: &str) -> String {
262 let mut out = String::with_capacity(s.len() + 2);
263 out.push('"');
264 for ch in s.chars() {
265 match ch {
266 '"' => out.push_str("\\\""),
267 '\\' => out.push_str("\\\\"),
268 '\n' => out.push_str("\\n"),
269 '\t' => out.push_str("\\t"),
270 '\r' => out.push_str("\\r"),
271 c => out.push(c),
272 }
273 }
274 out.push('"');
275 out
276}
277
278fn sanitize_key(raw: &str) -> String {
279 if raw.chars().enumerate().all(|(i, c)| {
280 c.is_alphanumeric() || c == '_' || (c == '-' && i > 0)
281 }) && !raw.is_empty() && !raw.starts_with('-') {
282 return raw.to_string();
283 }
284 let mut s: String = raw.chars().map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '_' }).collect();
285 if s.starts_with('-') { s = format!("_{}", &s[1..]); }
286 if s.is_empty() { s = "_key".into(); }
287 s
288}
289
290#[derive(Debug, Clone)]
293enum JsonValue {
294 Null,
295 Bool(bool),
296 Number(String),
297 String(String),
298 Array(Vec<JsonValue>),
299 Object(Vec<(String, JsonValue)>),
300}
301
302impl JsonValue {
303 fn is_scalar(&self) -> bool {
304 matches!(self, JsonValue::Null | JsonValue::Bool(_) | JsonValue::Number(_) | JsonValue::String(_))
305 }
306
307 fn to_sas_scalar(&self) -> String {
308 match self {
309 JsonValue::Null => "null".into(),
310 JsonValue::Bool(b) => b.to_string(),
311 JsonValue::Number(n) => n.clone(),
312 JsonValue::String(s) => sas_escape_string(s),
313 _ => "null".into(),
314 }
315 }
316}
317
318fn parse_json_value(s: &str) -> Result<JsonValue, String> {
319 let s = s.trim();
320 if s.starts_with('{') { return parse_json_object(s); }
321 if s.starts_with('[') { return parse_json_array(s); }
322 if s.starts_with('"') { return parse_json_string(s).map(JsonValue::String); }
323 if s == "null" { return Ok(JsonValue::Null); }
324 if s == "true" { return Ok(JsonValue::Bool(true)); }
325 if s == "false" { return Ok(JsonValue::Bool(false)); }
326 if s.starts_with('-') || s.starts_with(|c: char| c.is_ascii_digit()) {
328 return Ok(JsonValue::Number(s.to_string()));
329 }
330 Err(format!("unexpected token: {}", &s[..s.len().min(20)]))
331}
332
333fn parse_json_string(s: &str) -> Result<String, String> {
334 let chars: Vec<char> = s.chars().collect();
335 if chars[0] != '"' { return Err("expected '\"'".into()); }
336 let mut result = String::new();
337 let mut i = 1;
338 while i < chars.len() {
339 match chars[i] {
340 '"' => return Ok(result),
341 '\\' => {
342 i += 1;
343 match chars.get(i) {
344 Some('"') => result.push('"'),
345 Some('\\') => result.push('\\'),
346 Some('/') => result.push('/'),
347 Some('n') => result.push('\n'),
348 Some('t') => result.push('\t'),
349 Some('r') => result.push('\r'),
350 Some('b') => result.push('\x08'),
351 Some('f') => result.push('\x0C'),
352 Some('u') => {
353 let hex: String = chars.get(i+1..i+5).map(|c| c.iter().collect()).unwrap_or_default();
354 let cp = u32::from_str_radix(&hex, 16).map_err(|_| format!("bad \\u{}", hex))?;
355 result.push(char::from_u32(cp).unwrap_or('\u{FFFD}'));
356 i += 4;
357 }
358 _ => return Err("bad escape".into()),
359 }
360 }
361 c => result.push(c),
362 }
363 i += 1;
364 }
365 Err("unterminated string".into())
366}
367
368fn parse_json_object(s: &str) -> Result<JsonValue, String> {
369 let s = s.trim();
370 if !s.starts_with('{') { return Err("expected '{'".into()); }
371 let chars: Vec<char> = s.chars().collect();
373 let mut i = 1;
374 let mut pairs: Vec<(String, JsonValue)> = Vec::new();
375
376 skip_ws(&chars, &mut i);
377 if chars.get(i) == Some(&'}') { return Ok(JsonValue::Object(pairs)); }
378
379 loop {
380 skip_ws(&chars, &mut i);
381 let key_str: String = chars[i..].iter().collect();
382 let key = parse_json_string(key_str.trim())?;
383 let key_len = json_string_len(&chars[i..]);
384 i += key_len;
385 skip_ws(&chars, &mut i);
386 if chars.get(i) != Some(&':') { return Err("expected ':'".into()); }
387 i += 1;
388 skip_ws(&chars, &mut i);
389 let rest: String = chars[i..].iter().collect();
390 let (val, consumed) = parse_json_value_len(rest.trim())?;
391 let consumed_in_original = rest.find(|_| true).unwrap_or(0) + chars[i..].iter().collect::<String>().find(&rest.trim()[..1]).unwrap_or(0);
392 let _ = consumed_in_original;
393 i += json_value_skip(&chars[i..], &consumed);
394 pairs.push((key, val));
395 skip_ws(&chars, &mut i);
396 match chars.get(i) {
397 Some(',') => { i += 1; }
398 Some('}') => return Ok(JsonValue::Object(pairs)),
399 _ => return Err("expected ',' or '}'".into()),
400 }
401 }
402}
403
404fn parse_json_array(s: &str) -> Result<JsonValue, String> {
405 let s = s.trim();
406 let chars: Vec<char> = s.chars().collect();
407 let mut i = 1;
408 let mut items: Vec<JsonValue> = Vec::new();
409 skip_ws(&chars, &mut i);
410 if chars.get(i) == Some(&']') { return Ok(JsonValue::Array(items)); }
411 loop {
412 skip_ws(&chars, &mut i);
413 let rest: String = chars[i..].iter().collect();
414 let (val, consumed) = parse_json_value_len(rest.trim())?;
415 i += json_value_skip(&chars[i..], &consumed);
416 items.push(val);
417 skip_ws(&chars, &mut i);
418 match chars.get(i) {
419 Some(',') => { i += 1; }
420 Some(']') => return Ok(JsonValue::Array(items)),
421 _ => return Err("expected ',' or ']'".into()),
422 }
423 }
424}
425
426fn skip_ws(chars: &[char], i: &mut usize) {
427 while *i < chars.len() && chars[*i].is_whitespace() { *i += 1; }
428}
429
430fn json_string_len(chars: &[char]) -> usize {
431 let mut i = 1;
432 while i < chars.len() {
433 if chars[i] == '\\' { i += 2; continue; }
434 if chars[i] == '"' { return i + 1; }
435 i += 1;
436 }
437 i
438}
439
440fn parse_json_value_len(s: &str) -> Result<(JsonValue, String), String> {
441 let val = parse_json_value(s)?;
442 Ok((val, s.to_string()))
443}
444
445fn json_value_skip(chars: &[char], _consumed: &str) -> usize {
446 let mut depth = 0i32;
448 let mut in_str = false;
449 let mut i = 0;
450 let first = chars.first().copied().unwrap_or(' ');
451 let is_container = first == '{' || first == '[';
452
453 while i < chars.len() {
454 let ch = chars[i];
455 if in_str {
456 if ch == '\\' { i += 2; continue; }
457 if ch == '"' { in_str = false; if !is_container { return i + 1; } }
458 } else {
459 match ch {
460 '"' => { in_str = true; if !is_container { } }
461 '{' | '[' => depth += 1,
462 '}' | ']' => {
463 depth -= 1;
464 if depth <= 0 { return i + 1; }
465 }
466 _ if !is_container && (ch == ',' || ch == '}' || ch == ']' || ch.is_whitespace()) => {
467 return i;
468 }
469 _ => {}
470 }
471 }
472 i += 1;
473 }
474 i
475}