rest-sql 0.1.0

RSQL/FIQL filter parser and validator for REST APIs — parse, validate, compile to native DB queries
Documentation
use std::fmt;
use thiserror::Error;

#[derive(Debug, Error)]
pub enum RestSqlError {
    #[error("parse error: {0}")]
    ParseError(ParseError),
    #[error("validation error: {0:?}")]
    ValidationError(Vec<ValidationError>),
}

/// A structured parse error with source location and visual context.
///
/// Designed to be shown directly to end users (e.g. REST API consumers).
/// Example output:
/// ```text
/// parse error at 1:5 — expected an operator (==, !=, =in=, ...)
///   name=bad=value
///       ^
/// ```
#[derive(Debug, Clone, PartialEq)]
pub struct ParseErrorAt {
    /// 1-based line number.
    pub line: usize,
    /// 1-based column number (byte-based; accurate for ASCII input).
    pub col: usize,
    /// Number of characters to underline with '^'. At least 1.
    pub span_len: usize,
    /// The full source line containing the error.
    pub snippet: String,
    /// Human-readable description of what went wrong.
    pub message: String,
}

impl fmt::Display for ParseErrorAt {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let caret = " ".repeat(self.col - 1) + &"^".repeat(self.span_len.max(1));
        write!(
            f,
            "parse error at {}:{}{}\n  {}\n  {}",
            self.line, self.col, self.message, self.snippet, caret
        )
    }
}

#[derive(Debug, Error)]
pub enum ParseError {
    /// Plain message error, used by winnow and peg parsers.
    #[error("parse error: {0}")]
    Message(String),

    /// Structured error with source location. Produced by the canonical parser.
    #[error("{0}")]
    At(ParseErrorAt),
}

#[derive(Debug, Error)]
pub enum ValidationError {
    #[error("field '{0}' is not allowed")]
    ForbiddenField(String),
    #[error("operator {operator:?} requires a list argument on field '{field}'")]
    ExpectedList { field: String, operator: String },
    #[error("operator {operator:?} requires exactly 2 values on field '{field}'")]
    BetweenArity { field: String, operator: String },
    #[error("operator {operator:?} does not accept a list on field '{field}'")]
    UnexpectedList { field: String, operator: String },
}