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](https://github.com/dohrm/rest-sql/actions/workflows/ci.yml/badge.svg)](https://github.com/dohrm/rest-sql/actions)
[![Crates.io](https://img.shields.io/crates/v/rest-sql.svg)](https://crates.io/crates/rest-sql)
[![Docs.rs](https://docs.rs/rest-sql/badge.svg)](https://docs.rs/rest-sql)
[![MSRV](https://img.shields.io/badge/MSRV-1.85-orange?logo=rust)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0/)
[![WASM ready](https://img.shields.io/badge/WASM-ready-brightgreen?logo=webassembly)](https://github.com/dohrm/rest-sql#wasm-compatibility)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Parse and validate [RSQL / FIQL](https://github.com/jirutka/rsql-parser) 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`](https://crates.io/crates/rest-sql-drivers).

---

## Parse a query string

```rust
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:

```rust
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:

```rust
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:

```rust
// 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:

```rust
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`:

```rust
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](../../LICENSE).

Full documentation and examples: [github.com/dohrm/rest-sql](https://github.com/dohrm/rest-sql).