Skip to content

Commit

Permalink
update readme and unit test and example test
Browse files Browse the repository at this point in the history
  • Loading branch information
sado committed May 21, 2022
1 parent bfbd49a commit 7749d16
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 151 deletions.
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func test() {

- [x] [**ast rule engine**](https://github.com/sado0823/go-kitx/tree/master/kit/rule)

__supported operator__
__supported operator__

* **comparator**: `>` `>=` `<` `<=` `==`

Expand All @@ -36,10 +36,15 @@ __supported operator__

* **logic**: `&&` `||`

* **others**: `(` `)` `,` `func`(do func call with build in function and custom function)
* **func call**: `(` `)` `,` `func`(do func call with build in function and custom function)

* **params type**: `Ident` `Number` `String` `Bool` (DO Not support `array` `func` `struct`)
* **params type**: `Ident` `Number` `String` `Bool` `array`, `struct` (DO Not support `func` )

* **recursive params call with `.`**: `map.mapKey.mapKey.arrayIndex.structFiledName` (foo.bar.2.Name)

* Link
* [See Example Here]()
* [Check Unit Test Here]()

```go
// example
Expand Down
93 changes: 88 additions & 5 deletions kit/rule/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ __supported operator__

* **logic**: `&&` `||`

* **others**: `(` `)` `,` `func`(do func call with build in function and custom function)

* **params type**: `Ident` `Number` `String` `Bool` (DO Not support `array` `func` `struct`)
* **func call**: `(` `)` `,` `func`(do func call with build in function and custom function)

* **params type**: `Ident` `Number` `String` `Bool` `array`, `struct` (DO Not support `func` )

* **recursive params call with `.`**: `map.mapKey.mapKey.arrayIndex.structFiledName` (foo.bar.2.Name)

* Link
* [See Example Here]()
* [Check Unit Test Here]()
##### ExampleDo
```go
import (
Expand Down Expand Up @@ -102,15 +105,15 @@ func ExampleWithCustomFn() {
params,

/* custom func `strlen` return args[0]'s count with float64 type */
rule.WithCustomFn("strlen", func(arguments ...interface{}) (interface{}, error) {
rule.WithCustomFn("strlen", func(evalParam interface{}, arguments ...interface{}) (interface{}, error) {
if len(arguments) == 0 {
return 0, nil
}
return float64(utf8.RuneCount([]byte(arguments[0].(string)))), nil
}),

/*custom func `isTrue` return if args[0] is true with bool type*/
rule.WithCustomFn("isTrue", func(arguments ...interface{}) (interface{}, error) {
rule.WithCustomFn("isTrue", func(evalParam interface{}, arguments ...interface{}) (interface{}, error) {
if len(arguments) == 0 {
return 0, nil
}
Expand All @@ -126,4 +129,84 @@ func ExampleWithCustomFn() {
// Output:
// true
}
```

##### ExampleWithFullFunctional
```go
import (
"context"
"fmt"
"unicode/utf8"

"github.com/sado0823/go-kitx/kit/rule"
)

func ExampleWithCustomFn() {
type Child struct {
Name string
Age int
IsVIP bool
Map map[string]int
Nested *Child
}
type User struct {
Name string
Age int
IsVIP bool
Nil interface{}
Children []Child
}

params := &User{
Name: "foo",
Age: 18,
IsVIP: true,
Nil: nil,
Children: []Child{
{
// 0
Name: "child0", Age: 0, IsVIP: false, Map: map[string]int{"child0": 0}, Nested: &Child{Name: "child0-child"},
},
{
// 1
Name: "child1", Age: 1, IsVIP: true, Map: map[string]int{"child1": 1}, Nested: &Child{},
},
},
}

value, err := rule.Do(
context.Background(),
`Name == "foo" &&
(Name + "bar" == "foobar") &&
(Age == 17 || Age == 18) &&
(Age + 1 == 19) &&
func in(Name,2,"foo",1) &&
func strlen("abc") == 3 &&
func isVIP() &&
Children.1.Name == "child1" &&
Children.1.Map.child1 == 1 &&
Children.0.Nested.Name == "child0-child"`,
params,
/* custom func `strlen` return args[0]'s count with float64 type */
rule.WithCustomFn("strlen", func(evalParam interface{}, arguments ...interface{}) (interface{}, error) {
if len(arguments) == 0 {
return 0, nil
}
return float64(utf8.RuneCount([]byte(arguments[0].(string)))), nil
}),
/*custom func `isVIP` return if evalParam.IsVIP is true with bool type*/
rule.WithCustomFn("isVIP", func(evalParam interface{}, arguments ...interface{}) (interface{}, error) {
userCurrent := evalParam.(*User)
return userCurrent.IsVIP == true, nil
}),
)
if err != nil {
fmt.Println(err)
}

fmt.Print(value)

// Output:
// true
}
```
15 changes: 15 additions & 0 deletions kit/rule/collection.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ import (
"strconv"
)

func typeEqual(x, y interface{}) (xT, yT reflect.Type, ok bool) {
xT = reflect.TypeOf(x)
yT = reflect.TypeOf(y)

return xT, yT, xT.Kind() == yT.Kind()
}

func isString(value interface{}) bool {
switch value.(type) {
case string:
return true
}
return false
}

func convertToFloat(o interface{}) (float64, bool) {
if i, ok := o.(float64); ok {
return i, true
Expand Down
9 changes: 2 additions & 7 deletions kit/rule/custom_func.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ import (
"reflect"
)

type CustomFn func(arguments ...interface{}) (interface{}, error)
type CustomFn func(evalParam interface{}, arguments ...interface{}) (interface{}, error)

var _buildInCustomFn = map[string]CustomFn{
// func(inValue,arr[0],arr[1])
"in": func(arguments ...interface{}) (interface{}, error) {
logger.Println("build func [in], len args=", len(arguments))
logger.Println(arguments...)

"in": func(evalParam interface{}, arguments ...interface{}) (interface{}, error) {
if len(arguments) == 0 {
return false, fmt.Errorf("no args with func `in`")
}
Expand All @@ -26,9 +23,7 @@ var _buildInCustomFn = map[string]CustomFn{
)

arr = append(arr, arguments[1:]...)
logger.Printf("key=%v, key_type=%T \n", key, key)
for _, arg := range arr {
logger.Printf("arg=%v, arg_type=%T \n", arg, arg)
if reflect.DeepEqual(key, arg) {
return true, nil
}
Expand Down
55 changes: 46 additions & 9 deletions kit/rule/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,25 +43,62 @@ func ExampleNew() {
}

func ExampleWithCustomFn() {
params := map[string]interface{}{"foo": 1}
type Child struct {
Name string
Age int
IsVIP bool
Map map[string]int
Nested *Child
}
type User struct {
Name string
Age int
IsVIP bool
Nil interface{}
Children []Child
}

params := &User{
Name: "foo",
Age: 18,
IsVIP: true,
Nil: nil,
Children: []Child{
{
// 0
Name: "child0", Age: 0, IsVIP: false, Map: map[string]int{"child0": 0}, Nested: &Child{Name: "child0-child"},
},
{
// 1
Name: "child1", Age: 1, IsVIP: true, Map: map[string]int{"child1": 1}, Nested: &Child{},
},
},
}

value, err := rule.Do(
context.Background(),
`func in(foo,2,"abc",1) && func strlen("abc") == 3 && func isTrue(true) && func isTrue(false) == false`,
`Name == "foo" &&
(Name + "bar" == "foobar") &&
(Age == 17 || Age == 18) &&
(Age + 1 == 19) &&
func in(Name,2,"foo",1) &&
func strlen("abc") == 3 &&
func isVIP() &&
Children.1.Name == "child1" &&
Children.1.Map.child1 == 1 &&
Children.0.Nested.Name == "child0-child"`,
params,
/* custom func `strlen` return args[0]'s count with float64 type */
rule.WithCustomFn("strlen", func(arguments ...interface{}) (interface{}, error) {
rule.WithCustomFn("strlen", func(evalParam interface{}, arguments ...interface{}) (interface{}, error) {
if len(arguments) == 0 {
return 0, nil
}
return float64(utf8.RuneCount([]byte(arguments[0].(string)))), nil
}),
/*custom func `isTrue` return if args[0] is true with bool type*/
rule.WithCustomFn("isTrue", func(arguments ...interface{}) (interface{}, error) {
if len(arguments) == 0 {
return 0, nil
}
return arguments[0].(bool) == true, nil
/*custom func `isVIP` return if evalParam.IsVIP is true with bool type*/
rule.WithCustomFn("isVIP", func(evalParam interface{}, arguments ...interface{}) (interface{}, error) {
userCurrent := evalParam.(*User)
return userCurrent.IsVIP == true, nil
}),
)
if err != nil {
Expand Down
6 changes: 3 additions & 3 deletions kit/rule/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func Check(ctx context.Context, expr string, options ...WithOption) (err error)
return err
}

func Do(ctx context.Context, expr string, params map[string]interface{}, options ...WithOption) (interface{}, error) {
func Do(ctx context.Context, expr string, params interface{}, options ...WithOption) (interface{}, error) {
parser, err := New(ctx, expr, options...)
if err != nil {
return nil, err
Expand Down Expand Up @@ -102,7 +102,7 @@ func New(ctx context.Context, expr string, options ...WithOption) (parser *Parse
return p, err
}

func (p *Parser) Eval(params map[string]interface{}) (interface{}, error) {
func (p *Parser) Eval(params interface{}) (interface{}, error) {
return doStage(p.stageV, params)
}

Expand Down Expand Up @@ -354,7 +354,7 @@ func mirrorStageSubtree(stages []*stage) {
}
}

func doStage(stage *stage, parameters map[string]interface{}) (interface{}, error) {
func doStage(stage *stage, parameters interface{}) (interface{}, error) {

if stage == nil {
return nil, nil
Expand Down
68 changes: 14 additions & 54 deletions kit/rule/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ package rule

import (
"context"
goscanner "go/scanner"
"go/token"
"strings"
"testing"
"text/scanner"
)

func Test_New(t *testing.T) {
Expand All @@ -19,22 +15,27 @@ func Test_New(t *testing.T) {
Hobby []T `json:"hobby"`
}
//expr := `(foo - 90 > 0 ) && ( foo > 1 || foo <1 ) && foo > 1`
expr := `foo.bar.Hobby.0.Name`
param := map[string]interface{}{
"foo": map[string]interface{}{
"bar": T{Name: "tom", Hobby: []T{{Name: "jay"}}},
},
"in": 12.2,
}
parser, err := New(context.Background(), expr, WithCustomFn("test", func(arguments ...interface{}) (interface{}, error) {
//expr := `foo.bar.Hobby.0.Name == "jay" && func test(foo.bar.Hobby.0,1,2,3)`
//param := map[string]interface{}{
// "foo": map[string]interface{}{
// "bar": T{Name: "tom", Hobby: []T{{Name: "jay"}}},
// },
// "in": 12.2,
//}
tt := T{
Name: "ttt",
Hobby: nil,
}
parser, err := New(context.Background(), "Name", WithCustomFn("test", func(evalParam interface{}, arguments ...interface{}) (interface{}, error) {
logger.Println("i am test func")
logger.Println("evalParam: ", evalParam)
logger.Println(arguments...)
return true, nil
}))
if err != nil {
panic(err)
}
res, err := parser.Eval(param)
res, err := parser.Eval(tt)
if err != nil {
panic(err)
}
Expand All @@ -55,44 +56,3 @@ func Test_Do(t *testing.T) {
}
logger.Printf("res=%v\t type=%T\t err=%+v \n", res, res, err)
}

func Test_B(t *testing.T) {
// src is the input that we want to tokenize.
src := []byte(`;a >= 6 && a != "abc" && a.ABC() != 0;; // Euler.`)
//src := []byte(`func A() int64 { return 1 } // Euler.`)

// Initialize the scanner.
var s goscanner.Scanner
fset := token.NewFileSet() // positions are relative to fset
file := fset.AddFile("", fset.Base(), len(src)) // register input "file"
s.Init(file, src, nil /* no error handler */, goscanner.ScanComments)

// Repeated calls to Scan yield the token sequence found in the input.
for {
pos, tok, lit := s.Scan()
if tok == token.EOF {
break
}
if tok == token.SEMICOLON {
logger.Println("got SEMICOLON")
}
logger.Printf("pos:%s\t token:%s\t lit:%q\n", fset.Position(pos), tok, lit)
}
}

func Test_A(t *testing.T) {
exp := "a >= 6"

var s scanner.Scanner
s.Init(strings.NewReader(exp))

s.Filename = exp + "\t"
s.Error = func(s *scanner.Scanner, msg string) {
logger.Println("Scanner err msg: ", msg)
}

for tok := s.Scan(); tok != scanner.EOF; tok = s.Scan() {
sT := s.TokenText()
logger.Println("token text: ", sT)
}
}
Loading

0 comments on commit 7749d16

Please sign in to comment.