add routing rule for node
This commit is contained in:
@@ -0,0 +1,464 @@
|
||||
package routing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/go-gost/core/routing"
|
||||
"github.com/go-gost/x/routing/rules"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultParser rules.Parser
|
||||
)
|
||||
|
||||
func init() {
|
||||
var matchers []string
|
||||
for matcher := range httpFuncs {
|
||||
matchers = append(matchers, matcher)
|
||||
}
|
||||
|
||||
parser, err := rules.NewParser(matchers)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
defaultParser = parser
|
||||
}
|
||||
|
||||
type matcher struct {
|
||||
// matchers tree structure reflecting the rule.
|
||||
tree matchersTree
|
||||
}
|
||||
|
||||
func NewMatcher(rule string) (routing.Matcher, error) {
|
||||
parse, err := defaultParser.Parse(rule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while parsing rule %s: %w", rule, err)
|
||||
}
|
||||
|
||||
buildTree, ok := parse.(rules.TreeBuilder)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("error while parsing rule %s", rule)
|
||||
}
|
||||
|
||||
var matchers matchersTree
|
||||
err = matchers.addRule(buildTree(), httpFuncs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while adding rule %s: %w", rule, err)
|
||||
}
|
||||
|
||||
return &matcher{
|
||||
tree: matchers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *matcher) Match(req *routing.Request) bool {
|
||||
if m == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return m.tree.match(req)
|
||||
}
|
||||
|
||||
// matchersTree represents the matchers tree structure.
|
||||
type matchersTree struct {
|
||||
// matcher is a matcher func used to match HTTP request properties.
|
||||
// If matcher is not nil, it means that this matcherTree is a leaf of the tree.
|
||||
// It is therefore mutually exclusive with left and right.
|
||||
matcher func(*routing.Request) bool
|
||||
// operator to combine the evaluation of left and right leaves.
|
||||
operator string
|
||||
// Mutually exclusive with matcher.
|
||||
left *matchersTree
|
||||
right *matchersTree
|
||||
}
|
||||
|
||||
func (m *matchersTree) match(req *routing.Request) bool {
|
||||
if m == nil {
|
||||
// This should never happen as it should have been detected during parsing.
|
||||
return false
|
||||
}
|
||||
|
||||
if m.matcher != nil {
|
||||
return m.matcher(req)
|
||||
}
|
||||
|
||||
switch m.operator {
|
||||
case "or":
|
||||
return m.left.match(req) || m.right.match(req)
|
||||
case "and":
|
||||
return m.left.match(req) && m.right.match(req)
|
||||
default:
|
||||
// This should never happen as it should have been detected during parsing.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type matcherFuncs map[string]func(*matchersTree, ...string) error
|
||||
|
||||
func (m *matchersTree) addRule(rule *rules.Tree, funcs matcherFuncs) error {
|
||||
switch rule.Matcher {
|
||||
case "and", "or":
|
||||
m.operator = rule.Matcher
|
||||
m.left = &matchersTree{}
|
||||
err := m.left.addRule(rule.RuleLeft, funcs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err)
|
||||
}
|
||||
|
||||
m.right = &matchersTree{}
|
||||
return m.right.addRule(rule.RuleRight, funcs)
|
||||
default:
|
||||
err := rules.CheckRule(rule)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while checking rule %s: %w", rule.Matcher, err)
|
||||
}
|
||||
|
||||
err = funcs[rule.Matcher](m, rule.Value...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while adding rule %s: %w", rule.Matcher, err)
|
||||
}
|
||||
|
||||
if rule.Not {
|
||||
matcherFunc := m.matcher
|
||||
m.matcher = func(req *routing.Request) bool {
|
||||
return !matcherFunc(req)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var httpFuncs = map[string]func(*matchersTree, ...string) error{
|
||||
"ClientIP": expectNParameters(clientIP, 1),
|
||||
"Proto": expectNParameters(proto, 1),
|
||||
"Host": expectNParameters(host, 1),
|
||||
"HostRegexp": expectNParameters(hostRegexp, 1),
|
||||
"Method": expectNParameters(method, 1),
|
||||
"Path": expectNParameters(path, 1),
|
||||
"PathRegexp": expectNParameters(pathRegexp, 1),
|
||||
"PathPrefix": expectNParameters(pathPrefix, 1),
|
||||
"Header": expectNParameters(header, 1, 2),
|
||||
"HeaderRegexp": expectNParameters(headerRegexp, 1, 2),
|
||||
"Query": expectNParameters(query, 1, 2),
|
||||
"QueryRegexp": expectNParameters(queryRegexp, 1, 2),
|
||||
}
|
||||
|
||||
func expectNParameters(fn func(*matchersTree, ...string) error, n ...int) func(*matchersTree, ...string) error {
|
||||
return func(tree *matchersTree, s ...string) error {
|
||||
if !slices.Contains(n, len(s)) {
|
||||
return fmt.Errorf("unexpected number of parameters; got %d, expected one of %v", len(s), n)
|
||||
}
|
||||
|
||||
return fn(tree, s...)
|
||||
}
|
||||
}
|
||||
|
||||
func clientIP(tree *matchersTree, clientIP ...string) error {
|
||||
ip := net.ParseIP(clientIP[0])
|
||||
|
||||
var ipNet *net.IPNet
|
||||
if ip == nil {
|
||||
_, ipNet, _ = net.ParseCIDR(clientIP[0])
|
||||
}
|
||||
if ip == nil && ipNet == nil {
|
||||
return fmt.Errorf("invalid value %q for ClientIP matcher", clientIP[0])
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
if req.ClientIP == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if ip != nil {
|
||||
return ip.Equal(req.ClientIP)
|
||||
}
|
||||
|
||||
return ipNet.Contains(req.ClientIP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func proto(tree *matchersTree, protos ...string) error {
|
||||
proto := strings.ToLower(protos[0])
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
// logger.Default().Debugf("proto: %s %s", proto, req.Protocol)
|
||||
return proto == req.Protocol
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func method(tree *matchersTree, methods ...string) error {
|
||||
method := strings.ToUpper(methods[0])
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
return method == req.Method
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func host(tree *matchersTree, hosts ...string) error {
|
||||
host := hosts[0]
|
||||
|
||||
if !IsASCII(host) {
|
||||
return fmt.Errorf("invalid value %q for Host matcher, non-ASCII characters are not allowed", host)
|
||||
}
|
||||
|
||||
host = strings.ToLower(strings.TrimSpace(host))
|
||||
|
||||
if strings.HasPrefix(host, "*") {
|
||||
host = host[1:]
|
||||
if !strings.HasPrefix(host, ".") {
|
||||
host = "." + host
|
||||
}
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
// logger.Default().Debugf("host: %s %s", host, req.Host)
|
||||
reqHost := strings.ToLower(strings.TrimSpace(parseHost(req.Host)))
|
||||
if len(reqHost) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if reqHost == host {
|
||||
return true
|
||||
}
|
||||
|
||||
if host[0] == '.' && strings.HasSuffix(reqHost, host[1:]) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func hostRegexp(tree *matchersTree, hosts ...string) error {
|
||||
host := hosts[0]
|
||||
|
||||
if !IsASCII(host) {
|
||||
return fmt.Errorf("invalid value %q for HostRegexp matcher, non-ASCII characters are not allowed", host)
|
||||
}
|
||||
|
||||
re, err := regexp.Compile(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling HostRegexp matcher: %w", err)
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
// logger.Default().Debugf("hostRegexp: %s %s", host, req.Host)
|
||||
return re.MatchString(strings.ToLower(strings.TrimSpace(parseHost(req.Host))))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func path(tree *matchersTree, paths ...string) error {
|
||||
path := paths[0]
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return fmt.Errorf("path %q does not start with a '/'", path)
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
return req.Path == path
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathRegexp(tree *matchersTree, paths ...string) error {
|
||||
path := paths[0]
|
||||
|
||||
re, err := regexp.Compile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling PathPrefix matcher: %w", err)
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
return re.MatchString(req.Path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func pathPrefix(tree *matchersTree, paths ...string) error {
|
||||
path := paths[0]
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
return fmt.Errorf("path %q does not start with a '/'", path)
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
return strings.HasPrefix(req.Path, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func header(tree *matchersTree, headers ...string) error {
|
||||
key := http.CanonicalHeaderKey(headers[0])
|
||||
|
||||
var value string
|
||||
var hasValue bool
|
||||
if len(headers) == 2 {
|
||||
value = headers[1]
|
||||
hasValue = true
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
if req.Header == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
values, ok := req.Header[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !hasValue {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, headerValue := range values {
|
||||
if headerValue == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func headerRegexp(tree *matchersTree, headers ...string) error {
|
||||
if len(headers) == 1 {
|
||||
return header(tree, headers...)
|
||||
}
|
||||
|
||||
key, value := http.CanonicalHeaderKey(headers[0]), headers[1]
|
||||
|
||||
re, err := regexp.Compile(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling HeaderRegexp matcher: %w", err)
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
if req.Header == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, headerValue := range req.Header[key] {
|
||||
if re.MatchString(headerValue) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func query(tree *matchersTree, queries ...string) error {
|
||||
key := queries[0]
|
||||
|
||||
var value string
|
||||
var hasValue bool
|
||||
if len(queries) == 2 {
|
||||
value = queries[1]
|
||||
hasValue = true
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
if req.Query == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
values, ok := req.Query[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
if !hasValue {
|
||||
return true
|
||||
}
|
||||
|
||||
return slices.Contains(values, value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func queryRegexp(tree *matchersTree, queries ...string) error {
|
||||
if len(queries) == 1 {
|
||||
return query(tree, queries...)
|
||||
}
|
||||
|
||||
key, value := queries[0], queries[1]
|
||||
|
||||
re, err := regexp.Compile(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compiling QueryRegexp matcher: %w", err)
|
||||
}
|
||||
|
||||
tree.matcher = func(req *routing.Request) bool {
|
||||
if req.Query == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
values, ok := req.Query[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
idx := slices.IndexFunc(values, func(value string) bool {
|
||||
return re.MatchString(value)
|
||||
})
|
||||
|
||||
return idx >= 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsASCII checks if the given string contains only ASCII characters.
|
||||
func IsASCII(s string) bool {
|
||||
for i := range len(s) {
|
||||
if s[i] >= utf8.RuneSelf {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func parseHost(addr string) string {
|
||||
if !strings.Contains(addr, ":") {
|
||||
// IPv4 without port or empty address
|
||||
return addr
|
||||
}
|
||||
|
||||
// IPv4 with port or IPv6
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
if addr[0] == '[' && addr[len(addr)-1] == ']' {
|
||||
return addr[1 : len(addr)-1]
|
||||
}
|
||||
return addr
|
||||
}
|
||||
return host
|
||||
}
|
||||
@@ -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