ironwork-rt 0.2.0

ironwork for COBOL: the runtime a compiled program links, under the runtime exception
Documentation
//! JSON text as JSON PARSE reads it (Language Reference SC27-8713-03, pp. 384-396): RFC 8259's
//! grammar, with IBM's escape \x for NEXT LINE (U+0085).

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Value {
    /// Name/value pairs in the order of the text.
    Object(Vec<(String, Value)>),
    Array(Vec<Value>),
    String(String),
    /// A number as written.
    Number(String),
    Bool(bool),
    Null,
}

/// Why JSON text is refused: JSON-CODE 100, 101 and 102.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Invalid {
    Malformed,
    Empty,
    Trailing,
}

impl Invalid {
    pub fn code(self) -> i64 {
        match self {
            Invalid::Malformed => 100,
            Invalid::Empty => 101,
            Invalid::Trailing => 102,
        }
    }
}

struct Reader<'t> {
    text: &'t [char],
    at: usize,
}

type Read<T> = Result<T, Invalid>;

fn is_space(c: char) -> bool {
    matches!(c, ' ' | '\t' | '\n' | '\r')
}

impl Reader<'_> {
    fn skip_space(&mut self) {
        while self.text.get(self.at).is_some_and(|&c| is_space(c)) {
            self.at += 1;
        }
    }

    fn next(&mut self) -> Read<char> {
        let c = *self.text.get(self.at).ok_or(Invalid::Malformed)?;
        self.at += 1;
        Ok(c)
    }

    fn expect(&mut self, word: &str) -> Read<()> {
        for w in word.chars() {
            if self.next()? != w {
                return Err(Invalid::Malformed);
            }
        }
        Ok(())
    }

    fn value(&mut self) -> Read<Value> {
        self.skip_space();
        let value = match self.text.get(self.at).ok_or(Invalid::Malformed)? {
            '{' => self.object()?,
            '[' => self.array()?,
            '"' => Value::String(self.string()?),
            't' => {
                self.expect("true")?;
                Value::Bool(true)
            }
            'f' => {
                self.expect("false")?;
                Value::Bool(false)
            }
            'n' => {
                self.expect("null")?;
                Value::Null
            }
            _ => Value::Number(self.number()?),
        };
        self.skip_space();
        Ok(value)
    }

    fn object(&mut self) -> Read<Value> {
        self.at += 1;
        let mut pairs = Vec::new();
        self.skip_space();
        if self.text.get(self.at) == Some(&'}') {
            self.at += 1;
            return Ok(Value::Object(pairs));
        }
        loop {
            self.skip_space();
            if self.text.get(self.at) != Some(&'"') {
                return Err(Invalid::Malformed);
            }
            let name = self.string()?;
            self.skip_space();
            self.expect(":")?;
            pairs.push((name, self.value()?));
            match self.next()? {
                ',' => {}
                '}' => return Ok(Value::Object(pairs)),
                _ => return Err(Invalid::Malformed),
            }
        }
    }

    fn array(&mut self) -> Read<Value> {
        self.at += 1;
        let mut elements = Vec::new();
        self.skip_space();
        if self.text.get(self.at) == Some(&']') {
            self.at += 1;
            return Ok(Value::Array(elements));
        }
        loop {
            elements.push(self.value()?);
            match self.next()? {
                ',' => {}
                ']' => return Ok(Value::Array(elements)),
                _ => return Err(Invalid::Malformed),
            }
        }
    }

    fn hex4(&mut self) -> Read<u32> {
        let mut unit = 0;
        for _ in 0..4 {
            unit = unit * 16 + self.next()?.to_digit(16).ok_or(Invalid::Malformed)?;
        }
        Ok(unit)
    }

    fn string(&mut self) -> Read<String> {
        self.at += 1;
        let mut out = String::new();
        loop {
            match self.next()? {
                '"' => return Ok(out),
                '\\' => out.push(match self.next()? {
                    '"' => '"',
                    '\\' => '\\',
                    '/' => '/',
                    'b' => '\u{8}',
                    'f' => '\u{c}',
                    'n' => '\n',
                    'r' => '\r',
                    't' => '\t',
                    'x' => '\u{85}',
                    'u' => {
                        let unit = self.hex4()?;
                        let scalar = if (0xD800..0xDC00).contains(&unit) {
                            self.expect("\\u")?;
                            let low = self.hex4()?;
                            if !(0xDC00..0xE000).contains(&low) {
                                return Err(Invalid::Malformed);
                            }
                            0x10000 + ((unit - 0xD800) << 10) + (low - 0xDC00)
                        } else {
                            unit
                        };
                        char::from_u32(scalar).ok_or(Invalid::Malformed)?
                    }
                    _ => return Err(Invalid::Malformed),
                }),
                c if (c as u32) < 0x20 => return Err(Invalid::Malformed),
                c => out.push(c),
            }
        }
    }

    fn digits(&mut self) -> usize {
        let start = self.at;
        while self.text.get(self.at).is_some_and(char::is_ascii_digit) {
            self.at += 1;
        }
        self.at - start
    }

    fn number(&mut self) -> Read<String> {
        let start = self.at;
        if self.text.get(self.at) == Some(&'-') {
            self.at += 1;
        }
        let first = self.text.get(self.at).copied();
        let whole = self.digits();
        if whole == 0 || (first == Some('0') && whole > 1) {
            return Err(Invalid::Malformed);
        }
        if self.text.get(self.at) == Some(&'.') {
            self.at += 1;
            if self.digits() == 0 {
                return Err(Invalid::Malformed);
            }
        }
        if matches!(self.text.get(self.at), Some('e' | 'E')) {
            self.at += 1;
            if matches!(self.text.get(self.at), Some('+' | '-')) {
                self.at += 1;
            }
            if self.digits() == 0 {
                return Err(Invalid::Malformed);
            }
        }
        Ok(self.text[start..self.at].iter().collect())
    }
}

