rest-sql 0.3.0

RSQL/FIQL filter parser and validator for REST APIs — parse, validate, compile to native DB queries
Documentation
use crate::ast::Operator;
use crate::parsing::span::Span;
use std::collections::HashMap;
use std::sync::OnceLock;

/// Operator table — single source of truth for all supported operators.
///
/// Entries are ordered longest-match first so the lexer can scan top-to-bottom
/// and stop at the first match without look-ahead. To add a new operator,
/// insert it here (maintaining longest-first order). No other file needs to change.
pub const OPERATORS: &[(&str, Operator)] = &[
    ("=between=", Operator::Between),
    ("=notnull=", Operator::NotNull),
    ("=ilike=", Operator::Ilike),
    ("=like=", Operator::Like),
    ("=null=", Operator::Null),
    ("=neq=", Operator::Neq),
    ("=out=", Operator::Out),
    ("=in=", Operator::In),
    ("=eq=", Operator::Eq),
    ("=le=", Operator::Lte),
    ("=ge=", Operator::Gte),
    ("=lt=", Operator::Lt),
    ("=gt=", Operator::Gt),
    ("<=", Operator::Lte),
    (">=", Operator::Gte),
    ("!=", Operator::Neq),
    ("==", Operator::Eq),
    ("<", Operator::Lt),
    (">", Operator::Gt),
];

/// Lazily-initialized HashMap view of OPERATORS, keyed by operator string.
///
/// Built once on first call, then shared for the lifetime of the process.
/// Keys are `&'static str` — same pointers as in the OPERATORS slice, no heap
/// allocation for keys. Used by the lexer for O(1) lookup after delimiting a
/// `=`-enclosed token whose exact bounds are already known.
static OPERATOR_MAP: OnceLock<HashMap<&'static str, Operator>> = OnceLock::new();

pub fn operator_map() -> &'static HashMap<&'static str, Operator> {
    OPERATOR_MAP.get_or_init(|| OPERATORS.iter().map(|(k, v)| (*k, v.clone())).collect())
}

/// A single lexical token.
///
/// Literals are typed at lex time: `null`/`true`/`false` become their own
/// variants; unquoted digit sequences become `Integer` or `Float`.
/// This avoids re-scanning in the grammar layer.
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
    /// The bare keyword `null`.
    Null,
    /// The bare keyword `true` or `false`.
    Bool(bool),
    /// An unquoted integer literal (no `.`).
    Integer(i64),
    /// An unquoted floating-point literal (contains `.`).
    Float(f64),
    /// An unquoted date literal matching `YYYY-MM-DD`.
    Date(String),
    /// An unquoted datetime literal matching `YYYY-MM-DDTHH:MM:SSZ`.
    DateTime(String),
    /// A single- or double-quoted string (quotes stripped, no escape processing).
    QuotedStr(String),
    /// An unquoted identifier or bare-string value (not a keyword or number).
    Word(String),
    /// A matched operator from the OPERATORS table.
    Op(Operator),
    /// `(`
    LParen,
    /// `)`
    RParen,
    /// `,` — OR separator at expression level; element separator inside lists.
    Comma,
    /// `;` — AND separator.
    Semi,
}

/// A token paired with the byte range it occupies in the source string.
#[derive(Debug, Clone, PartialEq)]
pub struct Spanned {
    pub token: Token,
    pub span: Span,
}

impl Spanned {
    pub fn new(token: Token, span: Span) -> Self {
        Spanned { token, span }
    }
}

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

    #[test]
    fn operators_longest_first() {
        let mut prev_len = usize::MAX;
        for (s, _) in OPERATORS {
            assert!(
                s.len() <= prev_len,
                "operator {s:?} is longer than its predecessor — order is wrong"
            );
            prev_len = s.len();
        }
    }

    #[test]
    fn longest_operator_is_between() {
        assert_eq!(OPERATORS[0].0, "=between=");
    }

    #[test]
    fn single_char_operators_are_last() {
        let last = OPERATORS.last().unwrap().0;
        assert_eq!(
            last.len(),
            1,
            "last operator should be single-char, got {last:?}"
        );
    }

    #[test]
    fn spanned_carries_span() {
        let span = Span::new(0, 4);
        let s = Spanned::new(Token::Word("name".into()), span);
        assert_eq!(s.span, span);
        assert_eq!(s.token, Token::Word("name".into()));
    }
}