add routing rule for node

This commit is contained in:
ginuerzh
2024-11-15 20:28:47 +08:00
parent 79b6b9138e
commit c2aedbabd9
11 changed files with 1071 additions and 145 deletions
+141
View File
@@ -0,0 +1,141 @@
package rules
import (
"fmt"
"strings"
"github.com/vulcand/predicate"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
const (
and = "and"
or = "or"
)
type Parser = predicate.Parser
// TreeBuilder defines the type for a Tree builder.
type TreeBuilder func() *Tree
// Tree represents the rules' tree structure.
type Tree struct {
Matcher string
Not bool
Value []string
RuleLeft *Tree
RuleRight *Tree
}
// NewParser constructs a parser for the given matchers.
func NewParser(matchers []string) (predicate.Parser, error) {
parserFuncs := make(map[string]interface{})
for _, matcherName := range matchers {
fn := func(value ...string) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: matcherName,
Value: value,
}
}
}
parserFuncs[matcherName] = fn
parserFuncs[strings.ToLower(matcherName)] = fn
parserFuncs[strings.ToUpper(matcherName)] = fn
parserFuncs[cases.Title(language.Und).String(strings.ToLower(matcherName))] = fn
}
return predicate.NewParser(predicate.Def{
Operators: predicate.Operators{
AND: andFunc,
OR: orFunc,
NOT: notFunc,
},
Functions: parserFuncs,
})
}
func andFunc(left, right TreeBuilder) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: and,
RuleLeft: left(),
RuleRight: right(),
}
}
}
func orFunc(left, right TreeBuilder) TreeBuilder {
return func() *Tree {
return &Tree{
Matcher: or,
RuleLeft: left(),
RuleRight: right(),
}
}
}
func invert(t *Tree) *Tree {
switch t.Matcher {
case or:
t.Matcher = and
t.RuleLeft = invert(t.RuleLeft)
t.RuleRight = invert(t.RuleRight)
case and:
t.Matcher = or
t.RuleLeft = invert(t.RuleLeft)
t.RuleRight = invert(t.RuleRight)
default:
t.Not = !t.Not
}
return t
}
func notFunc(elem TreeBuilder) TreeBuilder {
return func() *Tree {
return invert(elem())
}
}
// ParseMatchers returns the subset of matchers in the Tree matching the given matchers.
func (tree *Tree) ParseMatchers(matchers []string) []string {
switch tree.Matcher {
case and, or:
return append(tree.RuleLeft.ParseMatchers(matchers), tree.RuleRight.ParseMatchers(matchers)...)
default:
for _, matcher := range matchers {
if tree.Matcher == matcher {
return lower(tree.Value)
}
}
return nil
}
}
// CheckRule validates the given rule.
func CheckRule(rule *Tree) error {
if len(rule.Value) == 0 {
return fmt.Errorf("no args for matcher %s", rule.Matcher)
}
for _, v := range rule.Value {
if len(v) == 0 {
return fmt.Errorf("empty args for matcher %s, %v", rule.Matcher, rule.Value)
}
}
return nil
}
func lower(slice []string) []string {
var lowerStrings []string
for _, value := range slice {
lowerStrings = append(lowerStrings, strings.ToLower(value))
}
return lowerStrings
}
+299
View File
@@ -0,0 +1,299 @@
package rules
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// testTree = Tree + CheckErr
type testTree struct {
Matcher string
Not bool
Value []string
RuleLeft *testTree
RuleRight *testTree
// CheckErr allow knowing if a Tree has a rule error.
CheckErr bool
}
func TestRuleMatch(t *testing.T) {
matchers := []string{"m"}
testCases := []struct {
desc string
rule string
tree testTree
matchers []string
values []string
expectParseErr bool
}{
{
desc: "No rule",
rule: "",
expectParseErr: true,
},
{
desc: "No matcher in rule",
rule: "m",
expectParseErr: true,
},
{
desc: "No value in rule",
rule: "m()",
tree: testTree{
Matcher: "m",
Value: []string{},
CheckErr: true,
},
},
{
desc: "Empty value in rule",
rule: "m(``)",
tree: testTree{
Matcher: "m",
Value: []string{""},
CheckErr: true,
},
matchers: []string{"m"},
values: []string{""},
},
{
desc: "One value in rule with and",
rule: "m(`1`) &&",
expectParseErr: true,
},
{
desc: "One value in rule with or",
rule: "m(`1`) ||",
expectParseErr: true,
},
{
desc: "One value in rule with missing back tick",
rule: "m(`1)",
expectParseErr: true,
},
{
desc: "One value in rule with missing opening parenthesis",
rule: "m(`1`))",
expectParseErr: true,
},
{
desc: "One value in rule with missing closing parenthesis",
rule: "(m(`1`)",
expectParseErr: true,
},
{
desc: "One value in rule",
rule: "m(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "One value in rule with superfluous parenthesis",
rule: "(m(`1`))",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Rule with CAPS matcher",
rule: "M(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Invalid matcher in rule",
rule: "w(`1`)",
expectParseErr: true,
},
{
desc: "Invalid matchers",
rule: "m(`1`)",
tree: testTree{
Matcher: "m",
Value: []string{"1"},
},
matchers: []string{"not-m"},
},
{
desc: "Two value in rule",
rule: "m(`1`, `2`)",
tree: testTree{
Matcher: "m",
Value: []string{"1", "2"},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Not one value in rule",
rule: "!m(`1`)",
tree: testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
matchers: []string{"m"},
values: []string{"1"},
},
{
desc: "Two value in rule with and",
rule: "m(`1`) && m(`2`)",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with or",
rule: "m(`1`) || m(`2`)",
tree: testTree{
Matcher: "or",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with and negated",
rule: "!(m(`1`) && m(`2`))",
tree: testTree{
Matcher: "or",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Not: true,
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "Two value in rule with or negated",
rule: "!(m(`1`) || m(`2`))",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Not: true,
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Not: true,
Value: []string{"2"},
},
},
matchers: []string{"m"},
values: []string{"1", "2"},
},
{
desc: "No value in rule",
rule: "m(`1`) && m()",
tree: testTree{
Matcher: "and",
CheckErr: true,
RuleLeft: &testTree{
Matcher: "m",
Value: []string{"1"},
},
RuleRight: &testTree{
Matcher: "m",
Value: []string{},
CheckErr: true,
},
},
matchers: []string{"m"},
values: []string{"1"},
},
}
parser, err := NewParser(matchers)
require.NoError(t, err)
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
parse, err := parser.Parse(test.rule)
if test.expectParseErr {
require.Error(t, err)
return
}
require.NoError(t, err)
treeBuilder, ok := parse.(TreeBuilder)
require.True(t, ok)
tree := treeBuilder()
checkEquivalence(t, &test.tree, tree)
assert.Equal(t, test.values, tree.ParseMatchers(test.matchers))
})
}
}
func checkEquivalence(t *testing.T, expected *testTree, actual *Tree) {
t.Helper()
if actual == nil {
return
}
if actual.RuleLeft != nil {
checkEquivalence(t, expected.RuleLeft, actual.RuleLeft)
}
if actual.RuleRight != nil {
checkEquivalence(t, expected.RuleRight, actual.RuleRight)
}
assert.Equal(t, expected.Matcher, actual.Matcher)
assert.Equal(t, expected.Not, actual.Not)
assert.Equal(t, expected.Value, actual.Value)
t.Logf("%+v -> %v", actual, CheckRule(actual))
if expected.CheckErr {
assert.Error(t, CheckRule(actual))
} else {
assert.NoError(t, CheckRule(actual))
}
}