An open API service indexing awesome lists of open source software.

https://github.com/ekzo-dev/ruby-rsql

RSQL/FIQL parser for Ruby
https://github.com/ekzo-dev/ruby-rsql

Last synced: 20 days ago
JSON representation

RSQL/FIQL parser for Ruby

Awesome Lists containing this project

README

          

# rsql_parser

A Ruby parser library for **RSQL** and **FIQL** query expressions. Parses query strings into structured Ruby hashes that can be used to build database queries, filter collections, or power search APIs.

- **FIQL** (Feed Item Query Language): [RFC draft](https://datatracker.ietf.org/doc/html/draft-nottingham-atompub-fiql-00)
- **RSQL**: a superset of FIQL with additional convenience syntax

## Installation

Add to your `Gemfile`:

```ruby
gem 'rsql_parser'
```

Or install directly:

```bash
gem install rsql_parser
```

## Quick Start

```ruby
require 'rsql_parser'

result = RsqlParser.parse('name=="Kill Bill";year=gt=2003')
# => {
# type: :COMBINATION,
# operator: :AND,
# lhs: { type: :CONSTRAINT, selector: "name", comparison: "==", argument: "Kill Bill" },
# rhs: { type: :CONSTRAINT, selector: "year", comparison: "=gt=", argument: "2003" }
# }
```

## Return Value Structure

Every call to `RsqlParser.parse` returns a **node hash**. There are two node types:

### `:CONSTRAINT` — a single condition

| Key | Type | Description |
|--------------|----------|------------------------------------|
| `:type` | Symbol | Always `:CONSTRAINT` |
| `:selector` | String | The field/attribute name |
| `:comparison`| String | The comparison operator |
| `:argument` | String or Array | The value(s) to compare against |

```ruby
RsqlParser.parse('year==2003')
# => { type: :CONSTRAINT, selector: "year", comparison: "==", argument: "2003" }
```

### `:COMBINATION` — two conditions joined by a logical operator

| Key | Type | Description |
|-------------|--------|-------------------------------------|
| `:type` | Symbol | Always `:COMBINATION` |
| `:operator` | Symbol | `:AND` or `:OR` |
| `:lhs` | Hash | Left-hand side node |
| `:rhs` | Hash | Right-hand side node |

```ruby
RsqlParser.parse('a==1;b==2')
# => {
# type: :COMBINATION,
# operator: :AND,
# lhs: { type: :CONSTRAINT, selector: "a", comparison: "==", argument: "1" },
# rhs: { type: :CONSTRAINT, selector: "b", comparison: "==", argument: "2" }
# }
```

## Syntax Reference

### Selectors

A selector is a field name consisting of unreserved characters: letters, digits, and `-._~:`.

```
name
created_at
user.email
http://schema.org/name
```

### Comparison Operators

#### FIQL / RSQL operators

| Operator | Meaning | Example |
|-----------|--------------------------|----------------------|
| `==` | Equal | `name==Alice` |
| `!=` | Not equal | `status!=inactive` |
| `=gt=` | Greater than | `year=gt=2000` |
| `=gte=` | Greater than or equal | `year=gte=2000` |
| `=lt=` | Less than | `price=lt=100` |
| `=lte=` | Less than or equal | `price=lte=100` |
| `=in=` | In a set | `status=in=(a,b,c)` |
| `=out=` | Not in a set | `status=out=(x,y)` |
| `=custom=`| Any custom operator | `field=op=value` |

Custom FIQL operators follow the pattern `=[a-z!]*=` — any lowercase letters or `!` between two `=` signs.

#### Simplified comparison operators

| Operator | Meaning | Example |
|----------|--------------------------|---------------|
| `>` | Greater than | `year>2000` |
| `>=` | Greater than or equal | `year>=2000` |
| `<` | Less than | `price<100` |
| `<=` | Less than or equal | `price<=100` |

### Logical Operators

Conditions can be combined with AND and OR. AND has higher precedence than OR.

#### Symbol syntax

| Symbol | Operator | Example |
|--------|----------|-------------------|
| `;` | AND | `a==1;b==2` |
| `,` | OR | `a==1,b==2` |

#### Keyword syntax (case-insensitive)

| Keyword | Operator | Example |
|---------------|----------|----------------------|
| `and` / `AND` | AND | `a==1 and b==2` |
| `or` / `OR` | OR | `a==1 or b==2` |

Symbol and keyword syntax can be mixed freely. Whitespace around keywords is ignored.

### Arguments

#### Unquoted values

Sequences of unreserved characters (`[a-zA-Z0-9\-._~:]`):

```
year==2003
status==active
date==2018-09-01T12:14:28Z
```

#### Single-quoted strings

Allows spaces, semicolons, commas, and double quotes inside the value. Use `\'` to include a literal single quote:

```
name=='Kill;"Bill"'
tag=='it\'s fine'
```

#### Double-quoted strings

Allows spaces, semicolons, commas, and single quotes inside the value. Use `\"` to include a literal double quote:

```
name=="Kill Bill"
title=="She said \"hello\""
```

#### Array arguments

A parenthesised, comma-separated list. Used with operators like `=in=`:

```
status=in=(active,pending,review)
name=in=("Kill Bill","Pulp Fiction")
```

The `:argument` key will contain a Ruby `Array` instead of a `String`:

```ruby
RsqlParser.parse('genre=in=(sci-fi,action)')
# => { type: :CONSTRAINT, selector: "genre", comparison: "=in=",
# argument: ["sci-fi", "action"] }
```

### Grouping

Parentheses override the default AND-before-OR precedence:

```ruby
# Without grouping: (a AND b) OR c
RsqlParser.parse('a==1;b==2,c==3')

# With grouping: a AND (b OR c)
RsqlParser.parse('a==1;(b==2,c==3)')
```

## Examples

```ruby
require 'rsql_parser'

# Single constraint
RsqlParser.parse('year==2003')
# => { type: :CONSTRAINT, selector: "year", comparison: "==", argument: "2003" }

# Simplified comparison syntax
RsqlParser.parse('price<=99')
# => { type: :CONSTRAINT, selector: "price", comparison: "<=", argument: "99" }

# AND combination (semicolon and keyword are equivalent)
RsqlParser.parse('name=="Kill Bill" and year=gt=2003')
RsqlParser.parse('name=="Kill Bill";year=gt=2003')

# OR combination
RsqlParser.parse('status==active or status==pending')
RsqlParser.parse('status==active,status==pending')

# Array argument
RsqlParser.parse("genre=in=(sci-fi,action);year>2000")
# => {
# type: :COMBINATION,
# operator: :AND,
# lhs: { type: :CONSTRAINT, selector: "genre", comparison: "=in=",
# argument: ["sci-fi", "action"] },
# rhs: { type: :CONSTRAINT, selector: "year", comparison: ">",
# argument: "2000" }
# }

# Chained AND — right-associative tree
RsqlParser.parse('a=eq=b;c=ne=d;e=gt=f')
# => {
# type: :COMBINATION, operator: :AND,
# lhs: { type: :CONSTRAINT, selector: "a", comparison: "=eq=", argument: "b" },
# rhs: {
# type: :COMBINATION, operator: :AND,
# lhs: { type: :CONSTRAINT, selector: "c", comparison: "=ne=", argument: "d" },
# rhs: { type: :CONSTRAINT, selector: "e", comparison: "=gt=", argument: "f" }
# }
# }

# Grouping to change precedence
RsqlParser.parse('a=eq=b;(c=ne=d,e=gt=f)')
# => {
# type: :COMBINATION, operator: :AND,
# lhs: { type: :CONSTRAINT, selector: "a", comparison: "=eq=", argument: "b" },
# rhs: {
# type: :COMBINATION, operator: :OR,
# lhs: { type: :CONSTRAINT, selector: "c", comparison: "=ne=", argument: "d" },
# rhs: { type: :CONSTRAINT, selector: "e", comparison: "=gt=", argument: "f" }
# }
# }

# Escaped quotes inside strings
RsqlParser.parse('title=="She said \"hello\""')
# => { type: :CONSTRAINT, selector: "title", comparison: "==",
# argument: 'She said "hello"' }
```

## Operator Precedence

From highest to lowest:

1. Parentheses `( )`
2. AND — `;` or `and`
3. OR — `,` or `or`

## Requirements

- Ruby >= 2.7.0
- [racc](https://github.com/ruby/racc) ~> 1.8

## Development

```bash
# Run tests
rake test

# Regenerate the lexer after editing lib/rsql_parser/lexer.rex
ruby -roedipus_lex -e "
lex = OedipusLex.new
lex.parse_file('lib/rsql_parser/lexer.rex')
File.write('lib/rsql_parser/lexer.rex.rb', lex.generate)
"
```

Development dependencies: `oedipus_lex ~> 2.6`, `minitest ~> 5.21`, `rake ~> 13.0`.

## Contributing

Open a pull request with your changes and a corresponding test.

## License

MIT © [Ekzo](https://github.com/ekzo-dev)