diff --git a/engine/account.go b/engine/account.go index 9cec58246..136ffd296 100644 --- a/engine/account.go +++ b/engine/account.go @@ -25,8 +25,8 @@ import ( "time" "github.com/cgrates/cgrates/cache2go" + "github.com/cgrates/cgrates/structmatcher" "github.com/cgrates/cgrates/utils" - "github.com/cgrates/structmatcher" "strings" ) diff --git a/engine/timespans.go b/engine/timespans.go index dfd8fb8b4..57668a636 100644 --- a/engine/timespans.go +++ b/engine/timespans.go @@ -57,10 +57,23 @@ type DebitInfo struct { AccountID string // used when debited from shared balance } -func (bi *DebitInfo) Equal(other *DebitInfo) bool { - return bi.Unit.Equal(other.Unit) && - bi.Monetary.Equal(other.Monetary) && - bi.AccountID == other.AccountID +func (di *DebitInfo) Equal(other *DebitInfo) bool { + return di.Unit.Equal(other.Unit) && + di.Monetary.Equal(other.Monetary) && + di.AccountID == other.AccountID +} + +func (di *DebitInfo) Clone() *DebitInfo { + nDi := &DebitInfo{ + AccountID: di.AccountID, + } + if di.Unit != nil { + nDi.Unit = di.Unit.Clone() + } + if di.Monetary != nil { + nDi.Monetary = di.Monetary.Clone() + } + return nDi } type MonetaryInfo struct { @@ -69,6 +82,11 @@ type MonetaryInfo struct { RateInterval *RateInterval } +func (mi *MonetaryInfo) Clone() *MonetaryInfo { + newMi := *mi + return &newMi +} + func (mi *MonetaryInfo) Equal(other *MonetaryInfo) bool { if mi == nil && other == nil { return true @@ -89,6 +107,11 @@ type UnitInfo struct { RateInterval *RateInterval } +func (ui *UnitInfo) Clone() *UnitInfo { + newUi := *ui + return &newUi +} + func (ui *UnitInfo) Equal(other *UnitInfo) bool { if ui == nil && other == nil { return true @@ -237,12 +260,14 @@ func (tss *TimeSpans) Decompress() { // must be pointer receiver } func (incr *Increment) Clone() *Increment { - nIncr := &Increment{ - Duration: incr.Duration, - Cost: incr.Cost, - BalanceInfo: incr.BalanceInfo, + nInc := &Increment{ + Duration: incr.Duration, + Cost: incr.Cost, } - return nIncr + if incr.BalanceInfo != nil { + nInc.BalanceInfo = incr.BalanceInfo.Clone() + } + return nInc } func (incr *Increment) Equal(other *Increment) bool { @@ -297,8 +322,19 @@ func (incs *Increments) Compress() { // must be pointer receiver func (incs *Increments) Decompress() { // must be pointer receiver var cIncrs Increments for _, cIncr := range *incs { - for i := 0; i < cIncr.GetCompressFactor(); i++ { - cIncrs = append(cIncrs, cIncr.Clone()) + cf := cIncr.GetCompressFactor() + for i := 0; i < cf; i++ { + incr := cIncr.Clone() + // set right Values + if incr.BalanceInfo != nil { + if incr.BalanceInfo.Monetary != nil { + incr.BalanceInfo.Monetary.Value += (float64(cf-(i+1)) * incr.Cost) + } + if incr.BalanceInfo.Unit != nil { + incr.BalanceInfo.Unit.Value += (float64(cf-(i+1)) * incr.BalanceInfo.Unit.Consumed) + } + } + cIncrs = append(cIncrs, incr) } } *incs = cIncrs diff --git a/engine/timespans_test.go b/engine/timespans_test.go index c89a078f0..f8fb19dcd 100644 --- a/engine/timespans_test.go +++ b/engine/timespans_test.go @@ -1511,42 +1511,42 @@ func TestTSIncrementsCompressDecompress(t *testing.T) { Increments: Increments{ &Increment{ Duration: time.Minute, - Cost: 10.4, + Cost: 2, BalanceInfo: &DebitInfo{ - Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, - Monetary: &MonetaryInfo{UUID: "2"}, + Unit: &UnitInfo{UUID: "1", Value: 25, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 98}, AccountID: "3"}, }, &Increment{ Duration: time.Minute, - Cost: 10.4, + Cost: 2, BalanceInfo: &DebitInfo{ - Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, - Monetary: &MonetaryInfo{UUID: "2"}, + Unit: &UnitInfo{UUID: "1", Value: 24, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 96}, AccountID: "3"}, }, &Increment{ Duration: time.Minute, - Cost: 10.4, + Cost: 2, BalanceInfo: &DebitInfo{ - Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, - Monetary: &MonetaryInfo{UUID: "2"}, + Unit: &UnitInfo{UUID: "1", Value: 23, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 94}, AccountID: "3"}, }, &Increment{ Duration: time.Minute, - Cost: 10.4, + Cost: 2, BalanceInfo: &DebitInfo{ - Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 1111 * time.Second, RateUnit: time.Second}}}}}, - Monetary: &MonetaryInfo{UUID: "2"}, + Unit: &UnitInfo{UUID: "1", Value: 22, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 1111 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 92}, AccountID: "3"}, }, &Increment{ Duration: time.Minute, - Cost: 10.4, + Cost: 2, BalanceInfo: &DebitInfo{ - Unit: &UnitInfo{UUID: "1", DestinationID: "1", Consumed: 2.3, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, - Monetary: &MonetaryInfo{UUID: "2"}, + Unit: &UnitInfo{UUID: "1", Value: 21, DestinationID: "1", Consumed: 1, TOR: utils.VOICE, RateInterval: &RateInterval{Rating: &RIRate{Rates: RateGroups{&Rate{GroupIntervalStart: 0, Value: 100, RateIncrement: 10 * time.Second, RateUnit: time.Second}}}}}, + Monetary: &MonetaryInfo{UUID: "2", Value: 90}, AccountID: "3"}, }, }, @@ -1554,11 +1554,11 @@ func TestTSIncrementsCompressDecompress(t *testing.T) { } tss.Compress() if len(tss[0].Increments) != 3 { - t.Error("Error compressing timespan: ", tss[0].Increments) + t.Error("Error compressing timespan: ", utils.ToIJSON(tss[0])) } tss.Decompress() if len(tss[0].Increments) != 5 { - t.Error("Error decompressing timespans: ", tss[0].Increments) + t.Error("Error decompressing timespans: ", utils.ToIJSON(tss[0])) } } diff --git a/engine/tp_reader.go b/engine/tp_reader.go index 82ac4d14f..bf662a335 100644 --- a/engine/tp_reader.go +++ b/engine/tp_reader.go @@ -8,8 +8,8 @@ import ( "strings" "time" + "github.com/cgrates/cgrates/structmatcher" "github.com/cgrates/cgrates/utils" - "github.com/cgrates/structmatcher" ) type TpReader struct { diff --git a/glide.lock b/glide.lock index 0c3abf76f..fa156bc51 100644 --- a/glide.lock +++ b/glide.lock @@ -13,8 +13,6 @@ imports: version: 3d6beed663452471dec3ca194137a30d379d9e8f - name: github.com/cgrates/rpcclient version: 79661b1e514823a9ac93b2b9e97e037ee190ba47 -- name: github.com/cgrates/structmatcher - version: 98feee0bab15ce165540fe5f0fa006db2e9f898c - name: github.com/DisposaBoy/JsonConfigReader version: 33a99fdf1d5ee1f79b5077e9c06f955ad356d5f4 - name: github.com/fiorix/go-diameter diff --git a/glide.yaml b/glide.yaml index 7b78d485f..fa8001b97 100644 --- a/glide.yaml +++ b/glide.yaml @@ -6,7 +6,6 @@ import: - package: github.com/cgrates/kamevapi - package: github.com/cgrates/osipsdagram - package: github.com/cgrates/rpcclient -- package: github.com/cgrates/structmatcher - package: github.com/fiorix/go-diameter subpackages: - diam diff --git a/structmatcher/.travis.yml b/structmatcher/.travis.yml new file mode 100644 index 000000000..d2e57b3a9 --- /dev/null +++ b/structmatcher/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - 1.5 + +branches: + only: master diff --git a/structmatcher/README.md b/structmatcher/README.md new file mode 100644 index 000000000..6220a301d --- /dev/null +++ b/structmatcher/README.md @@ -0,0 +1,27 @@ +# structmatcher +Query language for matching structures + +[![Build Status](https://secure.travis-ci.org/cgrates/structmatcher.png)](http://travis-ci.org/cgrates/structmatcher) + +The StructMatcher type will parse a condition string and match it against a given structure. + +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 +- *has: receives a list of elements and checks that the elements are present in the specified field (StringMap type) + +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}. diff --git a/structmatcher/structmatcher.go b/structmatcher/structmatcher.go new file mode 100644 index 000000000..419e23095 --- /dev/null +++ b/structmatcher/structmatcher.go @@ -0,0 +1,327 @@ +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 +- *has: receives a list of elements and checks that the elements are present in the specified field (StringMap type) + +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" + CondHAS = "*has" + CondRSR = "*rsr" +) + +func NewErrInvalidArgument(arg interface{}) 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 interface{}) (bool, error){ + CondEQ: func(field, value interface{}) (bool, error) { + return value == field, nil + }, + CondGT: func(field, value interface{}) (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 interface{}) (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 interface{}) (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 interface{}) (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 interface{}) (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 interface{}) (bool, error) { + var strMap StringMap + var ok bool + if strMap, ok = field.(StringMap); !ok { + return false, NewErrInvalidArgument(field) + } + var strSlice []interface{} + if strSlice, ok = value.([]interface{}); !ok { + return false, NewErrInvalidArgument(value) + } + for _, str := range strSlice { + if !strMap[str.(string)] { + return false, nil + } + } + return true, nil + }, + CondRSR: func(field, value interface{}) (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(interface{}) (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 interface{}) (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: + accumulator := true + for _, cond := range os.slice { + check, err := cond.checkStruct(o) + if err != nil { + return false, err + } + accumulator = accumulator && check + } + 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 interface{}) (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 interface{} +} + +func (ov *operatorValue) checkStruct(o interface{}) (bool, error) { + if f, ok := operatorMap[ov.operator]; ok { + return f(o, ov.value) + } + return false, nil +} + +type keyValue struct { + key string + value interface{} +} + +func (kv *keyValue) checkStruct(o interface{}) (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 interface{}) (bool, error) { + return true, nil +} + +func isOperator(s string) bool { + return strings.HasPrefix(s, "*") +} + +func notEmpty(x interface{}) 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]interface{}, parentElement compositeElement) (element, error) { + for key, value := range a { + var currentElement element + switch t := value.(type) { + case []interface{}: + if key == CondHAS { + currentElement = &operatorValue{operator: key, value: t} + } else { + currentElement = &operatorSlice{operator: key} + for _, e := range t { + sm.load(e.(map[string]interface{}), currentElement.(compositeElement)) + } + } + case map[string]interface{}: + currentElement = &keyStruct{key: key} + //log.Print("map: ", t) + sm.load(t, currentElement.(compositeElement)) + case interface{}: + 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]interface{}) + 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 interface{}) (bool, error) { + if sm.rootElement == nil { + return false, ErrParserError + } + return sm.rootElement.checkStruct(o) +} diff --git a/structmatcher/structmatcher_test.go b/structmatcher/structmatcher_test.go new file mode 100644 index 000000000..e221d1272 --- /dev/null +++ b/structmatcher/structmatcher_test.go @@ -0,0 +1,429 @@ +package structmatcher + +import ( + "encoding/json" + "strings" + "testing" + "time" +) + +func toJSON(v interface{}) string { + b, _ := json.MarshalIndent(v, "", " ") + return string(b) +} + +func TestStructMatcher(t *testing.T) { + cl := &StructMatcher{} + err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + + err = cl.Parse(`{"*has":["NAT","RET","EUR"]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + err = cl.Parse(`{"Field":7, "Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + err = cl.Parse(``) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } +} + +func TestStructMatcherKeyValue(t *testing.T) { + o := struct { + Test string + Field float64 + Other bool + ExpDate time.Time + }{ + Test: "test", + Field: 6.0, + Other: true, + ExpDate: time.Date(2016, 1, 19, 20, 47, 0, 0, time.UTC), + } + cl := &StructMatcher{} + err := cl.Parse(`{"Test":"test"}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6, "Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":7, "Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6, "Other":false}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true, "Field":{"*gt":5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true, "Field":{"*gt":7}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(``) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"ExpDate":{"*exp":true}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"ExpDate":{"*exp":false}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"*and":[{"Field":{"*gte":50}},{"Test":{"*eq":"test"}}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"WrongFieldName":{"*eq":1}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err == nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherKeyValuePointer(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"Test":"test"}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherOperatorValue(t *testing.T) { + root := &operatorValue{operator: CondGT, value: 3.4} + if check, err := root.checkStruct(3.5); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(root)) + } + root = &operatorValue{operator: CondEQ, value: 3.4} + if check, err := root.checkStruct(3.5); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(root)) + } + root = &operatorValue{operator: CondEQ, value: 3.4} + if check, err := root.checkStruct(3.4); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(root)) + } + root = &operatorValue{operator: CondEQ, value: "zinc"} + if check, err := root.checkStruct("zinc"); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(root)) + } + root = &operatorValue{operator: CondHAS, value: []interface{}{"NAT", "RET", "EUR"}} + if check, err := root.checkStruct(StringMap{"WOR": true, "EUR": true, "NAT": true, "RET": true, "ROM": true}); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, toJSON(root)) + } +} + +func TestStructMatcherKeyStruct(t *testing.T) { + o := struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"Field":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*gte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lt": 7}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*eq": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*eq": "test"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherKeyStructPointer(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"Field":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*gte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lt": 7}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*eq": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*eq": "test"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherOperatorSlice(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"*or":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"*or":[{"Test":"test"},{"Field":{"*gt":7}},{"Other":false}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":7}},{"Other":false}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherMixed(t *testing.T) { + o := &struct { + Test string + Field float64 + Categories StringMap + Other bool + }{ + Test: "test", + Field: 6.0, + Categories: StringMap{"call": true, "data": true, "voice": true}, + Other: true, + } + cl := &StructMatcher{} + err := cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true},{"Categories":{"*has":["data", "call"]}}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherBalanceType(t *testing.T) { + type Balance struct { + Value float64 + } + + o := &struct { + BalanceType string + Balance + }{ + BalanceType: "*monetary", + Balance: Balance{Value: 10}, + } + cl := &StructMatcher{} + err := cl.Parse(`{"BalanceType":"*monetary","Value":10}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, toJSON(cl.rootElement)) + } +} + +func TestStructMatcherRSR(t *testing.T) { + o := &struct { + BalanceType string + }{ + BalanceType: "*monetary", + } + cl := &StructMatcher{} + err := cl.Parse(`{"BalanceType":{"*rsr":"^*mon"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, toJSON(cl.rootElement)) + } + err = cl.Parse(`{"BalanceType":{"*rsr":"^*min"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", toJSON(cl.rootElement), err) + } + if check, err := cl.Match(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, toJSON(cl.rootElement)) + } +}