asexp 0.1.2

Advanced S-expression-like format suitable for configuration and serialization
#![feature(str_char)]

use std::char;

#[derive(Debug, PartialEq)]
enum Atom {
    // Unquoted string
    Str(String),

    // Quoted string
    QStr(String),

    // Unsigned 64-bit integer
    UInt(u64),

    // Signed 64-bit integer
    SInt(i64),

    // 64-bit floating point number
    Float(f64),
}

#[derive(Debug, PartialEq)]
enum Expr {
    Atom(Atom),

    // ( ... )
    Tuple(Vec<Atom>),

    // [ ... ]
    Array(Vec<Atom>),

    // { key val ... }
    Map(Vec<(Atom, Atom)>),
}

#[derive(Debug, PartialEq)]
enum ParseError {
    PrematureEOS,
}

impl Atom {
    fn from_str(s: &str) -> Result<(Atom, &str), ParseError> {
        let delim: &[_] = &[' ', '\n', '\t', '(', ')', '[', ']', '{', '}'];
        match s.find(delim) {
            Some(pos) => Ok((Atom::Str(s[..pos].to_string()), &s[pos..])),
            None => {
                // complete string
                Ok((Atom::Str(s.to_string()), ""))
            }
        }
    }
}

#[derive(Debug, PartialEq)]
enum Token<'a> {
    Error,

    Str(&'a str),
    QStr(&'a str),

    OpenBracket,
    CloseBracket,
    OpenBrace,
    CloseBrace,
    OpenCurly,
    CloseCurly,

    Int(&'a str),
    Float(&'a str),
}

#[inline]
fn scan<F: Fn(char) -> bool>(s: &str, cond: F) -> (&str, &str) {
    // split at the first non-"cond" character
    match s.find(|c: char| !cond(c)) {
        None => (s, ""),
        Some(pos) => s.split_at(pos),
    }
}


#[inline]
fn is_token_delim(c: char) -> bool {
    c.is_whitespace() || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' ||
    c == '"'
}

fn skip_ws(s: &str) -> &str {
    scan(s, char::is_whitespace).1
}


// ; comment
fn next_token<'a>(s: &'a str) -> Option<(Token<'a>, &'a str)> {
    let s = skip_ws(s);
    match s.slice_shift_char() {
        None => None,
        Some((c, s2)) => {
            match c {
                '(' => Some((Token::OpenBrace, s2)),
                ')' => Some((Token::CloseBrace, s2)),

                '[' => Some((Token::OpenBracket, s2)),
                ']' => Some((Token::CloseBracket, s2)),

                '{' => Some((Token::OpenCurly, s2)),
                '}' => Some((Token::CloseCurly, s2)),

                '"' => {
                    // TODO: QStr
                    None
                }

                '+' | '-' => {
                    // TODO
                    None
                }

                '0'...'9' => {
                    // Int, Float

                    let (basis, rest) = scan(s, char::is_numeric);
                    match rest.slice_shift_char() {
                        Some(('.', mantisse)) => {
                            // integer followed by "." => float
                            let (mantisse, _) = scan(mantisse, char::is_numeric);
                            let (full_float, rest) = s.split_at(basis.len() + 1 + mantisse.len());

                            Some((Token::Float(full_float), rest))
                        }
                        _ => Some((Token::Int(basis), rest)),
                    }
                }

                _ => {
                    // Str
                    let (string, rest) = scan(s, |c| !is_token_delim(c));
                    Some((Token::Str(string), rest))
                }
            }
        }
    }
}

#[test]
fn test_tokenize() {
    assert_eq!(Some((Token::Str("abc"), "")), next_token("  abc"));
    assert_eq!(Some((Token::Str("abc"), "")), next_token("abc"));
    assert_eq!(Some((Token::Str("abc"), "(")), next_token("abc("));

    assert_eq!(Some((Token::OpenBrace, ")")), next_token("()"));

    assert_eq!(Some((Token::Int("12345"), "")), next_token("12345"));
    assert_eq!(Some((Token::Int("12345"), " ")), next_token("12345 "));
    assert_eq!(Some((Token::Int("12345"), "+")), next_token("12345+"));
    assert_eq!(Some((Token::Float("12345.123"), "")),
               next_token("12345.123"));
    assert_eq!(Some((Token::Float("12345.123"), "(")),
               next_token("12345.123("));
}