rest-sql 0.3.0

RSQL/FIQL filter parser and validator for REST APIs — parse, validate, compile to native DB queries
Documentation

rest-sql

Build Status Crates.io Docs.rs MSRV WASM ready License: MIT

Parse and validate RSQL / FIQL filter queries into a typed AST, or build filters programmatically — then hand the result to a backend driver.

name=like=Chris*;year=gt=1990;genre=in=(Drama,Thriller)

This crate is pure and WASM-compatible — it has no I/O and no backend dependency. For compiling to SQL/MongoDB/SurrealQL, see rest-sql-drivers.


Parse a query string

use rest_sql::RestSql;

let rsql = RestSql::new("title=like=Godfather*;year=gt=1970")?;

Build programmatically

Use the filter module when the query comes from code rather than a string:

use rest_sql::{RestSql, filter::{eq, gte, ilike, in_}};
use rest_sql::Ast;

// Simple composition
let ast = ilike("title", "godfather*") & gte("year", 1970i64);
let rsql = RestSql::from_ast(ast)?;

// Conditional construction — None entries are silently skipped
let ast = Ast::try_and_opts([
    title_filter.map(|t| ilike("title", t)),
    min_year.map(|y| gte("year", y)),
    genres.map(|g| in_("genre", g)),
]);

let rsql: Option<RestSql> = ast.map(RestSql::from_ast).transpose()?;

from_ast runs the same validation as new — operator/value compatibility, =between= arity, etc. Use from_ast_for::<T>() to also enforce a field allowlist derived from a #[derive(Deserialize)] struct.

BitAnd / BitOr combinators

& and | operators flatten adjacent nodes of the same type:

let a = eq("status", "active") & gte("age", 18i64);  // And([eq, gte])
let b = eq("role", "admin")   & eq("verified", true); // And([eq, eq])
let merged = a & b; // And([eq, gte, eq, eq]) — flat, not nested

Operators

Short Long Meaning
== =eq= Equal
!= =neq= Not equal
< =lt= Less than
<= =le= Less than or equal
> =gt= Greater than
>= =ge= Greater than or equal
=in= In list
=out= Not in list
=between= Range (inclusive)
=null= Null / absent
=notnull= Not null / present
=like= Pattern (* = any chars, _ = one char)
=ilike= Case-insensitive pattern

Logical connectors: ; / and / AND = AND, , / or / OR = OR, (...) = grouping. and / or used as values inside lists are never misinterpreted as connectors.

Value types recognized at parse time: String, Integer, Float, Boolean, Null, Date (YYYY-MM-DD), DateTime (YYYY-MM-DDTHH:MM:SSZ).


Field allowlisting

Reject queries that reference undeclared fields — important when filters come from user input:

// Explicit list
let rsql = RestSql::new_for_fields("title==Inception;secret==x", &["title", "year"])?;
// → Err: field 'secret' is not allowed

// Derive from a serde struct (feature `serde`)
#[derive(serde::Deserialize)]
struct Movie { title: String, year: i32, rating: f64 }

let rsql = RestSql::new_for::<Movie>("title==Inception;year=gt=2000")?;

Field mapping

FieldMapper renames logical field names before the AST reaches a driver. Useful for JSONB columns, table aliases, or any naming mismatch:

use rest_sql::{FieldMapper, RestSql};
use std::borrow::Cow;

struct PrefixMapper(&'static str);
impl FieldMapper for PrefixMapper {
    fn map<'a>(&self, field: &'a str) -> Cow<'a, str> {
        Cow::Owned(format!("{}.{}", self.0, field))
    }
}

let rsql = RestSql::new("title==Inception")?.map_fields(&PrefixMapper("f"));
// AST field is now "f.title"

Consumer pattern

The idiomatic way to convert your query structs into a RestSql:

use rest_sql::{RestSql, Ast, filter::{ilike, gte, lte, in_}};

struct FilmQuery {
    title: Option<String>,
    min_year: Option<i64>,
    max_year: Option<i64>,
    genres: Option<Vec<String>>,
}

impl TryFrom<FilmQuery> for RestSql {
    type Error = rest_sql::RestSqlError;

    fn try_from(q: FilmQuery) -> Result<Self, Self::Error> {
        let nodes: Vec<Ast> = [
            q.title.map(|t| ilike("title", t)),
            q.min_year.map(|y| gte("year", y)),
            q.max_year.map(|y| lte("year", y)),
            q.genres.map(|g| in_("genre", g)),
        ]
        .into_iter()
        .flatten()
        .collect();

        RestSql::from_ast(Ast::try_and(nodes).unwrap_or(Ast::and([gte("year", 0i64)])))
    }
}

Feature flags

Feature Default Description
serde off Enables RestSql::new_for::<T>() — field allowlist from #[derive(Deserialize)]

Implementation

Hand-written lexer + recursive descent grammar — no nom, pest, or combine. Those frameworks add measurable overhead on short inputs; a typical REST filter is 20–100 bytes parsed on every request, so zero framework cost matters.


License

MIT — see LICENSE.

Full documentation and examples: github.com/dohrm/rest-sql.