diff --git a/pkg/driver/uis/uis.go b/pkg/driver/uis/uis.go new file mode 100644 index 0000000..541193f --- /dev/null +++ b/pkg/driver/uis/uis.go @@ -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 +} diff --git a/pkg/driver/uis/uis_test.go b/pkg/driver/uis/uis_test.go new file mode 100644 index 0000000..def7fd3 --- /dev/null +++ b/pkg/driver/uis/uis_test.go @@ -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) + } +}