Skip to content

Instantly share code, notes, and snippets.

@mirusky
Last active November 17, 2023 16:08
Show Gist options
  • Save mirusky/be624c9d0966e4b55f51cb02ac9613b7 to your computer and use it in GitHub Desktop.
Save mirusky/be624c9d0966e4b55f51cb02ac9613b7 to your computer and use it in GitHub Desktop.
Golang RQL parser
package main
import (
"bytes"
"encoding/json"
"fmt"
"strings"
)
var (
prefix = "$"
// A sorting expression can be optionally prefixed with + or - to control the
// sorting direction, ascending or descending. For example, '+field' or '-field'.
// If the predicate is missing or empty then it defaults to '+'
sortDirection = map[byte]string{
'+': "asc",
'-': "desc",
}
opFormat = map[Op]string{
EQ: "=",
NEQ: "<>",
LT: "<",
GT: ">",
LTE: "<=",
GTE: ">=",
LIKE: "LIKE",
OR: "OR",
AND: "AND",
}
)
// Operators that support by rql.
const (
EQ = Op("eq") // =
NEQ = Op("neq") // <>
LT = Op("lt") // <
GT = Op("gt") // >
LTE = Op("lte") // <=
GTE = Op("gte") // >=
LIKE = Op("like") // LIKE "PATTERN"
OR = Op("or") // disjunction
AND = Op("and") // conjunction
)
type Query struct {
Limit int `json:"limit,omitempty"`
Offset int `json:"offset,omitempty"`
Select []string `json:"select,omitempty"`
Sort []string `json:"sort,omitempty"`
Filter map[string]interface{} `json:"filter,omitempty"`
}
type Params struct {
Limit int
Offset int
Select string
Sort string
FilterExp string
FilterArgs []interface{}
}
// Op is a filter operator used by rql.
type Op string
// SQL returns the SQL representation of the operator.
func (o Op) SQL() string {
return opFormat[o]
}
type parseState struct {
*bytes.Buffer // query builder
values []interface{} // query values
}
func (p *parseState) relOp(op Op, terms []interface{}) {
var i int
if len(terms) > 1 {
p.WriteByte('(')
}
for _, t := range terms {
if i > 0 {
p.WriteByte(' ')
p.WriteString(op.SQL())
p.WriteByte(' ')
}
mt, ok := t.(map[string]interface{})
if !ok {
fmt.Printf("expressions for $%s operator must be type object", op)
return
}
p.and(mt)
i++
}
if len(terms) > 1 {
p.WriteByte(')')
}
}
func (p *parseState) op(op Op) string {
return prefix + string(op)
}
func (p *parseState) and(f map[string]interface{}) {
var i int
for k, v := range f {
if i > 0 {
p.WriteString(" AND ")
}
switch {
case k == p.op(OR):
terms, ok := v.([]interface{})
if !ok {
fmt.Printf("$or must be type array")
return
}
p.relOp(OR, terms)
case k == p.op(AND):
terms, ok := v.([]interface{})
if !ok {
fmt.Printf("$and must be type array")
return
}
p.relOp(AND, terms)
// case p.fields[k] != nil:
// expect(p.fields[k].Filterable, "field %q is not filterable", k)
// p.field(p.fields[k], v)
default:
if !strings.Contains(k, prefix) {
p.field(k, v)
} else {
fmt.Printf("unrecognized key %q for filtering", k)
return
}
}
i++
}
}
func (p *parseState) field(k string, v interface{}) {
terms, ok := v.(map[string]interface{})
// default equality check.
if !ok {
// must(f.ValidateFn(v), "invalid datatype for field %q", f.Name)
p.WriteString(p.fmtOp(k, EQ))
p.values = append(p.values, v)
}
var i int
if len(terms) > 1 {
p.WriteByte('(')
}
for opName, opVal := range terms {
if i > 0 {
p.WriteString(" AND ")
}
// expect(f.FilterOps[opName], "can not apply op %q on field %q", opName, f.Name)
// must(f.ValidateFn(opVal), "invalid datatype or format for field %q", f.Name)
p.WriteString(p.fmtOp(k, Op(opName[1:])))
if Op(opName[1:]) == Op(LIKE) {
p.values = append(p.values, fmt.Sprintf("%%%s%%", opVal))
} else {
p.values = append(p.values, opVal)
}
i++
}
if len(terms) > 1 {
p.WriteByte(')')
}
}
func (p *parseState) fmtOp(field string, op Op) string {
colName := p.colName(field)
if op == Op(LIKE) {
// HACK: Postgres does not have support to insensitive case so we check by lower equality
return fmt.Sprintf("lower(%s) %s ?", colName, op.SQL())
}
return fmt.Sprintf("%s %s ?", colName, op.SQL())
}
// colName formats the query field to database column name in cases the user configured a custom
// field separator. for example: if the user configured the field separator to be ".", the fields
// like "address.name" will be changed to "address_name".
func (p *parseState) colName(field string) string {
return strings.ReplaceAll(field, ".", "_")
}
func parseFilter(f map[string]interface{}) (string, []interface{}) {
ps := &parseState{
Buffer: &bytes.Buffer{},
values: []interface{}{},
}
ps.and(f)
return ps.String(), ps.values
}
func parseSelect(fields []string) string {
return strings.Join(fields, ", ")
}
func parseSort(fields []string) string {
sortParams := make([]string, len(fields))
for i, field := range fields {
var orderBy string
// if the sort field prefixed by an order indicator.
if order, ok := sortDirection[field[0]]; ok {
orderBy = order
field = field[1:]
}
colName := field
if orderBy != "" {
colName += " " + orderBy
}
sortParams[i] = colName
}
return strings.Join(sortParams, ", ")
}
func ParseRawQuery(raw []byte) (Params, error) {
var q Query
err := json.Unmarshal(raw, &q)
if err != nil {
return Params{}, err
}
return ParseQuery(q)
}
func ParseQuery(q Query) (Params, error) {
p := Params{
Limit: q.Limit,
Offset: q.Offset,
}
p.Sort = parseSort(q.Sort)
p.Select = parseSelect(q.Select)
expr, args := parseFilter(q.Filter)
p.FilterExp = expr
p.FilterArgs = args
return p, nil
}
func main() {
raw := `
{
"select": ["name", "value", "summary"],
"sort": ["-price", "+address"],
"limit": 10,
"filter": {
"$or": [
{"shipped": false},
{"username": "admin"}
]
}
}
`
p, err := ParseRawQuery([]byte(raw))
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("%+v", p)
}
@mirusky
Copy link
Author

mirusky commented Nov 16, 2023

It's not 100% functional but it does some stuff

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment