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
- RSQL: a superset of FIQL with additional convenience syntax
Add to your Gemfile:
gem 'rsql_parser'Or install directly:
gem install rsql_parserrequire '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" }
# }Every call to RsqlParser.parse returns a node hash. There are two node types:
| 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 |
RsqlParser.parse('year==2003')
# => { type: :CONSTRAINT, selector: "year", comparison: "==", argument: "2003" }| Key | Type | Description |
|---|---|---|
:type |
Symbol | Always :COMBINATION |
:operator |
Symbol | :AND or :OR |
:lhs |
Hash | Left-hand side node |
:rhs |
Hash | Right-hand side node |
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" }
# }A selector is a field name consisting of unreserved characters: letters, digits, and -._~:.
name
created_at
user.email
http://schema.org/name
| 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.
| Operator | Meaning | Example |
|---|---|---|
> |
Greater than | year>2000 |
>= |
Greater than or equal | year>=2000 |
< |
Less than | price<100 |
<= |
Less than or equal | price<=100 |
Conditions can be combined with AND and OR. AND has higher precedence than OR.
| Symbol | Operator | Example |
|---|---|---|
; |
AND | a==1;b==2 |
, |
OR | a==1,b==2 |
| 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.
Sequences of unreserved characters ([a-zA-Z0-9\-._~:]):
year==2003
status==active
date==2018-09-01T12:14:28Z
Allows spaces, semicolons, commas, and double quotes inside the value. Use \' to include a literal single quote:
name=='Kill;"Bill"'
tag=='it\'s fine'
Allows spaces, semicolons, commas, and single quotes inside the value. Use \" to include a literal double quote:
name=="Kill Bill"
title=="She said \"hello\""
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:
RsqlParser.parse('genre=in=(sci-fi,action)')
# => { type: :CONSTRAINT, selector: "genre", comparison: "=in=",
# argument: ["sci-fi", "action"] }Parentheses override the default AND-before-OR precedence:
# 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)')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"' }From highest to lowest:
- Parentheses
( ) - AND —
;orand - OR —
,oror
- Ruby >= 2.7.0
- racc ~> 1.8
# 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.
Open a pull request with your changes and a corresponding test.
MIT © Ekzo