/// The value JSON text holds; nothing but whitespace may follow it.
pub fn parse(text: &str) -> Result<Value, Invalid> {
    let chars: Vec<char> = text.chars().collect();
    if chars.iter().all(|&c| is_space(c)) {
        return Err(Invalid::Empty);
    }
    let mut reader = Reader { text: &chars, at: 0 };
    let value = reader.value()?;
    if reader.at < chars.len() {
        return Err(Invalid::Trailing);
    }
    Ok(value)
}

/// A number's sign, integer digits without leading zeros and fraction digits, its exponent applied.
pub fn decimal(number: &str) -> (bool, String, String) {
    let (negative, body) = match number.strip_prefix('-') {
        Some(rest) => (true, rest),
        None => (false, number),
    };
    let (mantissa, exponent) = match body.find(['e', 'E']) {
        Some(e) => (&body[..e], body[e + 1..].parse::<i64>().unwrap_or(0)),
        None => (body, 0),
    };
    let (int, frac) = mantissa.split_once('.').unwrap_or((mantissa, ""));
    let digits = format!("{int}{frac}");
    let point = int.len() as i64 + exponent;
    let (int, frac) = if point <= 0 {
        (String::new(), format!("{}{digits}", "0".repeat(point.unsigned_abs() as usize)))
    } else if point as usize >= digits.len() {
        (format!("{digits}{}", "0".repeat(point as usize - digits.len())), String::new())
    } else {
        (digits[..point as usize].to_owned(), digits[point as usize..].to_owned())
    };
    (negative, int.trim_start_matches('0').to_owned(), frac)
}

/// A string JSON PARSE takes as a number (p. 396): spaces, a sign, digits with at most one decimal
/// point among or before them, and spaces; as sign, integer digits and fraction digits.
pub fn numeric_string(text: &str, point: char) -> Option<(bool, String, String)> {
    let body = text.trim_matches(' ');
    let (negative, body) = match body.as_bytes().first() {
        Some(b'-') => (true, &body[1..]),
        Some(b'+') => (false, &body[1..]),
        _ => (false, body),
    };
    let (int, frac) = body.split_once(point).unwrap_or((body, ""));
    let all_digits = |s: &str| s.bytes().all(|b| b.is_ascii_digit());
    (!(int.is_empty() && frac.is_empty()) && all_digits(int) && all_digits(frac)).then(|| (negative, int.trim_start_matches('0').to_owned(), frac.to_owned()))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn s(t: &str) -> Value {
        Value::String(t.into())
    }

    #[test]
    fn objects_arrays_and_scalars_parse_in_order() {
        let v = parse(" {\"g\": {\"A\": \"Eh?\", \"3_\": 5, \"t\": [true, false, null, -1.5e2]}} ").unwrap();
        let inner = Value::Object(vec![
            ("A".into(), s("Eh?")),
            ("3_".into(), Value::Number("5".into())),
            ("t".into(), Value::Array(vec![Value::Bool(true), Value::Bool(false), Value::Null, Value::Number("-1.5e2".into())])),
        ]);
        assert_eq!(v, Value::Object(vec![("g".into(), inner)]));
    }

    #[test]
    fn escapes_include_ibms_next_line() {
        assert_eq!(parse(r#"["a\"b\\c\/\né\x😀"]"#).unwrap(), Value::Array(vec![s("a\"b\\c/\né\u{85}😀")]));
    }

    #[test]
    fn bad_text_gives_its_code() {
        assert_eq!(parse("   "), Err(Invalid::Empty));
        assert_eq!(parse("{\"a\":1} x"), Err(Invalid::Trailing));
        for bad in ["{\"a\":01}", "{\"a\":1,}", "{a:1}", "[\"\u{1}\"]", "{\"a\":tru}", "{\"a\":.5}", "{\"a\":1."] {
            assert_eq!(parse(bad), Err(Invalid::Malformed), "{bad}");
        }
    }

    #[test]
    fn numbers_become_digits_either_side_of_the_point() {
        assert_eq!(decimal("-125.53"), (true, "125".into(), "53".into()));
        assert_eq!(decimal("1.5E+3"), (false, "1500".into(), String::new()));
        assert_eq!(decimal("12e-3"), (false, String::new(), "012".into()));
        assert_eq!(decimal("0"), (false, String::new(), String::new()));
        assert_eq!(numeric_string(" -1,234 ", ','), Some((true, "1".into(), "234".into())));
        assert_eq!(numeric_string("0042", '.'), Some((false, "42".into(), String::new())));
        assert_eq!(numeric_string("4x", '.'), None);
        assert_eq!(numeric_string(" ", '.'), None);
    }
}