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 cRsqlParser.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 constraintRsqlParser.parse('year==2003')# => { type: :CONSTRAINT, selector: "year", comparison: "==", argument: "2003" }# Simplified comparison syntaxRsqlParser.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 combinationRsqlParser.parse('status==active or status==pending')RsqlParser.parse('status==active,status==pending')# Array argumentRsqlParser.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 treeRsqlParser.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 precedenceRsqlParser.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 stringsRsqlParser.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