Skip to content

ekzo-dev/ruby-rsql

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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
  • RSQL: a superset of FIQL with additional convenience syntax

Installation

Add to your Gemfile:

gem 'rsql_parser'

Or install directly:

gem install rsql_parser

Quick Start

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

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:

# 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

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 ~> 1.8

Development

# 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

About

RSQL/FIQL parser for Ruby

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors