Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions pkg/driver/uis/uis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package uis

import (
"fmt"
"strconv"
"strings"
"time"

"github.com/douglaslim/gorql"
"github.com/douglaslim/gorql/pkg/driver"
)

type Translator struct {
rootNode *gorql.RqlRootNode
OpsDic map[string]driver.TranslatorOpFunc
}

type AlterValueFunc func(interface{}) (interface{}, error)

var keepValueFunc = AlterValueFunc(func(value interface{}) (interface{}, error) {
v, ok := value.(string)
if !ok {
return nil, fmt.Errorf("unable to convert %v to string", value)
}
return fmt.Sprintf(`%v`, v), nil
})

var convert = AlterValueFunc(func(value interface{}) (interface{}, error) {
switch v := value.(type) {
case string:
return v, nil
case time.Time:
return newDateTimeFromTime(v), nil
}
return value, nil
})

func (mt *Translator) SetOpFunc(op string, f driver.TranslatorOpFunc) {
mt.OpsDic[strings.ToUpper(op)] = f
}

func (mt *Translator) DeleteOpFunc(op string) {
delete(mt.OpsDic, strings.ToUpper(op))
}

func (mt *Translator) Where() (string, error) {
if mt.rootNode == nil {
return "", nil
}
where, err := mt.where(mt.rootNode.Node)
return fmt.Sprintf("(%s)", where), err
}

func (mt *Translator) Limit() (limit string) {
if mt.rootNode == nil {
return
}
l := mt.rootNode.Limit()
if l != "" && strings.ToUpper(l) != "INFINITY" {
v, _ := strconv.Atoi(l)
limit = fmt.Sprintf(`%d`, v)
}
return
}

func (mt *Translator) Offset() (offset string) {
if mt.rootNode != nil && mt.rootNode.Offset() != "" {
v, _ := strconv.Atoi(mt.rootNode.Offset())
offset = fmt.Sprintf(`%d`, v)
}
return
}

func (mt *Translator) where(n *gorql.RqlNode) (string, error) {
if n == nil {
return ``, nil
}
f := mt.OpsDic[strings.ToUpper(n.Op)]
if f == nil {
return "", fmt.Errorf("no TranslatorOpFunc for op : '%s'", n.Op)
}
return f(n)
}

func NewUISTranslator(r *gorql.RqlRootNode) (mt *Translator) {
mt = &Translator{r, map[string]driver.TranslatorOpFunc{}}

mt.SetOpFunc(driver.AndOp, mt.GetJoinTranslatorOpFunc(strings.ToLower(driver.AndOp)))
mt.SetOpFunc(driver.OrOp, mt.GetJoinTranslatorOpFunc(strings.ToLower(driver.OrOp)))
mt.SetOpFunc(driver.EqOp, mt.GetFieldValueTranslatorFunc(strings.ToLower(driver.EqOp), convert))
mt.SetOpFunc(driver.LikeOp, mt.GetFieldValueTranslatorFunc("regex", keepValueFunc))
mt.SetOpFunc(driver.MatchOp, mt.GetFieldValueTranslatorFunc("regex", keepValueFunc))
mt.SetOpFunc(driver.InOp, mt.GetSliceTranslatorFunc(strings.ToLower(driver.InOp), convert))
return
}

func (mt *Translator) GetJoinTranslatorOpFunc(op string) driver.TranslatorOpFunc {
return func(n *gorql.RqlNode) (s string, err error) {
var ops []string
for _, a := range n.Args {
switch v := a.(type) {
case *gorql.RqlNode:
var tempS string
tempS, err = mt.where(v)
if err != nil {
return "", err
}
ops = append(ops, tempS)
default:
return "", fmt.Errorf("%s operation need query as arguments", op)
}
}
if op == strings.ToLower(driver.AndOp) {
return fmt.Sprintf(`%s`, strings.Join(ops, ",")), nil
} else if op == strings.ToLower(driver.OrOp) {
return fmt.Sprintf(`%s`, strings.Join(ops, "|")), nil
} else {
return "", fmt.Errorf("unsupported operator %s for join translator", op)
}
}
}

func (mt *Translator) GetFieldValueTranslatorFunc(op string, alterValueFunc AlterValueFunc) driver.TranslatorOpFunc {
return func(n *gorql.RqlNode) (s string, err error) {
sep := ""
for i, a := range n.Args {
s += sep
var tempS string
if i == 0 {
if gorql.IsValidField(a.(string)) {
tempS = a.(string)
} else {
return "", fmt.Errorf("first argument must be a valid field name (arg: %v)", a)
}
} else {
convertedValue, err := alterValueFunc(a)
if err != nil {
return "", err
}
s += fmt.Sprintf(`%v`, convertedValue)
}
s += tempS
sep = fmt.Sprintf(`=`)
}
return fmt.Sprintf(`%s`, s), nil
}
}

func (mt *Translator) GetSliceTranslatorFunc(op string, alterValueFunc AlterValueFunc) driver.TranslatorOpFunc {
return func(n *gorql.RqlNode) (s string, err error) {
var values []string
var field string
if len(n.Args) > 0 {
a := n.Args[0]
if gorql.IsValidField(a.(string)) {
field = a.(string)
} else {
return "", fmt.Errorf("first argument must be a valid field name (arg: %s)", a)
}
}
subArgs := n.Args[1:]
if len(subArgs) > 1 {
return "", fmt.Errorf("expect enclosed arrays with square brackets argument")
}
groupNode, ok := subArgs[0].(*gorql.RqlNode)
if !ok {
return "", fmt.Errorf("expected group node but got %v", subArgs[0])
}
if len(groupNode.Args) < 2 {
return "", fmt.Errorf("array of values not found")
}
for _, a := range groupNode.Args[1:] {
convertedValue, err := alterValueFunc(a)
if err != nil {
return "", err
}
values = append(values, fmt.Sprintf("%s=%v", field, convertedValue))
}
s += fmt.Sprintf(`%s`, strings.Join(values, ","))
return fmt.Sprintf(`%s`, s), nil
}
}

func newDateTimeFromTime(t time.Time) int64 {
return t.Unix()*1e3 + int64(t.Nanosecond())/1e6
}
212 changes: 212 additions & 0 deletions pkg/driver/uis/uis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package uis

import (
"strings"
"testing"

"github.com/douglaslim/gorql"
)

type Test struct {
Name string // Name of the test
RQL string // Input RQL query
ExpectedWhere string // Expected Where clause output
ExpectedLimit string // Expected Limit output (only checked when non-empty)
ExpectedOffset string // Expected Offset output (only checked when non-empty)
WantParseError bool // Test should raise an error when parsing the RQL query
WantTranslatorError bool // Test should raise an error when translating
}

func (test *Test) Run(t *testing.T) {
p, err := gorql.NewParser(nil)
if err != nil {
t.Fatalf("(%s) New parser error :%v\n", test.Name, err)
}

rqlNode, err := p.Parse(strings.NewReader(test.RQL))
if test.WantParseError != (err != nil) {
t.Fatalf("(%s) Expecting parse error :%v\nGot error : %v", test.Name, test.WantParseError, err)
}
if test.WantParseError {
return
}

uisTranslator := NewUISTranslator(rqlNode)
where, err := uisTranslator.Where()
if test.WantTranslatorError != (err != nil) {
t.Fatalf("(%s) Expecting translator error :%v\nGot error : %v\n\tWhere = %s", test.Name, test.WantTranslatorError, err, where)
}
if test.WantTranslatorError {
return
}

if where != test.ExpectedWhere {
t.Fatalf("(%s) Translated Where clause doesn't match expected: '%s' vs '%s'", test.Name, where, test.ExpectedWhere)
}

if test.ExpectedLimit != "" {
l := uisTranslator.Limit()
if l != test.ExpectedLimit {
t.Fatalf("(%s) Limit doesn't match expected: '%s' vs '%s'", test.Name, l, test.ExpectedLimit)
}
}

if test.ExpectedOffset != "" {
o := uisTranslator.Offset()
if o != test.ExpectedOffset {
t.Fatalf("(%s) Offset doesn't match expected: '%s' vs '%s'", test.Name, o, test.ExpectedOffset)
}
}
}

var tests = []Test{
{
Name: `Empty RQL`,
RQL: ``,
ExpectedWhere: `()`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `Basic EQ operator`,
RQL: `eq(foo,42)`,
ExpectedWhere: `(foo=42)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `Simple equal style`,
RQL: `foo=42`,
ExpectedWhere: `(foo=42)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `AND operator`,
RQL: `and(eq(foo,42),eq(price,10))`,
ExpectedWhere: `(foo=42,price=10)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `AND operator with simple equal style`,
RQL: `foo=42&price=10`,
ExpectedWhere: `(foo=42,price=10)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `OR operator`,
RQL: `or(eq(foo,42),eq(price,10))`,
ExpectedWhere: `(foo=42|price=10)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `LIKE operator`,
RQL: `like(foo,weird)`,
ExpectedWhere: `(foo=weird)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `MATCH operator`,
RQL: `match(foo,weird)`,
ExpectedWhere: `(foo=weird)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `IN operator`,
RQL: `in(foo,[hello,world])`,
ExpectedWhere: `(foo=hello,foo=world)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `Nested AND and OR operators`,
RQL: `or(and(eq(foo,42),eq(price,10)),eq(bar,hello))`,
ExpectedWhere: `(foo=42,price=10|bar=hello)`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `Limit and Offset`,
RQL: `eq(foo,42)&limit(10,20)`,
ExpectedWhere: `(foo=42)`,
ExpectedLimit: `10`,
ExpectedOffset: `20`,
WantParseError: false,
WantTranslatorError: false,
},
{
Name: `Unmanaged RQL operator`,
RQL: `missing_operator(foo,42)`,
ExpectedWhere: ``,
WantParseError: false,
WantTranslatorError: true,
},
{
Name: `Invalid RQL query (Unescaped character)`,
RQL: `like(foo,hello world)`,
ExpectedWhere: ``,
WantParseError: true,
WantTranslatorError: false,
},
{
Name: `Invalid RQL query (Missing comma)`,
RQL: `and(eq(foo,42)eq(price,10))`,
ExpectedWhere: ``,
WantParseError: true,
WantTranslatorError: false,
},
{
Name: `Invalid field name`,
RQL: `eq(foo%20tot,42)`,
ExpectedWhere: ``,
WantParseError: false,
WantTranslatorError: true,
},
{
Name: `Invalid field name 2`,
RQL: `eq(foo*,toto)`,
ExpectedWhere: ``,
WantParseError: false,
WantTranslatorError: true,
},
}

func TestUISTranslator(t *testing.T) {
for _, test := range tests {
test.Run(t)
}
}

func TestSetAndDeleteOpFunc(t *testing.T) {
p, err := gorql.NewParser(nil)
if err != nil {
t.Fatalf("New parser error :%v\n", err)
}
rqlNode, err := p.Parse(strings.NewReader(`eq(foo,42)`))
if err != nil {
t.Fatalf("Parse error :%v\n", err)
}
uisTranslator := NewUISTranslator(rqlNode)

uisTranslator.DeleteOpFunc("eq")
_, err = uisTranslator.Where()
if err == nil {
t.Fatalf("Expected translator error after deleting EQ op, got nil")
}

uisTranslator.SetOpFunc("eq", func(n *gorql.RqlNode) (string, error) {
return "custom=result", nil
})
where, err := uisTranslator.Where()
if err != nil {
t.Fatalf("Unexpected translator error after setting custom op func: %v", err)
}
if where != "(custom=result)" {
t.Fatalf("Unexpected Where result after custom op func: '%s'", where)
}
}