Files
cgrates/structmatcher/structmatcher.go
2023-06-05 10:55:32 +02:00

352 lines
8.3 KiB
Go

/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package structmatcher
/*
The condition syntax is a json encoded string similar to mongodb query language.
Examples:
- {"Weight":{"*gt":50}} checks for a balance with weight greater than 50
- {"*or":[{"Value":{"*eq":0}},{"Value":{"*gte":100}}] checks for a balance with value equal to 0 or equal or highr than 100
Available operators:
- *eq: equal
- *gt: greater than
- *gte: greater or equal than
- *lt: less then
- *lte: less or equal than
- *exp: expired
- *or: logical or
- *and: logical and
- *not: logical not
- *has: receives a list of elements and checks that the elements are present in the specified field (StringMap type)
- *rsr: will apply a rsr check to the field (see utils/rsrfield.go)
Equal (*eq) and local and (*and) operators are implicit for shortcuts. In this way:
{"*and":[{"Value":{"*eq":3}},{"Weight":{"*eq":10}}]} is equivalent to: {"Value":3, "Weight":10}.
*/
import (
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/cgrates/cgrates/utils"
)
const (
CondEQ = "*eq"
CondGT = "*gt"
CondGTE = "*gte"
CondLT = "*lt"
CondLTE = "*lte"
CondEXP = "*exp"
CondOR = "*or"
CondAND = "*and"
CondNOT = "*not"
CondHAS = "*has"
CondRSR = "*rsr"
)
func NewErrInvalidArgument(arg any) error {
return fmt.Errorf("INVALID_ARGUMENT: %v", arg)
}
type StringMap map[string]bool
var (
ErrParserError = errors.New("PARSER_ERROR")
operatorMap = map[string]func(field, value any) (bool, error){
CondEQ: func(field, value any) (bool, error) {
return value == field, nil
},
CondGT: func(field, value any) (bool, error) {
var of, vf float64
var ok bool
if of, ok = field.(float64); !ok {
return false, NewErrInvalidArgument(field)
}
if vf, ok = value.(float64); !ok {
return false, NewErrInvalidArgument(value)
}
return of > vf, nil
},
CondGTE: func(field, value any) (bool, error) {
var of, vf float64
var ok bool
if of, ok = field.(float64); !ok {
return false, NewErrInvalidArgument(field)
}
if vf, ok = value.(float64); !ok {
return false, NewErrInvalidArgument(value)
}
return of >= vf, nil
},
CondLT: func(field, value any) (bool, error) {
var of, vf float64
var ok bool
if of, ok = field.(float64); !ok {
return false, NewErrInvalidArgument(field)
}
if vf, ok = value.(float64); !ok {
return false, NewErrInvalidArgument(value)
}
return of < vf, nil
},
CondLTE: func(field, value any) (bool, error) {
var of, vf float64
var ok bool
if of, ok = field.(float64); !ok {
return false, NewErrInvalidArgument(field)
}
if vf, ok = value.(float64); !ok {
return false, NewErrInvalidArgument(value)
}
return of <= vf, nil
},
CondEXP: func(field, value any) (bool, error) {
var expDate time.Time
var ok bool
if expDate, ok = field.(time.Time); !ok {
return false, NewErrInvalidArgument(field)
}
var expired bool
if expired, ok = value.(bool); !ok {
return false, NewErrInvalidArgument(value)
}
if expired { // check for expiration
return !expDate.IsZero() && expDate.Before(time.Now()), nil
} else { // check not expired
return expDate.IsZero() || expDate.After(time.Now()), nil
}
},
CondHAS: func(field, value any) (bool, error) {
var strMap StringMap
var ok bool
if strMap, ok = field.(StringMap); !ok {
return false, NewErrInvalidArgument(field)
}
var strSlice []any
if strSlice, ok = value.([]any); !ok {
return false, NewErrInvalidArgument(value)
}
for _, str := range strSlice {
if !strMap[str.(string)] {
return false, nil
}
}
return true, nil
},
CondRSR: func(field, value any) (bool, error) {
fltr, err := utils.NewRSRFilter(value.(string))
if err != nil {
return false, err
}
return fltr.Pass(fmt.Sprintf("%v", field)), nil
},
}
)
type compositeElement interface {
element
addChild(element) error
}
type element interface {
checkStruct(any) (bool, error)
}
type operatorSlice struct {
operator string
slice []element
}
func (os *operatorSlice) addChild(ce element) error {
os.slice = append(os.slice, ce)
return nil
}
func (os *operatorSlice) checkStruct(o any) (bool, error) {
switch os.operator {
case CondOR:
for _, cond := range os.slice {
check, err := cond.checkStruct(o)
if err != nil {
return false, err
}
if check {
return true, nil
}
}
case CondAND, CondNOT:
accumulator := true
for _, cond := range os.slice {
check, err := cond.checkStruct(o)
if err != nil {
return false, err
}
accumulator = accumulator && check
}
if os.operator == CondAND {
return accumulator, nil
} else {
return !accumulator, nil
}
}
return false, nil
}
type keyStruct struct {
key string
elem element
}
func (ks *keyStruct) addChild(ce element) error {
ks.elem = ce
return nil
}
func (ks *keyStruct) checkStruct(o any) (bool, error) {
obj := reflect.ValueOf(o)
if obj.Kind() == reflect.Ptr {
obj = obj.Elem()
}
value := obj.FieldByName(ks.key)
if !value.IsValid() {
return false, NewErrInvalidArgument(ks.key)
}
return ks.elem.checkStruct(value.Interface())
}
type operatorValue struct {
operator string
value any
}
func (ov *operatorValue) checkStruct(o any) (bool, error) {
if f, ok := operatorMap[ov.operator]; ok {
return f(o, ov.value)
}
return false, nil
}
type keyValue struct {
key string
value any
}
func (kv *keyValue) checkStruct(o any) (bool, error) {
obj := reflect.ValueOf(o)
if obj.Kind() == reflect.Ptr {
obj = obj.Elem()
}
value := obj.FieldByName(kv.key)
if !value.IsValid() {
return false, NewErrInvalidArgument(kv.key)
}
return value.Interface() == kv.value, nil
}
type trueElement struct{}
func (te *trueElement) checkStruct(o any) (bool, error) {
return true, nil
}
func isOperator(s string) bool {
return strings.HasPrefix(s, "*")
}
func notEmpty(x any) bool {
return !reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface())
}
type StructMatcher struct {
rootElement element
}
func NewStructMatcher(q string) (sm *StructMatcher, err error) {
sm = &StructMatcher{}
err = sm.Parse(q)
return
}
func (sm *StructMatcher) load(a map[string]any, parentElement compositeElement) (element, error) {
for key, value := range a {
var currentElement element
switch t := value.(type) {
case []any:
if key == CondHAS {
currentElement = &operatorValue{operator: key, value: t}
} else {
currentElement = &operatorSlice{operator: key}
for _, e := range t {
sm.load(e.(map[string]any), currentElement.(compositeElement))
}
}
case map[string]any:
currentElement = &keyStruct{key: key}
//log.Print("map: ", t)
sm.load(t, currentElement.(compositeElement))
case any:
if isOperator(key) {
currentElement = &operatorValue{operator: key, value: t}
} else {
currentElement = &keyValue{key: key, value: t}
}
//log.Print("generic interface: ", t)
default:
return nil, ErrParserError
}
if parentElement != nil { // normal recurrent action
parentElement.addChild(currentElement)
} else {
if len(a) > 1 { // we have more keys in the map
parentElement = &operatorSlice{operator: CondAND}
parentElement.addChild(currentElement)
} else { // it was only one key value
return currentElement, nil
}
}
}
return parentElement, nil
}
func (sm *StructMatcher) Parse(s string) (err error) {
a := make(map[string]any)
if len(s) != 0 {
if err := json.Unmarshal([]byte([]byte(s)), &a); err != nil {
return err
}
sm.rootElement, err = sm.load(a, nil)
} else {
sm.rootElement = &trueElement{}
}
return
}
func (sm *StructMatcher) Match(o any) (bool, error) {
if sm.rootElement == nil {
return false, ErrParserError
}
return sm.rootElement.checkStruct(o)
}