ntriples-parser 2.0.0

A parser for the rdf n-triples format.
Documentation
use logos::Logos;

#[derive(Debug, Logos, PartialEq)]
#[logos(skip r"[ \t\n\f]+", utf8 = false)] // Ignore this regex pattern between tokens
pub enum Token<'a> {
    #[regex(r"#[^\n]*", allow_greedy = true)]
    Comment,
    #[token(".")]
    Dot,
    #[regex(r#"<[^<>\"{}|^`\\\u{00}-\u{20}]*>"#)]
    Iri(&'a [u8]),
    #[regex(r"_:(?:(?:[A-Za-z\u{00C0}-\u{00D6}\u{00D8}-\u{00F6}\u{00F8}-\u{02FF}\u{0370}-\u{037D}\u{037F}-\u{1FFF}\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFFD}\u{10000}-\u{EFFFF}_])|(?:[0-9]))(?:[A-Za-z\u{00C0}-\u{00D6}\u{00D8}-\u{00F6}\u{00F8}-\u{02FF}\u{0370}-\u{037D}\u{037F}-\u{1FFF}\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFFD}\u{10000}-\u{EFFFF}_0-9\u{00B7}\u{0300}-\u{036F}\u{203F}-\u{2040}\.-]*[A-Za-z\u{00C0}-\u{00D6}\u{00D8}-\u{00F6}\u{00F8}-\u{02FF}\u{0370}-\u{037D}\u{037F}-\u{1FFF}\u{200C}-\u{200D}\u{2070}-\u{218F}\u{2C00}-\u{2FEF}\u{3001}-\u{D7FF}\u{F900}-\u{FDCF}\u{FDF0}-\u{FFFD}\u{10000}-\u{EFFFF}_0-9\u{00B7}\u{0300}-\u{036F}\u{203F}-\u{2040}-])?")]
    BlankNode(&'a [u8]),
    #[regex(r#""([^\u{5C}\u{A}\u{D}"]|\\[tbnrf"'\\]|\\u[0-9A-Fa-f]{4}|\\U[0-9A-Fa-f]{8})*"(\^\^<[^<>\"{}|^`\\\u{00}-\u{20}]*>|@[a-zA-Z]+(-[a-zA-Z0-9]+)*)?"#)]
    // NOTE: unquoted numeric literals (integer, decimal, double) are not part of the
    // N-Triples spec but QLever responds with them, so we accept them here.
    #[regex(r"[+-]?[0-9]+\.[0-9]+([eE][+-]?[0-9]+)?")]
    #[regex(r"[+-]?[0-9]+([eE][+-]?[0-9]+)?")]
    Literal(&'a [u8]),
}

#[cfg(test)]
mod test {
    use super::Token;
    use logos::Logos;

    #[test]
    fn tokenize_triple_1() {
        let tokens = Token::lexer(br#"_:a <iri> "strings"@en"#);
        assert_eq!(
            tokens.into_iter().map(|token| token).collect::<Vec<_>>(),
            vec![
                Ok(Token::BlankNode(b"_:a")),
                Ok(Token::Iri(b"<iri>")),
                Ok(Token::Literal(br#""strings"@en"#))
            ]
        );
    }

    #[test]
    fn tokenize_triple_2() {
        let tokens = Token::lexer(br#"<test> <http://www.w3.org/2001/XMLSchema#test> 8.0 ."#);
        assert_eq!(
            tokens.into_iter().map(|token| token).collect::<Vec<_>>(),
            vec![
                Ok(Token::Iri(b"<test>")),
                Ok(Token::Iri(b"<http://www.w3.org/2001/XMLSchema#test>")),
                Ok(Token::Literal(br#"8.0"#)),
                Ok(Token::Dot)
            ]
        );
    }

    #[test]
    fn tokenize_blanknode() {
        let tokens = Token::lexer(br#"_:a _:n1 _:asda"#);
        assert_eq!(
            tokens.into_iter().map(|token| token).collect::<Vec<_>>(),
            vec![
                Ok(Token::BlankNode(b"_:a")),
                Ok(Token::BlankNode(b"_:n1")),
                Ok(Token::BlankNode(b"_:asda"))
            ]
        );
    }

    #[test]
    fn tokenize_iri() {
        let tokens = Token::lexer(
            br#"<asdasdsa> <http://www.wikidata.org/prop/> <http://www.wikidata.org/prop/statement/value-normalized/>"#,
        );
        assert_eq!(
            tokens.into_iter().map(|token| token).collect::<Vec<_>>(),
            vec![
                Ok(Token::Iri(b"<asdasdsa>")),
                Ok(Token::Iri(b"<http://www.wikidata.org/prop/>")),
                Ok(Token::Iri(
                    b"<http://www.wikidata.org/prop/statement/value-normalized/>"
                ))
            ]
        );
    }

    #[test]
    fn tokenize_literal() {
        let tokens = Token::lexer(br#""simple string" "hello"@en "x"^^<asdadasd> 8.0"#);
        assert_eq!(
            tokens.into_iter().map(|token| token).collect::<Vec<_>>(),
            vec![
                Ok(Token::Literal(br#""simple string""#)),
                Ok(Token::Literal(br#""hello"@en"#)),
                Ok(Token::Literal(br#""x"^^<asdadasd>"#)),
                Ok(Token::Literal(b"8.0"))
            ]
        );
    }

    #[test]
    fn tokenize_blanknode_with_inner_dot() {
        let tokens = Token::lexer(b"_:a.b");
        assert_eq!(
            tokens.into_iter().collect::<Vec<_>>(),
            vec![Ok(Token::BlankNode(b"_:a.b"))]
        );
    }

    #[test]
    fn tokenize_blanknode_trailing_dot() {
        // NOTE: trailing dot must not be consumed as part of the blank node label
        let tokens = Token::lexer(b"_:a.");
        assert_eq!(
            tokens.into_iter().collect::<Vec<_>>(),
            vec![Ok(Token::BlankNode(b"_:a")), Ok(Token::Dot)]
        );
    }

    // BUG 5: apostrophes in strings are incorrectly rejected
    #[test]
    fn tokenize_literal_with_apostrophe() {
        let tokens = Token::lexer(br#""it's a test""#);
        assert_eq!(
            tokens.into_iter().collect::<Vec<_>>(),
            vec![Ok(Token::Literal(br#""it's a test""#))]
        );
    }

    // BUG 6: escape sequences in strings are not supported
    #[test]
    fn tokenize_literal_with_escapes() {
        let tokens = Token::lexer(br#""line1\nline2" "quote\"inside" "back\\slash""#);
        assert_eq!(
            tokens.into_iter().collect::<Vec<_>>(),
            vec![
                Ok(Token::Literal(br#""line1\nline2""#)),
                Ok(Token::Literal(br#""quote\"inside""#)),
                Ok(Token::Literal(br#""back\\slash""#)),
            ]
        );
    }

    // BUG 7: only @en is accepted as a language tag
    #[test]
    fn tokenize_literal_with_language_tags() {
        let tokens = Token::lexer(br#""hallo"@de "bonjour"@fr "color"@en-US"#);
        assert_eq!(
            tokens.into_iter().collect::<Vec<_>>(),
            vec![
                Ok(Token::Literal(br#""hallo"@de"#)),
                Ok(Token::Literal(br#""bonjour"@fr"#)),
                Ok(Token::Literal(br#""color"@en-US"#)),
            ]
        );
    }
}