Skip to main content

sas/
json.rs

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// ── Options ───────────────────────────────────────────────────────────────────
9
10/// Options for SAS → JSON conversion.
11#[derive(Debug, Clone)]
12pub struct ToJsonOptions {
13    /// Indentation string (default: `"  "`).
14    pub indent: String,
15    /// Remove `__sas_version__` from the output (default: `true`).
16    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/// Options for JSON → SAS conversion.
26#[derive(Debug, Clone)]
27pub struct FromJsonOptions {
28    /// Emit `__sas_version__ -> "1.1"` header (default: `true`).
29    pub version_header: bool,
30    /// Indentation string (default: `"    "`).
31    pub indent: String,
32}
33
34impl Default for FromJsonOptions {
35    fn default() -> Self {
36        Self { version_header: true, indent: "    ".into() }
37    }
38}
39
40// ── SAS → JSON ────────────────────────────────────────────────────────────────
41
42/// Parse a SAS document and return a JSON string.
43pub 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            // Match JSON's number representation
63            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
128// ── JSON → SAS ────────────────────────────────────────────────────────────────
129
130/// Parse a JSON string and convert it to a SAS 1.1 document.
131pub fn from_json(json_src: &str, opts: FromJsonOptions) -> Result<String, ConvertError> {
132    // Minimal JSON parser using serde_json-free approach:
133    // We re-use Rust's standard library via a simple recursive descent.
134    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    // Try inline
202    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// ── Minimal JSON parser ───────────────────────────────────────────────────────
291
292#[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    // Number
327    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    // Use a character-level scanner
372    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    // Skip over the value in the char slice
447    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}