mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
352 lines
8.4 KiB
Go
352 lines
8.4 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 Affero 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 Affero General Public License for more details.
|
|
|
|
You should have received a copy of the GNU Affero General Public License
|
|
along with this program. If not, see <https://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)
|
|
}
|