add routing rule for node
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user