diff --git a/README.md b/README.md index 0d9c4be..524af2f 100644 --- a/README.md +++ b/README.md @@ -65,58 +65,88 @@ First we start off with loading the `TupleDescriptor`. It accepts a JSON string Next create a `RuleSession` and add all the `Rule`s with their `Condition`s and `Actions`s. //Create a RuleSession - rs, _ := ruleapi.GetOrCreateRuleSession("asession") + rs, _ := ruleapi.GetOrCreateRuleSession("asession", "") //// check for name "Bob" in n1 rule := ruleapi.NewRule("n1.name == Bob") - rule.AddCondition("c1", []string{"n1"}, checkForBob, nil) - rule.SetAction(checkForBobAction) - rule.SetContext("This is a test of context") + rule.AddCondition("c1", []string{"n1"}, checkForBob, events) + serviceCfg := &config.ServiceDescriptor{ + Name: "checkForBobAction", + Function: checkForBobAction, + Type: "function", + } + aService,_ := ruleapi.NewActionService(serviceCfg) + rule.SetActionService(aService) + rule.SetContext(events) rs.AddRule(rule) - fmt.Printf("Rule added: [%s]\n", rule.GetName()) // check for name "Bob" in n1, match the "name" field in n2, // in effect, fire the rule when name field in both tuples is "Bob" rule2 := ruleapi.NewRule("n1.name == Bob && n1.name == n2.name") - rule2.AddCondition("c1", []string{"n1"}, checkForBob, nil) - rule2.AddCondition("c2", []string{"n1", "n2"}, checkSameNamesCondition, nil) - rule2.SetAction(checkSameNamesAction) + rule2.AddCondition("c1", []string{"n1"}, checkForBob, events) + rule2.AddCondition("c2", []string{"n1", "n2"}, checkSameNamesCondition, events) + serviceCfg2 := &config.ServiceDescriptor{ + Name: "checkSameNamesAction", + Function: checkSameNamesAction, + Type: "function", + } + aService2,_ := ruleapi.NewActionService(serviceCfg2) + rule2.SetActionService(aService2) + rule2.SetContext(events) rs.AddRule(rule2) - fmt.Printf("Rule added: [%s]\n", rule2.GetName()) - - //Finally, start the rule session before asserting tuples - //Your startup function, if registered will be invoked here + + // check for name in n1, match the env variable "name" + rule3 := ruleapi.NewRule("n1.name == envname") + rule3.AddCondition("c1", []string{"n1"}, checkSameEnvName, events) + serviceCfg3 := &config.ServiceDescriptor{ + Name: "checkSameEnvNameAction", + Function: checkSameEnvNameAction, + Type: "function", + } + aService3,_ := ruleapi.NewActionService(serviceCfg3) + rule3.SetActionService(aService3) + rule3.SetContext(events) + rs.AddRule(rule3) + + //set a transaction handler + rs.RegisterRtcTransactionHandler(txHandler, nil) + //Start the rule session rs.Start(nil) Here we create and assert the actual `Tuple's` which will be evaluated against the `Rule's` `Condition's` defined above. //Now assert a "n1" tuple - fmt.Println("Asserting n1 tuple with name=Tom") t1, _ := model.NewTupleWithKeyValues("n1", "Tom") t1.SetString(nil, "name", "Tom") rs.Assert(nil, t1) //Now assert a "n1" tuple - fmt.Println("Asserting n1 tuple with name=Bob") t2, _ := model.NewTupleWithKeyValues("n1", "Bob") t2.SetString(nil, "name", "Bob") rs.Assert(nil, t2) //Now assert a "n2" tuple - fmt.Println("Asserting n2 tuple with name=Bob") t3, _ := model.NewTupleWithKeyValues("n2", "Bob") t3.SetString(nil, "name", "Bob") rs.Assert(nil, t3) + //Now assert a "n1" tuple + t4, _ := model.NewTupleWithKeyValues("n1", "Smith") + t4.SetString(nil, "name", "Smith") + rs.Assert(nil, t4) + Finally, once all `Rule` `Condition's` are evaluated and `Action's` are executed, we can `Retract` all the `Tuple's` from the `RuleSession` and unregister the RuleSession. //Retract tuples rs.Retract(nil, t1) rs.Retract(nil, t2) rs.Retract(nil, t3) + rs.Retract(nil, t4) //delete the rule rs.DeleteRule(rule.GetName()) + rs.DeleteRule(rule2.GetName()) + rs.DeleteRule(rule3.GetName()) //unregister the session, i.e; cleanup rs.Unregister() diff --git a/activity/dtable/README.md b/activity/dtable/README.md new file mode 100644 index 0000000..f2b40b8 --- /dev/null +++ b/activity/dtable/README.md @@ -0,0 +1,46 @@ +# Decision Table +This is a `flogo activity` based `Decision Table` implementation used inside a rules application. It can be invoked as rule action service. `Decision Table` provide a tabular way to build complex business rules. Each column can be created based on predefined properties. These properties are defined in the `tuple descriptor` used inside a rule application. Each row can be thought of as one rule in a table made up of many rules. The individual rules are often straightforward, as in the following example. + +Rule conditions: +``` +person.age > 30 +person.gender == "male" +``` +Rule actions: +``` +application.status = "ACCEPTED" +application.credit = 4000 +``` +A decision table can consist of hundreds, even thousands of rules each of which is executed only when its specific conditions are satisfied. + +## Usage + +The available activity `settings` are as follows: + +| Name | Type | Description | +|:-----------|:--------|:--------------| +| dTableFile | string | Decision table file path (xlsx & csv extensions are supported) | + +A sample `decision table` definition is: +```json + { + "name": "ApplicantSimple", + "description": "Simple Applicants approval dt", + "type": "activity", + "ref": "github.com/project-flogo/rules/activity/dtable", + "settings": { + "dTableFile":"creditcard-file.xlsx" + } + } + +``` + +An example rule action service that invokes the above `ApplicantSimple` decision table is: +```json + "actionService": { + "service": "ApplicantSimple", + "input": { + "message": "test ApplicantSimple" + } + } +``` \ No newline at end of file diff --git a/activity/dtable/activity.go b/activity/dtable/activity.go new file mode 100644 index 0000000..4ae1ac1 --- /dev/null +++ b/activity/dtable/activity.go @@ -0,0 +1,64 @@ +package dtable + +import ( + "context" + + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data/metadata" + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/ruleapi" +) + +func init() { + _ = activity.Register(&Activity{}, New) +} + +// Activity decision table based rule action +type Activity struct { + dtable ruleapi.DecisionTable +} + +// New creates new decision table activity +func New(ctx activity.InitContext) (activity.Activity, error) { + // Read settings + settings := &Settings{} + err := metadata.MapToStruct(ctx.Settings(), settings, true) + if err != nil { + return nil, err + } + + // Read decision table from file + dtable, err := ruleapi.LoadDecisionTableFromFile(settings.DTableFile) + if err != nil { + return nil, err + } + // dtable.print() + err = dtable.Compile() + if err != nil { + return nil, err + } + // dtable.print() + + // Read setting from init context + act := &Activity{ + dtable: dtable, + } + return act, nil +} + +// Metadata activity metadata +func (a *Activity) Metadata() *activity.Metadata { + return activity.ToMetadata(&Input{}) +} + +// Eval implements decision table action +func (a *Activity) Eval(ctx activity.Context) (done bool, err error) { + + context := ctx.GetInput("ctx").(context.Context) + tuples := ctx.GetInput("tuples").(map[model.TupleType]model.Tuple) + + // evaluate decision table + a.dtable.Apply(context, tuples) + + return true, nil +} diff --git a/activity/dtable/activity_test.go b/activity/dtable/activity_test.go new file mode 100644 index 0000000..b293a0a --- /dev/null +++ b/activity/dtable/activity_test.go @@ -0,0 +1,179 @@ +package dtable + +import ( + "context" + "testing" + + "github.com/project-flogo/core/data/resolve" + "github.com/project-flogo/core/support/test" + "github.com/project-flogo/rules/common/model" + "github.com/stretchr/testify/assert" + + "github.com/project-flogo/core/data/mapper" +) + +const tupleDescriptor = `[ + { + "name":"applicant", + "properties":[ + { + "name":"name", + "pk-index":0, + "type":"string" + }, + { + "name":"gender", + "type":"string" + }, + { + "name":"age", + "type":"int" + }, + { + "name":"address", + "type":"string" + }, + { + "name":"hasDL", + "type":"bool" + }, + { + "name":"ssn", + "type":"long" + }, + { + "name":"income", + "type":"double" + }, + { + "name":"maritalStatus", + "type":"string" + }, + { + "name":"creditScore", + "type":"int" + }, + { + "name":"status", + "type":"string" + }, + { + "name":"eligible", + "type":"bool" + }, + { + "name":"creditLimit", + "type":"double" + } + ] + }, + { + "name":"processapplication", + "ttl":0, + "properties":[ + { + "name":"ssn", + "pk-index":0, + "type":"long" + }, + { + "name":"start", + "type":"bool" + } + ] + } +]` + +var testApplicants = []map[string]interface{}{ + { + "name": "JohnDoe", + "gender": "Male", + "age": 20, + "address": "BoltonUK", + "hasDL": true, + "ssn": "1231231234", + "income": 45000, + "maritalStatus": "single", + "creditScore": 500, + }, + { + "name": "JaneDoe", + "gender": "Female", + "age": 38, + "address": "BoltonUK", + "hasDL": false, + "ssn": "2424354532", + "income": 32000, + "maritalStatus": "single", + "creditScore": 650, + }, + { + "name": "PrakashY", + "gender": "Male", + "age": 30, + "address": "RedwoodShore", + "hasDL": true, + "ssn": "2345342132", + "income": 150000, + "maritalStatus": "married", + "creditScore": 750, + }, + { + "name": "SandraW", + "gender": "Female", + "age": 26, + "address": "RedwoodShore", + "hasDL": true, + "ssn": "3213214321", + "income": 50000, + "maritalStatus": "single", + "creditScore": 625, + }, +} + +func TestNew(t *testing.T) { + err := model.RegisterTupleDescriptors(string(tupleDescriptor)) + + settings := &Settings{ + DTableFile: "test_dtable.csv", + } + mf := mapper.NewFactory(resolve.GetBasicResolver()) + initCtx := test.NewActivityInitContext(settings, mf) + act, err := New(initCtx) + assert.Nil(t, err) + assert.NotNil(t, act) +} + +func TestEval(t *testing.T) { + err := model.RegisterTupleDescriptors(string(tupleDescriptor)) + + settings := &Settings{ + DTableFile: "test_dtable.xlsx", + } + mf := mapper.NewFactory(resolve.GetBasicResolver()) + initCtx := test.NewActivityInitContext(settings, mf) + act, err := New(initCtx) + assert.Nil(t, err) + assert.NotNil(t, act) + + tuples := make(map[model.TupleType]model.Tuple) + tuple, err := model.NewTuple(model.TupleType("applicant"), testApplicants[0]) + assert.Nil(t, err) + assert.NotNil(t, tuple) + tuples[tuple.GetTupleType()] = tuple + + tc := test.NewActivityContext(act.Metadata()) + tc.SetInput("ctx", context.Background()) + tc.SetInput("tuples", tuples) + act.Eval(tc) + + expectedStatus, err := tuple.GetString("status") + assert.Nil(t, err) + assert.Equal(t, "VISA-Granted", expectedStatus) + expectedEligible, err := tuple.GetBool("eligible") + assert.Nil(t, err) + assert.Equal(t, true, expectedEligible) + expectedCreditLimit, err := tuple.GetDouble("creditLimit") + assert.Nil(t, err) + assert.Equal(t, 2500.0, expectedCreditLimit) +} diff --git a/activity/dtable/metadata.go b/activity/dtable/metadata.go new file mode 100644 index 0000000..fb5c4af --- /dev/null +++ b/activity/dtable/metadata.go @@ -0,0 +1,28 @@ +package dtable + +import ( + "github.com/project-flogo/core/data/coerce" +) + +// Input activity input +type Input struct { + TBDMessage string `md:"message"` +} + +// ToMap converts Input struct to map +func (i *Input) ToMap() map[string]interface{} { + return map[string]interface{}{ + "message": i.TBDMessage, + } +} + +// FromMap fills Input struct from map +func (i *Input) FromMap(values map[string]interface{}) (err error) { + i.TBDMessage, err = coerce.ToString(values["message"]) + return +} + +// Settings activity settings +type Settings struct { + DTableFile string `md:"dTableFile,required"` // Path to decision table file +} diff --git a/activity/dtable/test_dtable.csv b/activity/dtable/test_dtable.csv new file mode 100644 index 0000000..a887aee --- /dev/null +++ b/activity/dtable/test_dtable.csv @@ -0,0 +1,3 @@ +Id,Condition (1),Condition (2),Condition (3),Condition (4),Condition (5),Action (6),Action (7),Action (8),Description,Priority +,applicant.age,applicant.creditScore,applicant.hasDL,applicant.maritalStatus,applicant.income,applicant.eligible,applicant.status,applicant.creditLimit,, +1,>= 16,>= 500,== true,== 'single',> 10000,true,VISA-Granted,2500.0,, \ No newline at end of file diff --git a/activity/dtable/test_dtable.xlsx b/activity/dtable/test_dtable.xlsx new file mode 100644 index 0000000..ae8aef9 Binary files /dev/null and b/activity/dtable/test_dtable.xlsx differ diff --git a/common/model/services.go b/common/model/services.go new file mode 100644 index 0000000..793edda --- /dev/null +++ b/common/model/services.go @@ -0,0 +1,15 @@ +package model + +type Service interface { + Init() +} + +type TupleStore interface { + Service + GetTupleByKey(key TupleKey) Tuple + SaveTuple(tuple Tuple) + SaveTuples(added map[string]map[string]Tuple) + SaveModifiedTuples(modified map[string]map[string]RtcModified) + DeleteTuple(key TupleKey) + DeleteTuples(deleted map[string]map[string]Tuple) +} diff --git a/common/model/td_test.go b/common/model/td_test.go index d982a5d..9584616 100644 --- a/common/model/td_test.go +++ b/common/model/td_test.go @@ -37,7 +37,7 @@ func TestOne(t *testing.T) { } func TestTwo(t *testing.T) { - tupleDescAbsFileNm := common.GetAbsPathForResource("src/github.com/project-flogo/rules/examples/rulesapp/rulesapp.json") + tupleDescAbsFileNm := common.GetPathForResource("examples/rulesapp/rulesapp.json", "../../examples/rulesapp/rulesapp.json") tupleDescriptor := common.FileToString(tupleDescAbsFileNm) t.Logf("Loaded tuple descriptor: \n%s\n", tupleDescriptor) diff --git a/common/model/tuple.go b/common/model/tuple.go index d7e3bd3..9f0ae1b 100644 --- a/common/model/tuple.go +++ b/common/model/tuple.go @@ -23,6 +23,7 @@ type Tuple interface { GetBool(name string) (val bool, err error) //GetDateTime(name string) time.Time GetKey() TupleKey + ToMap() map[string]interface{} } //MutableTuple mutable part of the tuple @@ -210,7 +211,7 @@ func (t *tupleImpl) initTupleWithKeyValues(td *TupleDescriptor, values ...interf t.key = tk //populate the tuple key fields with the key values for _, keyProp := range td.GetKeyProps() { - t.tuples [keyProp] = tk.GetValue(keyProp) + t.tuples[keyProp] = tk.GetValue(keyProp) } return err } @@ -273,3 +274,12 @@ func (t *tupleImpl) isKeyProp(propName string) bool { } return found } + +func (t *tupleImpl) ToMap() map[string]interface{} { + m := make(map[string]interface{}) + // Copy from the original map to the target map + for key, value := range t.tuples { + m[key] = value + } + return m +} diff --git a/common/model/tuplekey.go b/common/model/tuplekey.go index 996e2e3..568dd92 100644 --- a/common/model/tuplekey.go +++ b/common/model/tuplekey.go @@ -3,6 +3,8 @@ package model import ( "fmt" "reflect" + "strings" + "github.com/project-flogo/core/data/coerce" ) @@ -69,24 +71,38 @@ func NewTupleKeyWithKeyValues(tupleType TupleType, values ...interface{}) (tuple tk.td = *td tk.keys = make(map[string]interface{}) - if len(values) != len(td.GetKeyProps()) { - return nil, fmt.Errorf("Wrong number of key values in type [%s]. Expecting [%d], got [%d]", - td.Name, len(td.GetKeyProps()), len(values)) + switch vt := values[0].(type) { + case map[string]interface{}: + if len(vt) != len(td.GetKeyProps()) { + return nil, fmt.Errorf("Wrong number of key values in type [%s]. Expecting [%d], got [%d]", + td.Name, len(td.GetKeyProps()), len(vt)) + } + default: + if len(values) != len(td.GetKeyProps()) { + return nil, fmt.Errorf("Wrong number of key values in type [%s]. Expecting [%d], got [%d]", + td.Name, len(td.GetKeyProps()), len(values)) + } } - i := 0 - for _, keyProp := range td.GetKeyProps() { + for i, keyProp := range td.GetKeyProps() { tdp := td.GetProperty(keyProp) val := values[i] + + switch typ := val.(type) { + case map[string]interface{}: + val = typ[keyProp] + } + coerced, err := coerce.ToType(val, tdp.PropType) + if err == nil { tk.keys[keyProp] = coerced } else { return nil, fmt.Errorf("Type mismatch for field [%s] in type [%s] Expecting [%s], got [%v]", keyProp, td.Name, tdp.PropType.String(), reflect.TypeOf(val)) } - i++ } + tk.keyAsStr = tk.keysAsString() return &tk, err } @@ -118,3 +134,32 @@ func (tk *tupleKeyImpl) keysAsString() string { } return str } + +//TODO: Validations +func FromStringKey(strTupleKey string) TupleKey { + + keyComps := strings.Split(strTupleKey, ":") + + tupleType := TupleType(keyComps[0]) + + key := "" + + keyMap := make(map[string]interface{}) + + for i, v := range keyComps { + if i == 0 { + continue + } else if i%2 == 1 { + //this is a key + key = v + } else { + //this is a value + keyMap[key] = v + } + } + + tupleKey, _ := NewTupleKeyWithKeyValues(tupleType, keyMap) + + return tupleKey + +} diff --git a/common/model/types.go b/common/model/types.go index d9fb129..4b7f9eb 100644 --- a/common/model/types.go +++ b/common/model/types.go @@ -12,7 +12,7 @@ type Rule interface { GetName() string GetIdentifiers() []TupleType GetConditions() []Condition - GetActionFn() ActionFunction + GetActionService() ActionService String() string GetPriority() int GetDeps() map[TupleType]map[string]bool @@ -23,19 +23,21 @@ type Rule interface { type MutableRule interface { Rule AddCondition(conditionName string, idrs []string, cFn ConditionEvaluator, ctx RuleContext) (err error) - SetAction(actionFn ActionFunction) + SetActionService(actionService ActionService) SetPriority(priority int) SetContext(ctx RuleContext) + AddExprCondition(conditionName string, cExpr string, ctx RuleContext) error + AddIdrsToRule(idrs []TupleType) } //Condition interface to maintain/get various condition properties type Condition interface { GetName() string - GetEvaluator() ConditionEvaluator GetRule() Rule GetIdentifiers() []TupleType GetContext() RuleContext String() string + Evaluate(string, string, map[TupleType]Tuple, RuleContext) (bool, error) } // RuleSession to maintain rules and assert tuples against those rules @@ -47,7 +49,7 @@ type RuleSession interface { GetRules() []Rule Assert(ctx context.Context, tuple Tuple) (err error) - Retract(ctx context.Context, tuple Tuple) + Retract(ctx context.Context, tuple Tuple) error ScheduleAssert(ctx context.Context, delayInMillis uint64, key interface{}, tuple Tuple) CancelScheduledAssert(ctx context.Context, key interface{}) @@ -64,14 +66,16 @@ type RuleSession interface { Start(startupCtx map[string]interface{}) (err error) //return the asserted tuple, nil if not found - GetAssertedTuple(key TupleKey) Tuple + GetAssertedTuple(ctx context.Context, key TupleKey) Tuple //Retract, and remove - Delete(ctx context.Context, tuple Tuple) + Delete(ctx context.Context, tuple Tuple) error //RtcTransactionHandler RegisterRtcTransactionHandler(txnHandler RtcTransactionHandler, handlerCtx interface{}) + //SetStore + GetStore() TupleStore //replay existing tuples into a rule ReplayTuplesForRule(ruleName string) (err error) } @@ -84,6 +88,12 @@ type ConditionEvaluator func(string, string, map[TupleType]Tuple, RuleContext) b //i.e part of the server side API type ActionFunction func(context.Context, RuleSession, string, map[TupleType]Tuple, RuleContext) +// ActionService action service +type ActionService interface { + SetInput(input map[string]interface{}) + Execute(context.Context, RuleSession, string, map[TupleType]Tuple, RuleContext) (done bool, err error) +} + //StartupRSFunction is called once after creation of a RuleSession type StartupRSFunction func(ctx context.Context, rs RuleSession, sessionCtx map[string]interface{}) (err error) diff --git a/common/utils.go b/common/utils.go index 47131e8..5094a7f 100644 --- a/common/utils.go +++ b/common/utils.go @@ -57,6 +57,28 @@ func GetAbsPathForResource(resourcepath string) string { return "" } +// GetPathForResource gets the resource path off $GOPATH or relative +func GetPathForResource(resource, relative string) string { + GOPATH := os.Getenv("GOPATH") + // fmt.Printf("GOPATH - [%s]\n", GOPATH) + paths := strings.Split(GOPATH, ":") + for _, path := range paths { + // fmt.Printf("path[%s]\n", path) + absPath := path + "/src/github.com/project-flogo/rules/" + resource + _, err := os.Stat(absPath) + if err == nil { + return absPath + } + } + + _, err := os.Stat(relative) + if err == nil { + return relative + } + + return "" +} + // FileToString reads file content to string func FileToString(fileName string) string { dat, err := ioutil.ReadFile(fileName) diff --git a/config/config.go b/config/config.go index ec9429e..c6d8065 100644 --- a/config/config.go +++ b/config/config.go @@ -3,30 +3,45 @@ package config import ( "bytes" "encoding/json" + "fmt" "strconv" "github.com/project-flogo/core/data/metadata" "github.com/project-flogo/rules/common/model" ) +const ( + // TypeServiceFunction function based rule service + TypeServiceFunction = "function" + // TypeServiceActivity activity based rule service + TypeServiceActivity = "activity" + // TypeServiceAction action based rule service + TypeServiceAction = "action" + // TypeDecisionTable decision table based rule service + TypeDecisionTable = "decisiontable" +) + // RuleSessionDescriptor is a collection of rules to be loaded type RuleActionDescriptor struct { Name string `json:"name"` IOMetadata *metadata.IOMetadata `json:"metadata"` Rules []*RuleDescriptor `json:"rules"` + Services []*ServiceDescriptor `json:"services,omitempty"` } type RuleSessionDescriptor struct { - Rules []*RuleDescriptor `json:"rules"` + Rules []*RuleDescriptor `json:"rules"` + Services []*ServiceDescriptor `json:"services,omitempty"` } // RuleDescriptor defines a rule type RuleDescriptor struct { - Name string - Conditions []*ConditionDescriptor - ActionFunc model.ActionFunction - Priority int + Name string + Conditions []*ConditionDescriptor + ActionService *ActionServiceDescriptor + Priority int + Identifiers []string } // ConditionDescriptor defines a condition in a rule @@ -34,14 +49,33 @@ type ConditionDescriptor struct { Name string Identifiers []string Evaluator model.ConditionEvaluator + Expression string +} + +// ActionServiceDescriptor defines rule action service +type ActionServiceDescriptor struct { + Service string `json:"service"` + Input map[string]interface{} `json:"input,omitempty"` } +// ServiceDescriptor defines a functional target that may be invoked by a rule +type ServiceDescriptor struct { + Name string + Description string + Type string + Function model.ActionFunction + Ref string + Settings map[string]interface{} +} + +// UnmarshalJSON unmarshals JSON into struct RuleDescriptor func (c *RuleDescriptor) UnmarshalJSON(d []byte) error { ser := &struct { - Name string `json:"name"` - Conditions []*ConditionDescriptor `json:"conditions"` - ActionFuncId string `json:"actionFunction"` - Priority int `json:"priority"` + Name string `json:"name"` + Conditions []*ConditionDescriptor `json:"conditions"` + ActionService *ActionServiceDescriptor `json:"actionService,omitempty"` + Priority int `json:"priority"` + Identifiers []string `json:"identifiers"` }{} if err := json.Unmarshal(d, ser); err != nil { @@ -50,15 +84,25 @@ func (c *RuleDescriptor) UnmarshalJSON(d []byte) error { c.Name = ser.Name c.Conditions = ser.Conditions - c.ActionFunc = GetActionFunction(ser.ActionFuncId) + c.ActionService = ser.ActionService c.Priority = ser.Priority + c.Identifiers = ser.Identifiers return nil } +// MarshalJSON returns JSON encoding of RuleDescriptor func (c *RuleDescriptor) MarshalJSON() ([]byte, error) { buffer := bytes.NewBufferString("{") buffer.WriteString("\"name\":" + "\"" + c.Name + "\",") + if c.Identifiers != nil { + buffer.WriteString("\"identifiers\":[") + for _, id := range c.Identifiers { + buffer.WriteString("\"" + id + "\",") + } + buffer.Truncate(buffer.Len() - 1) + buffer.WriteString("],") + } buffer.WriteString("\"conditions\":[") for _, condition := range c.Conditions { @@ -70,18 +114,22 @@ func (c *RuleDescriptor) MarshalJSON() ([]byte, error) { buffer.Truncate(buffer.Len() - 1) buffer.WriteString("],") - actionFunctionID := GetActionFunctionID(c.ActionFunc) - buffer.WriteString("\"actionFunction\":\"" + actionFunctionID + "\",") + jsonActionService, err := json.Marshal(c.ActionService) + if err == nil { + buffer.WriteString("\"actionService\":" + string(jsonActionService) + ",") + } buffer.WriteString("\"priority\":" + strconv.Itoa(c.Priority) + "}") return buffer.Bytes(), nil } +// UnmarshalJSON unmarshals JSON into struct ConditionDescriptor func (c *ConditionDescriptor) UnmarshalJSON(d []byte) error { ser := &struct { Name string `json:"name"` Identifiers []string `json:"identifiers"` EvaluatorId string `json:"evaluator"` + Expression string `json:"expression"` }{} if err := json.Unmarshal(d, ser); err != nil { @@ -91,22 +139,79 @@ func (c *ConditionDescriptor) UnmarshalJSON(d []byte) error { c.Name = ser.Name c.Identifiers = ser.Identifiers c.Evaluator = GetConditionEvaluator(ser.EvaluatorId) + c.Expression = ser.Expression return nil } +// MarshalJSON returns JSON encoding of ConditionDescriptor func (c *ConditionDescriptor) MarshalJSON() ([]byte, error) { buffer := bytes.NewBufferString("{") buffer.WriteString("\"name\":" + "\"" + c.Name + "\",") - buffer.WriteString("\"identifiers\":[") - for _, id := range c.Identifiers { - buffer.WriteString("\"" + id + "\",") + if c.Identifiers != nil { + buffer.WriteString("\"identifiers\":[") + for _, id := range c.Identifiers { + buffer.WriteString("\"" + id + "\",") + } + buffer.Truncate(buffer.Len() - 1) + buffer.WriteString("],") } - buffer.Truncate(buffer.Len() - 1) - buffer.WriteString("],") conditionEvaluatorID := GetConditionEvaluatorID(c.Evaluator) - buffer.WriteString("\"evaluator\":\"" + conditionEvaluatorID + "\"}") + buffer.WriteString("\"evaluator\":\"" + conditionEvaluatorID + "\",") + buffer.WriteString("\"expression\":\"" + c.Expression + "\"}") + + return buffer.Bytes(), nil +} + +// UnmarshalJSON unmarshals JSON into struct ServiceDescriptor +func (sd *ServiceDescriptor) UnmarshalJSON(d []byte) error { + ser := &struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + Type string `json:"type"` + FunctionID string `json:"function,omitempty"` + Ref string `json:"ref"` + Settings map[string]interface{} `json:"settings,omitempty"` + }{} + + if err := json.Unmarshal(d, ser); err != nil { + return err + } + + sd.Name = ser.Name + sd.Description = ser.Description + if ser.Type == TypeServiceFunction || ser.Type == TypeServiceActivity || ser.Type == TypeServiceAction { + sd.Type = ser.Type + } else { + return fmt.Errorf("unsupported type - '%s' is referenced in the service '%s'", ser.Type, ser.Name) + } + if ser.FunctionID != "" { + fn := GetActionFunction(ser.FunctionID) + if fn == nil { + return fmt.Errorf("function - '%s' not found", ser.FunctionID) + } + sd.Function = fn + } + sd.Ref = ser.Ref + sd.Settings = ser.Settings + + return nil +} + +// MarshalJSON returns JSON encoding of ServiceDescriptor +func (sd *ServiceDescriptor) MarshalJSON() ([]byte, error) { + buffer := bytes.NewBufferString("{") + buffer.WriteString("\"name\":" + "\"" + sd.Name + "\",") + buffer.WriteString("\"description\":" + "\"" + sd.Description + "\",") + buffer.WriteString("\"type\":" + "\"" + sd.Type + "\",") + functionID := GetActionFunctionID(sd.Function) + buffer.WriteString("\"function\":" + "\"" + functionID + "\",") + buffer.WriteString("\"ref\":" + "\"" + sd.Ref + "\",") + jsonSettings, err := json.Marshal(sd.Settings) + if err == nil { + buffer.WriteString("\"settings\":" + string(jsonSettings) + "}") + } return buffer.Bytes(), nil } diff --git a/config/config_test.go b/config/config_test.go index 983ff3e..ea42644 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -21,7 +21,9 @@ var testRuleSessionDescriptorJson = `{ "evaluator": "checkForBob" } ], - "actionFunction": "checkForBobAction" + "actionService": { + "service": "checkForBobService" + } }, { "name": "n1.name == Bob && n1.name == n2.name", @@ -39,8 +41,24 @@ var testRuleSessionDescriptorJson = `{ "evaluator": "checkSameNamesCondition" } ], - "actionFunction": "checkSameNamesAction" + "actionService": { + "service": "checkSameNamesService" + } } + ], + "services": [ + { + "name": "checkForBobService", + "description": "service checkForBobService", + "type": "function", + "function": "checkForBobAction" + }, + { + "name": "checkSameNamesService", + "description": "service checkSameNamesService", + "type": "function", + "function": "checkSameNamesAction" + } ] } ` @@ -59,35 +77,32 @@ func TestDeserialize(t *testing.T) { assert.Nil(t, err) assert.NotNil(t, ruleSessionDescriptor.Rules) assert.Equal(t, 2, len(ruleSessionDescriptor.Rules)) + assert.Equal(t, 2, len(ruleSessionDescriptor.Services)) + // rule-0 r1Cfg := ruleSessionDescriptor.Rules[0] assert.Equal(t, "n1.name == Bob", r1Cfg.Name) assert.NotNil(t, r1Cfg.Conditions) assert.Equal(t, 1, len(r1Cfg.Conditions)) - - sf1 := reflect.ValueOf(checkForBobAction) - sf2 := reflect.ValueOf(r1Cfg.ActionFunc) - assert.Equal(t, sf1.Pointer(), sf2.Pointer()) + assert.Equal(t, "checkForBobService", r1Cfg.ActionService.Service) r1c1Cfg := r1Cfg.Conditions[0] assert.Equal(t, "c1", r1c1Cfg.Name) assert.NotNil(t, r1c1Cfg.Identifiers) assert.Equal(t, 1, len(r1c1Cfg.Identifiers)) - sf1 = reflect.ValueOf(checkForBob) - sf2 = reflect.ValueOf(r1c1Cfg.Evaluator) + sf1 := reflect.ValueOf(checkForBob) + sf2 := reflect.ValueOf(r1c1Cfg.Evaluator) assert.Equal(t, sf1.Pointer(), sf2.Pointer()) + // rule-1 r2Cfg := ruleSessionDescriptor.Rules[1] assert.Equal(t, "n1.name == Bob && n1.name == n2.name", r2Cfg.Name) assert.NotNil(t, r2Cfg.Conditions) assert.Equal(t, 2, len(r2Cfg.Conditions)) - - sf1 = reflect.ValueOf(checkSameNamesAction) - sf2 = reflect.ValueOf(r2Cfg.ActionFunc) - assert.Equal(t, sf1.Pointer(), sf2.Pointer()) + assert.Equal(t, "checkSameNamesService", r2Cfg.ActionService.Service) r2c1Cfg := r2Cfg.Conditions[0] assert.Equal(t, "c1", r2c1Cfg.Name) @@ -106,6 +121,22 @@ func TestDeserialize(t *testing.T) { sf1 = reflect.ValueOf(checkSameNamesCondition) sf2 = reflect.ValueOf(r2c2Cfg.Evaluator) assert.Equal(t, sf1.Pointer(), sf2.Pointer()) + + // service-0 + s1Cfg := ruleSessionDescriptor.Services[0] + assert.Equal(t, "checkForBobService", s1Cfg.Name) + assert.Equal(t, "service checkForBobService", s1Cfg.Description) + sf1 = reflect.ValueOf(checkForBobAction) + sf2 = reflect.ValueOf(s1Cfg.Function) + assert.Equal(t, sf1.Pointer(), sf2.Pointer()) + + // service-1 + s2Cfg := ruleSessionDescriptor.Services[1] + assert.Equal(t, "checkSameNamesService", s2Cfg.Name) + assert.Equal(t, "service checkSameNamesService", s2Cfg.Description) + sf1 = reflect.ValueOf(checkSameNamesAction) + sf2 = reflect.ValueOf(s2Cfg.Function) + assert.Equal(t, sf1.Pointer(), sf2.Pointer()) } // TEST FUNCTIONS diff --git a/config/manager.go b/config/manager.go index ccb6054..2a99ec2 100644 --- a/config/manager.go +++ b/config/manager.go @@ -43,7 +43,8 @@ func (m *ResourceManager) GetResource(id string) interface{} { func (m *ResourceManager) GetRuleSessionDescriptor(uri string) (*RuleSessionDescriptor, error) { if strings.HasPrefix(uri, uriSchemeRes) { - return &RuleSessionDescriptor{m.configs[uri[len(uriSchemeRes):]].Rules}, nil + id := uri[len(uriSchemeRes):] + return &RuleSessionDescriptor{Rules: m.configs[id].Rules, Services: m.configs[id].Services}, nil } return nil, errors.New("cannot find RuleSession: " + uri) @@ -57,26 +58,3 @@ func (m *ResourceManager) GetRuleActionDescriptor(uri string) (*RuleActionDescri return nil, errors.New("cannot find RuleSession: " + uri) } - -//ioMetadata support -/* -type ActionResource struct { - IOMetadata *metadata.IOMetadata `json:"metadata"` -} - -type ResManager struct { - IOMetadata *metadata.IOMetadata -} - -func (m *ResManager) LoadResource(resConfig *resource.Config) (*resource.Resource, error) { - - var res *ActionResource - err := json.Unmarshal(resConfig.Data, &res) - if err != nil { - return nil, fmt.Errorf("error unmarshalling metadata resource with id '%s', %s", resConfig.ID, err.Error()) - } - - m.IOMetadata = res.IOMetadata - return resource.New("ruleaction", m.IOMetadata), nil -} -*/ diff --git a/examples/dtable/README.md b/examples/dtable/README.md new file mode 100644 index 0000000..3673f66 --- /dev/null +++ b/examples/dtable/README.md @@ -0,0 +1,29 @@ +# Decision Table Usage + +This example demonstrates usage of decision table api model with student analysis example. + +The following rules are used in the example: +* `studentcare`: Gets fired when `student.careRequired` is true and invokes the `printstudentinfo` function. This function prints the Student name and comments. +* `studentanalysis`: Gets fired when `studentanalysis.name` and `student.name` are same. It invokes `dtableservice` which analyses the student and updates the `student.careRequired` and `student.comments` accordingly. + +### Pre-requisites +* Go 1.11 +* The **GOPATH** environment variable on your system must be set properly + +### Steps + +```sh +cd $GOPATH/src/github.com/project-flogo/rules/examples/dtable +go run main.go +``` + +### Testing + +s1 and s2 student tuples are Saved. se1 and se2 are events to analyse s1 and s2 students against the `dtable-file.xlsx` file respectively. +You should see following outputs in the logs: +``` +Student Name: s1 Comments: “additional study hours required” +``` +``` +Student Name: s2 Comments: “little care can be taken to achieve grade-a” +``` diff --git a/examples/dtable/dtable-file.xlsx b/examples/dtable/dtable-file.xlsx new file mode 100644 index 0000000..a9314e1 Binary files /dev/null and b/examples/dtable/dtable-file.xlsx differ diff --git a/examples/dtable/main.go b/examples/dtable/main.go new file mode 100644 index 0000000..5b230c4 --- /dev/null +++ b/examples/dtable/main.go @@ -0,0 +1,170 @@ +package main + +import ( + "context" + "fmt" + + "github.com/project-flogo/rules/common" + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" + "github.com/project-flogo/rules/ruleapi" +) + +func main() { + err := example(true) + if err != nil { + panic(err) + } +} + +func example(redis bool) error { + //Load the tuple descriptor file (relative to GOPATH) + tupleDescAbsFileNm := common.GetPathForResource("examples/dtable/rulesapp.json", "./rulesapp.json") + tupleDescriptor := common.FileToString(tupleDescAbsFileNm) + + //First register the tuple descriptors + err := model.RegisterTupleDescriptors(tupleDescriptor) + if err != nil { + return err + } + + //Create a RuleSession + store := "" + if redis { + store = "rsconfig.json" + } + rs, err := ruleapi.GetOrCreateRuleSession("asession", store) + if err != nil { + return err + } + + // student care information rule + rule1 := ruleapi.NewRule("studentcare") + err = rule1.AddExprCondition("c2", "$.student.careRequired", nil) + if err != nil { + return err + } + printService := &config.ServiceDescriptor{ + Name: "printstudentinfo", + Function: printStudentInfo, + Type: "function", + } + aService1, err := ruleapi.NewActionService(printService) + if err != nil { + return err + } + rule1.SetActionService(aService1) + rule1.SetPriority(2) + + err = rs.AddRule(rule1) + if err != nil { + return err + } + + // student analysis rule + rule2 := ruleapi.NewRule("studentanalysis") + err = rule2.AddExprCondition("c2", "$.studentanalysis.name == $.student.name", nil) + if err != nil { + return err + } + + settings := make(map[string]interface{}) + settings["filename"] = "dtable-file.xlsx" + + dtableService := &config.ServiceDescriptor{ + Name: "dtableservice", + Type: "decisiontable", + Settings: settings, + } + aService2, err := ruleapi.NewActionService(dtableService) + if err != nil { + return err + } + rule2.SetActionService(aService2) + rule2.SetPriority(1) + + err = rs.AddRule(rule2) + if err != nil { + return err + } + + //set a transaction handler + rs.RegisterRtcTransactionHandler(txHandler, nil) + //Start the rule session + err = rs.Start(nil) + if err != nil { + return err + } + + // assert student info + s1, err := model.NewTupleWithKeyValues("student", "s1") + if err != nil { + return err + } + s1.SetString(nil, "grade", "GRADE-C") + s1.SetString(nil, "class", "X-A") + s1.SetBool(nil, "careRequired", false) + err = rs.Assert(nil, s1) + if err != nil { + return err + } + + // assert another student info + s2, err := model.NewTupleWithKeyValues("student", "s2") + if err != nil { + return err + } + s2.SetString(nil, "grade", "GRADE-B") + s2.SetString(nil, "class", "X-A") + s2.SetBool(nil, "careRequired", false) + err = rs.Assert(nil, s2) + if err != nil { + return err + } + + // assert studentanalysis event + se1, err := model.NewTupleWithKeyValues("studentanalysis", "s1") + if err != nil { + return err + } + err = rs.Assert(nil, se1) + if err != nil { + return err + } + + // assert studentanalysis event + se2, err := model.NewTupleWithKeyValues("studentanalysis", "s2") + if err != nil { + return err + } + err = rs.Assert(nil, se2) + if err != nil { + return err + } + + //unregister the session, i.e; cleanup + rs.Unregister() + + return nil +} + +func getFileContent(filePath string) string { + absPath := common.GetAbsPathForResource(filePath) + return common.FileToString(absPath) +} + +func txHandler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + + store := rs.GetStore() + store.SaveTuples(rtxn.GetRtcAdded()) + + store.SaveModifiedTuples(rtxn.GetRtcModified()) + + store.DeleteTuples(rtxn.GetRtcDeleted()) + +} + +func printStudentInfo(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + student := tuples["student"].ToMap() + fmt.Println("Student Name: ", student["name"], " Comments: ", student["comments"]) +} diff --git a/examples/dtable/rulesapp.json b/examples/dtable/rulesapp.json new file mode 100644 index 0000000..80f8be4 --- /dev/null +++ b/examples/dtable/rulesapp.json @@ -0,0 +1,39 @@ +[ + { + "name": "student", + "properties": [ + { + "name": "name", + "pk-index": 0, + "type": "string" + }, + { + "name": "grade", + "type": "string" + }, + { + "name": "class", + "type": "string" + }, + { + "name": "careRequired", + "type": "bool" + }, + { + "name": "comments", + "type": "string" + } + ] + }, + { + "name": "studentanalysis", + "ttl":0, + "properties": [ + { + "name": "name", + "pk-index": 0, + "type": "string" + } + ] + } + ] \ No newline at end of file diff --git a/examples/flogo/creditcard/README.md b/examples/flogo/creditcard/README.md index 8486172..dee8772 100644 --- a/examples/flogo/creditcard/README.md +++ b/examples/flogo/creditcard/README.md @@ -1,60 +1,51 @@ -## Flogo Rules based Creditcard application +# Credit Card example +This example demonstrates how to use decision table activity for credit card application example. -This example demonstrates rule based processing of credit card application. In this example three tuples are used, tuples description is given below. +## Setup and build +Once you have the `flogo.json` file, you are ready to build your Flogo App +### Pre-requisites +* Go 1.11 +* Download and build the Flogo CLI 'flogo' and add it to your system PATH -* `UserAccount` tuple is always stored in network, while the other tuples `NewAccount` and `UpdateCreditScore` are removed after usage as ttl is given as 0. +### Steps +Note: Store implementation can be configured via given `rsconfig.json` file. Start redis-server and use `export STORECONFIG=` before running binary.
-## Usage - -Get the repo and in this example main.go, functions.go both are available. We can directly build and run the app or create flogo rule app and run it. - -#### Conditions - -``` -cBadUser : Check for new user input data - check if age <18 and >=45, empty address and salary less than 10k -cNewUser : Check for new user input data - check if age >=18 and <= 44, address and salary >= 10k -cUserIdMatch : Check for id match from 'UserAccount' and 'UpdateCreditScore' tuples -cUserCreditScore : Check for CreditScore >= 750 && < 820 -cUserLowCreditScore : Check for CreditScore < 750 -cUserHighCreditScore : Check for CreditScore >= 820 && <= 900 -``` -#### Actions -``` -aBadUser : Executes when age - < 18 and >=45, address empty, salary less than 10k -aNewUser : Add the newuser info to userAccount tuple -aApproveWithLowerLimit : Provides credit card application status approved with lower credit limit -aApproveWithHigherLimit : Provides credit card application status approved with higher credit limit -aUserReject : Rejects when lower Credit score provided and retracts NewAccount -``` -### Direct build and run -``` +```sh cd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/creditcard -go build -./creditcard -``` -### Create app using flogo cli -``` -cd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/creditcard -flogo create -f flogo.json creditcard -cp functions.go creditcard/src +flogo create -f flogo.json cd creditcard flogo build -./bin/creditcard +cd bin +cp ../../creditcard-file.xlsx . +./creditcard ``` -* Input new user details +### Testing +#### #1 Invoke applicant decision table + +Store aplicants information. +```sh +curl localhost:7777/test/applicant?name=JohnDoe\&gender=Male\&age=20\&address=BoltonUK\&hasDL=false\&ssn=1231231234\&income=45000\&maritalStatus=single\&creditScore=500 +curl localhost:7777/test/applicant?name=JaneDoe\&gender=Female\&age=38\&address=BoltonUK\&hasDL=false\&ssn=2424354532\&income=32000\&maritalStatus=single\&creditScore=650 +curl localhost:7777/test/applicant?name=PrakashY\&gender=Male\&age=30\&address=RedwoodShore\&hasDL=true\&ssn=2345342132\&income=150000\&maritalStatus=married\&creditScore=750 +curl localhost:7777/test/applicant?name=SandraW\&gender=Female\&age=26\&address=RedwoodShore\&hasDL=true\&ssn=3213214321\&income=50000\&maritalStatus=single\&creditScore=625 ``` -$ curl -X PUT http://localhost:7777/newaccount -H 'Content-Type: application/json' -d '{"Name":"Test","Age":"26","Income":"60100","Address":"TEt","Id":"12312","Gender":"male","maritalStatus":"single"}' -``` -* Update credit score details of the user +Send a process application event. +```sh +curl localhost:7777/test/processapplication?start=true\&ssn=1231231234 +curl localhost:7777/test/processapplication?start=true\&ssn=2345342132 +curl localhost:7777/test/processapplication?start=true\&ssn=3213214321 +curl localhost:7777/test/processapplication?start=true\&ssn=2424354532 ``` -$ curl -X PUT http://localhost:7777/credit -H 'Content-Type: application/json' -d '{"Id":12312,"creditScore":680}' +You should see following output: +``` +2019-09-24T12:54:08.674+0530 INFO [flogo.rules] - Applicant: JohnDoe -- CreditLimit: 2500 -- status: VISA-Granted +2019-09-24T12:54:08.683+0530 INFO [flogo.rules] - Applicant: PrakashY -- CreditLimit: 7500 -- status: Pending +2019-09-24T12:54:08.696+0530 INFO [flogo.rules] - Applicant: SandraW -- CreditLimit: 0 -- status: Loan-Rejected +2019-09-24T12:54:09.884+0530 INFO [flogo.rules] - Applicant: JaneDoe -- CreditLimit: 25000 -- status: Platinum-Status ``` - -* Application status will be printed on the console - \ No newline at end of file diff --git a/examples/flogo/creditcard/creditcard-file.xlsx b/examples/flogo/creditcard/creditcard-file.xlsx new file mode 100644 index 0000000..89304b1 Binary files /dev/null and b/examples/flogo/creditcard/creditcard-file.xlsx differ diff --git a/examples/flogo/creditcard/flogo.json b/examples/flogo/creditcard/flogo.json index 48126cb..cb4c1f5 100644 --- a/examples/flogo/creditcard/flogo.json +++ b/examples/flogo/creditcard/flogo.json @@ -1,13 +1,15 @@ { - "name": "cardapp", + "name": "creditcard", "type": "flogo:app", "version": "0.0.1", - "description": "Sample Flogo App", + "description": "Sample Rules App", "appModel": "1.0.0", "imports": [ "github.com/project-flogo/contrib/trigger/rest", + "github.com/project-flogo/contrib/activity/log", "github.com/project-flogo/rules/ruleaction", - "github.com/project-flogo/legacybridge" + "github.com/project-flogo/rules/activity/dtable", + "github.com/project-flogo/contrib/function/string" ], "triggers": [ { @@ -19,30 +21,15 @@ "handlers": [ { "settings": { - "method": "PUT", - "path": "/newaccount" + "method": "GET", + "path": "/test/:tupleType" }, "actions": [ { "id": "simple_rule", "input": { - "tupletype": "NewAccount", - "values": "=$.content" - } - } - ] - }, - { - "settings": { - "method": "PUT", - "path": "/credit" - }, - "actions": [ - { - "id": "simple_rule", - "input": { - "tupletype": "UpdateCreditScore", - "values": "=$.content" + "tupletype": "=$.pathParams.tupleType", + "values": "=$.queryParams" } } ] @@ -74,93 +61,57 @@ }, "rules": [ { - "name": "UserData", + "name": "Process Application with applicant simple dt", "conditions": [ { - "name": "cBadUser", - "identifiers": [ - "NewAccount" - ], - "evaluator": "cBadUser" - } - ], - "actionFunction": "aBadUser" - }, - { - "name": "NewUser", - "conditions": [ - { - "name": "cNewUser", - "identifiers": [ - "NewAccount" - ], - "evaluator": "cNewUser" - } - ], - "actionFunction": "aNewUser" - }, - { - "name": "NewUser1", - "conditions": [ - { - "name": "cUserIdMatch", - "identifiers": [ - "UpdateCreditScore", - "UserAccount" - ], - "evaluator": "cUserIdMatch" + "expression": "$.processapplication.start == true " }, { - "name": "cUserCreditScore", - "identifiers": [ - "UpdateCreditScore" - ], - "evaluator": "cUserCreditScore" + "expression": "$.processapplication.ssn == $.applicant.ssn" } ], - "actionFunction": "aApproveWithLowerLimit" + "actionService": { + "service": "ApplicantSimple", + "input": { + "message": "test ApplicantSimple" + } + }, + "priority": 1 }, { - "name": "NewUser2", + "name": "Display Information", "conditions": [ { - "name": "cUserIdMatch", - "identifiers": [ - "UpdateCreditScore", - "UserAccount" - ], - "evaluator": "cUserIdMatch" + "expression": "$.applicant.creditLimit >= 0 " }, { - "name": "cUserCreditScore", - "identifiers": [ - "UpdateCreditScore" - ], - "evaluator": "cUserHighCreditScore" + "expression": "$.applicant.status != nil " } ], - "actionFunction": "aApproveWithHigherLimit" + "actionService": { + "service": "LogInformation", + "input": { + "message": "=string.concat(\" Applicant: \",$.applicant.name, \" -- CreditLimit: \",$.applicant.creditLimit, \" -- status: \",$.applicant.status)" + } + }, + "priority": 3 + } + ], + "services": [ + { + "name": "ApplicantSimple", + "description": "Simple Applicants approval dt", + "type": "activity", + "ref": "github.com/project-flogo/rules/activity/dtable", + "settings": { + "dTableFile":"creditcard-file.xlsx" + } }, { - "name": "Rejected", - "conditions": [ - { - "name": "cUserIdMatch", - "identifiers": [ - "UpdateCreditScore", - "UserAccount" - ], - "evaluator": "cUserIdMatch" - }, - { - "name": "cUserCreditScore", - "identifiers": [ - "UpdateCreditScore" - ], - "evaluator": "cUserLowCreditScore" - } - ], - "actionFunction": "aUserReject" + "name": "LogInformation", + "description": "Logs Applicant Information", + "type": "activity", + "ref": "github.com/project-flogo/contrib/activity/log" } ] } @@ -173,100 +124,73 @@ "ruleSessionURI": "res://rulesession:simple", "tds": [ { - "name": "UserAccount", + "name": "applicant", "properties": [ { - "name": "Id", + "name": "name", "pk-index": 0, - "type": "int" - }, - { - "name": "Name", "type": "string" }, { - "name": "Gender", - "type": "string" - }, - { - "name": "Age", - "type": "int" - }, - { - "name": "Address", + "name": "gender", "type": "string" }, { - "name": "Income", + "name": "age", "type": "int" }, { - "name": "maritalStatus", + "name": "address", "type": "string" }, { - "name": "creditScore", - "type": "int" + "name": "hasDL", + "type": "bool" }, { - "name": "approvedLimit", - "type": "int" - }, - { - "name": "appStatus", - "type": "string" - } - ] - }, - { - "name": "NewAccount", - "ttl": 0, - "properties": [ - { - "name": "Id", - "pk-index": 0, - "type": "int" + "name": "ssn", + "type": "long" }, { - "name": "Name", - "type": "string" + "name": "income", + "type": "double" }, { - "name": "Gender", + "name": "maritalStatus", "type": "string" }, { - "name": "Age", + "name": "creditScore", "type": "int" }, { - "name": "Address", + "name": "status", "type": "string" }, { - "name": "Income", - "type": "int" + "name": "eligible", + "type": "bool" }, { - "name": "maritalStatus", - "type": "string" + "name": "creditLimit", + "type": "double" } ] }, { - "name": "UpdateCreditScore", + "name": "processapplication", + "ttl":0, "properties": [ { - "name": "Id", + "name": "ssn", "pk-index": 0, - "type": "int" + "type": "long" }, { - "name": "creditScore", - "type": "int" + "name": "start", + "type": "bool" } - ], - "ttl": 0 + ] } ] }, diff --git a/examples/flogo/creditcard/functions.go b/examples/flogo/creditcard/functions.go deleted file mode 100644 index 0587fb6..0000000 --- a/examples/flogo/creditcard/functions.go +++ /dev/null @@ -1,177 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "github.com/project-flogo/rules/config" - - "github.com/project-flogo/rules/common/model" -) - -//add this sample file to your flogo project -func init() { - - // rule UserData - config.RegisterConditionEvaluator("cNewUser", cNewUser) - config.RegisterActionFunction("aNewUser", aNewUser) - - // rule NewUser - config.RegisterConditionEvaluator("cBadUser", cBadUser) - config.RegisterActionFunction("aBadUser", aBadUser) - - // rule NewUserApprove - config.RegisterConditionEvaluator("cUserIdMatch", cUserIdMatch) - config.RegisterConditionEvaluator("cUserCreditScore", cUserCreditScore) - config.RegisterActionFunction("aApproveWithLowerLimit", aApproveWithLowerLimit) - - // // rule NewUserReject - config.RegisterConditionEvaluator("cUserIdMatch", cUserIdMatch) - config.RegisterConditionEvaluator("cUserLowCreditScore", cUserLowCreditScore) - config.RegisterActionFunction("aUserReject", aUserReject) - - // // rule NewUserApprove1 - config.RegisterConditionEvaluator("cUserIdMatch", cUserIdMatch) - config.RegisterConditionEvaluator("cUserHighCreditScore", cUserHighCreditScore) - config.RegisterActionFunction("aApproveWithHigherLimit", aApproveWithHigherLimit) -} - -func cNewUser(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { - newaccount := tuples["NewAccount"] - if newaccount != nil { - address, _ := newaccount.GetString("Address") - age, _ := newaccount.GetInt("Age") - income, _ := newaccount.GetInt("Income") - if address != "" && age >= 18 && income >= 10000 && age <= 44 { - return true - } - } - return false -} - -func aNewUser(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - fmt.Println("Rule fired:", ruleName) - newaccount := tuples["NewAccount"] - Id, _ := newaccount.GetInt("Id") - name, _ := newaccount.GetString("Name") - address, _ := newaccount.GetString("Address") - age, _ := newaccount.GetInt("Age") - income, _ := newaccount.GetInt("Income") - userInfo, _ := model.NewTupleWithKeyValues("UserAccount", Id) - userInfo.SetString(ctx, "Name", name) - userInfo.SetString(ctx, "Addresss", address) - userInfo.SetInt(ctx, "Age", age) - userInfo.SetInt(ctx, "Income", income) - fmt.Println(userInfo) - rs.Assert(ctx, userInfo) - fmt.Println("User information received") - -} - -func cBadUser(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { - newaccount := tuples["NewAccount"] - if newaccount != nil { - address, _ := newaccount.GetString("Address") - age, _ := newaccount.GetInt("Age") - income, _ := newaccount.GetInt("Income") - if address == "" || age < 18 || income < 10000 || age >= 45 { - return true - } - } - return false -} - -func aBadUser(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - fmt.Println("Rule fired:", ruleName) - fmt.Println("Applicant is not eligible to apply for creditcard") -} - -func cUserIdMatch(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { - userInfo := tuples["UserAccount"] - updateScore := tuples["UpdateCreditScore"] - if userInfo != nil || updateScore != nil { - userId, _ := userInfo.GetInt("Id") - newUserId, _ := updateScore.GetInt("Id") - if userId == newUserId { - fmt.Println("Userid match found") - return true - } - } - return false -} - -func cUserCreditScore(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { - updateScore := tuples["UpdateCreditScore"] - if updateScore != nil { - CreditScore, _ := updateScore.GetInt("creditScore") - if CreditScore >= 750 && CreditScore < 820 { - fmt.Println("cUserCreditScore") - return true - } - } - return false -} - -func cUserLowCreditScore(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { - updateScore := tuples["UpdateCreditScore"] - if updateScore != nil { - CreditScore, _ := updateScore.GetInt("creditScore") - if CreditScore < 750 { - fmt.Println("cUserLowCreditScore") - return true - } - } - return false -} - -func cUserHighCreditScore(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { - updateScore := tuples["UpdateCreditScore"] - if updateScore != nil { - CreditScore, _ := updateScore.GetInt("creditScore") - if CreditScore >= 820 && CreditScore <= 900 { - fmt.Println("cUserHighCreditScore") - return true - } - } - return false -} - -func aApproveWithLowerLimit(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - fmt.Println("Rule fired:", ruleName) - userInfo := tuples["UserAccount"] - updateScore := tuples["UpdateCreditScore"] - CreditScore, _ := updateScore.GetInt("creditScore") - income, _ := userInfo.GetInt("Income") - var limit = 2 * income - userInfoMutable := userInfo.(model.MutableTuple) - userInfoMutable.SetInt(ctx, "creditScore", CreditScore) - userInfoMutable.SetString(ctx, "appStatus", "Approved") - userInfoMutable.SetInt(ctx, "approvedLimit", limit) - fmt.Println(userInfo) -} - -func aApproveWithHigherLimit(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - fmt.Println("Rule fired:", ruleName) - userInfo := tuples["UserAccount"] - updateScore := tuples["UpdateCreditScore"] - CreditScore, _ := updateScore.GetInt("creditScore") - income, _ := userInfo.GetInt("Income") - var limit = 3 * income - userInfoMutable := userInfo.(model.MutableTuple) - userInfoMutable.SetInt(ctx, "creditScore", CreditScore) - userInfoMutable.SetString(ctx, "appStatus", "Approved") - userInfoMutable.SetInt(ctx, "approvedLimit", limit) - fmt.Println(userInfo) -} - -func aUserReject(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - fmt.Println("Rule fired:", ruleName) - userInfo := tuples["UserAccount"] - updateScore := tuples["UpdateCreditScore"] - CreditScore, _ := updateScore.GetInt("creditScore") - userInfoMutable := userInfo.(model.MutableTuple) - userInfoMutable.SetInt(ctx, "creditScore", CreditScore) - userInfoMutable.SetString(ctx, "appStatus", "Rejected") - userInfoMutable.SetInt(ctx, "approvedLimit", 0) - fmt.Println(userInfo) -} diff --git a/examples/flogo/creditcard/imports.go b/examples/flogo/creditcard/imports.go new file mode 100644 index 0000000..f95bf3c --- /dev/null +++ b/examples/flogo/creditcard/imports.go @@ -0,0 +1,10 @@ +package main + +import ( + _ "github.com/project-flogo/contrib/activity/log" + _ "github.com/project-flogo/contrib/function/string" + _ "github.com/project-flogo/contrib/trigger/rest" + _ "github.com/project-flogo/legacybridge" + _ "github.com/project-flogo/rules/activity/dtable" + _ "github.com/project-flogo/rules/ruleaction" +) diff --git a/examples/flogo/creditcard/main.go b/examples/flogo/creditcard/main.go index 132d1a2..234f2eb 100644 --- a/examples/flogo/creditcard/main.go +++ b/examples/flogo/creditcard/main.go @@ -9,10 +9,6 @@ import ( _ "github.com/project-flogo/core/data/expression/script" "github.com/project-flogo/core/engine" - "github.com/project-flogo/core/support/log" - - _ "github.com/project-flogo/contrib/trigger/rest" - _ "github.com/project-flogo/rules/ruleaction" ) var ( @@ -28,22 +24,25 @@ func main() { if *cpuProfile != "" { f, err := os.Create(*cpuProfile) if err != nil { - fmt.Println(fmt.Sprintf("Failed to create CPU profiling file due to error - %s", err.Error())) + fmt.Fprintf(os.Stderr, "Failed to create CPU profiling file: %v\n", err) + os.Exit(1) + } + if err = pprof.StartCPUProfile(f); err != nil { + fmt.Fprintf(os.Stderr, "Failed to start CPU profiling: %v\n", err) os.Exit(1) } - pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } cfg, err := engine.LoadAppConfig(cfgJson, cfgCompressed) if err != nil { - log.RootLogger().Errorf("Failed to create engine: %s", err.Error()) + fmt.Fprintf(os.Stderr, "Failed to create engine: %v\n", err) os.Exit(1) } e, err := engine.New(cfg) if err != nil { - log.RootLogger().Errorf("Failed to create engine: %s", err.Error()) + fmt.Fprintf(os.Stderr, "Failed to create engine: %v\n", err) os.Exit(1) } @@ -52,16 +51,16 @@ func main() { if *memProfile != "" { f, err := os.Create(*memProfile) if err != nil { - fmt.Println(fmt.Sprintf("Failed to create memory profiling file due to error - %s", err.Error())) + fmt.Fprintf(os.Stderr, "Failed to create memory profiling file: %v\n", err) os.Exit(1) } runtime.GC() // get up-to-date statistics if err := pprof.WriteHeapProfile(f); err != nil { - fmt.Println(fmt.Sprintf("Failed to write memory profiling data to file due to error - %s", err.Error())) + fmt.Fprintf(os.Stderr, "Failed to write memory profiling data: %v", err) os.Exit(1) } - f.Close() + _ = f.Close() } os.Exit(code) diff --git a/examples/flogo/creditcard/rsconfig.json b/examples/flogo/creditcard/rsconfig.json new file mode 100644 index 0000000..31cb504 --- /dev/null +++ b/examples/flogo/creditcard/rsconfig.json @@ -0,0 +1,36 @@ +{ + "mode": "consistency", + "rs": { + "prefix": "x", + "store-ref": "redis" + }, + "rete": { + "jt-ref": "redis", + "idgen-ref": "redis", + "jt":"redis" + }, + "stores": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + }, + "idgens": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + }, + "jts": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + } +} diff --git a/examples/flogo/creditcard/sanity.sh b/examples/flogo/creditcard/sanity.sh deleted file mode 100644 index 74e2bb5..0000000 --- a/examples/flogo/creditcard/sanity.sh +++ /dev/null @@ -1,72 +0,0 @@ -#!/bin/bash - -function get_test_cases { - local my_list=( testcase1 testcase2 testcase3 ) - echo "${my_list[@]}" -} - -# Test cases performs credit card application status as approved if Creditscore > 750 -function testcase1 { -pushd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/creditcard -go build -./creditcard > /tmp/testcase1.log 2>&1 & -pId=$! - -response=$(curl -X PUT http://localhost:7777/newaccount -H 'Content-Type: application/json' -d '{"Name":"Sam4","Age":"26","Income":"50100","Address":"SFO","Id":"4"}' --write-out '%{http_code}' --silent --output /dev/null) -response1=$(curl -X PUT http://localhost:7777/credit -H 'Content-Type: application/json' -d '{"Id":"4","creditScore":"850"}' --write-out '%{http_code}' --silent --output /dev/null) -kill -9 $pId - -if [ $response -eq 200 ] && [ $response1 -eq 200 ] && [[ "echo $(cat /tmp/testcase1.log)" =~ "Rule fired" ]] - then - echo "PASS" - else - echo "FAIL" -fi -cd .. -rm -rf /tmp/testcase1.log -popd -} - -# Test cases performs credit card application status rejected if Creditscore < 750 -function testcase2 { -pushd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/creditcard -go build -./creditcard > /tmp/testcase2.log 2>&1 & -pId=$! - -response=$(curl -X PUT http://localhost:7777/newaccount -H 'Content-Type: application/json' -d '{"Name":"Sam4","Age":"26","Income":"50100","Address":"SFO","Id":"5"}' --write-out '%{http_code}' --silent --output /dev/null) -response1=$(curl -X PUT http://localhost:7777/credit -H 'Content-Type: application/json' -d '{"Id":"5","creditScore":"650"}' --write-out '%{http_code}' --silent --output /dev/null) - -kill -9 $pId -if [ $response -eq 200 ] && [ $response1 -eq 200 ] && [[ "echo $(cat /tmp/testcase2.log)" =~ "c" ]] - then - echo "PASS" - else - echo "FAIL" -fi -cd .. -rm -rf /tmp/testcase2.log -popd -} - - -# Test cases performs invalid applicant when age address or income data is not matching requirements -function testcase3 { -pushd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/creditcard -go build -./creditcard > /tmp/testcase3.log 2>&1 & -pId=$! - -response=$(curl -X PUT http://localhost:7777/newaccount -H 'Content-Type: application/json' -d '{"Name":"Sam4","Age":"26","Income":"5010","Address":"SFO","Id":"6"}' --write-out '%{http_code}' --silent --output /dev/null) - -kill -9 $pId -if [ $response -eq 200 ] && [[ "echo $(cat /tmp/testcase3.log)" =~ "Applicant is not eligible to apply for creditcard" ]] - then - echo "PASS" - else - echo "FAIL" -fi -cd .. -rm -rf /tmp/testcase3.log -popd -} diff --git a/examples/flogo/dtable/README.md b/examples/flogo/dtable/README.md new file mode 100644 index 0000000..1f73076 --- /dev/null +++ b/examples/flogo/dtable/README.md @@ -0,0 +1,63 @@ +# Decision Table Usage + +This example demonstrates how to use decision table activity with student analysis example. + +## Setup and build +Once you have the `flogo.json` file, you are ready to build your Flogo App + +### Pre-requisites +* Go 1.11 +* Download and build the Flogo CLI 'flogo' and add it to your system PATH + +### Steps + +```sh +cd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/dtable +flogo create -f flogo.json +cd decisiontable +flogo build +cp ../dtable-file.xlsx . +cd bin +``` + +#### With mem store + +```sh +./decisiontable +``` + +#### With redis store + +```sh +docker run -p 6381:6379 -d redis +STORECONFIG=../../rsconfig.json ./decisiontable +``` + +#### With keydb store + +```sh +docker run -p 6381:6379 -d eqalpha/keydb +STORECONFIG=../../rsconfig.json ./decisiontable +``` + +### Testing + +#### #1 Invoke student analysis decision table + +Store student information. +```sh +curl localhost:7777/test/student?grade=GRADE-C\&name=s1\&class=X-A\&careRequired=false +curl localhost:7777/test/student?grade=GRADE-B\&name=s2\&class=X-A\&careRequired=false +``` + +Send a curl student analysis event. +```sh +curl localhost:7777/test/studentanalysis?name=s1 +curl localhost:7777/test/studentanalysis?name=s2 + +``` +You should see following output: +``` +2019-09-05T18:35:12.142+0530 INFO [flogo.rules] - Student: s1 -- Comments: additional study hours required +2019-09-05T18:35:12.142+0530 INFO [flogo.rules] - Student: s2 -- Comments: little care can be taken to achieve grade-a +``` \ No newline at end of file diff --git a/examples/flogo/dtable/dtable-file.xlsx b/examples/flogo/dtable/dtable-file.xlsx new file mode 100644 index 0000000..a9314e1 Binary files /dev/null and b/examples/flogo/dtable/dtable-file.xlsx differ diff --git a/examples/flogo/dtable/flogo.json b/examples/flogo/dtable/flogo.json new file mode 100644 index 0000000..aa5e1e2 --- /dev/null +++ b/examples/flogo/dtable/flogo.json @@ -0,0 +1,162 @@ +{ + "name": "decisiontable", + "type": "flogo:app", + "version": "0.0.1", + "description": "Sample Rules App", + "appModel": "1.0.0", + "imports": [ + "github.com/project-flogo/contrib/trigger/rest", + "github.com/project-flogo/contrib/activity/log", + "github.com/project-flogo/rules/ruleaction", + "github.com/project-flogo/rules/activity/dtable", + "github.com/project-flogo/contrib/function/string" + ], + "triggers": [ + { + "id": "receive_http_message", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "7777" + }, + "handlers": [ + { + "settings": { + "method": "GET", + "path": "/test/:tupleType" + }, + "actions": [ + { + "id": "simple_rule", + "input": { + "tupletype": "=$.pathParams.tupleType", + "values": "=$.queryParams" + } + } + ] + } + ] + } + ], + "resources": [ + { + "id": "rulesession:simple", + "data": { + "metadata": { + "input": [ + { + "name": "values", + "type": "string" + }, + { + "name": "tupletype", + "type": "string" + } + ], + "output": [ + { + "name": "outputData", + "type": "any" + } + ] + }, + "rules": [ + { + "name": "student.careRequired", + "conditions": [ + { + "expression": "$.student.careRequired" + } + ], + "actionService": { + "service": "LogCareRequiredStudents", + "input": { + "message": "=string.concat(\" Student: \",$.student.name, \" -- Comments: \",$.student.comments)" + } + }, + "priority": 2 + }, + { + "name": "student.name == studentanalysis.name", + "conditions": [ + { + "expression": "$.studentanalysis.name == $.student.name" + } + ], + "actionService": { + "service": "AnalyseStudent", + "input": { + "message": "=$.studentanalysis.name" + } + }, + "priority": 2 + } + ], + "services": [ + { + "name": "AnalyseStudent", + "description": "Analysing student data", + "type": "activity", + "ref": "github.com/project-flogo/rules/activity/dtable", + "settings": { + "dTableFile":"../dtable-file.xlsx" + } + }, + { + "name": "LogCareRequiredStudents", + "description": "Logs Care required student names", + "type": "activity", + "ref": "github.com/project-flogo/contrib/activity/log" + } + ] + } + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/rules/ruleaction", + "settings": { + "ruleSessionURI": "res://rulesession:simple", + "tds": [ + { + "name": "student", + "properties": [ + { + "name": "name", + "pk-index": 0, + "type": "string" + }, + { + "name": "grade", + "type": "string" + }, + { + "name": "class", + "type": "string" + }, + { + "name": "careRequired", + "type": "bool" + }, + { + "name": "comments", + "type": "string" + } + ] + }, + { + "name": "studentanalysis", + "ttl":0, + "properties": [ + { + "name": "name", + "pk-index": 0, + "type": "string" + } + ] + } + ] + }, + "id": "simple_rule" + } + ] +} \ No newline at end of file diff --git a/examples/flogo/dtable/imports.go b/examples/flogo/dtable/imports.go new file mode 100644 index 0000000..f95bf3c --- /dev/null +++ b/examples/flogo/dtable/imports.go @@ -0,0 +1,10 @@ +package main + +import ( + _ "github.com/project-flogo/contrib/activity/log" + _ "github.com/project-flogo/contrib/function/string" + _ "github.com/project-flogo/contrib/trigger/rest" + _ "github.com/project-flogo/legacybridge" + _ "github.com/project-flogo/rules/activity/dtable" + _ "github.com/project-flogo/rules/ruleaction" +) diff --git a/examples/flogo/dtable/keydb/keydb_test.go b/examples/flogo/dtable/keydb/keydb_test.go new file mode 100644 index 0000000..e83a880 --- /dev/null +++ b/examples/flogo/dtable/keydb/keydb_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/project-flogo/rules/examples/flogo/dtable/test" + "github.com/project-flogo/rules/ruleapi/tests" + + "github.com/stretchr/testify/assert" +) + +func TestDTableKeyDB(t *testing.T) { + command := exec.Command("docker", "run", "-p", "6381:6379", "-d", "eqalpha/keydb") + hash, err := command.Output() + if err != nil { + assert.Nil(t, err) + } + tests.Pour("6381") + + defer func() { + command := exec.Command("docker", "stop", strings.TrimSpace(string(hash))) + err := command.Run() + if err != nil { + assert.Nil(t, err) + } + command = exec.Command("docker", "rm", strings.TrimSpace(string(hash))) + err = command.Run() + if err != nil { + assert.Nil(t, err) + } + tests.Drain("6381") + }() + os.Setenv("STORECONFIG", filepath.FromSlash("../rsconfig.json")) + test.Test(t) +} diff --git a/examples/flogo/dtable/main.go b/examples/flogo/dtable/main.go new file mode 100644 index 0000000..234f2eb --- /dev/null +++ b/examples/flogo/dtable/main.go @@ -0,0 +1,67 @@ +package main + +import ( + "flag" + "fmt" + "os" + "runtime" + "runtime/pprof" + + _ "github.com/project-flogo/core/data/expression/script" + "github.com/project-flogo/core/engine" +) + +var ( + cpuProfile = flag.String("cpuprofile", "", "Writes CPU profile to the specified file") + memProfile = flag.String("memprofile", "", "Writes memory profile to the specified file") + cfgJson string + cfgCompressed bool +) + +func main() { + + flag.Parse() + if *cpuProfile != "" { + f, err := os.Create(*cpuProfile) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create CPU profiling file: %v\n", err) + os.Exit(1) + } + if err = pprof.StartCPUProfile(f); err != nil { + fmt.Fprintf(os.Stderr, "Failed to start CPU profiling: %v\n", err) + os.Exit(1) + } + defer pprof.StopCPUProfile() + } + + cfg, err := engine.LoadAppConfig(cfgJson, cfgCompressed) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create engine: %v\n", err) + os.Exit(1) + } + + e, err := engine.New(cfg) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create engine: %v\n", err) + os.Exit(1) + } + + code := engine.RunEngine(e) + + if *memProfile != "" { + f, err := os.Create(*memProfile) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed to create memory profiling file: %v\n", err) + os.Exit(1) + } + + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + fmt.Fprintf(os.Stderr, "Failed to write memory profiling data: %v", err) + os.Exit(1) + } + _ = f.Close() + } + + os.Exit(code) +} diff --git a/examples/flogo/dtable/mem/mem_test.go b/examples/flogo/dtable/mem/mem_test.go new file mode 100644 index 0000000..827da1e --- /dev/null +++ b/examples/flogo/dtable/mem/mem_test.go @@ -0,0 +1,11 @@ +package main + +import ( + "testing" + + "github.com/project-flogo/rules/examples/flogo/dtable/test" +) + +func TestDTableMem(t *testing.T) { + test.Test(t) +} diff --git a/examples/flogo/dtable/redis/redis_test.go b/examples/flogo/dtable/redis/redis_test.go new file mode 100644 index 0000000..7eff948 --- /dev/null +++ b/examples/flogo/dtable/redis/redis_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/project-flogo/rules/examples/flogo/dtable/test" + "github.com/project-flogo/rules/ruleapi/tests" + + "github.com/stretchr/testify/assert" +) + +func TestDTableRedis(t *testing.T) { + command := exec.Command("docker", "run", "-p", "6381:6379", "-d", "redis") + hash, err := command.Output() + if err != nil { + assert.Nil(t, err) + } + tests.Pour("6381") + + defer func() { + command := exec.Command("docker", "stop", strings.TrimSpace(string(hash))) + err := command.Run() + if err != nil { + assert.Nil(t, err) + } + command = exec.Command("docker", "rm", strings.TrimSpace(string(hash))) + err = command.Run() + if err != nil { + assert.Nil(t, err) + } + tests.Drain("6381") + }() + os.Setenv("STORECONFIG", filepath.FromSlash("../rsconfig.json")) + test.Test(t) +} diff --git a/examples/flogo/dtable/rsconfig.json b/examples/flogo/dtable/rsconfig.json new file mode 100644 index 0000000..7511a15 --- /dev/null +++ b/examples/flogo/dtable/rsconfig.json @@ -0,0 +1,36 @@ +{ + "mode": "consistency", + "rs": { + "prefix": "x", + "store-ref": "redis" + }, + "rete": { + "jt-ref": "redis", + "idgen-ref": "redis", + "jt":"redis" + }, + "stores": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6381" + } + }, + "idgens": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6381" + } + }, + "jts": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6381" + } + } +} diff --git a/examples/flogo/dtable/test/imports.go b/examples/flogo/dtable/test/imports.go new file mode 100644 index 0000000..95890fc --- /dev/null +++ b/examples/flogo/dtable/test/imports.go @@ -0,0 +1,10 @@ +package test + +import ( + _ "github.com/project-flogo/contrib/activity/log" + _ "github.com/project-flogo/contrib/function/string" + _ "github.com/project-flogo/contrib/trigger/rest" + _ "github.com/project-flogo/legacybridge" + _ "github.com/project-flogo/rules/activity/dtable" + _ "github.com/project-flogo/rules/ruleaction" +) diff --git a/examples/flogo/dtable/test/testing.go b/examples/flogo/dtable/test/testing.go new file mode 100644 index 0000000..a7ead00 --- /dev/null +++ b/examples/flogo/dtable/test/testing.go @@ -0,0 +1,64 @@ +package test + +import ( + "io/ioutil" + "net/http" + "path/filepath" + "testing" + + _ "github.com/project-flogo/core/data/expression/script" + "github.com/project-flogo/core/engine" + "github.com/project-flogo/rules/ruleapi/tests" + + "github.com/stretchr/testify/assert" +) + +func Test(t *testing.T) { + data, err := ioutil.ReadFile(filepath.FromSlash("../flogo.json")) + assert.Nil(t, err) + cfg, err := engine.LoadAppConfig(string(data), false) + assert.Nil(t, err) + e, err := engine.New(cfg) + assert.Nil(t, err) + tests.Drain("7777") + err = e.Start() + assert.Nil(t, err) + defer func() { + err := e.Stop() + assert.Nil(t, err) + }() + tests.Pour("7777") + + client := &http.Client{} + request := func() { + response, err := client.Get("http://localhost:7777/test/student?grade=GRADE-C&name=s1&class=X-A&careRequired=false") + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + } + output := tests.CaptureStdOutput(request) + assert.Contains(t, output, "Saving tuple.") + + request = func() { + response, err := client.Get("http://localhost:7777/test/student?grade=GRADE-B&name=s2&class=X-A&careRequired=false") + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + } + output = tests.CaptureStdOutput(request) + assert.Contains(t, output, "Saving tuple.") + + request = func() { + response, err := client.Get("http://localhost:7777/test/studentanalysis?name=s1") + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + } + output = tests.CaptureStdOutput(request) + assert.Contains(t, output, "Saving tuple.") + + request = func() { + response, err := client.Get("http://localhost:7777/test/studentanalysis?name=s2") + assert.Nil(t, err) + assert.Equal(t, http.StatusOK, response.StatusCode) + } + output = tests.CaptureStdOutput(request) + assert.Contains(t, output, "Saving tuple.") +} diff --git a/examples/flogo/invokeservice/README.md b/examples/flogo/invokeservice/README.md new file mode 100644 index 0000000..a921ca6 --- /dev/null +++ b/examples/flogo/invokeservice/README.md @@ -0,0 +1,62 @@ +# Invoke service when rule fires + +This example demonstrates how a rule can invoke rule `service`. A rule `service` can be a `go function` or a `flogo action` or a `flogo activity`. + +## Setup and build +Once you have the `flogo.json` file and a `functions.go` file, you are ready to build your Flogo App + +### Pre-requisites +* Go 1.11 +* Download and build the Flogo CLI 'flogo' and add it to your system PATH + +### Steps + +```sh +cd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/invokeservice +flogo create -f flogo.json +cp functions.go ./invokeservice/src +cd invokeservice +flogo build +cd bin +./invokeservice +``` +### Testing + +#### #1 Invoke function based service + +Send a curl request +`curl http://localhost:7777/test/n1?name=function` +You should see following output: +``` +Rule[n1.name == function] fired. serviceFunctionAction() function got invoked. +``` + +#### #2 Invoke flogo async action (Ex: flow action) based service + +Send a curl request +`curl http://localhost:7777/test/n1?name=asyncaction` +You should see following output: +``` +2019-08-20T09:57:46.780+0530 INFO [flogo.activity.log] - asyncaction +2019-08-20T09:57:46.781+0530 INFO [flogo.rules] - service[ServiceFlowAction] outputs: map[] + +2019-08-20T09:57:46.781+0530 INFO [flogo.flow] - Instance [39470b3be53593aa827043a05086504f] Done +2019-08-20T09:57:46.781+0530 INFO [flogo.rules] - service[ServiceFlowAction] executed successfully asynchronously +``` + +#### #3 Invoke flogo sync action based service +Send a curl request +`curl http://localhost:7777/test/n1?name=syncaction` +You should see following output: +``` +2019-08-20T09:58:21.090+0530 INFO [flogo] - Input: syncaction +2019-08-20T09:58:21.090+0530 INFO [flogo.rules] - service[ServiceCoreAction] executed successfully. Service outputs: map[anOutput:syncaction] +``` + +#### #4 Invoke activity based service +Send a curl request +`curl http://localhost:7777/test/n1?name=activity` +You should see following output: +``` +2019-08-19T20:11:11.068+0530 INFO [flogo.test] - activity +``` \ No newline at end of file diff --git a/examples/flogo/invokeservice/flogo.json b/examples/flogo/invokeservice/flogo.json new file mode 100644 index 0000000..3129393 --- /dev/null +++ b/examples/flogo/invokeservice/flogo.json @@ -0,0 +1,230 @@ +{ + "name": "invokeservice", + "type": "flogo:app", + "version": "0.0.1", + "description": "Sample Flogo App", + "appModel": "1.0.0", + "imports": [ + "github.com/project-flogo/contrib/trigger/rest", + "github.com/project-flogo/core/examples/action", + "github.com/project-flogo/rules/ruleaction", + "github.com/project-flogo/contrib/activity/log", + "github.com/project-flogo/flow" + ], + "triggers": [ + { + "id": "receive_http_message", + "ref": "github.com/project-flogo/contrib/trigger/rest", + "settings": { + "port": "7777" + }, + "handlers": [ + { + "settings": { + "method": "GET", + "path": "/test/n1" + }, + "actions": [ + { + "id": "simple_rule", + "input": { + "tupletype": "n1", + "values": "=$.queryParams" + } + } + ] + } + ] + } + ], + "resources": [ + { + "id": "rulesession:simple", + "data": { + "metadata": { + "input": [ + { + "name": "values", + "type": "string" + }, + { + "name": "tupletype", + "type": "string" + } + ], + "output": [ + { + "name": "outputData", + "type": "any" + } + ] + }, + "rules": [ + { + "name": "n1.name == function", + "conditions": [ + { + "name": "c1", + "identifiers": [ + "n1" + ], + "evaluator": "cServiceFunction" + } + ], + "actionService": { + "service": "ServiceFunction", + "input": { + "message": "=$.n1.name" + } + } + }, + { + "name": "n1.name == activity", + "conditions": [ + { + "name": "c1", + "identifiers": [ + "n1" + ], + "evaluator": "cServiceActivity" + } + ], + "actionService": { + "service": "ServiceLogActivity", + "input": { + "message": "=$.n1.name" + } + } + }, + { + "name": "n1.name == syncaction", + "conditions": [ + { + "name": "c1", + "identifiers": [ + "n1" + ], + "evaluator": "cServiceSyncAction" + } + ], + "actionService": { + "service": "ServiceCoreAction", + "input": { + "anInput": "=$.n1.name" + } + } + }, + { + "name": "n1.name == asyncaction", + "conditions": [ + { + "name": "c1", + "identifiers": [ + "n1" + ], + "evaluator": "cServiceASyncAction" + } + ], + "actionService": { + "service": "ServiceFlowAction", + "input": { + "message": "=$.n1.name" + } + } + } + ], + "services": [ + { + "name": "ServiceFunction", + "description": "function service", + "type": "function", + "function": "serviceFunctionAction" + }, + { + "name": "ServiceLogActivity", + "description": "log activity service", + "type": "activity", + "ref": "github.com/project-flogo/contrib/activity/log" + }, + { + "name": "ServiceCoreAction", + "description": "synchronous action service", + "type": "action", + "ref": "github.com/project-flogo/core/examples/action", + "settings": { + "aSetting": "test setting" + } + }, + { + "name": "ServiceFlowAction", + "description": "Asynchronous flow action service", + "type": "action", + "ref": "github.com/project-flogo/flow", + "settings": { + "flowURI": "res://flow:sample_flow" + } + } + ] + } + }, + { + "id": "flow:sample_flow", + "data": { + "name": "SampleFlow", + "metadata": { + "input": [ + { + "name": "message", + "type": "string" + } + ] + }, + "tasks": [ + { + "id": "log_message", + "name": "Log Message", + "description": "Simple Log Activity", + "activity": { + "ref": "#log", + "input": { + "message": "=$flow.message" + } + } + } + ] + } + } + ], + "actions": [ + { + "ref": "github.com/project-flogo/rules/ruleaction", + "settings": { + "ruleSessionURI": "res://rulesession:simple", + "tds": [ + { + "name": "n1", + "properties": [ + { + "name": "name", + "pk-index": 0, + "type": "string" + } + ], + "ttl": 0 + }, + { + "name": "n2", + "properties": [ + { + "name": "name", + "pk-index": 0, + "type": "string" + } + ] + } + ] + }, + "id": "simple_rule" + } + ] +} \ No newline at end of file diff --git a/examples/flogo/invokeservice/functions.go b/examples/flogo/invokeservice/functions.go new file mode 100644 index 0000000..7459cf5 --- /dev/null +++ b/examples/flogo/invokeservice/functions.go @@ -0,0 +1,109 @@ +package main + +import ( + "context" + "fmt" + + "github.com/project-flogo/rules/config" + + "github.com/project-flogo/rules/common/model" +) + +//add this sample file to your flogo project +func init() { + config.RegisterActionFunction("serviceFunctionAction", serviceFunctionAction) + + config.RegisterConditionEvaluator("cServiceFunction", cServiceFunction) + config.RegisterConditionEvaluator("cServiceActivity", cServiceActivity) + config.RegisterConditionEvaluator("cServiceSyncAction", cServiceSyncAction) + config.RegisterConditionEvaluator("cServiceASyncAction", cServiceASyncAction) + + config.RegisterStartupRSFunction("simple", StartupRSFunction) +} + +func cServiceFunction(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + //This conditions filters on name="Tom" + t1 := tuples["n1"] + if t1 == nil { + fmt.Println("Should not get a nil tuple in FilterCondition! This is an error") + return false + } + name, _ := t1.GetString("name") + return name == "function" +} + +func cServiceActivity(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + //This conditions filters on name="Bob" + t1 := tuples["n1"] + if t1 == nil { + fmt.Println("Should not get a nil tuple in FilterCondition! This is an error") + return false + } + name, _ := t1.GetString("name") + return name == "activity" +} + +func cServiceSyncAction(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + //This conditions filters on name="Tom" + t1 := tuples["n1"] + if t1 == nil { + fmt.Println("Should not get a nil tuple in FilterCondition! This is an error") + return false + } + name, _ := t1.GetString("name") + return name == "syncaction" +} + +func cServiceASyncAction(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + //This conditions filters on name="Michael" + t1 := tuples["n1"] + if t1 == nil { + fmt.Println("Should not get a nil tuple in FilterCondition! This is an error") + return false + } + name, _ := t1.GetString("name") + return name == "asyncaction" +} + +func serviceFunctionAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + fmt.Printf("Rule[%s] fired. serviceFunctionAction() function got invoked. \n", ruleName) + t1 := tuples["n1"] + if t1 == nil { + fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") + return + } +} + +func checkSameNamesCondition(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + t1 := tuples["n1"] + t2 := tuples["n2"] + if t1 == nil || t2 == nil { + fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") + return false + } + name1, _ := t1.GetString("name") + name2, _ := t2.GetString("name") + return name1 == name2 +} + +func checkSameNamesAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + fmt.Printf("Rule fired: [%s]\n", ruleName) + t1 := tuples["n1"] + t2 := tuples["n2"] + if t1 == nil || t2 == nil { + fmt.Println("Should not get nil tuples here in Action! This is an error") + return + } + name1, _ := t1.GetString("name") + name2, _ := t2.GetString("name") + fmt.Printf("n1.name = [%s], n2.name = [%s]\n", name1, name2) +} + +func StartupRSFunction(ctx context.Context, rs model.RuleSession, startupCtx map[string]interface{}) (err error) { + + fmt.Printf("In startup rule function..\n") + t3, _ := model.NewTupleWithKeyValues("n1", "Bob") + t3.SetString(nil, "name", "Bob") + rs.Assert(nil, t3) + return nil +} diff --git a/examples/flogo/trackntrace/main.go b/examples/flogo/invokeservice/main.go similarity index 100% rename from examples/flogo/trackntrace/main.go rename to examples/flogo/invokeservice/main.go diff --git a/examples/flogo/simple-kafka/docker-compose.yml b/examples/flogo/simple-kafka/docker-compose.yml new file mode 100644 index 0000000..eacafc7 --- /dev/null +++ b/examples/flogo/simple-kafka/docker-compose.yml @@ -0,0 +1,19 @@ +version: '2' + +services: + + zookeeper: + image: wurstmeister/zookeeper:3.4.6 + expose: + - "2181" + + kafka: + image: wurstmeister/kafka:2.11-2.0.0 + depends_on: + - zookeeper + ports: + - "9092:9092" + environment: + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 \ No newline at end of file diff --git a/examples/flogo/simple-kafka/flogo.json b/examples/flogo/simple-kafka/flogo.json index 806a19a..a3c584e 100644 --- a/examples/flogo/simple-kafka/flogo.json +++ b/examples/flogo/simple-kafka/flogo.json @@ -101,7 +101,9 @@ "evaluator": "checkForGrocery" } ], - "actionFunction": "groceryAction" + "actionService": { + "service": "FunctionService" + } }, { "name": "furnitureCheckRule", @@ -114,7 +116,23 @@ "evaluator": "checkForFurniture" } ], - "actionFunction": "furnitureAction" + "actionService": { + "service": "FunctionService1" + } + } + ], + "services": [ + { + "name": "FunctionService", + "description": "function service for groceryAction", + "type": "function", + "function": "groceryAction" + }, + { + "name": "FunctionService1", + "description": "function service", + "type": "function", + "function": "furnitureAction" } ] } diff --git a/examples/flogo/simple-kafka/kakfa_test.go b/examples/flogo/simple-kafka/kakfa_test.go new file mode 100644 index 0000000..7b284c4 --- /dev/null +++ b/examples/flogo/simple-kafka/kakfa_test.go @@ -0,0 +1,110 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/Shopify/sarama" + "github.com/project-flogo/core/engine" + "github.com/project-flogo/rules/ruleapi/tests" + "github.com/stretchr/testify/assert" +) + +const ( + kafkaConn = "localhost:9092" + topic = "orderinfo" +) + +func TestMain(m *testing.M) { + sarama.Logger = log.New(os.Stdout, "", log.Ltime) + os.Exit(m.Run()) +} + +func initProducer() (sarama.SyncProducer, error) { + + // producer config + config := sarama.NewConfig() + config.Producer.Retry.Max = 5 + config.Producer.RequiredAcks = sarama.WaitForAll + config.Producer.Return.Successes = true + + // sync producer + prd, err := sarama.NewSyncProducer([]string{kafkaConn}, config) + + return prd, err +} + +func publish(message string, producer sarama.SyncProducer) { + // publish sync + msg := &sarama.ProducerMessage{ + Topic: topic, + Value: sarama.StringEncoder(message), + } + p, o, err := producer.SendMessage(msg) + if err != nil { + fmt.Println("Error publish: ", err.Error()) + } + + fmt.Println("Partition: ", p) + fmt.Println("Offset: ", o) +} + +func testApplication(t *testing.T, e engine.Engine) { + err := e.Start() + assert.Nil(t, err) + defer func() { + err := e.Stop() + assert.Nil(t, err) + tests.Command("docker-compose", "down") + }() + tests.Pour("9092") + + producer, err := initProducer() + if err != nil { + fmt.Println("Error producer: ", err.Error()) + os.Exit(1) + } + + request := func() { + publish(`{"type":"grocery","totalPrice":"2001.0"}`, producer) + } + outpt := tests.CaptureStdOutput(request) + + var result string + if strings.Contains(outpt, "Rule fired") { + result = "success" + } + assert.Equal(t, "success", result) + outpt = "" + result = "" + +} + +func TestSimpleKafkaJSON(t *testing.T) { + + if testing.Short() { + t.Skip("skipping simpleKafkaJSON test") + } + + _, err := exec.LookPath("docker-compose") + if err != nil { + t.Skip("skipping test - docker-compose not found") + } + + data, err := ioutil.ReadFile(filepath.FromSlash("./flogo.json")) + assert.Nil(t, err) + tests.Command("docker-compose", "up", "-d") + time.Sleep(50 * time.Second) + cfg, err := engine.LoadAppConfig(string(data), false) + assert.Nil(t, err) + e, err := engine.New(cfg) + assert.Nil(t, err) + testApplication(t, e) +} diff --git a/examples/flogo/simple/flogo.json b/examples/flogo/simple/flogo.json index 41e8ca1..83031ac 100644 --- a/examples/flogo/simple/flogo.json +++ b/examples/flogo/simple/flogo.json @@ -4,6 +4,13 @@ "version": "0.0.1", "description": "Sample Flogo App", "appModel": "1.0.0", + "properties": [ + { + "name": "name", + "type": "string", + "value": "testprop" + } + ], "triggers": [ { "id": "receive_http_message", @@ -12,32 +19,17 @@ "port": "7777" }, "handlers": [ - { - "settings": { - "method": "GET", - "path": "/test/n1" - }, - "actions": [ - { - "id": "simple_rule", - "input": { - "tupletype": "n1", - "values": "=$.queryParams" - } - } - ] - }, { "settings": { "method": "GET", - "path": "/test/n2" + "path": "/test/:tupleType" }, "actions": [ { "id": "simple_rule", "input": { - "tupletype": "n2", - "values": "=$.queryParams" + "tupletype": "=$.pathParams.tupleType", + "values": "=$.queryParams" } } ] @@ -92,10 +84,10 @@ } ], "output": [ - { - "name": "outputData", - "type": "any" - } + { + "name": "outputData", + "type": "any" + } ] }, "rules": [ @@ -103,35 +95,80 @@ "name": "n1.name == Bob", "conditions": [ { - "name": "c1", - "identifiers": [ - "n1" - ], - "evaluator": "checkForBob" + "expression" : "$.n1.name == 'Bob'" } ], - "actionFunction": "checkForBobAction" + "actionService": { + "service": "FunctionService" + } }, { "name": "n1.name == Bob \u0026\u0026 n1.name == n2.name", "conditions": [ { - "name": "c1", "identifiers": [ "n1" ], "evaluator": "checkForBob" }, { - "name": "c2", - "identifiers": [ - "n1", - "n2" - ], - "evaluator": "checkSameNamesCondition" + "expression" : "($.n1.name == 'Bob') \u0026\u0026 ($.n1.name == $.n2.name)" } ], - "actionFunction": "checkSameNamesAction" + "actionService": { + "service": "FunctionService1" + } + }, + { + "name": "env variable example", + "conditions": [ + { + "expression" : "($.n1.name == $env['name'])" + } + ], + "actionService": { + "service": "FunctionService2" + } + }, + { + "name": "flogo property example", + "identifiers": [ + "n1" + ], + "conditions": [ + { + "expression" : "('testprop' == $property['name'])" + } + ], + "actionService": { + "service": "FunctionService3" + } + } + ], + "services": [ + { + "name": "FunctionService", + "description": "function service for checkForBobAction", + "type": "function", + "function": "checkForBobAction" + }, + { + "name": "FunctionService1", + "description": "function service for checkSameNamesAction", + "type": "function", + "function": "checkSameNamesAction" + }, + { + "name": "FunctionService2", + "description": "function service for envVarExampleAction", + "type": "function", + "function": "envVarExampleAction" + }, + { + "name": "FunctionService3", + "description": "function service for propertyExampleAction", + "type": "function", + "function": "propertyExampleAction" } ] } diff --git a/examples/flogo/simple/functions.go b/examples/flogo/simple/functions.go index ffc838c..e2e4792 100644 --- a/examples/flogo/simple/functions.go +++ b/examples/flogo/simple/functions.go @@ -12,6 +12,8 @@ import ( func init() { config.RegisterActionFunction("checkForBobAction", checkForBobAction) config.RegisterActionFunction("checkSameNamesAction", checkSameNamesAction) + config.RegisterActionFunction("envVarExampleAction", envVarExampleAction) + config.RegisterActionFunction("propertyExampleAction", propertyExampleAction) config.RegisterConditionEvaluator("checkForBob", checkForBob) config.RegisterConditionEvaluator("checkSameNamesCondition", checkSameNamesCondition) @@ -34,7 +36,7 @@ func checkForBobAction(ctx context.Context, rs model.RuleSession, ruleName strin fmt.Printf("Context is [%s]\n", ruleCtx) t1 := tuples["n1"] if t1 == nil { - fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") + fmt.Println("Should not get nil tuples here in JoinCondition3! This is an error") return } } @@ -43,7 +45,7 @@ func checkSameNamesCondition(ruleName string, condName string, tuples map[model. t1 := tuples["n1"] t2 := tuples["n2"] if t1 == nil || t2 == nil { - fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") + fmt.Println("Should not get nil tuples here in JoinCondition4! This is an error") return false } name1, _ := t1.GetString("name") @@ -72,3 +74,26 @@ func StartupRSFunction(ctx context.Context, rs model.RuleSession, startupCtx map rs.Assert(nil, t3) return nil } + +func envVarExampleAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + fmt.Printf("Rule fired: [%s]\n", ruleName) + t1 := tuples["n1"] + if t1 == nil { + fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") + return + } else { + nm, _ := t1.GetString("name") + fmt.Printf("n1.name is [%s]\n", nm) + } +} +func propertyExampleAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + fmt.Printf("Rule fired: [%s]\n", ruleName) + t1 := tuples["n1"] + if t1 == nil { + fmt.Println("Should not get nil tuples here ! This is an error") + return + } else { + nm, _ := t1.GetString("name") + fmt.Printf("n1.name is [%s]\n", nm) + } +} diff --git a/examples/flogo/simple/sanity.sh b/examples/flogo/simple/sanity.sh deleted file mode 100755 index b43527e..0000000 --- a/examples/flogo/simple/sanity.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/bin/bash - -function get_test_cases { - local my_list=( testcase1 ) - echo "${my_list[@]}" -} - -# This Testcase creates flogo rules binary and checks for name bob -function testcase1 { -pushd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/simple -flogo create -f flogo.json -cp functions.go simplerules/src -cd simplerules -flogo build -./bin/simplerules > /tmp/testcase1.log 2>&1 & -pId=$! - -response=$(curl --request GET localhost:7777/test/n1?name=Bob --write-out '%{http_code}' --silent --output /dev/null) -response1=$(curl --request GET localhost:7777/test/n2?name=Bob --write-out '%{http_code}' --silent --output /dev/null) - -kill -9 $pId -if [ $response -eq 200 ] && [ $response1 -eq 200 ] && [[ "echo $(cat /tmp/testcase1.log)" =~ "Rule fired" ]] - then - echo "PASS" - else - echo "FAIL" -fi -cd .. -rm -rf simplerules -popd -} \ No newline at end of file diff --git a/examples/flogo/simple/simple_test.go b/examples/flogo/simple/simple_test.go new file mode 100644 index 0000000..0367358 --- /dev/null +++ b/examples/flogo/simple/simple_test.go @@ -0,0 +1,123 @@ +package main + +import ( + "io/ioutil" + "net/http" + "path/filepath" + "strings" + "testing" + + "github.com/project-flogo/core/engine" + "github.com/project-flogo/rules/ruleapi/tests" + "github.com/stretchr/testify/assert" +) + +func testApplication(t *testing.T, e engine.Engine) { + tests.Drain("7777") + err := e.Start() + assert.Nil(t, err) + defer func() { + err := e.Stop() + assert.Nil(t, err) + }() + tests.Pour("7777") + + transport := &http.Transport{ + MaxIdleConns: 1, + } + defer transport.CloseIdleConnections() + client := &http.Client{ + Transport: transport, + } + + // check for samename condition + request := func() { + req, err := http.NewRequest(http.MethodGet, "http://localhost:7777/test/n1?name=Bob", nil) + assert.Nil(t, err) + response, err := client.Do(req) + assert.Nil(t, err) + _, err = ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + } + + outpt := tests.CaptureStdOutput(request) + + var result string + if strings.Contains(outpt, "Rule fired: [n1.name == Bob]") { + result = "success" + } + assert.Equal(t, "success", result) + result = "" + outpt = "" + + // check for tuples match n1 and n2 + request1 := func() { + req, err := http.NewRequest(http.MethodGet, "http://localhost:7777/test/n2?name=Bob", nil) + assert.Nil(t, err) + response, err := client.Do(req) + assert.Nil(t, err) + _, err = ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + } + + outpt = tests.CaptureStdOutput(request1) + if strings.Contains(outpt, "Rule fired: [n1.name == Bob && n1.name == n2.name]") { + result = "success" + } + assert.Equal(t, "success", result) + result = "" + outpt = "" + + // check for name mismatch + request2 := func() { + req, err := http.NewRequest(http.MethodGet, "http://localhost:7777/test/n1?name=Tom", nil) + assert.Nil(t, err) + response, err := client.Do(req) + assert.Nil(t, err) + _, err = ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + } + + outpt = tests.CaptureStdOutput(request2) + if strings.Contains(outpt, "Rule fired: [n1.name == Tom]") { + result = "fail" + } + assert.Equal(t, "", result) + result = "" + + // Already asserted tuple check + request3 := func() string { + req, err := http.NewRequest(http.MethodGet, "http://localhost:7777/test/n1?name=Bob", nil) + assert.Nil(t, err) + response, err := client.Do(req) + assert.Nil(t, err) + body, err := ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + return string(body) + } + + body := request3() + if strings.Contains(body, "already asserted") { + result = "success" + } + assert.Equal(t, "success", result) + result = "" +} + +func TestSimpleJSON(t *testing.T) { + if testing.Short() { + t.Skip("skipping Handler Routing JSON integration test in short mode") + } + + data, err := ioutil.ReadFile(filepath.FromSlash("./flogo.json")) + assert.Nil(t, err) + cfg, err := engine.LoadAppConfig(string(data), false) + assert.Nil(t, err) + e, err := engine.New(cfg) + assert.Nil(t, err) + testApplication(t, e) +} diff --git a/examples/flogo/trackntrace/README.md b/examples/flogo/statemachine/README.md similarity index 91% rename from examples/flogo/trackntrace/README.md rename to examples/flogo/statemachine/README.md index d240f9b..1eae2f4 100644 --- a/examples/flogo/trackntrace/README.md +++ b/examples/flogo/statemachine/README.md @@ -1,6 +1,6 @@ -## Flogo Rules based Track and Trace +## Flogo Rules based State Machine -This example demonstrates the capability of rules to track and trace for a flogo app. In this example three tuples are used, tuples description is given below. +This example demonstrates the capability of rules to process package states using a flogo app. In this example three tuples are used, tuples description is given below. ```json { @@ -64,7 +64,7 @@ Consider system having incoming packages. In order to move packages from source ### Package State Info

- +

In detail, above image represents state change for a given package. Consider insert package event this will insert a package into network with state as `normal`. This package now accepts only `sitting` event. When `sitting` event is triggered, a 10 seconds timer is created to trigger `delayed` event, within 10s only `moving` event can cancel the timer and state is changed to `moving` otherwise `delayed` event gets triggered. If `dropped` event occurs on a package with `moving` state, then package state is changed to `dropped`. Any package with state as `dropped` or `delayed` is retracted from network. You have to insert the package again to use. @@ -85,19 +85,19 @@ Get the repo and in this example `main.go`, `functions.go` both are available. W ### Direct build and run ```sh -cd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/trackntrace +cd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/statemachine go build -./trackntrace +./statemachine ``` ### Create app using flogo cli ```sh -cd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/trackntrace -flogo create -f flogo.json trackNTraceApp -cp functions.go trackNTraceApp/src -cd trackNTraceApp +cd $GOPATH/src/github.com/project-flogo/rules/examples/flogo/statemachine +flogo create -f flogo.json statemachine +cp functions.go statemachine/src +cd statemachine flogo build -./bin/trackNTraceApp +./bin/statemachine ``` ## Moveevent test diff --git a/examples/flogo/trackntrace/flogo.json b/examples/flogo/statemachine/flogo.json similarity index 64% rename from examples/flogo/trackntrace/flogo.json rename to examples/flogo/statemachine/flogo.json index dbaad3c..d379c21 100644 --- a/examples/flogo/trackntrace/flogo.json +++ b/examples/flogo/statemachine/flogo.json @@ -1,5 +1,5 @@ { - "name": "trackntrace", + "name": "statemachine", "type": "flogo:app", "version": "0.0.1", "appModel": "1.0.0", @@ -108,14 +108,13 @@ } ], "output": [ - { - "name": "outputData", - "type": "any" - } + { + "name": "outputData", + "type": "any" + } ] }, "rules": [ - { "name": "packageInSittingRule", "conditions": [ @@ -127,8 +126,10 @@ "evaluator": "cPackageInSitting" } ], - "actionFunction": "aPackageInSitting", - "priority":1 + "actionService": { + "service": "FunctionService" + }, + "priority": 1 }, { "name": "packageInDelayedRule", @@ -141,8 +142,10 @@ "evaluator": "cPackageInDelayed" } ], - "actionFunction": "aPackageInDelayed", - "priority":1 + "actionService": { + "service": "FunctionService1" + }, + "priority": 1 }, { "name": "packageInMovingRule", @@ -155,8 +158,10 @@ "evaluator": "cPackageInMoving" } ], - "actionFunction": "aPackageInMoving", - "priority":1 + "actionService": { + "service": "FunctionService2" + }, + "priority": 1 }, { "name": "packageInDroppedRule", @@ -169,8 +174,10 @@ "evaluator": "cPackageInDropped" } ], - "actionFunction": "aPackageInDropped", - "priority":1 + "actionService": { + "service": "FunctionService3" + }, + "priority": 1 }, { "name": "printPackageRule", @@ -183,8 +190,10 @@ "evaluator": "cPackageEvent" } ], - "actionFunction": "aPrintPackage", - "priority":2 + "actionService": { + "service": "FunctionService4" + }, + "priority": 2 }, { "name": "printMoveEventRule", @@ -197,8 +206,10 @@ "evaluator": "cMoveEvent" } ], - "actionFunction": "aPrintMoveEvent", - "priority":3 + "actionService": { + "service": "FunctionService5" + }, + "priority": 3 }, { "name": "joinMoveEventAndPackageEventRule", @@ -212,8 +223,10 @@ "evaluator": "cJoinMoveEventAndPackage" } ], - "actionFunction": "aJoinMoveEventAndPackage", - "priority":4 + "actionService": { + "service": "FunctionService6" + }, + "priority": 4 }, { "name": "aMoveTimeoutEventRule", @@ -226,8 +239,10 @@ "evaluator": "cMoveTimeoutEvent" } ], - "actionFunction": "aMoveTimeoutEvent", - "priority":5 + "actionService": { + "service": "FunctionService7" + }, + "priority": 5 }, { "name": "joinMoveTimeoutEventAndPackage", @@ -241,8 +256,66 @@ "evaluator": "cJoinMoveTimeoutEventAndPackage" } ], - "actionFunction": "aJoinMoveTimeoutEventAndPackage", - "priority":6 + "actionService": { + "service": "FunctionService8" + }, + "priority": 6 + } + ], + "services": [ + { + "name": "FunctionService", + "description": "function service for aPackageInSitting", + "type": "function", + "function": "aPackageInSitting" + }, + { + "name": "FunctionService1", + "description": "function service for aPackageInDelayed", + "type": "function", + "function": "aPackageInDelayed" + }, + { + "name": "FunctionService2", + "description": "function service for aPackageInMoving", + "type": "function", + "function": "aPackageInMoving" + }, + { + "name": "FunctionService3", + "description": "function service for aPackageInDropped", + "type": "function", + "function": "aPackageInDropped" + }, + { + "name": "FunctionService4", + "description": "function service for aPrintPackage", + "type": "function", + "function": "aPrintPackage" + }, + { + "name": "FunctionService5", + "description": "function service for aPrintMoveEvent", + "type": "function", + "function": "aPrintMoveEvent" + }, + { + "name": "FunctionService6", + "description": "function service for aJoinMoveEventAndPackage", + "type": "function", + "function": "aJoinMoveEventAndPackage" + }, + { + "name": "FunctionService7", + "description": "function service for aMoveTimeoutEvent", + "type": "function", + "function": "aMoveTimeoutEvent" + }, + { + "name": "FunctionService8", + "description": "function service for aJoinMoveTimeoutEventAndPackage", + "type": "function", + "function": "aJoinMoveTimeoutEventAndPackage" } ] } diff --git a/examples/flogo/trackntrace/functions.go b/examples/flogo/statemachine/functions.go similarity index 100% rename from examples/flogo/trackntrace/functions.go rename to examples/flogo/statemachine/functions.go diff --git a/examples/flogo/statemachine/main.go b/examples/flogo/statemachine/main.go new file mode 100644 index 0000000..b1f6c5b --- /dev/null +++ b/examples/flogo/statemachine/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "flag" + "fmt" + "os" + "runtime" + "runtime/pprof" + + _ "github.com/project-flogo/core/data/expression/script" + "github.com/project-flogo/core/engine" + "github.com/project-flogo/core/support/log" + + _ "github.com/project-flogo/contrib/trigger/rest" + _ "github.com/project-flogo/rules/ruleaction" + +) + +var ( + cpuProfile = flag.String("cpuprofile", "", "Writes CPU profile to the specified file") + memProfile = flag.String("memprofile", "", "Writes memory profile to the specified file") + cfgJson string + cfgCompressed bool +) + +func main() { + + flag.Parse() + if *cpuProfile != "" { + f, err := os.Create(*cpuProfile) + if err != nil { + fmt.Println(fmt.Sprintf("Failed to create CPU profiling file due to error - %s", err.Error())) + os.Exit(1) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + + cfg, err := engine.LoadAppConfig(cfgJson, cfgCompressed) + if err != nil { + log.RootLogger().Errorf("Failed to create engine: %s", err.Error()) + os.Exit(1) + } + + e, err := engine.New(cfg) + if err != nil { + log.RootLogger().Errorf("Failed to create engine: %s", err.Error()) + os.Exit(1) + } + + code := engine.RunEngine(e) + + if *memProfile != "" { + f, err := os.Create(*memProfile) + if err != nil { + fmt.Println(fmt.Sprintf("Failed to create memory profiling file due to error - %s", err.Error())) + os.Exit(1) + } + + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + fmt.Println(fmt.Sprintf("Failed to write memory profiling data to file due to error - %s", err.Error())) + os.Exit(1) + } + f.Close() + } + + os.Exit(code) +} diff --git a/examples/flogo/trackntrace/trackntrace.png b/examples/flogo/statemachine/statemachine.png similarity index 100% rename from examples/flogo/trackntrace/trackntrace.png rename to examples/flogo/statemachine/statemachine.png diff --git a/examples/flogo/statemachine/statemachine_test.go b/examples/flogo/statemachine/statemachine_test.go new file mode 100644 index 0000000..0218606 --- /dev/null +++ b/examples/flogo/statemachine/statemachine_test.go @@ -0,0 +1,115 @@ +package main + +import ( + "io/ioutil" + "net/http" + "path/filepath" + "strings" + "testing" + + "github.com/project-flogo/core/engine" + "github.com/project-flogo/rules/ruleapi/tests" + "github.com/stretchr/testify/assert" +) + +func testApplication(t *testing.T, e engine.Engine) { + tests.Drain("7777") + err := e.Start() + assert.Nil(t, err) + defer func() { + err := e.Stop() + assert.Nil(t, err) + }() + tests.Pour("7777") + + transport := &http.Transport{ + MaxIdleConns: 1, + } + defer transport.CloseIdleConnections() + client := &http.Client{ + Transport: transport, + } + + request := func() { + req, err := http.NewRequest(http.MethodGet, "http://localhost:7777/moveevent?packageid=PACKAGE1&targetstate=sitting", nil) + assert.Nil(t, err) + response, err := client.Do(req) + assert.Nil(t, err) + _, err = ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + } + + outpt := tests.CaptureStdOutput(request) + var result string + if strings.Contains(outpt, "target state [sitting]") { + result = "success" + } + assert.Equal(t, "success", result) + result = "" + + request1 := func() { + req, err := http.NewRequest(http.MethodGet, "http://localhost:7777/moveevent?packageid=PACKAGE1&targetstate=sitting", nil) + assert.Nil(t, err) + response, err := client.Do(req) + assert.Nil(t, err) + _, err = ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + } + + outpt = tests.CaptureStdOutput(request1) + if strings.Contains(outpt, "target state [sitting]") { + result = "success" + } + assert.Equal(t, "success", result) + result = "" + + request2 := func() { + req, err := http.NewRequest(http.MethodGet, "http://localhost:7777/moveevent?packageid=PACKAGE1&targetstate=moving", nil) + assert.Nil(t, err) + response, err := client.Do(req) + assert.Nil(t, err) + _, err = ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + } + + outpt = tests.CaptureStdOutput(request2) + if strings.Contains(outpt, "target state [moving]") { + result = "success" + } + assert.Equal(t, "success", result) + result = "" + + request3 := func() { + req, err := http.NewRequest(http.MethodGet, "http://localhost:7777/moveevent?packageid=PACKAGE2&targetstate=normal", nil) + assert.Nil(t, err) + response, err := client.Do(req) + assert.Nil(t, err) + _, err = ioutil.ReadAll(response.Body) + assert.Nil(t, err) + response.Body.Close() + } + + outpt = tests.CaptureStdOutput(request3) + if strings.Contains(outpt, "Tuple inserted successfully") { + result = "success" + } + assert.Equal(t, "success", result) + result = "" +} + +func TestStateMachineJSON(t *testing.T) { + if testing.Short() { + t.Skip("skipping Handler Routing JSON integration test in short mode") + } + + data, err := ioutil.ReadFile(filepath.FromSlash("./flogo.json")) + assert.Nil(t, err) + cfg, err := engine.LoadAppConfig(string(data), false) + assert.Nil(t, err) + e, err := engine.New(cfg) + assert.Nil(t, err) + testApplication(t, e) +} diff --git a/examples/ordermanagement/main.go b/examples/ordermanagement/main.go index a09cc25..5a6ea7b 100644 --- a/examples/ordermanagement/main.go +++ b/examples/ordermanagement/main.go @@ -15,9 +15,11 @@ import ( //go:generate go run $GOPATH/src/github.com/TIBCOSoftware/flogo-lib/flogo/gen/gen.go $GOPATH const ( - msgValueField = "message" - tupleSchemaPath = "src/github.com/project-flogo/rules/examples/ordermanagement/schema/oms_schema.json" - ruleDefinitionPath = "src/github.com/project-flogo/rules/examples/ordermanagement/schema/rule_definition.json" + msgValueField = "message" + tupleSchemaPath = "examples/ordermanagement/schema/oms_schema.json" + ruleDefinitionPath = "examples/ordermanagement/schema/rule_definition.json" + tupleSchemaRelativePath = "./schema/oms_schema.json" + ruleDefinitionRelativePath = "./schema/rule_definition.json" ) // cli arguments @@ -75,7 +77,7 @@ func main() { // loads the tuple schema func loadTupleSchema() error { - content := getFileContent(tupleSchemaPath) + content := getFileContent(tupleSchemaPath, tupleSchemaRelativePath) err := model.RegisterTupleDescriptors(string(content)) if err != nil { return err @@ -85,13 +87,13 @@ func loadTupleSchema() error { // create rulesession and load rules in it func createAndLoadRuleSession() (model.RuleSession, error) { - content := getFileContent(ruleDefinitionPath) - return ruleapi.GetOrCreateRuleSessionFromConfig("oms_session", string(content)) + content := getFileContent(ruleDefinitionPath, ruleDefinitionRelativePath) + return ruleapi.GetOrCreateRuleSessionFromConfig("oms_session", "", string(content)) } // Get file content -func getFileContent(filePath string) string { - absPath := common.GetAbsPathForResource(filePath) +func getFileContent(filePath, relative string) string { + absPath := common.GetPathForResource(filePath, relative) return common.FileToString(absPath) } diff --git a/examples/rulesapp/README.md b/examples/rulesapp/README.md new file mode 100644 index 0000000..fb1fd7c --- /dev/null +++ b/examples/rulesapp/README.md @@ -0,0 +1,31 @@ + +## Installation + +### Prerequisites +* The Go programming language version 1.8 or later should be [installed](https://golang.org/doc/install). +* The **GOPATH** environment variable on your system must be set properly +* Docker + +## Setup and Usage + +The following conditions are used in the example: + +* `checkForBob`: Checks if the `n1` tuple name is Bob +* `checkSameNamesCondition`: Checks if the name in `n1` tuple is same as name in `n2` tuple +* `checkSameEnvName`: Checks if the name in `n1` tuple matches the value stored in environment variable `name` + +The following rules are used in the example: + +* `checkForBobAction`: Gets fired when `checkForBob` is true +* `checkSameNamesAction`: Gets fired when `checkForBob` and `checkSameNamesCondition` conditions are true +* `checkSameEnvNameAction`: Gets fired when `checkSameEnvName` condition is true + +Run the example: + +``` +docker run -p 6383:6379 -d redis +go get -u github.com/project-flogo/rules/... +cd $GOPATH/src/github.com/project-flogo/rules/examples/rulesapp +export name=Smith +go run main.go +``` \ No newline at end of file diff --git a/examples/rulesapp/main.go b/examples/rulesapp/main.go index 0cfef18..338d7d1 100644 --- a/examples/rulesapp/main.go +++ b/examples/rulesapp/main.go @@ -3,123 +3,360 @@ package main import ( "context" "fmt" + "os" + "github.com/project-flogo/rules/common" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" "github.com/project-flogo/rules/ruleapi" - "github.com/project-flogo/rules/common" ) func main() { + err := example(true) + if err != nil { + panic(err) + } +} - fmt.Println("** rulesapp: Example usage of the Rules module/API **") - +func example(redis bool) error { //Load the tuple descriptor file (relative to GOPATH) - tupleDescAbsFileNm := common.GetAbsPathForResource("src/github.com/project-flogo/rules/examples/rulesapp/rulesapp.json") + tupleDescAbsFileNm := common.GetPathForResource("examples/rulesapp/rulesapp.json", "./rulesapp.json") tupleDescriptor := common.FileToString(tupleDescAbsFileNm) - fmt.Printf("Loaded tuple descriptor: \n%s\n", tupleDescriptor) //First register the tuple descriptors err := model.RegisterTupleDescriptors(tupleDescriptor) if err != nil { - fmt.Printf("Error [%s]\n", err) - return + return err } //Create a RuleSession - rs, _ := ruleapi.GetOrCreateRuleSession("asession") + store := "" + if redis { + store = "rsconfig.json" + } + rs, err := ruleapi.GetOrCreateRuleSession("asession", store) + if err != nil { + return err + } + events := make(map[string]int, 8) //// check for name "Bob" in n1 rule := ruleapi.NewRule("n1.name == Bob") - rule.AddCondition("c1", []string{"n1"}, checkForBob, nil) - rule.SetAction(checkForBobAction) - rule.SetContext("This is a test of context") - rs.AddRule(rule) - fmt.Printf("Rule added: [%s]\n", rule.GetName()) + err = rule.AddCondition("c1", []string{"n1"}, checkForBob, events) + if err != nil { + return err + } + serviceCfg := &config.ServiceDescriptor{ + Name: "checkForBobAction", + Function: checkForBobAction, + Type: "function", + } + aService, err := ruleapi.NewActionService(serviceCfg) + if err != nil { + return err + } + rule.SetActionService(aService) + rule.SetContext(events) + err = rs.AddRule(rule) + if err != nil { + return err + } // check for name "Bob" in n1, match the "name" field in n2, // in effect, fire the rule when name field in both tuples is "Bob" rule2 := ruleapi.NewRule("n1.name == Bob && n1.name == n2.name") - rule2.AddCondition("c1", []string{"n1"}, checkForBob, nil) - rule2.AddCondition("c2", []string{"n1", "n2"}, checkSameNamesCondition, nil) - rule2.SetAction(checkSameNamesAction) - rs.AddRule(rule2) - fmt.Printf("Rule added: [%s]\n", rule2.GetName()) + err = rule2.AddCondition("c1", []string{"n1"}, checkForBob, events) + if err != nil { + return err + } + err = rule2.AddCondition("c2", []string{"n1", "n2"}, checkSameNamesCondition, events) + if err != nil { + return err + } + serviceCfg2 := &config.ServiceDescriptor{ + Name: "checkSameNamesAction", + Function: checkSameNamesAction, + Type: "function", + } + aService2, err := ruleapi.NewActionService(serviceCfg2) + if err != nil { + return err + } + rule2.SetActionService(aService2) + rule2.SetContext(events) + err = rs.AddRule(rule2) + if err != nil { + return err + } + + // check for name in n1, match the env variable "name" + rule3 := ruleapi.NewRule("n1.name == envname") + err = rule3.AddCondition("c1", []string{"n1"}, checkSameEnvName, events) + if err != nil { + return err + } + serviceCfg3 := &config.ServiceDescriptor{ + Name: "checkSameEnvNameAction", + Function: checkSameEnvNameAction, + Type: "function", + } + aService3, err := ruleapi.NewActionService(serviceCfg3) + if err != nil { + return err + } + rule3.SetActionService(aService3) + rule3.SetContext(events) + err = rs.AddRule(rule3) + if err != nil { + return err + } + //set a transaction handler + rs.RegisterRtcTransactionHandler(txHandler, nil) //Start the rule session - rs.Start(nil) + err = rs.Start(nil) + if err != nil { + return err + } //Now assert a "n1" tuple - fmt.Println("Asserting n1 tuple with name=Tom") - t1, _ := model.NewTupleWithKeyValues("n1", "Tom") + t1, err := model.NewTupleWithKeyValues("n1", "Tom") + if err != nil { + return err + } t1.SetString(nil, "name", "Tom") - rs.Assert(nil, t1) + err = rs.Assert(nil, t1) + if err != nil { + return err + } + t11 := rs.GetStore().GetTupleByKey(t1.GetKey()) + if t11 == nil { + return fmt.Errorf("Warn: Tuple should be in store[%s]", t11.GetKey()) + } //Now assert a "n1" tuple - fmt.Println("Asserting n1 tuple with name=Bob") - t2, _ := model.NewTupleWithKeyValues("n1", "Bob") + t2, err := model.NewTupleWithKeyValues("n1", "Bob") + if err != nil { + return err + } t2.SetString(nil, "name", "Bob") - rs.Assert(nil, t2) + err = rs.Assert(nil, t2) + if err != nil { + return err + } //Now assert a "n2" tuple - fmt.Println("Asserting n2 tuple with name=Bob") - t3, _ := model.NewTupleWithKeyValues("n2", "Bob") + t3, err := model.NewTupleWithKeyValues("n2", "Bob") + if err != nil { + return err + } t3.SetString(nil, "name", "Bob") - rs.Assert(nil, t3) + err = rs.Assert(nil, t3) + if err != nil { + return err + } + + //Now assert a "n1" tuple + t4, err := model.NewTupleWithKeyValues("n1", "Smith") + if err != nil { + return err + } + t4.SetString(nil, "name", "Smith") + err = rs.Assert(nil, t4) + if err != nil { + return err + } //Retract tuples - rs.Retract(nil, t1) - rs.Retract(nil, t2) - rs.Retract(nil, t3) + err = rs.Retract(nil, t1) + if err != nil { + return err + } + err = rs.Retract(nil, t2) + if err != nil { + return err + } + err = rs.Retract(nil, t3) + if err != nil { + return err + } + err = rs.Retract(nil, t4) + if err != nil { + return err + } //delete the rule - rs.DeleteRule(rule.GetName()) + rs.DeleteRule(rule2.GetName()) //unregister the session, i.e; cleanup rs.Unregister() + + if events["checkForBob"] != 6 { + return fmt.Errorf("checkForBob should have been called 6 times") + } + if events["checkForBobAction"] != 1 { + return fmt.Errorf("checkForBobAction should have been called once") + } + if events["checkSameNamesCondition"] != 1 { + return fmt.Errorf("checkSameNamesCondition should have been called once") + } + if events["checkSameNamesAction"] != 1 { + return fmt.Errorf("checkSameNamesAction should have been called once") + } + if events["checkSameEnvName"] != 3 { + return fmt.Errorf("checkSameEnvName should have been called thrice") + } + if events["checkSameEnvNameAction"] != 1 { + return fmt.Errorf("checkSameEnvNameAction should have been called once") + } + + return nil } func checkForBob(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { //This conditions filters on name="Bob" + //fmt.Println("checkForBob") t1 := tuples["n1"] if t1 == nil { - fmt.Println("Should not get a nil tuple in FilterCondition! This is an error") return false } - name, _ := t1.GetString("name") + name, err := t1.GetString("name") + if err != nil { + return false + } + if name == "" { + return false + } + events := ctx.(map[string]int) + count := events["checkForBob"] + events["checkForBob"] = count + 1 return name == "Bob" } func checkForBobAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - fmt.Printf("Rule fired: [%s]\n", ruleName) - fmt.Printf("Context is [%s]\n", ruleCtx) + //fmt.Println("checkForBobAction") t1 := tuples["n1"] if t1 == nil { - fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") return } + name, err := t1.GetString("name") + if err != nil { + return + } + if name == "" { + return + } + fmt.Println("Rule checkForBobAction is fired") + events := ruleCtx.(map[string]int) + count := events["checkForBobAction"] + events["checkForBobAction"] = count + 1 } func checkSameNamesCondition(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + //fmt.Println("checkSameNamesCondition") t1 := tuples["n1"] t2 := tuples["n2"] if t1 == nil || t2 == nil { - fmt.Println("Should not get nil tuples here in JoinCondition! This is an error") return false } - name1, _ := t1.GetString("name") - name2, _ := t2.GetString("name") + name1, err := t1.GetString("name") + if err != nil { + return false + } + if name1 == "" { + return false + } + name2, err := t2.GetString("name") + if err != nil { + return false + } + if name2 == "" { + return false + } + events := ctx.(map[string]int) + count := events["checkSameNamesCondition"] + events["checkSameNamesCondition"] = count + 1 return name1 == name2 } func checkSameNamesAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - fmt.Printf("Rule fired: [%s]\n", ruleName) + //fmt.Println("checkSameNamesAction") t1 := tuples["n1"] t2 := tuples["n2"] if t1 == nil || t2 == nil { - fmt.Println("Should not get nil tuples here in Action! This is an error") return } - name1, _ := t1.GetString("name") - name2, _ := t2.GetString("name") - fmt.Printf("n1.name = [%s], n2.name = [%s]\n", name1, name2) + name1, err := t1.GetString("name") + if err != nil { + return + } + if name1 == "" { + return + } + name2, err := t2.GetString("name") + if err != nil { + return + } + if name2 == "" { + return + } + fmt.Println("Rule checkSameNamesAction is fired") + events := ruleCtx.(map[string]int) + count := events["checkSameNamesAction"] + events["checkSameNamesAction"] = count + 1 +} + +func checkSameEnvName(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + //fmt.Println("checkSameEnvName") + t1 := tuples["n1"] + if t1 == nil { + return false + } + name, err := t1.GetString("name") + if err != nil { + return false + } + if name == "" { + return false + } + name1 := os.Getenv("name") + events := ctx.(map[string]int) + count := events["checkSameEnvName"] + events["checkSameEnvName"] = count + 1 + return name == name1 +} + +func checkSameEnvNameAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + + t1 := tuples["n1"] + if t1 == nil { + return + } + name, err := t1.GetString("name") + if err != nil { + return + } + if name == "" { + return + } + fmt.Println("Rule checkSameEnvNameAction is fired") + events := ruleCtx.(map[string]int) + count := events["checkSameEnvNameAction"] + events["checkSameEnvNameAction"] = count + 1 +} + +func getFileContent(filePath string) string { + absPath := common.GetAbsPathForResource(filePath) + return common.FileToString(absPath) +} + +func txHandler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + + store := rs.GetStore() + store.SaveTuples(rtxn.GetRtcAdded()) + + store.SaveModifiedTuples(rtxn.GetRtcModified()) + + store.DeleteTuples(rtxn.GetRtcDeleted()) + } diff --git a/examples/rulesapp/rsconfig.json b/examples/rulesapp/rsconfig.json new file mode 100644 index 0000000..fae556a --- /dev/null +++ b/examples/rulesapp/rsconfig.json @@ -0,0 +1,36 @@ +{ + "mode": "consistency", + "rs": { + "prefix": "x", + "store-ref": "redis" + }, + "rete": { + "jt-ref": "redis", + "idgen-ref": "redis", + "jt":"redis" + }, + "stores": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6383" + } + }, + "idgens": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6383" + } + }, + "jts": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6383" + } + } +} diff --git a/examples/rulesapp/ruleapp_test.go b/examples/rulesapp/ruleapp_test.go new file mode 100644 index 0000000..633a283 --- /dev/null +++ b/examples/rulesapp/ruleapp_test.go @@ -0,0 +1,53 @@ +package main + +import ( + "os" + "os/exec" + "strings" + "testing" + + "github.com/project-flogo/rules/ruleapi/tests" + "github.com/stretchr/testify/assert" +) + +var redis = false + +func TestMain(m *testing.M) { + code := m.Run() + if code != 0 { + os.Exit(code) + } + + run := func() int { + command := exec.Command("docker", "run", "-p", "6383:6379", "-d", "redis") + hash, err := command.Output() + if err != nil { + panic(err) + } + tests.Pour("6383") + + defer func() { + command := exec.Command("docker", "stop", strings.TrimSpace(string(hash))) + err := command.Run() + if err != nil { + panic(err) + } + command = exec.Command("docker", "rm", strings.TrimSpace(string(hash))) + err = command.Run() + if err != nil { + panic(err) + } + tests.Drain("6383") + }() + + return m.Run() + } + redis = true + os.Exit(run()) +} + +func TestRuleApp(t *testing.T) { + os.Setenv("name", "Smith") + err := example(redis) + assert.Nil(t, err) +} diff --git a/examples/rulesapp/sanity.sh b/examples/rulesapp/sanity.sh deleted file mode 100644 index aff0c8a..0000000 --- a/examples/rulesapp/sanity.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -function get_test_cases { - local my_list=( testcase1 ) - echo "${my_list[@]}" -} - -# This testcase checks for name bob -function testcase1 { -pushd $GOPATH/src/github.com/project-flogo/rules/examples/rulesapp -rm -rf /tmp/testcase1.log -go run main.go > /tmp/testcase1.log 2>&1 - -if [[ "echo $(cat /tmp/testcase1.log)" =~ "Rule fired" ]] - then - echo "PASS" - else - echo "FAIL" -fi -popd -} \ No newline at end of file diff --git a/examples/statemachine/README.md b/examples/statemachine/README.md new file mode 100644 index 0000000..1a186cb --- /dev/null +++ b/examples/statemachine/README.md @@ -0,0 +1,111 @@ +## Rulesapi based State Machine + +This example demonstrates the capability of rules to process package states. In this example three tuples are used, tuples description is given below. + +```json +{ + "name": "package", + "properties": [ + { + "name": "id", + "type": "string", + "pk-index": 0 + }, + { + "name": "state", + "type": "string" + } + ] +}, +{ + "name": "moveevent", + "ttl": 0, + "properties": [ + { + "name": "id", + "type": "string", + "pk-index": 0 + }, + { + "name": "packageid", + "type": "string" + }, + { + "name": "targetstate", + "type": "string" + } + ] +}, +{ + "name": "movetimeoutevent", + "ttl": 0, + "properties": [ + { + "name": "id", + "type": "string", + "pk-index": 0 + }, + { + "name": "packageid", + "type": "string" + }, + { + "name": "timeoutinmillis", + "type": "integer" + } + ] +} +``` + +`package` tuple is always stored in network, while the other tuples `moveevent` and `movetimeoutevent` are removed after usage as `ttl` is given as `0`. During startup `PACKAGE1` is asserted into network. + +Consider system having incoming packages. In order to move packages from source to destination we have steps like sitting,moving,dropped and delayed. When the first event sitting comes, package is scheduled to 10s timeout. Within 10s if we receive moving event then scheduled timeout event will get canceled. Otherwise package is marked as delayed and retracted from network. + +### Package State Info + +

+ +

+ +In detail, above image represents state change for a given package. Consider insert package event this will insert a package into network with state as `normal`. This package now accepts only `sitting` event. When `sitting` event is triggered, a 10 seconds timer is created to trigger `delayed` event, within 10s only `moving` event can cancel the timer and state is changed to `moving` otherwise `delayed` event gets triggered. If `dropped` event occurs on a package with `moving` state, then package state is changed to `dropped`. Any package with state as `dropped` or `delayed` is retracted from network. You have to insert the package again to use. + +### Actions used here + +`aJoinMoveEventAndPackage` : If targetstate value sitting then package is scheduled to movetimeoutevent by 10 seconds.
+`aPrintMoveEvent`: Prints received moveevents and store packages into network based on targetstate.
+`aMoveTimeoutEvent`: Prints received movetimeoutevent.
+`aJoinMoveTimeoutEventAndPackage`: Package is modified to moveevent target state.
+`aPackageInSitting`: Prints package as sitting.
+`aPackageInMoving`: Prints package as moving.
+`aPackageInDropped`: Prints package as dropped and retracts package tuple from network.
+`aPackageInDelayed`: Prints package as delayed and retracts package tuple from network. + +## Run the example + +The example contains `main.go` and `rulesapp.json`. To run the application: +``` +go run main.go +``` + +In the given example 2 packages PACKAGE1 and PACKAGE2 are inserted. +The PACKAGE1 is asserted into the Rulesession with `normal` state. The package is further moved to `sitting`, `moving` and `dropped` states.When the package reaches the dropped state the package is removed from the rulesession. +The PACKAGE2 is asserted into the Rulesession with `normal` state. The package is then moved to sitting state. After 10 seconds a movetimeout event is asserted and the package state is changed to `delayed`. + +Below is the final output for package PACKAGE2: + +``` +Received a 'moveevent' [PACKAGE2] target state [dropped] +Joining a 'moveevent' with packageid [PACKAGE2] to package [PACKAGE2], target state [dropped] +PACKAGE [PACKAGE2] is Dropped +Saving tuple. Type [package] Key [package:id:PACKAGE2], Val [&{package map[id:PACKAGE2 state:dropped] 0xc0000bad20 0xc0000c1720}] +``` + + +Below is the final output for package PACKAGE1: + +``` +Received a 'movetimeoutevent' id [01E4JW2J1V9AEJJ1KTTGXAAW54], packageid [PACKAGE1], timeoutinmillis [10000] +Joining a 'movetimeoutevent' [PACKAGE1] to package [PACKAGE1], timeout [10000] +PACKAGE [PACKAGE1] is Delayed +Saving tuple. Type [package] Key [package:id:PACKAGE1], Val [&{package map[id:PACKAGE1 state:delayed] 0xc0000baa20 0xc0000c0c80}] +``` \ No newline at end of file diff --git a/examples/statemachine/main.go b/examples/statemachine/main.go new file mode 100644 index 0000000..737f9c4 --- /dev/null +++ b/examples/statemachine/main.go @@ -0,0 +1,598 @@ +package main + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/project-flogo/rules/common" + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" + "github.com/project-flogo/rules/ruleapi" +) + +func main() { + err := example(false) + if err != nil { + panic(err) + } +} + +var ( + currentEventType string +) + +func example(redis bool) error { + //Load the tuple descriptor file (relative to GOPATH) + tupleDescAbsFileNm := common.GetPathForResource("examples/statemachine/rulesapp.json", "./rulesapp.json") + tupleDescriptor := common.FileToString(tupleDescAbsFileNm) + currentEventType = "none" + //First register the tuple descriptors + err := model.RegisterTupleDescriptors(tupleDescriptor) + if err != nil { + return err + } + + //Create a RuleSession + store := "" + if redis { + store = "rsconfig.json" + } + rs, err := ruleapi.GetOrCreateRuleSession("asession", store) + if err != nil { + return err + } + + events := make(map[string]int, 8) + + //// check if the packaage is in sitting state + rule := ruleapi.NewRule("cPackageInSittingRule") + err = rule.AddCondition("c1", []string{"package.state"}, cPackageInSitting, events) + if err != nil { + return err + } + serviceCfg := &config.ServiceDescriptor{ + Name: "aPackageInSitting", + Function: aPackageInSitting, + Type: "function", + } + aService, err := ruleapi.NewActionService(serviceCfg) + if err != nil { + return err + } + rule.SetActionService(aService) + rule.SetContext(events) + rule.SetPriority(1) + err = rs.AddRule(rule) + if err != nil { + return err + } + + //// check if the packaage is in Delayed state + rule2 := ruleapi.NewRule("packageInDelayedRule") + err = rule2.AddCondition("c1", []string{"package.state"}, cPackageInDelayed, events) + if err != nil { + return err + } + serviceCfg2 := &config.ServiceDescriptor{ + Name: "aPackageInDelayed", + Function: aPackageInDelayed, + Type: "function", + } + aService2, err := ruleapi.NewActionService(serviceCfg2) + if err != nil { + return err + } + rule2.SetActionService(aService2) + rule2.SetContext(events) + rule2.SetPriority(1) + err = rs.AddRule(rule2) + if err != nil { + return err + } + + //// check if the packaage is in moving state + rule3 := ruleapi.NewRule("packageInMovingRule") + err = rule3.AddCondition("c1", []string{"package.state"}, cPackageInMoving, events) + if err != nil { + return err + } + serviceCfg3 := &config.ServiceDescriptor{ + Name: "aPackageInMoving", + Function: aPackageInMoving, + Type: "function", + } + aService3, err := ruleapi.NewActionService(serviceCfg3) + if err != nil { + return err + } + rule3.SetActionService(aService3) + rule3.SetContext(events) + rule3.SetPriority(1) + err = rs.AddRule(rule3) + if err != nil { + return err + } + + //// check if the packaage is in Dropped state + rule4 := ruleapi.NewRule("packageInDroppedRule") + err = rule4.AddCondition("c1", []string{"package.state"}, cPackageInDropped, events) + if err != nil { + return err + } + serviceCfg4 := &config.ServiceDescriptor{ + Name: "aPackageInDropped", + Function: aPackageInDropped, + Type: "function", + } + aService4, err := ruleapi.NewActionService(serviceCfg4) + if err != nil { + return err + } + rule4.SetActionService(aService4) + rule4.SetContext(events) + rule4.SetPriority(1) + err = rs.AddRule(rule4) + if err != nil { + return err + } + + //// check if the packaage is in normal state and print + rule5 := ruleapi.NewRule("printPackageRule") + err = rule5.AddCondition("c1", []string{"package"}, cPackageEvent, events) + if err != nil { + return err + } + serviceCfg5 := &config.ServiceDescriptor{ + Name: "aPrintPackage", + Function: aPrintPackage, + Type: "function", + } + aService5, err := ruleapi.NewActionService(serviceCfg5) + if err != nil { + return err + } + rule5.SetActionService(aService5) + rule5.SetContext(events) + rule5.SetPriority(2) + err = rs.AddRule(rule5) + if err != nil { + return err + } + + // check for moveevent + rule6 := ruleapi.NewRule("printMoveEventRule") + err = rule6.AddCondition("c1", []string{"moveevent"}, cMoveEvent, events) + if err != nil { + return err + } + serviceCfg6 := &config.ServiceDescriptor{ + Name: "aPrintMoveEvent", + Function: aPrintMoveEvent, + Type: "function", + } + aService6, err := ruleapi.NewActionService(serviceCfg6) + if err != nil { + return err + } + rule6.SetActionService(aService6) + rule6.SetContext(events) + rule6.SetPriority(3) + err = rs.AddRule(rule6) + if err != nil { + return err + } + + // check if the package exists for received moveevent and join it with the package + rule7 := ruleapi.NewRule("joinMoveEventAndPackageEventRule") + err = rule7.AddCondition("c1", []string{"moveevent", "package"}, cMoveEventPkg, events) + if err != nil { + return err + } + serviceCfg7 := &config.ServiceDescriptor{ + Name: "aJoinMoveEventAndPackage", + Function: aJoinMoveEventAndPackage, + Type: "function", + } + aService7, err := ruleapi.NewActionService(serviceCfg7) + if err != nil { + return err + } + rule7.SetActionService(aService7) + rule7.SetContext(events) + rule7.SetPriority(4) + err = rs.AddRule(rule7) + if err != nil { + return err + } + + // check for movetimeoutevent for package + rule8 := ruleapi.NewRule("aMoveTimeoutEventRule") + err = rule8.AddCondition("c1", []string{"movetimeoutevent"}, cMoveTimeoutEvent, events) + if err != nil { + return err + } + serviceCfg8 := &config.ServiceDescriptor{ + Name: "aMoveTimeoutEvent", + Function: aMoveTimeoutEvent, + Type: "function", + } + aService8, err := ruleapi.NewActionService(serviceCfg8) + if err != nil { + return err + } + rule8.SetActionService(aService8) + rule8.SetContext(events) + rule8.SetPriority(5) + err = rs.AddRule(rule8) + if err != nil { + return err + } + + //Join movetimeoutevent and package + rule9 := ruleapi.NewRule("joinMoveTimeoutEventAndPackage") + err = rule9.AddCondition("c1", []string{"movetimeoutevent", "package"}, cMoveTimeoutEventPkg, events) + if err != nil { + return err + } + serviceCfg9 := &config.ServiceDescriptor{ + Name: "aJoinMoveTimeoutEventAndPackage", + Function: aJoinMoveTimeoutEventAndPackage, + Type: "function", + } + aService9, err := ruleapi.NewActionService(serviceCfg9) + if err != nil { + return err + } + rule9.SetActionService(aService9) + rule9.SetContext(events) + rule9.SetPriority(6) + err = rs.AddRule(rule9) + if err != nil { + return err + } + + rs.SetStartupFunction(AssertThisPackage) + + //set a transaction handler + rs.RegisterRtcTransactionHandler(txHandler, nil) + //Start the rule session + err = rs.Start(nil) + if err != nil { + return err + } + + t6, err := model.NewTupleWithKeyValues("moveevent", "PACKAGE1") + if err != nil { + return err + } + t6.SetString(nil, "packageid", "PACKAGE1") + t6.SetString(nil, "targetstate", "sitting") + err = rs.Assert(nil, t6) + if err != nil { + return err + } + + finished := make(chan bool) + go sleepfunc(finished) + + t1, err := model.NewTupleWithKeyValues("moveevent", "PACKAGE2") + if err != nil { + return err + } + t1.SetString(nil, "packageid", "PACKAGE2") + t1.SetString(nil, "targetstate", "normal") + err = rs.Assert(nil, t1) + if err != nil { + return err + } + fmt.Println("package 2", t1) + + t2, err := model.NewTupleWithKeyValues("moveevent", "PACKAGE2") + if err != nil { + return err + } + t2.SetString(nil, "packageid", "PACKAGE2") + t2.SetString(nil, "targetstate", "sitting") + err = rs.Assert(nil, t2) + if err != nil { + return err + } + + t3, err := model.NewTupleWithKeyValues("moveevent", "PACKAGE2") + if err != nil { + return err + } + t3.SetString(nil, "packageid", "PACKAGE2") + t3.SetString(nil, "targetstate", "moving") + err = rs.Assert(nil, t3) + if err != nil { + return err + } + + t4, err := model.NewTupleWithKeyValues("moveevent", "PACKAGE2") + if err != nil { + return err + } + t4.SetString(nil, "packageid", "PACKAGE2") + t4.SetString(nil, "targetstate", "dropped") + err = rs.Assert(nil, t4) + if err != nil { + return err + } + + <-finished + + //delete the rule + rs.DeleteRule(rule2.GetName()) + + //unregister the session + rs.Unregister() + + return nil +} + +func AssertThisPackage(ctx context.Context, rs model.RuleSession, startupCtx map[string]interface{}) (err error) { + fmt.Printf("In startup rule function..\n") + pkg, _ := model.NewTupleWithKeyValues("package", "PACKAGE1") + pkg.SetString(nil, "state", "normal") + rs.Assert(nil, pkg) + return nil +} + +func cPackageEvent(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + pkg := tuples["package"] + if pkg != nil { + state, _ := pkg.GetString("state") + return state == "normal" + } + return false +} + +func aPrintPackage(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + pkg := tuples["package"] + pkgid, _ := pkg.GetString("id") + fmt.Printf("Received package [%s]\n", pkgid) +} + +func aPrintMoveEvent(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + me := tuples["moveevent"] + meid, _ := me.GetString("id") + s, _ := me.GetString("targetstate") + pkgID, _ := me.GetString("packageid") + + fmt.Printf("Received a 'moveevent' [%s] target state [%s]\n", meid, s) + + if s == "normal" { + pkg, _ := model.NewTupleWithKeyValues("package", pkgID) + pkg.SetString(nil, "state", "normal") + err := rs.Assert(ctx, pkg) + if err != nil { + fmt.Println("Tuple already inserted: ", pkgID) + } else { + fmt.Println("Tuple inserted successfully: ", pkgID) + } + } + +} + +func aJoinMoveEventAndPackage(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + me := tuples["moveevent"] + mepkgid, _ := me.GetString("packageid") + s, _ := me.GetString("targetstate") + + pkg := tuples["package"] + pkgid, _ := pkg.GetString("id") + pkgState, _ := pkg.GetString("state") + + if strings.Compare("sitting", s) == 0 { + currentEventType = "sitting" + } else { + currentEventType = "" + } + + if currentEventType == "sitting" { + + if pkgState == "normal" { + fmt.Printf("Joining a 'moveevent' with packageid [%s] to package [%s], target state [%s]\n", mepkgid, pkgid, s) + + //change the package's state to "sitting" + pkgMutable := pkg.(model.MutableTuple) + pkgMutable.SetString(ctx, "state", "sitting") + + //very first sitting event since the last notsitting event. + id, _ := common.GetUniqueId() + timeoutEvent, _ := model.NewTupleWithKeyValues("movetimeoutevent", id) + timeoutEvent.SetString(ctx, "packageid", pkgid) + timeoutEvent.SetInt(ctx, "timeoutinmillis", 10000) + fmt.Printf("Starting a 10s timer.. [%s]\n", pkgid) + rs.ScheduleAssert(ctx, 10000, pkgid, timeoutEvent) + } + } else { + if strings.Compare("moving", s) == 0 && pkgState == "sitting" { + + fmt.Printf("Joining a 'moveevent' with packageid [%s] to package [%s], target state [%s]\n", mepkgid, pkgid, s) + + //a non-sitting event, cancel a previous timer + rs.CancelScheduledAssert(ctx, pkgid) + + pkgMutable := pkg.(model.MutableTuple) + pkgMutable.SetString(ctx, "state", "moving") + + } else if strings.Compare("dropped", s) == 0 && pkgState == "moving" { + + fmt.Printf("Joining a 'moveevent' with packageid [%s] to package [%s], target state [%s]\n", mepkgid, pkgid, s) + + pkgMutable := pkg.(model.MutableTuple) + pkgMutable.SetString(ctx, "state", "dropped") + + } + + } + +} + +func aMoveTimeoutEvent(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + t1 := tuples["movetimeoutevent"] + id, _ := t1.GetString("id") + pkgid, _ := t1.GetString("packageid") + tomillis, _ := t1.GetInt("timeoutinmillis") + + if t1 != nil { + fmt.Printf("Received a 'movetimeoutevent' id [%s], packageid [%s], timeoutinmillis [%d]\n", id, pkgid, tomillis) + } +} + +func aJoinMoveTimeoutEventAndPackage(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + me := tuples["movetimeoutevent"] + //meid, _ := me.GetString("id") + epkgid, _ := me.GetString("packageid") + toms, _ := me.GetInt("timeoutinmillis") + + pkg := tuples["package"].(model.MutableTuple) + pkgid, _ := pkg.GetString("id") + + fmt.Printf("Joining a 'movetimeoutevent' [%s] to package [%s], timeout [%d]\n", epkgid, pkgid, toms) + + //change the package's state to "delayed" + pkg.SetString(ctx, "state", "delayed") +} + +func cPackageInSitting(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + pkg := tuples["package"] + if pkg != nil { + //pkgid, _ := pkg.GetString("id") + state, _ := pkg.GetString("state") + return state == "sitting" + } + return false +} + +func aPackageInSitting(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + pkg := tuples["package"] + if pkg != nil { + pkgid, _ := pkg.GetString("id") + fmt.Printf("PACKAGE [%s] is Sitting\n", pkgid) + } +} + +func cMoveEvent(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + mpkg := tuples["moveevent"] + if mpkg != nil { + mpkgid, _ := mpkg.GetString("packageid") + return len(mpkgid) != 0 + } + return false +} + +func cMoveTimeoutEvent(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + mpkg := tuples["movetimeoutevent"] + if mpkg != nil { + mpkgid, _ := mpkg.GetString("packageid") + return len(mpkgid) != 0 + } + return false +} + +func cMoveTimeoutEventPkg(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + mpkg := tuples["movetimeoutevent"] + pkg := tuples["package"] + if mpkg != nil { + if pkg != nil { + pkgid, _ := pkg.GetString("id") + mpkgid, _ := mpkg.GetString("packageid") + return pkgid == mpkgid + } + } + return false +} + +func cMoveEventPkg(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + mpkg := tuples["moveevent"] + pkg := tuples["package"] + if mpkg != nil { + if pkg != nil { + pkgid, _ := pkg.GetString("id") + mpkgid, _ := mpkg.GetString("packageid") + return pkgid == mpkgid + } + } + return false +} + +func cPackageInDelayed(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + pkg := tuples["package"] + if pkg != nil { + //pkgid, _ := pkg.GetString("id") + state, _ := pkg.GetString("state") + return state == "delayed" + } + return false +} + +func aPackageInDelayed(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + pkg := tuples["package"] + if pkg != nil { + pkgid, _ := pkg.GetString("id") + fmt.Printf("PACKAGE [%s] is Delayed\n", pkgid) + rs.Retract(ctx, pkg) + } +} + +func cPackageInMoving(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + pkg := tuples["package"] + if pkg != nil { + //pkgid, _ := pkg.GetString("id") + state, _ := pkg.GetString("state") + return state == "moving" + } + return false +} + +func aPackageInMoving(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + pkg := tuples["package"] + if pkg != nil { + pkgid, _ := pkg.GetString("id") + fmt.Printf("PACKAGE [%s] is Moving\n", pkgid) + } +} + +func cPackageInDropped(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + pkg := tuples["package"] + if pkg != nil { + //pkgid, _ := pkg.GetString("id") + state, _ := pkg.GetString("state") + return state == "dropped" + } + return false +} + +func aPackageInDropped(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + pkg := tuples["package"] + if pkg != nil { + pkgid, _ := pkg.GetString("id") + fmt.Printf("PACKAGE [%s] is Dropped\n", pkgid) + rs.Retract(ctx, pkg) + } +} + +func getFileContent(filePath string) string { + absPath := common.GetAbsPathForResource(filePath) + return common.FileToString(absPath) +} + +func txHandler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + + store := rs.GetStore() + store.SaveTuples(rtxn.GetRtcAdded()) + + store.SaveModifiedTuples(rtxn.GetRtcModified()) + + store.DeleteTuples(rtxn.GetRtcDeleted()) + +} +func sleepfunc(finished chan bool) { + time.Sleep(15 * time.Second) + finished <- true +} diff --git a/examples/statemachine/rulesapp.json b/examples/statemachine/rulesapp.json new file mode 100644 index 0000000..c0d5d25 --- /dev/null +++ b/examples/statemachine/rulesapp.json @@ -0,0 +1,54 @@ +[ + { + "name": "package", + "properties": [ + { + "name": "id", + "type": "string", + "pk-index": 0 + }, + { + "name": "state", + "type": "string" + } + ] + }, + { + "name": "moveevent", + "ttl": 0, + "properties": [ + { + "name": "id", + "type": "string", + "pk-index": 0 + }, + { + "name": "packageid", + "type": "string" + }, + { + "name": "targetstate", + "type": "string" + } + ] + }, + { + "name": "movetimeoutevent", + "ttl": 0, + "properties": [ + { + "name": "id", + "type": "string", + "pk-index": 0 + }, + { + "name": "packageid", + "type": "string" + }, + { + "name": "timeoutinmillis", + "type": "integer" + } + ] + } +] \ No newline at end of file diff --git a/examples/statemachine/statemachine.png b/examples/statemachine/statemachine.png new file mode 100644 index 0000000..374bf34 Binary files /dev/null and b/examples/statemachine/statemachine.png differ diff --git a/examples/trackntrace/trackntrace_test.go b/examples/trackntrace/trackntrace_test.go index b370547..2624e16 100644 --- a/examples/trackntrace/trackntrace_test.go +++ b/examples/trackntrace/trackntrace_test.go @@ -1,16 +1,21 @@ package trackntrace import ( + "reflect" + "runtime" + + "github.com/project-flogo/rules/common" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" "github.com/project-flogo/rules/ruleapi" - "github.com/project-flogo/rules/common" + "github.com/stretchr/testify/assert" - "testing" - "time" - "io/ioutil" - "strconv" "context" "fmt" + "io/ioutil" + "strconv" + "testing" + "time" ) func TestPkgFlowNormal(t *testing.T) { @@ -197,12 +202,10 @@ func TestSameTupleInstanceAssert(t *testing.T) { rs.Unregister() } - - func createRuleSessionAndRules(t *testing.T) (model.RuleSession, error) { - rs, _ := ruleapi.GetOrCreateRuleSession("asession") + rs, _ := ruleapi.GetOrCreateRuleSession("asession", "") - tupleDescFileAbsPath := common.GetAbsPathForResource("src/github.com/project-flogo/rules/examples/trackntrace/trackntrace.json") + tupleDescFileAbsPath := common.GetPathForResource("examples/trackntrace/trackntrace.json", "./trackntrace.json") dat, err := ioutil.ReadFile(tupleDescFileAbsPath) if err != nil { @@ -220,7 +223,7 @@ func loadPkgRulesWithDeps(t *testing.T, rs model.RuleSession) { //handle a package event, create a package in the packageAction rule := ruleapi.NewRule("packageevent") rule.AddCondition("truecondition", []string{"packageevent.none"}, truecondition, nil) - rule.SetAction(packageeventAction) + rule.SetActionService(createActionServiceFromFunction(t, packageeventAction)) rule.SetPriority(1) rs.AddRule(rule) t.Logf("Rule added: [%s]\n", rule.GetName()) @@ -228,7 +231,7 @@ func loadPkgRulesWithDeps(t *testing.T, rs model.RuleSession) { //handle a package, print package details in the packageAction rule1 := ruleapi.NewRule("package") rule1.AddCondition("packageCondition", []string{"package.none"}, packageCondition, nil) - rule1.SetAction(packageAction) + rule1.SetActionService(createActionServiceFromFunction(t, packageAction)) rule1.SetPriority(2) rs.AddRule(rule1) t.Logf("Rule added: [%s]\n", rule1.GetName()) @@ -237,7 +240,7 @@ func loadPkgRulesWithDeps(t *testing.T, rs model.RuleSession) { //for the next destination, etc in the scaneventAction rule2 := ruleapi.NewRule("scanevent") rule2.AddCondition("scaneventCondition", []string{"package.packageid", "scanevent.packageid", "package.curr", "package.next"}, scaneventCondition, nil) - rule2.SetAction(scaneventAction) + rule2.SetActionService(createActionServiceFromFunction(t, scaneventAction)) rule2.SetPriority(2) rs.AddRule(rule2) t.Logf("Rule added: [%s]\n", rule2.GetName()) @@ -245,7 +248,7 @@ func loadPkgRulesWithDeps(t *testing.T, rs model.RuleSession) { //handle a timeout event, triggered by scaneventAction, mark the package as delayed in scantimeoutAction rule3 := ruleapi.NewRule("scantimeout") rule3.AddCondition("scantimeoutCondition", []string{"package.packageid", "scantimeout.packageid"}, scantimeoutCondition, nil) - rule3.SetAction(scantimeoutAction) + rule3.SetActionService(createActionServiceFromFunction(t, scantimeoutAction)) rule3.SetPriority(1) rs.AddRule(rule3) t.Logf("Rule added: [%s]\n", rule3.GetName()) @@ -253,7 +256,7 @@ func loadPkgRulesWithDeps(t *testing.T, rs model.RuleSession) { //notify when a package is marked as delayed, print as such in the packagedelayedAction rule4 := ruleapi.NewRule("packagedelayed") rule4.AddCondition("packageDelayedCheck", []string{"package.status"}, packageDelayedCheck, nil) - rule4.SetAction(packagedelayedAction) + rule4.SetActionService(createActionServiceFromFunction(t, packagedelayedAction)) rule4.SetPriority(1) rs.AddRule(rule4) t.Logf("Rule added: [%s]\n", rule4.GetName()) @@ -392,4 +395,18 @@ func packagedelayedAction(ctx context.Context, rs model.RuleSession, ruleName st fmt.Printf("Package is now delayed id[%s]\n", pkgid) } -type TestKey struct {} \ No newline at end of file +type TestKey struct{} + +func createActionServiceFromFunction(t *testing.T, actionFunction model.ActionFunction) model.ActionService { + fname := runtime.FuncForPC(reflect.ValueOf(actionFunction).Pointer()).Name() + cfg := &config.ServiceDescriptor{ + Name: fname, + Description: fname, + Type: config.TypeServiceFunction, + Function: actionFunction, + } + aService, err := ruleapi.NewActionService(cfg) + assert.Nil(t, err) + assert.NotNil(t, aService) + return aService +} diff --git a/go.mod b/go.mod index f36faf1..317fdd1 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,21 @@ module github.com/project-flogo/rules +go 1.12 + require ( - github.com/aws/aws-sdk-go v1.18.3 - github.com/gorilla/websocket v1.4.0 + github.com/360EntSecGroup-Skylar/excelize/v2 v2.0.2 + github.com/Shopify/sarama v1.23.1 + github.com/aws/aws-sdk-go v1.24.1 + github.com/gomodule/redigo v2.0.0+incompatible + github.com/gorilla/websocket v1.4.1 github.com/oklog/ulid v1.3.1 - github.com/project-flogo/contrib/trigger/kafka v0.9.0 - github.com/project-flogo/contrib/trigger/rest v0.9.0 - github.com/project-flogo/core v0.9.3 - github.com/stretchr/testify v1.3.0 + github.com/project-flogo/contrib/activity/log v0.10.0 + github.com/project-flogo/contrib/function/string v0.10.0 + github.com/project-flogo/contrib/trigger/kafka v0.10.0 + github.com/project-flogo/contrib/trigger/rest v0.10.0 + github.com/project-flogo/core v0.10.2 + github.com/project-flogo/flow v0.10.1 // indirect + github.com/project-flogo/legacybridge v0.9.1 + github.com/stretchr/testify v1.4.0 + gopkg.in/jcmturner/goidentity.v3 v3.0.0 // indirect ) - -go 1.13 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..34d8c63 --- /dev/null +++ b/go.sum @@ -0,0 +1,160 @@ +github.com/360EntSecGroup-Skylar/excelize/v2 v2.0.2 h1:StMrA6UQ5Cm6206DxXGuV/NMqSIOIDoMXMYt8JPe1lE= +github.com/360EntSecGroup-Skylar/excelize/v2 v2.0.2/go.mod h1:EfRHD2k+Kd7ijnqlwOrH1IifwgWB9yYJ0pdXtBZmlpU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/zstd v1.3.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798 h1:2T/jmrHeTezcCM58lvEQXs0UpQJCo5SoGAcg+mbSTIg= +github.com/DataDog/zstd v1.3.6-0.20190409195224-796139022798/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Shopify/sarama v1.22.0/go.mod h1:lm3THZ8reqBDBQKQyb5HB3sY1lKp3grEbQ81aWSgPp4= +github.com/Shopify/sarama v1.23.1 h1:XxJBCZEoWJtoWjf/xRbmGUpAmTZGnuuF0ON0EvxxBrs= +github.com/Shopify/sarama v1.23.1/go.mod h1:XLH1GYJnLVE0XCr6KdJGVJRTwY30moWNJ4sERjXX6fs= +github.com/Shopify/toxiproxy v2.1.4+incompatible h1:TKdv8HiTLgE5wdJuEML90aBgNWsokNbMijUGhmcoBJc= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/TIBCOSoftware/flogo-contrib v0.5.8/go.mod h1:gWcVjvpNFzoQy9puLgyJaLHzJaxqhkoJ8yctKTVX5K4= +github.com/TIBCOSoftware/flogo-lib v0.5.8 h1:JX6PcxtyJbya++sKiWhFRADA6UnkSRS/INuTPzOMF0Y= +github.com/TIBCOSoftware/flogo-lib v0.5.8/go.mod h1:AE7tfFBvQNemM61frHCwTJYBWC9+iXohXyqrApe6xj4= +github.com/aws/aws-sdk-go v1.24.1 h1:B2NRyTV1/+h+Dg8Bh7vnuvW6QZz/NBL+uzgC2uILDMI= +github.com/aws/aws-sdk-go v1.24.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/eapache/go-resiliency v1.1.0 h1:1NtRmCAqadE2FN4ZcN6g90TP3uk8cg9rn9eNK2197aU= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 h1:FUwcHNlEqkqLjLBdCp5PRlCFijNjvcYANOZXzCfXwCM= +github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41 h1:GeinFsrjWz97fAxVUEd748aV0cYL+I6k44gFJTCVvpU= +github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/project-flogo/contrib/activity/log v0.10.0 h1:TPVeO2xUQfauFIPqLBsEBlNp7aGRaWiIMVDQcr7JQ6o= +github.com/project-flogo/contrib/activity/log v0.10.0/go.mod h1:M+qvbC3fVbONn1iYlhOsvxDUWCO7eAZT6rOj0N0Oexc= +github.com/project-flogo/contrib/function/string v0.10.0 h1:U+gHaAwiZIqwORHAc6Fx8bDP8VO1Gi9o6yaKerEbDeU= +github.com/project-flogo/contrib/function/string v0.10.0/go.mod h1:LU3UHYCkihq3BWMJssYU9zOQQgAql6E2F+THq5QGaCo= +github.com/project-flogo/contrib/trigger/kafka v0.10.0 h1:rU0k7S9U5GjF6nhXtKqySdVVMM/19ZoyIz9kyVTLPY4= +github.com/project-flogo/contrib/trigger/kafka v0.10.0/go.mod h1:nXuAgdpc9DoZCJNAe/MyLEI+oiPembca8v23aTzADvY= +github.com/project-flogo/contrib/trigger/rest v0.10.0 h1:v6wm+A3BLCnCZT/ufiELxW08HL2Yc4bHby61/gKRhYg= +github.com/project-flogo/contrib/trigger/rest v0.10.0/go.mod h1:Omd0fWOL8E+e6emN97/UZ8UK1cDsVwR5MtvGKgUzOA4= +github.com/project-flogo/core v0.9.2/go.mod h1:QGWi7TDLlhGUaYH3n/16ImCuulbEHGADYEXyrcHhX7U= +github.com/project-flogo/core v0.9.4-hf.1/go.mod h1:QGWi7TDLlhGUaYH3n/16ImCuulbEHGADYEXyrcHhX7U= +github.com/project-flogo/core v0.10.1/go.mod h1:4DhTlZ5re1DKHBXYwNZmUswiakcD2E4v3FzlZT/rAI8= +github.com/project-flogo/core v0.10.2 h1:w+OweLultHbY5712r3fiXHFTMw7HJ0vCDmdKRoN0gus= +github.com/project-flogo/core v0.10.2/go.mod h1:4DhTlZ5re1DKHBXYwNZmUswiakcD2E4v3FzlZT/rAI8= +github.com/project-flogo/flow v0.9.2 h1:0y+mWZ158yHWsbY/OsPB0pY0MVFXPRFqtEczHjtnNks= +github.com/project-flogo/flow v0.9.2/go.mod h1:DPUBHFS5+5N+zXBJ9Q6qeNy+AD6kUDrSzPhhwNdNn1M= +github.com/project-flogo/flow v0.10.1 h1:P1h5UPjwvdc7CNE+GERt3EJ8xtzFza6ssClo5cBW+nM= +github.com/project-flogo/flow v0.10.1/go.mod h1:ruIoGt/yPBZsDwoUi3dIgGVXOllt+3YCBah9tdEjffM= +github.com/project-flogo/legacybridge v0.9.1 h1:d0mYjPHUJCHtXA84tluEbG5sgK4gLFYgpYPWgLCsMpc= +github.com/project-flogo/legacybridge v0.9.1/go.mod h1:1HcW7VRlgfiSbnAhHwyAWdkiVKkqr0QjMM6kluv8/xo= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a h1:9ZKAASQSHhDYGoxY8uLVpewe1GDZ2vu2Tr/vTdVAkFQ= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= +github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/xeipuuv/gojsonschema v1.1.0/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.5.1 h1:rsqfU5vBkVknbhUGbAUwQKR2H4ItV8tjJ+6kJX4cxHM= +go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.4.0 h1:f3WCSC2KzAcBXGATIxAB1E2XuCpNU255wNKZ505qi3E= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= +go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE= +golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529 h1:iMGN4xG0cnqj3t+zOM8wUB0BiPKHEwSxEZCvzcbZuvk= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a h1:gHevYm0pO4QUbwy8Dmdr01R5r1BuKtfYqRqF0h/Cbh0= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5 h1:hKsoRgsbwY1NafxrwTs+k64bikrLBkAgPir1TNCj3Zs= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/jcmturner/aescts.v1 v1.0.1 h1:cVVZBK2b1zY26haWB4vbBiZrfFQnfbTVrE3xZq6hrEw= +gopkg.in/jcmturner/aescts.v1 v1.0.1/go.mod h1:nsR8qBOg+OucoIW+WMhB3GspUQXq9XorLnQb9XtvcOo= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1 h1:cIuC1OLRGZrld+16ZJvvZxVJeKPsvd5eUIvxfoN5hSM= +gopkg.in/jcmturner/dnsutils.v1 v1.0.1/go.mod h1:m3v+5svpVOhtFAP/wSz+yzh4Mc0Fg7eRhxkJMWSIz9Q= +gopkg.in/jcmturner/goidentity.v3 v3.0.0 h1:1duIyWiTaYvVx3YX2CYtpJbUFd7/UuPYCfgXtQ3VTbI= +gopkg.in/jcmturner/goidentity.v3 v3.0.0/go.mod h1:oG2kH0IvSYNIu80dVAyu/yoefjq1mNfM5bm88whjWx4= +gopkg.in/jcmturner/gokrb5.v7 v7.2.3 h1:hHMV/yKPwMnJhPuPx7pH2Uw/3Qyf+thJYlisUc44010= +gopkg.in/jcmturner/gokrb5.v7 v7.2.3/go.mod h1:l8VISx+WGYp+Fp7KRbsiUuXTTOnxIc3Tuvyavf11/WM= +gopkg.in/jcmturner/rpc.v1 v1.1.0 h1:QHIUxTX1ISuAv9dD2wJ9HWQVuWDX/Zc0PfeC2tjc4rU= +gopkg.in/jcmturner/rpc.v1 v1.1.0/go.mod h1:YIdkC4XfD6GXbzje11McwsDuOlZQSb9W4vfLvuNnlv8= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/redisutils/redisutil_test.go b/redisutils/redisutil_test.go new file mode 100644 index 0000000..be3b311 --- /dev/null +++ b/redisutils/redisutil_test.go @@ -0,0 +1,291 @@ +package redisutils + +import ( + "encoding/json" + "fmt" + "net" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + "github.com/gomodule/redigo/redis" + "github.com/stretchr/testify/assert" +) + +func Drain(port string) { + for { + conn, err := net.DialTimeout("tcp", net.JoinHostPort("", port), time.Second) + if conn != nil { + conn.Close() + } + if err != nil && strings.Contains(err.Error(), "connect: connection refused") { + break + } + } +} + +func Pour(port string) { + for { + conn, _ := net.Dial("tcp", net.JoinHostPort("", port)) + if conn != nil { + conn.Close() + break + } + } +} + +func TestMain(m *testing.M) { + run := func() int { + command := exec.Command("docker", "run", "-p", "6382:6379", "-d", "redis") + hash, err := command.Output() + if err != nil { + panic(err) + } + Pour("6382") + + defer func() { + command := exec.Command("docker", "stop", strings.TrimSpace(string(hash))) + err := command.Run() + if err != nil { + panic(err) + } + command = exec.Command("docker", "rm", strings.TrimSpace(string(hash))) + err = command.Run() + if err != nil { + panic(err) + } + Drain("6382") + }() + + return m.Run() + } + os.Exit(run()) +} + +func Test_first(t *testing.T) { + rd := NewRedisHdl(RedisConfig{Address: ":6382"}) + defer Shutdown() + + m := make(map[string]interface{}) + m["k1"] = "v1" + + rd.HSetAll("myhash", m) + x := rd.HGetAll("myhash") + + for k, v := range x { + fmt.Printf("key=[%s], value=[%s]\n", k, v) + } +} + +// ping tests connectivity for redisutils (PONG should be returned) +func ping(c redis.Conn) error { + // Send PING command to Redis + pong, err := c.Do("PING") + if err != nil { + return err + } + + // PING command returns a Redis "Simple String" + // Use redisutils.String to convert the interface type to string + s, err := redis.String(pong, err) + if err != nil { + return err + } + + fmt.Printf("PING Response = %s\n", s) + // Output: PONG + + set(c) + get(c) + setStruct(c) + getStruct(c) + return nil +} + +// set executes the redisutils SET command +func set(c redis.Conn) error { + _, err := c.Do("SET", "Favorite Movie", "Repo Man") + if err != nil { + fmt.Printf("Error") + return nil + } + _, err = c.Do("SET", "Release Year", 1984) + if err != nil { + fmt.Printf("Error") + return nil + } + return nil +} + +// get executes the redisutils GET command +func get(c redis.Conn) error { + + // Simple GET example with String helper + key := "Favorite Movie" + s, err := redis.String(c.Do("GET", key)) + if err != nil { + return (err) + } + fmt.Printf("%s = %s\n", key, s) + + // Simple GET example with Int helper + key = "Release Year" + i, err := redis.Int(c.Do("GET", key)) + if err != nil { + return (err) + } + fmt.Printf("%s = %d\n", key, i) + + // Example where GET returns no results + key = "Nonexistent Key" + s, err = redis.String(c.Do("GET", key)) + if err == redis.ErrNil { + fmt.Printf("%s does not exist\n", key) + } else if err != nil { + return err + } else { + fmt.Printf("%s = %s\n", key, s) + } + + return nil +} + +type User struct { + Username string + MobileID int + Email string + FirstName string + LastName string +} + +func setStruct(c redis.Conn) error { + + const objectPrefix string = "user:" + + usr := User{ + Username: "otto", + MobileID: 1234567890, + Email: "ottoM@repoman.com", + FirstName: "Otto", + LastName: "Maddox", + } + + // serialize User object to JSON + json, err := json.Marshal(usr) + if err != nil { + return err + } + + // SET object + _, err = c.Do("SET", objectPrefix+usr.Username, json) + if err != nil { + return err + } + + return nil +} + +func getStruct(c redis.Conn) error { + + const objectPrefix string = "user:" + + username := "otto" + s, err := redis.String(c.Do("GET", objectPrefix+username)) + if err == redis.ErrNil { + fmt.Println("User does not exist") + } else if err != nil { + return err + } + + usr := User{} + err = json.Unmarshal([]byte(s), &usr) + + fmt.Printf("%+v\n", usr) + + return nil + +} + +func Test_three(t *testing.T) { + hdl := NewRedisHdl(RedisConfig{Address: ":6382"}) + defer Shutdown() + + //iter := hdl.GetListIterator("x:jt:L_c2") + // + //for iter.HasNext() { + // key := iter.Next() + // fmt.Printf("KEY: [%s]\n", key) + //} + + miter := hdl.GetMapIterator("a") + + for miter.HasNext() { + key, value := miter.Next() + fmt.Printf("KEY: [%s], Value=[%s]\n", key, value) + miter.Remove() + } + //{ + // + // miter := hdl.GetMapIterator("a") + // + // for miter.HasNext() { + // key, value := miter.Next() + // fmt.Printf("KEY: [%s], Value=[%s]\n", key, value) + // } + //} +} + +func Test_four(t *testing.T) { + hdl := NewRedisHdl(RedisConfig{Address: ":6382"}) + defer Shutdown() + + //v := hdl.HGet("a", "d") + len := hdl.HLen("a") + fmt.Printf("[%d]\n", len) + +} + +func Test_five(t *testing.T) { + hdl := NewRedisHdl(RedisConfig{Address: ":6382"}) + defer Shutdown() + + for i := 0; i < 10; i++ { + m := make(map[string]interface{}) + m[""+strconv.Itoa(i)] = i + hdl.HSetAll("x", m) + } + +} + +func TestSet(t *testing.T) { + hdl := NewRedisHdl(RedisConfig{Address: ":6382"}) + defer Shutdown() + + ok, err := hdl.Set("lock", "123", true, 60000) + assert.Nil(t, err) + assert.True(t, ok) + ok, err = hdl.Set("lock", "123", true, 60000) + assert.NotNil(t, err) + assert.False(t, ok) + ok, err = hdl.Set("lock", "1234", false, 60000) + assert.Nil(t, err) + assert.True(t, ok) +} + +func TestDelIfEqual(t *testing.T) { + hdl := NewRedisHdl(RedisConfig{Address: ":6382"}) + defer Shutdown() + + ok, err := hdl.Set("lockx", "123", true, 60000) + assert.Nil(t, err) + assert.True(t, ok) + count, err := hdl.DelIfEqual("lockx", "1234") + assert.Nil(t, err) + assert.Equal(t, 0, count) + count, err = hdl.DelIfEqual("lockx", "123") + assert.Nil(t, err) + assert.Equal(t, 1, count) +} diff --git a/redisutils/redisutils.go b/redisutils/redisutils.go new file mode 100644 index 0000000..bd7a631 --- /dev/null +++ b/redisutils/redisutils.go @@ -0,0 +1,410 @@ +package redisutils + +import ( + "fmt" + + "github.com/gomodule/redigo/redis" +) + +type RedisHdl = *RedisHandle + +var rd = make([]RedisHdl, 0, 8) + +type RedisConfig struct { + Network string `json:"network"` + Address string `json:"address"` +} + +type RedisHandle struct { + config RedisConfig + pool *redis.Pool + network string + address string +} + +func NewRedisHdl(config RedisConfig) RedisHdl { + handle := RedisHandle{ + config: config, + network: config.Network, + address: config.Address, + } + if handle.network == "" { + handle.network = "tcp" + } + if handle.address == "" { + handle.address = ":6379" + } + handle.newPool(handle.network, handle.address) + rd = append(rd, &handle) + return &handle +} + +func (rh *RedisHandle) newPool(network string, address string) { + rh.pool = &redis.Pool{ + // Maximum number of idle connections in the pool. + MaxIdle: 80, + // max number of connections + MaxActive: 12000, + // Dial is an application supplied function for creating and + // configuring a connection. + Dial: func() (redis.Conn, error) { + c, err := redis.Dial(network, address) + if err != nil { + panic(err.Error()) + } + return c, err + }, + } +} + +func (rh *RedisHandle) getPool() *redis.Pool { + return rh.pool +} + +func (rh *RedisHandle) Set(key string, value string, nx bool, px int64) (bool, error) { + c := rh.getPool().Get() + defer c.Close() + args := []interface{}{key, value} + if nx { + args = append(args, "NX") + } + if px > 0 { + args = append(args, "PX", px) + } + ok, err := redis.String(c.Do("SET", args...)) + return ok == "OK", err +} + +const delIfEqualScript = `if redis.call("get",KEYS[1]) == ARGV[1] +then + return redis.call("del",KEYS[1]) +else + return 0 +end` + +func (rh *RedisHandle) DelIfEqual(key string, value string) (int, error) { + c := rh.getPool().Get() + defer c.Close() + return redis.Int(c.Do("EVAL", delIfEqualScript, 1, key, value)) +} + +func (rh *RedisHandle) HSet(key string, field string, value interface{}) (bool, error) { + c := rh.getPool().Get() + defer c.Close() + exists, err := redis.Int(c.Do("HSET", key, field, value)) + return exists == 0, err +} + +func (rh *RedisHandle) HSetNX(key string, field string, value interface{}) (bool, error) { + c := rh.getPool().Get() + defer c.Close() + exists, err := redis.Int(c.Do("HSETNX", key, field, value)) + return exists == 0, err +} + +func (rh *RedisHandle) HSetAll(key string, kvs map[string]interface{}) error { + + var args = []interface{}{key} + for f, v := range kvs { + args = append(args, f, v) + } + c := rh.getPool().Get() + defer c.Close() + _, error := c.Do("HMSET", args...) + + return error +} + +func (rh *RedisHandle) HGetAll(key string) map[string]interface{} { + hgetall := make(map[string]interface{}) + c := rh.getPool().Get() + defer c.Close() + vals, error := c.Do("HGETALL", key) + if error != nil { + fmt.Printf("error [%v]\n", error) + } else { + vals, err2 := redis.Values(vals, error) + if err2 != nil { + fmt.Printf("error [%v]\n", err2) + } else { + i := 0 + key := "" + value := "" + for _, val := range vals { + ba := val.([]byte) + s := string(ba) + //fmt.Printf("Value [%s]\n", s) + if i%2 == 0 { + key = s + } else { + value = s + hgetall[key] = value + } + i++ + } + } + } + return hgetall +} + +func (rh *RedisHandle) HGet(key string, field string) interface{} { + c := rh.getPool().Get() + defer c.Close() + vals, error := c.Do("HGET", key, field) + if error != nil { + fmt.Printf("error [%v]\n", error) + } else { + ba := vals.([]byte) + s := string(ba) + return s + } + return nil +} + +func (rh *RedisHandle) HLen(key string) int { + c := rh.getPool().Get() + defer c.Close() + vals, error := c.Do("HLEN", key) + if error != nil { + fmt.Printf("error [%v]\n", error) + } else { + len, _ := redis.Int(vals, error) + return len + } + return 0 +} + +func (rh *RedisHandle) HIncrBy(key string, field string, by int) int { + c := rh.getPool().Get() + defer c.Close() + i, err := c.Do("HINCRBY", key, field, 1) + + if err != nil { + fmt.Printf("error: [%s]", err) + return -1 + } + current := int(i.(int64)) + return current +} + +func (rh *RedisHandle) IncrBy(key string, by int) int { + c := rh.getPool().Get() + defer c.Close() + i, err := c.Do("INCRBY", key, 1) + + if err != nil { + fmt.Printf("error: [%s]", err) + return -1 + } + current := int(i.(int64)) + return current +} + +func (rh *RedisHandle) HGetAsInt(key string, field string) int { + c := rh.getPool().Get() + defer c.Close() + i, err := c.Do("HGET", key, field) + j := -1 + if err == nil { + j, _ = redis.Int(i, err) + } + return j +} + +func (rh *RedisHandle) GetAsInt(key string) int { + c := rh.getPool().Get() + defer c.Close() + i, err := c.Do("GET", key) + j := -1 + if err == nil { + j, _ = redis.Int(i, err) + } + return j +} + +func (rh *RedisHandle) Del(key string) int { + c := rh.getPool().Get() + defer c.Close() + i, err := c.Do("DEL", key) + j := -1 + if err == nil { + j, _ = redis.Int(i, err) + } + return j +} + +func (rh *RedisHandle) HDel(key string, field string) int { + c := rh.getPool().Get() + defer c.Close() + i, err := c.Do("HDEL", key, field) + j := -1 + if err == nil { + j, _ = redis.Int(i, err) + } + return j +} + +type LIterator struct { + key string + scanIdx int //iterator index + keyIdx int //local array current index + keys []string //array of keys in the current call + valueMap map[string]interface{} //map of key/value of the current keys + rh *RedisHandle +} + +func (iter *LIterator) HasNext() bool { + + if iter.scanIdx == 0 { // there is nothing more with redis + if len(iter.keys) == 0 { + return false + } else { + if iter.keyIdx == len(iter.keys) { + return false + } + } + } else { + if len(iter.keys) == 0 || iter.keyIdx >= len(iter.keys) { + c := iter.rh.getPool().Get() + defer c.Close() + if arr, err := redis.Values(c.Do("HSCAN", iter.key, iter.scanIdx)); err != nil { + panic(err) + } else { + // now we get the iter and the keys from the multi-bulk reply + iter.scanIdx, _ = redis.Int(arr[0], nil) + iter.keys, _ = redis.Strings(arr[1], nil) + iter.keyIdx = 0 + return iter.HasNext() + } + } + } + + return true + +} + +func (iter *LIterator) Next() string { + str := iter.keys[iter.keyIdx] + iter.keyIdx++ + return str +} + +func (rh *RedisHandle) GetListIterator(key string) *LIterator { + + iter := LIterator{} + iter.key = key + iter.rh = rh + c := rh.getPool().Get() + defer c.Close() + + if arr, err := redis.Values(c.Do("HSCAN", key, 0, "COUNT", 1)); err != nil { + panic(err) + } else { + // now we get the iter and the keys from the multi-bulk reply + iter.scanIdx, _ = redis.Int(arr[0], nil) + iter.keys, _ = redis.Strings(arr[1], nil) + iter.keyIdx = 0 + } + + return &iter +} + +type MapIterator struct { + LIterator + currKey string + currValue interface{} +} + +func (iter *MapIterator) HasNext() bool { + + if iter.scanIdx == 0 { // there is nothing more with redis + if len(iter.keys) == 0 { + return false + } else { + if iter.keyIdx > len(iter.keys)-2 { + return false + } + } + } else { + if len(iter.keys) == 0 || iter.keyIdx >= len(iter.keys)-1 { + c := iter.rh.getPool().Get() + defer c.Close() + if arr, err := redis.Values(c.Do("HSCAN", iter.key, iter.scanIdx)); err != nil { + panic(err) + } else { + iter.scanIdx, _ = redis.Int(arr[0], nil) + iter.keys, _ = redis.Strings(arr[1], nil) + iter.keyIdx = 0 + return iter.HasNext() + } + } + } + + return true + +} + +func (iter *MapIterator) Remove() { + iter.rh.HDel(iter.key, iter.currKey) +} + +func (iter *MapIterator) Next() (string, interface{}) { + iter.currKey = iter.keys[iter.keyIdx] + iter.currValue = iter.keys[iter.keyIdx+1] + + iter.keyIdx += 2 + return iter.currKey, iter.currValue +} + +func (rh *RedisHandle) GetMapIterator(key string) *MapIterator { + + iter := MapIterator{} + iter.key = key + iter.rh = rh + c := rh.getPool().Get() + defer c.Close() + + if arr, err := redis.Values(c.Do("HSCAN", key, 0, "COUNT", 1)); err != nil { + panic(err) + } else { + // now we get the iter and the keys from the multi-bulk reply + iter.scanIdx, _ = redis.Int(arr[0], nil) + iter.keys, _ = redis.Strings(arr[1], nil) + iter.keyIdx = 0 + } + + return &iter +} + +func Shutdown() { + for _, value := range rd { + value.pool.Close() + } + rd = rd[:0] +} + +// GetKeys returns all keys present in redis server +func (rh *RedisHandle) GetKeys(pattern string) ([]string, error) { + + conn := rh.getPool().Get() + defer conn.Close() + + iter := 0 + keys := []string{} + for { + arr, err := redis.Values(conn.Do("SCAN", iter, "MATCH", pattern)) + if err != nil { + return keys, fmt.Errorf("error retrieving '%s' keys", pattern) + } + + iter, _ = redis.Int(arr[0], nil) + k, _ := redis.Strings(arr[1], nil) + keys = append(keys, k...) + + if iter == 0 { + break + } + } + + return keys, nil +} diff --git a/rete/classnode.go b/rete/classnode.go index 2bfe352..6da445f 100644 --- a/rete/classnode.go +++ b/rete/classnode.go @@ -5,6 +5,7 @@ import ( "context" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" ) //classNode holds links to filter and join nodes eventually leading upto the rule node @@ -75,8 +76,8 @@ func (cn *classNodeImpl) String() string { } func (cn *classNodeImpl) assert(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, forRule string) { - handle := getOrCreateHandle(ctx, tuple) - handles := make([]reteHandle, 1) + handle := getHandleWithTuple(ctx, tuple) + handles := make([]types.ReteHandle, 1) handles[0] = handle propagate := false for e := cn.getClassNodeLinks().Front(); e != nil; e = e.Next() { diff --git a/rete/classnodelink.go b/rete/classnodelink.go index 15c2d16..92cae46 100644 --- a/rete/classnodelink.go +++ b/rete/classnodelink.go @@ -1,6 +1,9 @@ package rete -import "github.com/project-flogo/rules/common/model" +import ( + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" +) //classNodeLink links the classNode to the rest of the model.Rule's network type classNodeLink interface { @@ -17,14 +20,14 @@ type classNodeLinkImpl struct { classNodeVar classNode } -func newClassNodeLink(nw Network, classNodeVar classNode, child node, rule model.Rule, identifierVar model.TupleType) classNodeLink { +func newClassNodeLink(nw types.Network, classNodeVar classNode, child node, rule model.Rule, identifierVar model.TupleType) classNodeLink { cnl := classNodeLinkImpl{} cnl.initClassNodeLinkImpl(nw, classNodeVar, child, rule, identifierVar) return &cnl } -func (cnl *classNodeLinkImpl) initClassNodeLinkImpl(nw Network, classNodeVar classNode, child node, rule model.Rule, identifierVar model.TupleType) { - initClassNodeLink(nw, &cnl.nodeLinkImpl, child) +func (cnl *classNodeLinkImpl) initClassNodeLinkImpl(nw types.Network, classNodeVar classNode, child node, rule model.Rule, identifierVar model.TupleType) { + cnl.initClassNodeLink(nw, child) cnl.classNodeVar = classNodeVar cnl.rule = rule cnl.identifierVar = identifierVar diff --git a/rete/common/types.go b/rete/common/types.go new file mode 100644 index 0000000..5108316 --- /dev/null +++ b/rete/common/types.go @@ -0,0 +1,66 @@ +package common + +import ( + "context" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/redisutils" +) + +type RtcOprn int + +const ( + ADD RtcOprn = 1 + iota + RETRACT + MODIFY + DELETE +) + +//Network ... these are used by RuleSession +type Network interface { + AddRule(model.Rule) error + String() string + RemoveRule(string) model.Rule + GetRules() []model.Rule + //changedProps are the properties that changed in a previous action + Assert(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn) error + //mode can be one of retract, modify, delete + Retract(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn) error + + GetAssertedTuple(ctx context.Context, rs model.RuleSession, key model.TupleKey) model.Tuple + RegisterRtcTransactionHandler(txnHandler model.RtcTransactionHandler, txnContext interface{}) + //SetConfig(config map[string]string) + //GetConfigValue(key string) string + //GetConfig() map[string]string + + SetTupleStore(tupleStore model.TupleStore) + ReplayTuplesForRule(ruleName string, rs model.RuleSession) (err error) +} + +const ( + ServiceTypeMem = "mem" + ServiceTypeRedis = "redis" + ModeConsistency = "consistency" + ModePerformance = "performance" +) + +type Service struct { + Mem map[string]interface{} `json:"mem"` + Redis redisutils.RedisConfig `json:"redis"` +} + +type Config struct { + Mode string `json:"mode"` + Rs struct { + Prefix string `json:"prefix"` + StoreRef string `json:"store-ref"` + } `json:"rs"` + Rete struct { + JtRef string `json:"jt-ref"` + IDGenRef string `json:"idgen-ref"` + Jt string `json:"jt"` + } `json:"rete"` + Stores Service `json:"stores"` + IDGens Service `json:"idgens"` + Jts Service `json:"jts"` +} diff --git a/rete/conflict.go b/rete/conflict.go index 3aff2d1..6d2cba6 100644 --- a/rete/conflict.go +++ b/rete/conflict.go @@ -5,19 +5,14 @@ import ( "context" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" ) -type conflictRes interface { - addAgendaItem(rule model.Rule, tupleMap map[model.TupleType]model.Tuple) - resolveConflict(ctx context.Context) - deleteAgendaFor(ctx context.Context, tuple model.Tuple, changeProps map[string]bool) -} - type conflictResImpl struct { agendaList list.List } -func newConflictRes() conflictRes { +func newConflictRes() types.ConflictRes { cr := conflictResImpl{} cr.initCR() return &cr @@ -27,7 +22,7 @@ func (cr *conflictResImpl) initCR() { cr.agendaList = list.List{} } -func (cr *conflictResImpl) addAgendaItem(rule model.Rule, tupleMap map[model.TupleType]model.Tuple) { +func (cr *conflictResImpl) AddAgendaItem(rule model.Rule, tupleMap map[model.TupleType]model.Tuple) { item := newAgendaItem(rule, tupleMap) v := rule.GetPriority() found := false @@ -44,7 +39,7 @@ func (cr *conflictResImpl) addAgendaItem(rule model.Rule, tupleMap map[model.Tup } } -func (cr *conflictResImpl) resolveConflict(ctx context.Context) { +func (cr *conflictResImpl) ResolveConflict(ctx context.Context) { var item agendaItem front := cr.agendaList.Front() @@ -53,28 +48,29 @@ func (cr *conflictResImpl) resolveConflict(ctx context.Context) { if val != nil { item = val.(agendaItem) actionTuples := item.getTuples() - actionFn := item.getRule().GetActionFn() - if actionFn != nil { + // execute rule action service + aService := item.getRule().GetActionService() + if aService != nil { reteCtxV := getReteCtx(ctx) - actionFn(ctx, reteCtxV.getRuleSession(), item.getRule().GetName(), actionTuples, item.getRule().GetContext()) + aService.Execute(ctx, reteCtxV.GetRuleSession(), item.getRule().GetName(), actionTuples, item.getRule().GetContext()) } } reteCtxV := getReteCtx(ctx) - reteCtxV.addRuleModifiedToOpsList() + reteCtxV.AddRuleModifiedToOpsList() - reteCtxV.copyRuleModifiedToRtcModified() + reteCtxV.CopyRuleModifiedToRtcModified() //action scoped, clear it for the next action - reteCtxV.resetModified() + reteCtxV.ResetModified() if reteCtxV != nil { - opsFront := reteCtxV.getOpsList().Front() + opsFront := reteCtxV.GetOpsList().Front() for opsFront != nil { - opsVal := reteCtxV.getOpsList().Remove(opsFront) + opsVal := reteCtxV.GetOpsList().Remove(opsFront) oprn := opsVal.(opsEntry) oprn.execute(ctx) - opsFront = reteCtxV.getOpsList().Front() + opsFront = reteCtxV.GetOpsList().Front() } } @@ -82,20 +78,20 @@ func (cr *conflictResImpl) resolveConflict(ctx context.Context) { } reteCtxV := getReteCtx(ctx) - reteCtxV.normalize() + reteCtxV.Normalize() //reteCtxV.printRtcChangeList() } -func (cr *conflictResImpl) deleteAgendaFor(ctx context.Context, modifiedTuple model.Tuple, changeProps map[string]bool) { +func (cr *conflictResImpl) DeleteAgendaFor(ctx context.Context, modifiedTuple model.Tuple, changeProps map[string]bool) { - hdlModified := getOrCreateHandle(ctx, modifiedTuple) + hdlModified := getHandleWithTuple(ctx, modifiedTuple) for e := cr.agendaList.Front(); e != nil; { item := e.Value.(agendaItem) next := e.Next() for _, tuple := range item.getTuples() { - hdl := getOrCreateHandle(ctx, tuple) + hdl := getHandleWithTuple(ctx, tuple) if hdl == hdlModified { //this agendaitem has the modified tuple, remove the agenda item! toRemove := true //check if the rule depends on this change prop diff --git a/rete/context.go b/rete/context.go index 2b2f4ae..f5cdd51 100644 --- a/rete/context.go +++ b/rete/context.go @@ -3,42 +3,19 @@ package rete import ( "container/list" "context" + "fmt" "github.com/project-flogo/rules/common/model" - "fmt" + "github.com/project-flogo/rules/rete/internal/types" ) var reteCTXKEY = model.RetecontextKeyType{} -type reteCtx interface { - getConflictResolver() conflictRes - getOpsList() *list.List - getNetwork() Network - getRuleSession() model.RuleSession - OnValueChange(tuple model.Tuple, prop string) - - getRtcAdded() map[string]model.Tuple - getRtcModified() map[string]model.RtcModified - getRtcDeleted() map[string]model.Tuple - - addToRtcAdded(tuple model.Tuple) - addToRtcModified(tuple model.Tuple) - addToRtcDeleted(tuple model.Tuple) - addRuleModifiedToOpsList() - - normalize() - copyRuleModifiedToRtcModified() - resetModified() - - printRtcChangeList() - -} - //store any context, may not know all keys upfront type reteCtxImpl struct { - cr conflictRes + cr types.ConflictRes opsList *list.List - network Network + network *reteNetworkImpl rs model.RuleSession //newly added tuples in the current RTC @@ -52,10 +29,9 @@ type reteCtxImpl struct { //modified tuples in the current RTC rtcModifyMap map[string]model.RtcModified - } -func newReteCtxImpl(network Network, rs model.RuleSession) reteCtx { +func newReteCtxImpl(network *reteNetworkImpl, rs model.RuleSession) types.ReteCtx { reteCtxVal := reteCtxImpl{} reteCtxVal.cr = newConflictRes() reteCtxVal.opsList = list.New() @@ -68,26 +44,26 @@ func newReteCtxImpl(network Network, rs model.RuleSession) reteCtx { return &reteCtxVal } -func (rctx *reteCtxImpl) getConflictResolver() conflictRes { +func (rctx *reteCtxImpl) GetConflictResolver() types.ConflictRes { return rctx.cr } -func (rctx *reteCtxImpl) getOpsList() *list.List { +func (rctx *reteCtxImpl) GetOpsList() *list.List { return rctx.opsList } -func (rctx *reteCtxImpl) getNetwork() Network { +func (rctx *reteCtxImpl) GetNetwork() types.Network { return rctx.network } -func (rctx *reteCtxImpl) getRuleSession() model.RuleSession { +func (rctx *reteCtxImpl) GetRuleSession() model.RuleSession { return rctx.rs } func (rctx *reteCtxImpl) OnValueChange(tuple model.Tuple, prop string) { //if handle does not exist means its new - if nil != rctx.network.getHandle(tuple) { + if nil != rctx.network.getHandle(context.WithValue(context.Background(), reteCTXKEY, rctx), tuple) { rtcModified := rctx.modifyMap[tuple.GetKey().String()] if rtcModified == nil { rtcModified = NewRtcModified(tuple) @@ -97,37 +73,35 @@ func (rctx *reteCtxImpl) OnValueChange(tuple model.Tuple, prop string) { } } -func (rctx *reteCtxImpl) getRtcAdded() map[string]model.Tuple { +func (rctx *reteCtxImpl) GetRtcAdded() map[string]model.Tuple { return rctx.addMap } -func (rctx *reteCtxImpl) getRtcModified() map[string]model.RtcModified { +func (rctx *reteCtxImpl) GetRtcModified() map[string]model.RtcModified { return rctx.rtcModifyMap } -func (rctx *reteCtxImpl) getRtcDeleted() map[string]model.Tuple { +func (rctx *reteCtxImpl) GetRtcDeleted() map[string]model.Tuple { return rctx.deleteMap } -func (rctx *reteCtxImpl) addToRtcAdded (tuple model.Tuple) { +func (rctx *reteCtxImpl) AddToRtcAdded(tuple model.Tuple) { rctx.addMap[tuple.GetKey().String()] = tuple } -func (rctx *reteCtxImpl) addToRtcModified (tuple model.Tuple) { +func (rctx *reteCtxImpl) AddToRtcModified(tuple model.Tuple) { rctx.addMap[tuple.GetKey().String()] = tuple } -func (rctx *reteCtxImpl) addToRtcDeleted (tuple model.Tuple) { +func (rctx *reteCtxImpl) AddToRtcDeleted(tuple model.Tuple) { rctx.deleteMap[tuple.GetKey().String()] = tuple } -func (rctx *reteCtxImpl) addRuleModifiedToOpsList() { +func (rctx *reteCtxImpl) AddRuleModifiedToOpsList() { for _, rtcModified := range rctx.modifyMap { - rctx.getOpsList().PushBack(newModifyEntry(rtcModified.GetTuple(), rtcModified.GetModifiedProps())) + rctx.GetOpsList().PushBack(newModifyEntry(rtcModified.GetTuple(), rtcModified.GetModifiedProps())) } } - - -func (rctx *reteCtxImpl) normalize() { +func (rctx *reteCtxImpl) Normalize() { //remove from modify map, those in add map for k, _ := range rctx.addMap { @@ -139,49 +113,42 @@ func (rctx *reteCtxImpl) normalize() { } } -func (rctx *reteCtxImpl) copyRuleModifiedToRtcModified () { +func (rctx *reteCtxImpl) CopyRuleModifiedToRtcModified() { for k, v := range rctx.modifyMap { rctx.rtcModifyMap[k] = v } } -func (rctx *reteCtxImpl) resetModified() { +func (rctx *reteCtxImpl) ResetModified() { rctx.modifyMap = make(map[string]model.RtcModified) } -func (rctx *reteCtxImpl) printRtcChangeList() { - for k, _ := range rctx.getRtcAdded() { - +func (rctx *reteCtxImpl) PrintRtcChangeList() { + for k := range rctx.GetRtcAdded() { fmt.Printf("Added Tuple: [%s]\n", k) - } - for k, _ := range rctx.getRtcModified() { - + for k := range rctx.GetRtcModified() { fmt.Printf("Modified Tuple: [%s]\n", k) - } - for k, _ := range rctx.getRtcDeleted() { - + for k := range rctx.GetRtcDeleted() { fmt.Printf("Deleted Tuple: [%s]\n", k) - } - } -func getReteCtx(ctx context.Context) reteCtx { +func getReteCtx(ctx context.Context) types.ReteCtx { intr := ctx.Value(reteCTXKEY) if intr == nil { return nil } - return intr.(reteCtx) + return intr.(types.ReteCtx) } -func newReteCtx(ctx context.Context, network Network, rs model.RuleSession) (context.Context, reteCtx) { +func newReteCtx(ctx context.Context, network *reteNetworkImpl, rs model.RuleSession) (context.Context, types.ReteCtx) { reteCtxVar := newReteCtxImpl(network, rs) ctx = context.WithValue(ctx, reteCTXKEY, reteCtxVar) return ctx, reteCtxVar } -func getOrSetReteCtx(ctx context.Context, network Network, rs model.RuleSession) (reteCtx, bool, context.Context) { +func getOrSetReteCtx(ctx context.Context, network *reteNetworkImpl, rs model.RuleSession) (types.ReteCtx, bool, context.Context) { isRecursive := false newCtx := ctx reteCtxVar := getReteCtx(ctx) diff --git a/rete/debug.test b/rete/debug.test deleted file mode 100755 index 2eca2fb..0000000 Binary files a/rete/debug.test and /dev/null differ diff --git a/rete/factory.go b/rete/factory.go new file mode 100644 index 0000000..ebd2c7b --- /dev/null +++ b/rete/factory.go @@ -0,0 +1,77 @@ +package rete + +import ( + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/mem" + "github.com/project-flogo/rules/rete/internal/redis" + "github.com/project-flogo/rules/rete/internal/types" +) + +type TypeFactory struct { + nw *reteNetworkImpl + config common.Config +} + +func NewFactory(nw *reteNetworkImpl, config common.Config) (*TypeFactory, error) { + tf := TypeFactory{ + nw: nw, + config: config, + } + + return &tf, nil +} + +func (f *TypeFactory) getJoinTableRefs() types.JtRefsService { + switch f.config.Rete.Jt { + case common.ServiceTypeMem: + return mem.NewJoinTableRefsInHdlImpl(f.nw, f.config) + case common.ServiceTypeRedis: + return redis.NewJoinTableRefsInHdlImpl(f.nw, f.config) + default: + panic("invalid service type") + } +} + +func (f *TypeFactory) getJoinTableCollection() types.JtService { + switch f.config.Rete.Jt { + case common.ServiceTypeMem: + return mem.NewJoinTableCollection(f.nw, f.config) + case common.ServiceTypeRedis: + return redis.NewJoinTableCollection(f.nw, f.config) + default: + panic("invalid service type") + } +} + +func (f *TypeFactory) getHandleCollection() types.HandleService { + switch f.config.Rete.JtRef { + case common.ServiceTypeMem: + return mem.NewHandleCollection(f.nw, f.config) + case common.ServiceTypeRedis: + return redis.NewHandleCollection(f.nw, f.config) + default: + panic("invalid service type") + } +} + +func (f *TypeFactory) getIdGen() types.IdGen { + switch f.config.Rete.IDGenRef { + case common.ServiceTypeMem: + return mem.NewIdGenImpl(f.nw, f.config) + case common.ServiceTypeRedis: + return redis.NewIdGenImpl(f.nw, f.config) + default: + panic("invalid service type") + } +} + +func (f *TypeFactory) getLockService() types.LockService { + switch f.config.Rete.IDGenRef { + case common.ServiceTypeMem: + return mem.NewLockServiceImpl(f.nw, f.config) + case common.ServiceTypeRedis: + return redis.NewLockServiceImpl(f.nw, f.config) + default: + panic("invalid service type") + } +} diff --git a/rete/filternode.go b/rete/filternode.go index 0781935..539c6ba 100644 --- a/rete/filternode.go +++ b/rete/filternode.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" ) //filter node holds the filter condition @@ -18,13 +19,13 @@ type filterNodeImpl struct { convert []int } -func newFilterNode(nw Network, rule model.Rule, identifiers []model.TupleType, conditionVar model.Condition) filterNode { +func newFilterNode(nw *reteNetworkImpl, rule model.Rule, identifiers []model.TupleType, conditionVar model.Condition) filterNode { fn := filterNodeImpl{} fn.initFilterNodeImpl(nw, rule, identifiers, conditionVar) return &fn } -func (fn *filterNodeImpl) initFilterNodeImpl(nw Network, rule model.Rule, identifiers []model.TupleType, conditionVar model.Condition) { +func (fn *filterNodeImpl) initFilterNodeImpl(nw *reteNetworkImpl, rule model.Rule, identifiers []model.TupleType, conditionVar model.Condition) { fn.nodeImpl.initNodeImpl(nw, rule, identifiers) fn.conditionVar = conditionVar fn.setConvert() @@ -60,23 +61,23 @@ func (fn *filterNodeImpl) String() string { switch fn.nodeLinkVar.getChild().(type) { case *joinNodeImpl: if fn.nodeLinkVar.isRightNode() { - linkTo += "j" + strconv.Itoa(fn.nodeLinkVar.getChild().getID()) + "R" + linkTo += "j" + strconv.Itoa(fn.nodeLinkVar.getChild().GetID()) + "R" } else { - linkTo += "j" + strconv.Itoa(fn.nodeLinkVar.getChild().getID()) + "L" + linkTo += "j" + strconv.Itoa(fn.nodeLinkVar.getChild().GetID()) + "L" } case *filterNodeImpl: - linkTo += "f" + strconv.Itoa(fn.nodeLinkVar.getChild().getID()) + linkTo += "f" + strconv.Itoa(fn.nodeLinkVar.getChild().GetID()) case *ruleNodeImpl: - linkTo += "r" + strconv.Itoa(fn.nodeLinkVar.getChild().getID()) + linkTo += "r" + strconv.Itoa(fn.nodeLinkVar.getChild().GetID()) } - return "\t[FilterNode id(" + strconv.Itoa(fn.nodeImpl.id) + ") link(" + linkTo + "):\n" + + return "\t[FilterNode id(" + strconv.Itoa(fn.nodeImpl.GetID()) + ") link(" + linkTo + "):\n" + "\t\tIdentifier = " + model.IdentifiersToString(fn.identifiers) + " ;\n" + "\t\tCondition Identifiers = " + cond + ";\n" + "\t\tCondition = " + fn.conditionVar.String() + "]" } -func (fn *filterNodeImpl) assertObjects(ctx context.Context, handles []reteHandle, isRight bool) { +func (fn *filterNodeImpl) assertObjects(ctx context.Context, handles []types.ReteHandle, isRight bool) { if fn.conditionVar == nil { fn.nodeLinkVar.propagateObjects(ctx, handles) } else { @@ -88,15 +89,19 @@ func (fn *filterNodeImpl) assertObjects(ctx context.Context, handles []reteHandl } else { tuples = make([]model.Tuple, len(fn.convert)) for i := 0; i < len(fn.convert); i++ { - tuples[i] = handles[fn.convert[i]].getTuple() + tuples[i] = handles[fn.convert[i]].GetTuple() // tupleMap[tuples[i].GetTupleType()] = tuples[i] } } tupleMap := convertToTupleMap(tuples) cv := fn.conditionVar - toPropagate := cv.GetEvaluator()(cv.GetName(), cv.GetRule().GetName(), tupleMap, cv.GetContext()) - if toPropagate { - fn.nodeLinkVar.propagateObjects(ctx, handles) + toPropagate, err := cv.Evaluate(cv.GetName(), cv.GetRule().GetName(), tupleMap, cv.GetContext()) + if err == nil { + if toPropagate { + fn.nodeLinkVar.propagateObjects(ctx, handles) + } + } else { + //todo } } } diff --git a/rete/internal/mem/mhandleservice.go b/rete/internal/mem/mhandleservice.go new file mode 100644 index 0000000..81dc444 --- /dev/null +++ b/rete/internal/mem/mhandleservice.go @@ -0,0 +1,114 @@ +package mem + +import ( + "context" + "math/rand" + "sync" + "sync/atomic" + "time" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type handleServiceImpl struct { + types.NwServiceImpl + allHandles map[string]*reteHandleImpl + rand.Source + sync.RWMutex +} + +func NewHandleCollection(nw types.Network, config common.Config) types.HandleService { + hc := handleServiceImpl{ + NwServiceImpl: types.NwServiceImpl{ + Nw: nw, + }, + allHandles: make(map[string]*reteHandleImpl), + Source: rand.NewSource(time.Now().UnixNano()), + } + return &hc +} + +func (hc *handleServiceImpl) Init() { +} + +func (hc *handleServiceImpl) RemoveHandle(tuple model.Tuple) types.ReteHandle { + hc.Lock() + defer hc.Unlock() + rh, found := hc.allHandles[tuple.GetKey().String()] + if found { + delete(hc.allHandles, tuple.GetKey().String()) + return rh + } + return nil +} + +func (hc *handleServiceImpl) GetHandle(ctx context.Context, tuple model.Tuple) types.ReteHandle { + hc.RLock() + defer hc.RUnlock() + return hc.allHandles[tuple.GetKey().String()] +} + +func (hc *handleServiceImpl) GetHandleByKey(ctx context.Context, key model.TupleKey) types.ReteHandle { + hc.RLock() + defer hc.RUnlock() + reteHndl, ok := hc.allHandles[key.String()] + if ok { + return reteHndl + } + return nil +} + +func (hc *handleServiceImpl) GetOrCreateLockedHandle(nw types.Network, tuple model.Tuple) (types.ReteHandle, bool) { + hc.Lock() + defer hc.Unlock() + id := hc.Int63() + h, found := hc.allHandles[tuple.GetKey().String()] + if !found { + h = newReteHandleImpl(nw, tuple, types.ReteHandleStatusCreating, id) + hc.allHandles[tuple.GetKey().String()] = h + return h, false + } + if atomic.CompareAndSwapInt64(&h.id, -1, id) { + return h, false + } + + return nil, true +} + +func (hc *handleServiceImpl) GetLockedHandle(nw types.Network, tuple model.Tuple) (handle types.ReteHandle, locked, dne bool) { + hc.Lock() + defer hc.Unlock() + h, found := hc.allHandles[tuple.GetKey().String()] + if !found { + return nil, false, true + } + + id := hc.Int63() + if atomic.CompareAndSwapInt64(&h.id, -1, id) { + return h, false, false + } + + return nil, true, false +} + +func (hc *handleServiceImpl) GetHandleWithTuple(nw types.Network, tuple model.Tuple) types.ReteHandle { + hc.Lock() + defer hc.Unlock() + h, found := hc.allHandles[tuple.GetKey().String()] + if !found { + h = newReteHandleImpl(nw, tuple, types.ReteHandleStatusCreating, int64(-1)) + + hc.allHandles[tuple.GetKey().String()] = h //[tuple.GetKey().String()] = h + } + return h +} + +func (hc *handleServiceImpl) GetAllHandles(nw types.Network) map[string]types.ReteHandle { + all := make(map[string]types.ReteHandle) + for k, v := range hc.allHandles { + all[k] = v + } + return all +} diff --git a/rete/internal/mem/midgenservice.go b/rete/internal/mem/midgenservice.go new file mode 100644 index 0000000..81bc74d --- /dev/null +++ b/rete/internal/mem/midgenservice.go @@ -0,0 +1,36 @@ +package mem + +import ( + "sync/atomic" + + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type idGenServiceImpl struct { + types.NwServiceImpl + config common.Config + currentId int32 +} + +func NewIdGenImpl(nw types.Network, config common.Config) types.IdGen { + idg := idGenServiceImpl{} + idg.config = config + idg.currentId = 0 + idg.Nw = nw + return &idg +} + +func (id *idGenServiceImpl) Init() { + id.currentId = int32(id.GetMaxID()) +} + +func (id *idGenServiceImpl) GetNextID() int { + i := atomic.AddInt32(&id.currentId, 1) + return int(i) +} + +func (id *idGenServiceImpl) GetMaxID() int { + i := atomic.LoadInt32(&id.currentId) + return int(i) +} diff --git a/rete/internal/mem/mjointableimpl.go b/rete/internal/mem/mjointableimpl.go new file mode 100644 index 0000000..f561a20 --- /dev/null +++ b/rete/internal/mem/mjointableimpl.go @@ -0,0 +1,132 @@ +package mem + +import ( + "container/list" + "context" + "sync" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" +) + +type joinTableImpl struct { + types.NwElemIdImpl + table map[int]types.JoinTableRow + idr []model.TupleType + rule model.Rule + name string + sync.RWMutex +} + +func newJoinTableImpl(nw types.Network, rule model.Rule, identifiers []model.TupleType, name string) types.JoinTable { + jt := joinTableImpl{} + jt.initJoinTableImpl(nw, rule, identifiers, name) + return &jt +} + +func (jt *joinTableImpl) initJoinTableImpl(nw types.Network, rule model.Rule, identifiers []model.TupleType, name string) { + jt.SetID(nw) + jt.idr = identifiers + jt.table = map[int]types.JoinTableRow{} + jt.rule = rule + jt.name = name +} + +func (jt *joinTableImpl) AddRow(handles []types.ReteHandle) types.JoinTableRow { + row := newJoinTableRow(handles, jt.Nw) + for i := 0; i < len(row.GetHandles()); i++ { + handle := row.GetHandles()[i] + jt.Nw.GetJtRefService().AddEntry(handle, jt.name, row.GetID()) + } + jt.Lock() + defer jt.Unlock() + jt.table[row.GetID()] = row + return row +} + +func (jt *joinTableImpl) RemoveRow(rowID int) types.JoinTableRow { + jt.Lock() + defer jt.Unlock() + row, found := jt.table[rowID] + if found { + delete(jt.table, rowID) + return row + } + return nil +} + +func (jt *joinTableImpl) GetRowCount() int { + jt.RLock() + defer jt.RUnlock() + return len(jt.table) +} + +func (jt *joinTableImpl) GetRule() model.Rule { + return jt.rule +} + +func (jt *joinTableImpl) GetRowIterator(ctx context.Context) types.JointableRowIterator { + return newRowIterator(jt) +} + +func (jt *joinTableImpl) GetRow(ctx context.Context, rowID int) types.JoinTableRow { + jt.RLock() + defer jt.RUnlock() + return jt.table[rowID] +} + +func (jt *joinTableImpl) GetName() string { + return jt.name +} + +func (jt *joinTableImpl) RemoveAllRows(ctx context.Context) { + rowIter := jt.GetRowIterator(ctx) + for rowIter.HasNext() { + row := rowIter.Next() + //first, from jTable, remove row + jt.RemoveRow(row.GetID()) + for _, hdl := range row.GetHandles() { + jt.Nw.GetJtRefService().RemoveEntry(hdl, jt.GetName(), row.GetID()) + } + //Delete the rowRef itself + rowIter.Remove() + } +} + +type rowIteratorImpl struct { + table *joinTableImpl + list list.List + currKey int + curr *list.Element +} + +func newRowIterator(jt *joinTableImpl) types.JointableRowIterator { + ri := rowIteratorImpl{ + table: jt, + } + jt.RLock() + defer jt.RUnlock() + for k := range jt.table { + ri.list.PushBack(k) + } + ri.curr = ri.list.Front() + return &ri +} + +func (ri *rowIteratorImpl) HasNext() bool { + return ri.curr != nil +} + +func (ri *rowIteratorImpl) Next() types.JoinTableRow { + ri.currKey = ri.curr.Value.(int) + ri.curr = ri.curr.Next() + ri.table.RLock() + defer ri.table.RUnlock() + return ri.table.table[ri.currKey] +} + +func (ri *rowIteratorImpl) Remove() { + ri.table.Lock() + defer ri.table.Unlock() + delete(ri.table.table, ri.currKey) +} diff --git a/rete/internal/mem/mjointablerowimpl.go b/rete/internal/mem/mjointablerowimpl.go new file mode 100644 index 0000000..a7fd5c5 --- /dev/null +++ b/rete/internal/mem/mjointablerowimpl.go @@ -0,0 +1,24 @@ +package mem + +import "github.com/project-flogo/rules/rete/internal/types" + +type joinTableRowImpl struct { + types.NwElemIdImpl + handles []types.ReteHandle +} + +func newJoinTableRow(handles []types.ReteHandle, nw types.Network) types.JoinTableRow { + jtr := joinTableRowImpl{ + handles: append([]types.ReteHandle{}, handles...), + } + jtr.SetID(nw) + return &jtr +} + +func (jtr *joinTableRowImpl) Write() { + +} + +func (jtr *joinTableRowImpl) GetHandles() []types.ReteHandle { + return jtr.handles +} diff --git a/rete/internal/mem/mjtrefsservice.go b/rete/internal/mem/mjtrefsservice.go new file mode 100644 index 0000000..8fb76ed --- /dev/null +++ b/rete/internal/mem/mjtrefsservice.go @@ -0,0 +1,124 @@ +package mem + +import ( + "container/list" + "context" + "sync" + + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type jtRowsImpl struct { + rows map[int]string + sync.RWMutex +} + +type jtRefsServiceImpl struct { + types.NwServiceImpl + tablesAndRows map[string]*jtRowsImpl + sync.RWMutex +} + +func NewJoinTableRefsInHdlImpl(nw types.Network, config common.Config) types.JtRefsService { + hdlJt := jtRefsServiceImpl{} + hdlJt.Nw = nw + hdlJt.tablesAndRows = make(map[string]*jtRowsImpl) + return &hdlJt +} + +func (h *jtRefsServiceImpl) Init() { + +} + +func (h *jtRefsServiceImpl) AddEntry(handle types.ReteHandle, jtName string, rowID int) { + key := handle.GetTupleKey().String() + h.Lock() + defer h.Unlock() + tblMap, found := h.tablesAndRows[key] + if !found { + tblMap = &jtRowsImpl{ + rows: make(map[int]string), + } + h.tablesAndRows[handle.GetTupleKey().String()] = tblMap + } + tblMap.Lock() + defer tblMap.Unlock() + tblMap.rows[rowID] = jtName +} + +func (h *jtRefsServiceImpl) RemoveEntry(handle types.ReteHandle, jtName string, rowID int) { + key := handle.GetTupleKey().String() + h.Lock() + defer h.Unlock() + tblMap, found := h.tablesAndRows[key] + if found { + tblMap.Lock() + defer tblMap.Unlock() + delete(tblMap.rows, rowID) + if len(tblMap.rows) == 0 { + delete(h.tablesAndRows, key) + } + } +} + +type hdlRefsRowIterator struct { + ctx context.Context + refs *jtRefsServiceImpl + key string + rows *jtRowsImpl + list list.List + current *list.Element + rowID int + nw types.Network +} + +func (ri *hdlRefsRowIterator) HasNext() bool { + return ri.current != nil +} + +func (ri *hdlRefsRowIterator) Next() (types.JoinTableRow, types.JoinTable) { + rowID := ri.current.Value.(int) + ri.rowID = rowID + ri.current = ri.current.Next() + ri.rows.RLock() + defer ri.rows.RUnlock() + jT := ri.nw.GetJtService().GetJoinTable(ri.rows.rows[rowID]) + if jT != nil { + return jT.GetRow(ri.ctx, rowID), jT + } + return nil, jT +} + +func (ri *hdlRefsRowIterator) Remove() { + ri.rows.Lock() + defer ri.rows.Unlock() + delete(ri.rows.rows, ri.rowID) + if len(ri.rows.rows) == 0 { + ri.refs.Lock() + defer ri.refs.Unlock() + delete(ri.refs.tablesAndRows, ri.key) + } +} + +func (h *jtRefsServiceImpl) GetRowIterator(ctx context.Context, handle types.ReteHandle) types.JointableIterator { + key := handle.GetTupleKey().String() + ri := hdlRefsRowIterator{ + ctx: ctx, + refs: h, + key: key, + nw: h.Nw, + } + h.RLock() + defer h.RUnlock() + tblMap := h.tablesAndRows[key] + if tblMap == nil { + return &ri + } + ri.rows = tblMap + for rowID := range tblMap.rows { + ri.list.PushBack(rowID) + } + ri.current = ri.list.Front() + return &ri +} diff --git a/rete/internal/mem/mjtservice.go b/rete/internal/mem/mjtservice.go new file mode 100644 index 0000000..2d8115d --- /dev/null +++ b/rete/internal/mem/mjtservice.go @@ -0,0 +1,43 @@ +package mem + +import ( + "sync" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type jtServiceImpl struct { + types.NwServiceImpl + allJoinTables map[string]types.JoinTable + sync.RWMutex +} + +func NewJoinTableCollection(nw types.Network, config common.Config) types.JtService { + jtc := jtServiceImpl{} + jtc.Nw = nw + jtc.allJoinTables = make(map[string]types.JoinTable) + return &jtc +} + +func (jtc *jtServiceImpl) Init() { + +} + +func (jtc *jtServiceImpl) GetJoinTable(joinTableName string) types.JoinTable { + jtc.RLock() + defer jtc.RUnlock() + return jtc.allJoinTables[joinTableName] +} + +func (jtc *jtServiceImpl) GetOrCreateJoinTable(nw types.Network, rule model.Rule, identifiers []model.TupleType, name string) types.JoinTable { + jtc.Lock() + defer jtc.Unlock() + jT, found := jtc.allJoinTables[name] + if !found { + jT = newJoinTableImpl(nw, rule, identifiers, name) + jtc.allJoinTables[name] = jT + } + return jT +} diff --git a/rete/internal/mem/mlockservice.go b/rete/internal/mem/mlockservice.go new file mode 100644 index 0000000..e557853 --- /dev/null +++ b/rete/internal/mem/mlockservice.go @@ -0,0 +1,28 @@ +package mem + +import ( + "sync" + + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type lockServiceImpl struct { + types.NwServiceImpl + config common.Config + sync.Mutex +} + +func NewLockServiceImpl(nw types.Network, config common.Config) types.LockService { + lockService := lockServiceImpl{ + NwServiceImpl: types.NwServiceImpl{ + Nw: nw, + }, + config: config, + } + return &lockService +} + +func (l *lockServiceImpl) Init() { + +} diff --git a/rete/internal/mem/mretehandle.go b/rete/internal/mem/mretehandle.go new file mode 100644 index 0000000..2044782 --- /dev/null +++ b/rete/internal/mem/mretehandle.go @@ -0,0 +1,61 @@ +package mem + +import ( + "sync/atomic" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" +) + +//Holds a tuple reference and related state + +type reteHandleImpl struct { + types.NwElemIdImpl + tuple model.Tuple + tupleKey model.TupleKey + status types.ReteHandleStatus + id int64 +} + +func newReteHandleImpl(nw types.Network, tuple model.Tuple, status types.ReteHandleStatus, id int64) *reteHandleImpl { + h1 := reteHandleImpl{} + h1.initHandleImpl(nw, tuple, status, id) + return &h1 +} + +func (hdl *reteHandleImpl) SetTuple(tuple model.Tuple) { + hdl.tuple = tuple + hdl.tupleKey = tuple.GetKey() +} + +func (hdl *reteHandleImpl) initHandleImpl(nw types.Network, tuple model.Tuple, status types.ReteHandleStatus, id int64) { + hdl.SetID(nw) + hdl.SetTuple(tuple) + hdl.tupleKey = tuple.GetKey() + hdl.status = status + hdl.id = id +} + +func (hdl *reteHandleImpl) GetTuple() model.Tuple { + return hdl.tuple +} + +func (hdl *reteHandleImpl) GetTupleKey() model.TupleKey { + return hdl.tupleKey +} + +func (hdl *reteHandleImpl) SetStatus(status types.ReteHandleStatus) { + hdl.status = status +} + +func (hdl *reteHandleImpl) Unlock() { + atomic.StoreInt64(&hdl.id, -1) +} + +func (hdl *reteHandleImpl) GetStatus() types.ReteHandleStatus { + return hdl.status +} + +func (hdl *reteHandleImpl) AddJoinTableRowRef(joinTableRowVar types.JoinTableRow, joinTableVar types.JoinTable) { + hdl.Nw.GetJtRefService().AddEntry(hdl, joinTableVar.GetName(), joinTableRowVar.GetID()) +} diff --git a/rete/internal/redis/redis_test.go b/rete/internal/redis/redis_test.go new file mode 100644 index 0000000..522597f --- /dev/null +++ b/rete/internal/redis/redis_test.go @@ -0,0 +1,194 @@ +package redis + +import ( + "context" + "math/rand" + "net" + "os" + "os/exec" + "strings" + "testing" + "time" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +func Drain(port string) { + for { + conn, err := net.DialTimeout("tcp", net.JoinHostPort("", port), time.Second) + if conn != nil { + conn.Close() + } + if err != nil && strings.Contains(err.Error(), "connect: connection refused") { + break + } + } +} + +func Pour(port string) { + for { + conn, _ := net.Dial("tcp", net.JoinHostPort("", port)) + if conn != nil { + conn.Close() + break + } + } +} + +func TestMain(m *testing.M) { + run := func() int { + command := exec.Command("docker", "run", "-p", "6384:6379", "-d", "redis") + hash, err := command.Output() + if err != nil { + panic(err) + } + Pour("6384") + + defer func() { + command := exec.Command("docker", "stop", strings.TrimSpace(string(hash))) + err := command.Run() + if err != nil { + panic(err) + } + command = exec.Command("docker", "rm", strings.TrimSpace(string(hash))) + err = command.Run() + if err != nil { + panic(err) + } + Drain("6384") + }() + + return m.Run() + } + os.Exit(run()) +} + +type testNetwork struct { + Prefix string +} + +func (n *testNetwork) AddRule(model.Rule) error { + return nil +} + +func (n *testNetwork) String() string { + return "" +} + +func (n *testNetwork) RemoveRule(string) model.Rule { + return nil +} + +func (n *testNetwork) GetRules() []model.Rule { + return nil +} + +func (n *testNetwork) Assert(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error { + return nil +} + +func (n *testNetwork) Retract(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error { + return nil +} + +func (n *testNetwork) GetAssertedTuple(ctx context.Context, rs model.RuleSession, key model.TupleKey) model.Tuple { + return nil +} + +func (n *testNetwork) RegisterRtcTransactionHandler(txnHandler model.RtcTransactionHandler, txnContext interface{}) { + +} + +func (n *testNetwork) SetTupleStore(tupleStore model.TupleStore) { + +} + +func (n *testNetwork) GetHandleWithTuple(ctx context.Context, tuple model.Tuple) types.ReteHandle { + return nil +} + +func (n *testNetwork) AssertInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error { + return nil +} + +func (n *testNetwork) RetractInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error { + return nil +} + +func (n *testNetwork) GetPrefix() string { + return n.Prefix +} + +func (n *testNetwork) GetIdGenService() types.IdGen { + return nil +} + +func (n *testNetwork) GetLockService() types.LockService { + return nil +} + +func (n *testNetwork) GetJtService() types.JtService { + return nil +} + +func (n *testNetwork) GetHandleService() types.HandleService { + return nil +} + +func (n *testNetwork) GetJtRefService() types.JtRefsService { + return nil +} + +func (n *testNetwork) GetTupleStore() model.TupleStore { + return nil +} + +func (n *testNetwork) ReplayTuplesForRule(ruleName string, rs model.RuleSession) (err error) { + return nil +} + +func TestLockServiceImpl(t *testing.T) { + fini := make(chan bool, 1) + go func() { + config := common.Config{ + IDGens: common.Service{ + Redis: redisutils.RedisConfig{ + Network: "tcp", + Address: ":6384", + }, + }, + } + network := &testNetwork{ + Prefix: "a", + } + serviceA, serviceB := NewLockServiceImpl(network, config), NewLockServiceImpl(network, config) + done := make(chan bool, 8) + for i := 0; i < 100; i++ { + go func() { + serviceA.Lock() + time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond) + serviceA.Unlock() + done <- true + }() + go func() { + serviceB.Lock() + time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond) + serviceB.Unlock() + done <- true + }() + } + for i := 0; i < 100; i++ { + <-done + <-done + } + fini <- true + }() + select { + case <-time.After(60 * time.Second): + t.Fatal("test took too long") + case <-fini: + } +} diff --git a/rete/internal/redis/rhandleservice.go b/rete/internal/redis/rhandleservice.go new file mode 100644 index 0000000..78dd135 --- /dev/null +++ b/rete/internal/redis/rhandleservice.go @@ -0,0 +1,236 @@ +package redis + +import ( + "context" + "math/rand" + "strconv" + "strings" + "sync" + "time" + + "github.com/project-flogo/core/support/log" + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type handleServiceImpl struct { + types.NwServiceImpl + prefix string + config common.Config + rand.Source + sync.Mutex + redisutils.RedisHdl +} + +func NewHandleCollection(nw types.Network, config common.Config) types.HandleService { + hc := handleServiceImpl{ + NwServiceImpl: types.NwServiceImpl{ + Nw: nw, + }, + config: config, + Source: rand.NewSource(time.Now().UnixNano()), + RedisHdl: redisutils.NewRedisHdl(config.Jts.Redis), + } + return &hc +} + +func (hc *handleServiceImpl) Int63() int64 { + hc.Lock() + defer hc.Unlock() + return hc.Source.Int63() +} + +func (hc *handleServiceImpl) Init() { + hc.prefix = hc.Nw.GetPrefix() + ":h:" +} + +func (hc *handleServiceImpl) RemoveHandle(tuple model.Tuple) types.ReteHandle { + rkey := hc.prefix + tuple.GetKey().String() + hc.Del(rkey) + //TODO: Dummy handle + h := newReteHandleImpl(hc.GetNw(), hc.RedisHdl, tuple, rkey, types.ReteHandleStatusUnknown, -1) + return h + +} + +func (hc *handleServiceImpl) GetHandle(ctx context.Context, tuple model.Tuple) types.ReteHandle { + return hc.GetHandleByKey(ctx, tuple.GetKey()) +} + +func (hc *handleServiceImpl) GetHandleByKey(ctx context.Context, key model.TupleKey) types.ReteHandle { + rkey := hc.prefix + key.String() + + m := hc.HGetAll(rkey) + if len(m) == 0 { + return nil + } + status := types.ReteHandleStatusUnknown + if value, ok := m["status"]; ok { + if value, ok := value.(string); ok { + number, err := strconv.Atoi(value) + if err != nil { + panic(err) + } + status = types.ReteHandleStatus(number) + } else { + panic("status not string") + } + } else { + panic("missing status") + } + id := int64(-1) + if value, ok := m["id"]; ok { + if value, ok := value.(string); ok { + number, err := strconv.ParseInt(value, 10, 64) + if err != nil { + panic(err) + } + id = number + } + } + + var tuple model.Tuple + if ctx != nil { + if value := ctx.Value(model.RetecontextKeyType{}); value != nil { + if value, ok := value.(types.ReteCtx); ok { + if modified := value.GetRtcModified(); modified != nil { + if value := modified[key.String()]; value != nil { + tuple = value.GetTuple() + } + } + if tuple == nil { + if added := value.GetRtcAdded(); added != nil { + tuple = added[key.String()] + } + } + } + } + } + if tuple == nil { + tuple = hc.Nw.GetTupleStore().GetTupleByKey(key) + } + if tuple == nil { + //TODO: error handling + return nil + } + + h := newReteHandleImpl(hc.GetNw(), hc.RedisHdl, tuple, rkey, status, id) + return h +} + +func (hc *handleServiceImpl) GetOrCreateLockedHandle(nw types.Network, tuple model.Tuple) (types.ReteHandle, bool) { + id := hc.Int63() + key, status := hc.prefix+tuple.GetKey().String(), types.ReteHandleStatusCreating + + exists, _ := hc.HSetNX(key, "id", id) + if exists { + return nil, true + } + + exists, _ = hc.HSetNX(key, "status", status) + if exists { + m := hc.HGetAll(key) + if len(m) > 0 { + if value, ok := m["status"]; ok { + if value, ok := value.(string); ok { + number, err := strconv.Atoi(value) + if err != nil { + panic(err) + } + status = types.ReteHandleStatus(number) + } else { + panic("status not string") + } + } else { + panic("missing status") + } + } + } + + h := newReteHandleImpl(nw, hc.RedisHdl, tuple, key, status, id) + return h, false +} + +func (hc *handleServiceImpl) GetLockedHandle(nw types.Network, tuple model.Tuple) (handle types.ReteHandle, locked, dne bool) { + id := hc.Int63() + key := hc.prefix + tuple.GetKey().String() + + locked, _ = hc.HSetNX(key, "id", id) + if locked { + return nil, true, false + } + + m := hc.HGetAll(key) + if len(m) > 0 { + if value, ok := m["status"]; ok { + if value, ok := value.(string); ok { + number, err := strconv.Atoi(value) + if err != nil { + panic(err) + } + handle = newReteHandleImpl(nw, hc.RedisHdl, tuple, key, types.ReteHandleStatus(number), id) + return handle, false, false + } + } + } + + hc.Del(key) + + return nil, false, true +} + +func (hc *handleServiceImpl) GetHandleWithTuple(nw types.Network, tuple model.Tuple) types.ReteHandle { + key, status, id := hc.prefix+tuple.GetKey().String(), types.ReteHandleStatusCreating, int64(-1) + m := hc.HGetAll(key) + if len(m) > 0 { + if value, ok := m["status"]; ok { + if value, ok := value.(string); ok { + number, err := strconv.Atoi(value) + if err != nil { + panic(err) + } + status = types.ReteHandleStatus(number) + } else { + panic("status not string") + } + } else { + panic("missing status") + } + if value, ok := m["id"]; ok { + if value, ok := value.(string); ok { + number, err := strconv.ParseInt(value, 10, 64) + if err != nil { + panic(err) + } + id = number + } + } + } + + h := newReteHandleImpl(nw, hc.RedisHdl, tuple, key, status, id) + return h +} + +func (hc *handleServiceImpl) GetAllHandles(nw types.Network) map[string]types.ReteHandle { + + allHandles := make(map[string]types.ReteHandle) + + data, err := hc.GetKeys(hc.prefix + "*") + if err != nil { + log.RootLogger().Errorf("No keys found in redis: %s", err) + return allHandles + } + + for _, v := range data { + tupleKey := strings.Replace(v, hc.prefix, "", 1) + tuple := nw.GetTupleStore().GetTupleByKey(model.FromStringKey(tupleKey)) + if tuple != nil { + reteHandle := hc.GetHandleWithTuple(nw, tuple) + allHandles[tupleKey] = reteHandle + } + } + + return allHandles +} diff --git a/rete/internal/redis/ridgenservice.go b/rete/internal/redis/ridgenservice.go new file mode 100644 index 0000000..40235f0 --- /dev/null +++ b/rete/internal/redis/ridgenservice.go @@ -0,0 +1,41 @@ +package redis + +import ( + "fmt" + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type idGenServiceImpl struct { + types.NwServiceImpl + config common.Config + key string // key used to access idgen + fld string // redis field in key + redisutils.RedisHdl +} + +func NewIdGenImpl(nw types.Network, config common.Config) types.IdGen { + r := idGenServiceImpl{ + NwServiceImpl: types.NwServiceImpl{ + Nw: nw, + }, + config: config, + RedisHdl: redisutils.NewRedisHdl(config.IDGens.Redis), + } + return &r +} + +func (ri *idGenServiceImpl) Init() { + ri.key = ri.Nw.GetPrefix() + ":idgen" + j := ri.GetMaxID() + fmt.Printf("maxid : [%d]\n ", j) +} + +func (ri *idGenServiceImpl) GetMaxID() int { + return ri.GetAsInt(ri.key) +} + +func (ri *idGenServiceImpl) GetNextID() int { + return ri.IncrBy(ri.key, 1) +} diff --git a/rete/internal/redis/rjointableimpl.go b/rete/internal/redis/rjointableimpl.go new file mode 100644 index 0000000..9a4230d --- /dev/null +++ b/rete/internal/redis/rjointableimpl.go @@ -0,0 +1,123 @@ +package redis + +import ( + "context" + "strconv" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/internal/types" +) + +type joinTableImpl struct { + types.NwElemIdImpl + redisutils.RedisHdl + idr []model.TupleType + rule model.Rule + name string + jtKey string +} + +func newJoinTableImpl(nw types.Network, handle redisutils.RedisHdl, rule model.Rule, identifiers []model.TupleType, name string) types.JoinTable { + jt := joinTableImpl{ + RedisHdl: handle, + } + jt.initJoinTableImpl(nw, rule, identifiers, name) + return &jt +} + +func (jt *joinTableImpl) initJoinTableImpl(nw types.Network, rule model.Rule, identifiers []model.TupleType, name string) { + jt.SetID(nw) + jt.idr = identifiers + jt.rule = rule + jt.name = name + jt.jtKey = nw.GetPrefix() + ":" + "jt:" + name +} + +func (jt *joinTableImpl) AddRow(handles []types.ReteHandle) types.JoinTableRow { + row := newJoinTableRow(jt.RedisHdl, jt.jtKey, handles, jt.Nw) + for i := 0; i < len(row.GetHandles()); i++ { + handle := row.GetHandles()[i] + jt.Nw.GetJtRefService().AddEntry(handle, jt.name, row.GetID()) + } + row.Write() + return row +} + +func (jt *joinTableImpl) RemoveRow(rowID int) types.JoinTableRow { + row := jt.GetRow(nil, rowID) + rowId := strconv.Itoa(rowID) + jt.HDel(jt.jtKey, rowId) + return row +} + +func (jt *joinTableImpl) RemoveAllRows(ctx context.Context) { + rowIter := jt.GetRowIterator(ctx) + for rowIter.HasNext() { + row := rowIter.Next() + //first, from jTable, remove row + jt.RemoveRow(row.GetID()) + for _, hdl := range row.GetHandles() { + jt.Nw.GetJtRefService().RemoveEntry(hdl, jt.GetName(), row.GetID()) + } + //Delete the rowRef itself + rowIter.Remove() + } +} + +func (jt *joinTableImpl) GetRowCount() int { + return jt.HLen(jt.name) +} + +func (jt *joinTableImpl) GetRule() model.Rule { + return jt.rule +} + +func (jt *joinTableImpl) GetRowIterator(ctx context.Context) types.JointableRowIterator { + return newRowIterator(ctx, jt.RedisHdl, jt) +} + +func (jt *joinTableImpl) GetRow(ctx context.Context, rowID int) types.JoinTableRow { + key := jt.HGet(jt.jtKey, strconv.Itoa(rowID)) + rowId := strconv.Itoa(rowID) + return createRow(ctx, jt.RedisHdl, jt.name, rowId, key.(string), jt.Nw) +} + +func (jt *joinTableImpl) GetName() string { + return jt.name +} + +type rowIteratorImpl struct { + ctx context.Context + iter *redisutils.MapIterator + jtName string + nw types.Network + curr types.JoinTableRow + redisutils.RedisHdl +} + +func newRowIterator(ctx context.Context, handle redisutils.RedisHdl, jTable types.JoinTable) types.JointableRowIterator { + key := jTable.GetNw().GetPrefix() + ":jt:" + jTable.GetName() + ri := rowIteratorImpl{} + ri.ctx = ctx + ri.iter = handle.GetMapIterator(key) + ri.nw = jTable.GetNw() + ri.jtName = jTable.GetName() + ri.RedisHdl = handle + return &ri +} + +func (ri *rowIteratorImpl) HasNext() bool { + return ri.iter.HasNext() +} + +func (ri *rowIteratorImpl) Next() types.JoinTableRow { + rowId, key := ri.iter.Next() + tupleKeyStr := key.(string) + ri.curr = createRow(ri.ctx, ri.RedisHdl, ri.jtName, rowId, tupleKeyStr, ri.nw) + return ri.curr +} + +func (ri *rowIteratorImpl) Remove() { + ri.iter.Remove() +} diff --git a/rete/internal/redis/rjointablerowimpl.go b/rete/internal/redis/rjointablerowimpl.go new file mode 100644 index 0000000..d2c4678 --- /dev/null +++ b/rete/internal/redis/rjointablerowimpl.go @@ -0,0 +1,95 @@ +package redis + +import ( + "context" + "strconv" + "strings" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/internal/types" +) + +type joinTableRowImpl struct { + types.NwElemIdImpl + handles []types.ReteHandle + jtKey string + redisutils.RedisHdl +} + +func newJoinTableRow(handle redisutils.RedisHdl, jtKey string, handles []types.ReteHandle, nw types.Network) types.JoinTableRow { + jtr := joinTableRowImpl{ + RedisHdl: handle, + jtKey: jtKey, + handles: append([]types.ReteHandle{}, handles...), + } + jtr.SetID(nw) + return &jtr +} + +func newJoinTableRowLoadedFromStore(handle redisutils.RedisHdl, jtKey string, rowID int, handles []types.ReteHandle, nw types.Network) types.JoinTableRow { + jtr := joinTableRowImpl{ + handles: handles, + jtKey: jtKey, + NwElemIdImpl: types.NwElemIdImpl{ + ID: rowID, + Nw: nw, + }, + RedisHdl: handle, + } + return &jtr +} + +func (jtr *joinTableRowImpl) Write() { + handles, row := jtr.handles, make(map[string]interface{}) + end, str := len(handles)-1, "" + for i, v := range handles { + str += v.GetTupleKey().String() + if i < end { + str += "," + } + } + row[strconv.Itoa(jtr.ID)] = str + jtr.HSetAll(jtr.jtKey, row) +} + +func (jtr *joinTableRowImpl) GetHandles() []types.ReteHandle { + return jtr.handles +} + +func createRow(ctx context.Context, handle redisutils.RedisHdl, jtKey string, rowID string, key string, nw types.Network) types.JoinTableRow { + + values := strings.Split(key, ",") + + handles := []types.ReteHandle{} + for _, key := range values { + tupleKey := model.FromStringKey(key) + var tuple model.Tuple + if ctx != nil { + if value := ctx.Value(model.RetecontextKeyType{}); value != nil { + if value, ok := value.(types.ReteCtx); ok { + if modified := value.GetRtcModified(); modified != nil { + if value := modified[tupleKey.String()]; value != nil { + tuple = value.GetTuple() + } + } + if tuple == nil { + if added := value.GetRtcAdded(); added != nil { + tuple = added[tupleKey.String()] + } + } + } + } + } + if tuple == nil { + tuple = nw.GetTupleStore().GetTupleByKey(tupleKey) + } + handle := newReteHandleImpl(nw, handle, tuple, "", types.ReteHandleStatusUnknown, 0) + handles = append(handles, handle) + } + + rowId, _ := strconv.Atoi(rowID) + jtRow := newJoinTableRowLoadedFromStore(handle, jtKey, rowId, handles, nw) + + return jtRow +} diff --git a/rete/internal/redis/rjtrefsservice.go b/rete/internal/redis/rjtrefsservice.go new file mode 100644 index 0000000..307f5aa --- /dev/null +++ b/rete/internal/redis/rjtrefsservice.go @@ -0,0 +1,76 @@ +package redis + +import ( + "context" + "strconv" + + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type jtRefsServiceImpl struct { + types.NwServiceImpl + redisutils.RedisHdl +} + +func NewJoinTableRefsInHdlImpl(nw types.Network, config common.Config) types.JtRefsService { + hdlJt := jtRefsServiceImpl{ + NwServiceImpl: types.NwServiceImpl{ + Nw: nw, + }, + RedisHdl: redisutils.NewRedisHdl(config.Jts.Redis), + } + return &hdlJt +} + +func (h *jtRefsServiceImpl) Init() { + +} + +func (h *jtRefsServiceImpl) AddEntry(handle types.ReteHandle, jtName string, rowID int) { + // format: prefix:rtbls:tkey ==> {rowID=jtname, ...} + key := h.Nw.GetPrefix() + ":rtbls:" + handle.GetTupleKey().String() + valMap := make(map[string]interface{}) + valMap[strconv.Itoa(rowID)] = jtName + h.HSetAll(key, valMap) +} + +func (h *jtRefsServiceImpl) RemoveEntry(handle types.ReteHandle, jtName string, rowID int) { + key := h.Nw.GetPrefix() + ":rtbls:" + handle.GetTupleKey().String() + h.HDel(key, strconv.Itoa(rowID)) +} + +type hdlRefsRowIteratorImpl struct { + ctx context.Context + key string + iter *redisutils.MapIterator + nw types.Network +} + +func (r *hdlRefsRowIteratorImpl) HasNext() bool { + return r.iter.HasNext() +} + +func (r *hdlRefsRowIteratorImpl) Next() (types.JoinTableRow, types.JoinTable) { + rowIDStr, jtName := r.iter.Next() + rowID, _ := strconv.Atoi(rowIDStr) + jT := r.nw.GetJtService().GetJoinTable(jtName.(string)) + if jT != nil { + return jT.GetRow(r.ctx, rowID), jT + } + return nil, jT +} + +func (r *hdlRefsRowIteratorImpl) Remove() { + r.iter.Remove() +} + +func (h *jtRefsServiceImpl) GetRowIterator(ctx context.Context, handle types.ReteHandle) types.JointableIterator { + r := hdlRefsRowIteratorImpl{} + r.ctx = ctx + r.nw = h.Nw + r.key = h.Nw.GetPrefix() + ":rtbls:" + handle.GetTupleKey().String() + r.iter = h.GetMapIterator(r.key) + return &r +} diff --git a/rete/internal/redis/rjtservice.go b/rete/internal/redis/rjtservice.go new file mode 100644 index 0000000..ef659b2 --- /dev/null +++ b/rete/internal/redis/rjtservice.go @@ -0,0 +1,49 @@ +package redis + +import ( + "sync" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type jtServiceImpl struct { + types.NwServiceImpl + allJoinTables map[string]types.JoinTable + redisutils.RedisHdl + sync.RWMutex +} + +func NewJoinTableCollection(nw types.Network, config common.Config) types.JtService { + jtc := jtServiceImpl{ + NwServiceImpl: types.NwServiceImpl{ + Nw: nw, + }, + allJoinTables: make(map[string]types.JoinTable), + RedisHdl: redisutils.NewRedisHdl(config.Jts.Redis), + } + return &jtc +} + +func (jtc *jtServiceImpl) Init() { + +} + +func (jtc *jtServiceImpl) GetJoinTable(name string) types.JoinTable { + jtc.RLock() + defer jtc.RUnlock() + return jtc.allJoinTables[name] +} + +func (jtc *jtServiceImpl) GetOrCreateJoinTable(nw types.Network, rule model.Rule, identifiers []model.TupleType, name string) types.JoinTable { + jtc.Lock() + defer jtc.Unlock() + jT, found := jtc.allJoinTables[name] + if !found { + jT = newJoinTableImpl(nw, jtc.RedisHdl, rule, identifiers, name) + jtc.allJoinTables[name] = jT + } + return jT +} diff --git a/rete/internal/redis/rlockservice.go b/rete/internal/redis/rlockservice.go new file mode 100644 index 0000000..137b31b --- /dev/null +++ b/rete/internal/redis/rlockservice.go @@ -0,0 +1,69 @@ +package redis + +import ( + "math/rand" + "strconv" + "sync" + "time" + + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" +) + +type lockServiceImpl struct { + types.NwServiceImpl + config common.Config + key string // key used to access lock + redisutils.RedisHdl + rand.Source + sync.Mutex + done chan bool +} + +func NewLockServiceImpl(nw types.Network, config common.Config) types.LockService { + r := lockServiceImpl{ + NwServiceImpl: types.NwServiceImpl{ + Nw: nw, + }, + config: config, + RedisHdl: redisutils.NewRedisHdl(config.IDGens.Redis), + Source: rand.NewSource(time.Now().UnixNano()), + done: make(chan bool, 1), + } + return &r +} + +func (l *lockServiceImpl) Init() { + l.key = l.Nw.GetPrefix() + ":lock" +} + +func (l *lockServiceImpl) Lock() { + l.Mutex.Lock() + value := strconv.FormatInt(l.Int63(), 10) + for { + ok, _ := l.Set(l.key, value, true, 16000) + if ok { + go func() { + defer l.Mutex.Unlock() + ticker := time.NewTicker(4 * time.Second) + for { + select { + case <-ticker.C: + l.Set(l.key, value, false, 16000) + case <-l.done: + ticker.Stop() + l.DelIfEqual(l.key, value) + return + } + } + }() + return + } + time.Sleep(128 * time.Millisecond) + } +} + +func (l *lockServiceImpl) Unlock() { + l.done <- true +} diff --git a/rete/internal/redis/rretehandle.go b/rete/internal/redis/rretehandle.go new file mode 100644 index 0000000..83edf65 --- /dev/null +++ b/rete/internal/redis/rretehandle.go @@ -0,0 +1,74 @@ +package redis + +import ( + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/internal/types" +) + +//Holds a tuple reference and related state + +type reteHandleImpl struct { + types.NwElemIdImpl + tuple model.Tuple + tupleKey model.TupleKey + key string + status types.ReteHandleStatus + id int64 + redisutils.RedisHdl + //jtRefs types.JtRefsService +} + +func newReteHandleImpl(nw types.Network, handle redisutils.RedisHdl, tuple model.Tuple, key string, status types.ReteHandleStatus, id int64) types.ReteHandle { + h1 := reteHandleImpl{ + RedisHdl: handle, + } + h1.initHandleImpl(nw, tuple, key, status) + return &h1 +} + +func (hdl *reteHandleImpl) SetTuple(tuple model.Tuple) { + hdl.tuple = tuple + if tuple != nil { + hdl.tupleKey = tuple.GetKey() + } +} + +func (hdl *reteHandleImpl) initHandleImpl(nw types.Network, tuple model.Tuple, key string, status types.ReteHandleStatus) { + hdl.SetID(nw) + hdl.SetTuple(tuple) + hdl.key = key + hdl.status = status +} + +func (hdl *reteHandleImpl) GetTuple() model.Tuple { + return hdl.tuple +} + +func (hdl *reteHandleImpl) GetTupleKey() model.TupleKey { + return hdl.tupleKey +} + +func (hdl *reteHandleImpl) SetStatus(status types.ReteHandleStatus) { + if hdl.key == "" { + return + } + hdl.HSet(hdl.key, "status", status) +} + +func (hdl *reteHandleImpl) Unlock() { + hdl.HDel(hdl.key, "id") +} + +func (hdl *reteHandleImpl) GetStatus() types.ReteHandleStatus { + return hdl.status +} + +func (hdl *reteHandleImpl) AddJoinTableRowRef(joinTableRowVar types.JoinTableRow, joinTableVar types.JoinTable) { + hdl.Nw.GetJtRefService().AddEntry(hdl, joinTableVar.GetName(), joinTableRowVar.GetID()) +} + +func (hdl *reteHandleImpl) GetRefTableIterator() types.JointableIterator { + refTblIterator := hdl.Nw.GetJtRefService().GetRowIterator(nil, hdl) + return refTblIterator +} diff --git a/rete/internal/types/nwelem.go b/rete/internal/types/nwelem.go new file mode 100644 index 0000000..e81f4f5 --- /dev/null +++ b/rete/internal/types/nwelem.go @@ -0,0 +1,23 @@ +package types + +type NwElemId interface { + SetID(nw Network) + GetID() int + GetNw() Network +} + +type NwElemIdImpl struct { + ID int + Nw Network +} + +func (ide *NwElemIdImpl) SetID(nw Network) { + ide.Nw = nw + ide.ID = nw.GetIdGenService().GetNextID() +} +func (ide *NwElemIdImpl) GetID() int { + return ide.ID +} +func (ide *NwElemIdImpl) GetNw() Network { + return ide.Nw +} diff --git a/rete/internal/types/nwservice.go b/rete/internal/types/nwservice.go new file mode 100644 index 0000000..314a18c --- /dev/null +++ b/rete/internal/types/nwservice.go @@ -0,0 +1,16 @@ +package types + +import "github.com/project-flogo/rules/common/model" + +type NwService interface { + model.Service + GetNw() Network +} + +type NwServiceImpl struct { + Nw Network +} + +func (nws *NwServiceImpl) GetNw() Network { + return nws.Nw +} diff --git a/rete/internal/types/types.go b/rete/internal/types/types.go new file mode 100644 index 0000000..305f538 --- /dev/null +++ b/rete/internal/types/types.go @@ -0,0 +1,141 @@ +package types + +import ( + "container/list" + "context" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/common" +) + +type Network interface { + common.Network + GetHandleWithTuple(ctx context.Context, tuple model.Tuple) ReteHandle + AssertInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error + RetractInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error + GetPrefix() string + GetIdGenService() IdGen + GetLockService() LockService + GetJtService() JtService + GetHandleService() HandleService + GetJtRefService() JtRefsService + GetTupleStore() model.TupleStore +} + +type ConflictRes interface { + AddAgendaItem(rule model.Rule, tupleMap map[model.TupleType]model.Tuple) + ResolveConflict(ctx context.Context) + DeleteAgendaFor(ctx context.Context, tuple model.Tuple, changeProps map[string]bool) +} + +type ReteCtx interface { + GetConflictResolver() ConflictRes + GetOpsList() *list.List + GetNetwork() Network + GetRuleSession() model.RuleSession + OnValueChange(tuple model.Tuple, prop string) + + GetRtcAdded() map[string]model.Tuple + GetRtcModified() map[string]model.RtcModified + GetRtcDeleted() map[string]model.Tuple + + AddToRtcAdded(tuple model.Tuple) + AddToRtcModified(tuple model.Tuple) + AddToRtcDeleted(tuple model.Tuple) + AddRuleModifiedToOpsList() + + Normalize() + CopyRuleModifiedToRtcModified() + ResetModified() + + PrintRtcChangeList() +} + +type JoinTable interface { + NwElemId + GetName() string + GetRule() model.Rule + + AddRow(handles []ReteHandle) JoinTableRow + RemoveRow(rowID int) JoinTableRow + GetRow(ctx context.Context, rowID int) JoinTableRow + GetRowIterator(ctx context.Context) JointableRowIterator + + GetRowCount() int + RemoveAllRows(ctx context.Context) //used when join table needs to be deleted +} + +type JoinTableRow interface { + NwElemId + Write() + GetHandles() []ReteHandle +} + +type ReteHandleStatus uint + +const ( + ReteHandleStatusUnknown ReteHandleStatus = iota + ReteHandleStatusCreating + ReteHandleStatusCreated + ReteHandleStatusDeleting + ReteHandleStatusRetracting + ReteHandleStatusRetracted +) + +type ReteHandle interface { + NwElemId + SetTuple(tuple model.Tuple) + GetTuple() model.Tuple + GetTupleKey() model.TupleKey + SetStatus(status ReteHandleStatus) + GetStatus() ReteHandleStatus + Unlock() +} + +type JtRefsService interface { + NwService + AddEntry(handle ReteHandle, jtName string, rowID int) + RemoveEntry(handle ReteHandle, jtName string, rowID int) + GetRowIterator(ctx context.Context, handle ReteHandle) JointableIterator +} + +type JtService interface { + NwService + GetOrCreateJoinTable(nw Network, rule model.Rule, identifiers []model.TupleType, name string) JoinTable + GetJoinTable(name string) JoinTable +} + +type HandleService interface { + NwService + RemoveHandle(tuple model.Tuple) ReteHandle + GetHandle(ctx context.Context, tuple model.Tuple) ReteHandle + GetHandleByKey(ctx context.Context, key model.TupleKey) ReteHandle + GetHandleWithTuple(nw Network, tuple model.Tuple) ReteHandle + GetOrCreateLockedHandle(nw Network, tuple model.Tuple) (ReteHandle, bool) + GetLockedHandle(nw Network, tuple model.Tuple) (handle ReteHandle, locked, dne bool) + GetAllHandles(nw Network) map[string]ReteHandle +} + +type IdGen interface { + NwService + GetMaxID() int + GetNextID() int +} + +type LockService interface { + NwService + Lock() + Unlock() +} + +type JointableIterator interface { + HasNext() bool + Next() (JoinTableRow, JoinTable) + Remove() +} + +type JointableRowIterator interface { + HasNext() bool + Next() JoinTableRow + Remove() +} diff --git a/rete/joinnode.go b/rete/joinnode.go index 6f533e1..5ddcb38 100644 --- a/rete/joinnode.go +++ b/rete/joinnode.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" ) //joinNode holds the join tables for unmatched entries @@ -26,23 +27,30 @@ type joinNodeImpl struct { joinIndexForLeft []int joinIndexForRight []int - leftTable joinTable - rightTable joinTable + leftTable types.JoinTable + rightTable types.JoinTable } -func newJoinNode(nw Network, rule model.Rule, leftIdrs []model.TupleType, rightIdrs []model.TupleType, conditionVar model.Condition) joinNode { +func newJoinNode(nw *reteNetworkImpl, rule model.Rule, leftIdrs []model.TupleType, rightIdrs []model.TupleType, conditionVar model.Condition) joinNode { jn := joinNodeImpl{} jn.initjoinNodeImplVar(nw, rule, leftIdrs, rightIdrs, conditionVar) return &jn } -func (jn *joinNodeImpl) initjoinNodeImplVar(nw Network, rule model.Rule, leftIdrs []model.TupleType, rightIdrs []model.TupleType, conditionVar model.Condition) { +func (jn *joinNodeImpl) initjoinNodeImplVar(nw *reteNetworkImpl, rule model.Rule, leftIdrs []model.TupleType, rightIdrs []model.TupleType, conditionVar model.Condition) { jn.initNodeImpl(nw, rule, nil) jn.leftIdrs = leftIdrs jn.rightIdrs = rightIdrs jn.conditionVar = conditionVar - jn.leftTable = newJoinTable(nw, rule, leftIdrs) - jn.rightTable = newJoinTable(nw, rule, rightIdrs) + + name := "" + if conditionVar == nil { + name = nw.getJoinNodeName() + } else { + name = conditionVar.GetName() + } + jn.leftTable = nw.GetJtService().GetOrCreateJoinTable(nw, rule, leftIdrs, "L_"+name) + jn.rightTable = nw.GetJtService().GetOrCreateJoinTable(nw, rule, rightIdrs, "R_"+name) jn.setJoinIdentifiers() } @@ -128,12 +136,12 @@ func (jn *joinNodeImpl) String() string { switch jn.nodeLinkVar.getChild().(type) { case *joinNodeImpl: if jn.nodeLinkVar.isRightNode() { - linkTo += strconv.Itoa(jn.nodeLinkVar.getChild().getID()) + "R" + linkTo += strconv.Itoa(jn.nodeLinkVar.getChild().GetID()) + "R" } else { - linkTo += strconv.Itoa(jn.nodeLinkVar.getChild().getID()) + "L" + linkTo += strconv.Itoa(jn.nodeLinkVar.getChild().GetID()) + "L" } default: - linkTo += strconv.Itoa(jn.nodeLinkVar.getChild().getID()) + linkTo += strconv.Itoa(jn.nodeLinkVar.getChild().GetID()) } joinConditionStr := "nil" @@ -152,9 +160,9 @@ func (jn *joinNodeImpl) String() string { "\t\tCondition = " + joinConditionStr + "]\n" } -func (jn *joinNodeImpl) assertObjects(ctx context.Context, handles []reteHandle, isRight bool) { +func (jn *joinNodeImpl) assertObjects(ctx context.Context, handles []types.ReteHandle, isRight bool) { //TODO: - joinedHandles := make([]reteHandle, jn.totalIdrLen) + joinedHandles := make([]types.ReteHandle, jn.totalIdrLen) if isRight { jn.assertFromRight(ctx, handles, joinedHandles) } else { @@ -162,15 +170,43 @@ func (jn *joinNodeImpl) assertObjects(ctx context.Context, handles []reteHandle, } } -func (jn *joinNodeImpl) assertFromRight(ctx context.Context, handles []reteHandle, joinedHandles []reteHandle) { +func (jn *joinNodeImpl) assertFromRight(ctx context.Context, handles []types.ReteHandle, joinedHandles []types.ReteHandle) { + + var err error //TODO: other stuff. right now focus on tuple table jn.joinRightObjects(handles, joinedHandles) - tupleTableRow := newJoinTableRow(handles) - jn.rightTable.addRow(tupleTableRow) + //tupleTableRow := newJoinTableRow(handles, jn.nw.incrementAndGetId()) + add := true + for _, handle := range handles { + if status := handle.GetStatus(); status == types.ReteHandleStatusCreating { + tuple := handle.GetTuple() + if descriptor := model.GetTupleDescriptor(tuple.GetTupleType()); descriptor.TTLInSeconds != 0 { + jn.GetNw().GetTupleStore().SaveTuple(tuple) + } else { + add = false + } + } + } + if add { + jn.rightTable.AddRow(handles) + } //TODO: rete listeners etc. - for tupleTableRowLeft := range jn.leftTable.getMap() { - success := jn.joinLeftObjects(tupleTableRowLeft.getHandles(), joinedHandles) + rIterator := jn.leftTable.GetRowIterator(ctx) +LOOP: + for rIterator.HasNext() { + tupleTableRowLeft := rIterator.Next() + handles := tupleTableRowLeft.GetHandles() + for _, handle := range handles { + if jn.GetNw().GetHandleService().GetHandle(ctx, handle.GetTuple()) == nil { + rIterator.Remove() + for _, otherHdl := range handles { + jn.GetNw().GetJtRefService().RemoveEntry(otherHdl, jn.leftTable.GetName(), tupleTableRowLeft.GetID()) + } + continue LOOP + } + } + success := jn.joinLeftObjects(handles, joinedHandles) if !success { //TODO: handle it continue @@ -181,7 +217,10 @@ func (jn *joinNodeImpl) assertFromRight(ctx context.Context, handles []reteHandl } else { tupleMap := copyIntoTupleMap(joinedHandles) cv := jn.conditionVar - toPropagate = cv.GetEvaluator()(cv.GetName(), cv.GetRule().GetName(), tupleMap, cv.GetContext()) + toPropagate, err = cv.Evaluate(cv.GetName(), cv.GetRule().GetName(), tupleMap, cv.GetContext()) + if err != nil { + //todo + } } if toPropagate { jn.nodeLinkVar.propagateObjects(ctx, joinedHandles) @@ -189,10 +228,10 @@ func (jn *joinNodeImpl) assertFromRight(ctx context.Context, handles []reteHandl } } -func (jn *joinNodeImpl) joinLeftObjects(leftHandles []reteHandle, joinedHandles []reteHandle) bool { +func (jn *joinNodeImpl) joinLeftObjects(leftHandles []types.ReteHandle, joinedHandles []types.ReteHandle) bool { for i := 0; i < jn.leftIdrLen; i++ { handle := leftHandles[i] - if handle.getTuple() == nil { + if handle.GetTuple() == nil { return false } joinedHandles[jn.joinIndexForLeft[i]] = handle @@ -200,10 +239,10 @@ func (jn *joinNodeImpl) joinLeftObjects(leftHandles []reteHandle, joinedHandles return true } -func (jn *joinNodeImpl) joinRightObjects(rightHandles []reteHandle, joinedHandles []reteHandle) bool { +func (jn *joinNodeImpl) joinRightObjects(rightHandles []types.ReteHandle, joinedHandles []types.ReteHandle) bool { for i := 0; i < jn.rightIdrLen; i++ { handle := rightHandles[i] - if handle.getTuple() == nil { + if handle.GetTuple() == nil { return false } joinedHandles[jn.joinIndexForRight[i]] = handle @@ -211,14 +250,43 @@ func (jn *joinNodeImpl) joinRightObjects(rightHandles []reteHandle, joinedHandle return true } -func (jn *joinNodeImpl) assertFromLeft(ctx context.Context, handles []reteHandle, joinedHandles []reteHandle) { +func (jn *joinNodeImpl) assertFromLeft(ctx context.Context, handles []types.ReteHandle, joinedHandles []types.ReteHandle) { + + var err error + jn.joinLeftObjects(handles, joinedHandles) //TODO: other stuff. right now focus on tuple table - tupleTableRow := newJoinTableRow(handles) - jn.leftTable.addRow(tupleTableRow) + //tupleTableRow := newJoinTableRow(handles, jn.nw.incrementAndGetId()) + add := true + for _, handle := range handles { + if status := handle.GetStatus(); status == types.ReteHandleStatusCreating { + tuple := handle.GetTuple() + if descriptor := model.GetTupleDescriptor(tuple.GetTupleType()); descriptor.TTLInSeconds != 0 { + jn.GetNw().GetTupleStore().SaveTuple(tuple) + } else { + add = false + } + } + } + if add { + jn.leftTable.AddRow(handles) + } //TODO: rete listeners etc. - for tupleTableRowRight := range jn.rightTable.getMap() { - success := jn.joinRightObjects(tupleTableRowRight.getHandles(), joinedHandles) + rIterator := jn.rightTable.GetRowIterator(ctx) +LOOP: + for rIterator.HasNext() { + tupleTableRowRight := rIterator.Next() + handles := tupleTableRowRight.GetHandles() + for _, handle := range handles { + if jn.GetNw().GetHandleService().GetHandle(ctx, handle.GetTuple()) == nil { + rIterator.Remove() + for _, otherHdl := range handles { + jn.GetNw().GetJtRefService().RemoveEntry(otherHdl, jn.rightTable.GetName(), tupleTableRowRight.GetID()) + } + continue LOOP + } + } + success := jn.joinRightObjects(handles, joinedHandles) if !success { //TODO: handle it continue @@ -229,7 +297,10 @@ func (jn *joinNodeImpl) assertFromLeft(ctx context.Context, handles []reteHandle } else { tupleMap := copyIntoTupleMap(joinedHandles) cv := jn.conditionVar - toPropagate = cv.GetEvaluator()(cv.GetName(), cv.GetRule().GetName(), tupleMap, cv.GetContext()) + toPropagate, err = cv.Evaluate(cv.GetName(), cv.GetRule().GetName(), tupleMap, cv.GetContext()) + if err != nil { + //todo + } } if toPropagate { jn.nodeLinkVar.propagateObjects(ctx, joinedHandles) diff --git a/rete/jointable.go b/rete/jointable.go deleted file mode 100644 index 9007347..0000000 --- a/rete/jointable.go +++ /dev/null @@ -1,60 +0,0 @@ -package rete - -import "github.com/project-flogo/rules/common/model" - -type joinTable interface { - addRow(row joinTableRow) //list of Tuples - getID() int - len() int - getMap() map[joinTableRow]joinTableRow - removeRow(row joinTableRow) - getRule() model.Rule -} - -type joinTableImpl struct { - id int - table map[joinTableRow]joinTableRow - idr []model.TupleType - rule model.Rule -} - -func newJoinTable(nw Network, rule model.Rule, identifiers []model.TupleType) joinTable { - jT := joinTableImpl{} - jT.initJoinTableImpl(nw, rule, identifiers) - return &jT -} - -func (jt *joinTableImpl) initJoinTableImpl(nw Network, rule model.Rule, identifiers []model.TupleType) { - jt.id = nw.incrementAndGetId() - jt.idr = identifiers - jt.table = map[joinTableRow]joinTableRow{} - jt.rule = rule -} - -func (jt *joinTableImpl) getID() int { - return jt.id -} - -func (jt *joinTableImpl) addRow(row joinTableRow) { - jt.table[row] = row - for i := 0; i < len(row.getHandles()); i++ { - handle := row.getHandles()[i] - handle.addJoinTableRowRef(row, jt) - } -} - -func (jt *joinTableImpl) removeRow(row joinTableRow) { - delete(jt.table, row) -} - -func (jt *joinTableImpl) len() int { - return len(jt.table) -} - -func (jt *joinTableImpl) getMap() map[joinTableRow]joinTableRow { - return jt.table -} - -func (jt *joinTableImpl) getRule() model.Rule { - return jt.rule -} diff --git a/rete/jointablerow.go b/rete/jointablerow.go deleted file mode 100644 index 390c4fd..0000000 --- a/rete/jointablerow.go +++ /dev/null @@ -1,23 +0,0 @@ -package rete - -type joinTableRow interface { - getHandles() []reteHandle -} - -type joinTableRowImpl struct { - handles []reteHandle -} - -func newJoinTableRow(handles []reteHandle) joinTableRow { - jtr := joinTableRowImpl{} - jtr.initJoinTableRow(handles) - return &jtr -} - -func (jtr *joinTableRowImpl) initJoinTableRow(handles []reteHandle) { - jtr.handles = append([]reteHandle{}, handles...) -} - -func (jtr *joinTableRowImpl) getHandles() []reteHandle { - return jtr.handles -} diff --git a/rete/network.go b/rete/network.go index 273a6eb..5bca476 100644 --- a/rete/network.go +++ b/rete/network.go @@ -2,51 +2,25 @@ package rete import ( "context" + "encoding/json" "fmt" "math" - "time" + "strconv" "github.com/project-flogo/rules/common/model" "container/list" "sync" -) - -type RtcOprn int + "time" -const ( - ADD RtcOprn = 1 + iota - RETRACT - MODIFY - DELETE + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/rete/internal/types" ) -//Network ... the rete network -type Network interface { - AddRule(rule model.Rule) error - String() string - RemoveRule(string) model.Rule - GetRules() []model.Rule - //changedProps are the properties that changed in a previous action - Assert(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn) - //mode can be one of retract, modify, delete - Retract(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn) - - retractInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn) - - assertInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn, forRule string) - getOrCreateHandle(ctx context.Context, tuple model.Tuple) reteHandle - getHandle(tuple model.Tuple) reteHandle - - incrementAndGetId() int - GetAssertedTuple(key model.TupleKey) model.Tuple - GetAssertedTupleByStringKey(key string) model.Tuple - //RtcTransactionHandler - RegisterRtcTransactionHandler(txnHandler model.RtcTransactionHandler, txnContext interface{}) - ReplayTuplesForRule(ruleName string, rs model.RuleSession) (err error) -} - type reteNetworkImpl struct { + //unique name of the network. used for namespacing in storage, etc + prefix string + //All rules in the network allRules map[string]model.Rule //(Rule) @@ -59,34 +33,88 @@ type reteNetworkImpl struct { //Holds the Rule name as key and a pointer to a slice of NodeLinks as value ruleNameClassNodeLinksOfRule map[string]*list.List //*list.List of ClassNodeLink - allHandles map[string]reteHandle + //handleService map[string]types.ReteHandle + handleService types.HandleService - currentId int + txnHandler []model.RtcTransactionHandler + txnContext []interface{} - assertLock sync.Mutex - //crudLock sync.Mutex - txnHandler model.RtcTransactionHandler - txnContext interface{} + //jtService map[int]types.JoinTable + jtService types.JtService + + jtRefsService types.JtRefsService + + config map[string]string + + factory *TypeFactory + idGen types.IdGen + lock types.LockService + tupleStore model.TupleStore + joinNodeNames int + + sync.RWMutex } //NewReteNetwork ... creates a new rete network -func NewReteNetwork() Network { +func NewReteNetwork(sessionName string, jsonConfig string) types.Network { reteNetworkImpl := reteNetworkImpl{} - reteNetworkImpl.initReteNetwork() + reteNetworkImpl.initReteNetwork(sessionName, jsonConfig) return &reteNetworkImpl } -func (nw *reteNetworkImpl) initReteNetwork() { +func (nw *reteNetworkImpl) initReteNetwork(sessionName string, config string) error { + //nw.currentId = 0 nw.allRules = make(map[string]model.Rule) nw.allClassNodes = make(map[string]classNode) nw.ruleNameNodesOfRule = make(map[string]*list.List) nw.ruleNameClassNodeLinksOfRule = make(map[string]*list.List) - nw.allHandles = make(map[string]reteHandle) + nw.txnHandler = []model.RtcTransactionHandler{} + + var parsed common.Config + err := json.Unmarshal([]byte(config), &parsed) + if err != nil { + return err + } + factory, err := NewFactory(nw, parsed) + if err != nil { + return err + } + //nw.factory = factory + + //if factory.parsedJson != nil { + // reteCfg := factory.parsedJson["rs"].(map[string]interface{}) + // nw.prefix = reteCfg["prefix"].(string) + //} + nw.prefix = sessionName + nw.idGen = factory.getIdGen() + switch parsed.Mode { + case "", common.ModeConsistency: + nw.lock = factory.getLockService() + case common.ModePerformance: + default: + return fmt.Errorf("%s is an invalid mode", parsed.Mode) + } + nw.jtService = factory.getJoinTableCollection() + nw.handleService = factory.getHandleCollection() + nw.jtRefsService = factory.getJoinTableRefs() + nw.initNwServices() + return nil + +} + +func (nw *reteNetworkImpl) initNwServices() { + nw.idGen.Init() + if nw.lock != nil { + nw.lock.Init() + } + nw.jtService.Init() + nw.handleService.Init() + nw.jtRefsService.Init() } func (nw *reteNetworkImpl) AddRule(rule model.Rule) (err error) { - nw.assertLock.Lock() - defer nw.assertLock.Unlock() + nw.Lock() + defer nw.Unlock() if nw.allRules[rule.GetName()] != nil { return fmt.Errorf("Rule already exists.." + rule.GetName()) @@ -99,6 +127,7 @@ func (nw *reteNetworkImpl) AddRule(rule model.Rule) (err error) { classNodeLinksOfRule := list.New() conditions := rule.GetConditions() + noIdrConditionCnt := 0 if len(conditions) == 0 { identifierVar := pickIdentifier(rule.GetIdentifiers()) nw.createClassFilterNode(rule, nodesOfRule, classNodeLinksOfRule, identifierVar, nil, nodeSet) @@ -106,6 +135,7 @@ func (nw *reteNetworkImpl) AddRule(rule model.Rule) (err error) { for i := 0; i < len(conditions); i++ { if conditions[i].GetIdentifiers() == nil || len(conditions[i].GetIdentifiers()) == 0 { conditionSetNoIdr.PushBack(conditions[i]) + noIdrConditionCnt++ } else if len(conditions[i].GetIdentifiers()) == 1 && !contains(nodeSet, conditions[i].GetIdentifiers()[0]) { cond := conditions[i] @@ -115,13 +145,15 @@ func (nw *reteNetworkImpl) AddRule(rule model.Rule) (err error) { } } } - + if len(rule.GetConditions()) != 0 && noIdrConditionCnt == len(rule.GetConditions()) { + idr := pickIdentifier(rule.GetIdentifiers()) + nw.createClassFilterNode(rule, nodesOfRule, classNodeLinksOfRule, idr, nil, nodeSet) + } nw.buildNetwork(rule, nodesOfRule, classNodeLinksOfRule, conditionSet, nodeSet, conditionSetNoIdr) cntxt := make([]interface{}, 2) cntxt[0] = nw cntxt[1] = nodesOfRule - for _, classNode := range nw.allClassNodes { optimizeNetwork(classNode, cntxt) } @@ -141,29 +173,13 @@ func (nw *reteNetworkImpl) AddRule(rule model.Rule) (err error) { return nil } -func (nw *reteNetworkImpl) ReplayTuplesForRule(ruleName string, rs model.RuleSession) error { - if rule, exists := nw.allRules[ruleName]; !exists { - return fmt.Errorf("Rule not found [%s]", ruleName) - } else { - for _, h := range nw.allHandles { - tt := h.getTuple().GetTupleType() - if ContainedByFirst(rule.GetIdentifiers(), []model.TupleType{tt}) { - //assert it but only for this rule. - nw.assert(nil, rs, h.getTuple(), nil, ADD, ruleName) - } - } - } - return nil -} - func (nw *reteNetworkImpl) setClassNodeAndLinkJoinTables(nodesOfRule *list.List, classNodeLinksOfRule *list.List) { } func (nw *reteNetworkImpl) RemoveRule(ruleName string) model.Rule { - - nw.assertLock.Lock() - defer nw.assertLock.Unlock() + nw.Lock() + defer nw.Unlock() rule := nw.allRules[ruleName] delete(nw.allRules, ruleName) @@ -191,17 +207,20 @@ func (nw *reteNetworkImpl) RemoveRule(ruleName string) model.Rule { //case *classNodeImpl: //case *ruleNodeImpl: case *joinNodeImpl: - removeRefsFromReteHandles(nodeImpl.leftTable) - removeRefsFromReteHandles(nodeImpl.rightTable) + //nw.removeRefsFromReteHandles(nodeImpl.leftTable) + //nw.removeRefsFromReteHandles(nodeImpl.rightTable) + nodeImpl.leftTable.RemoveAllRows(nil) + nodeImpl.rightTable.RemoveAllRows(nil) } } } - rstr := nw.String() - fmt.Printf(rstr) return rule } func (nw *reteNetworkImpl) GetRules() []model.Rule { + nw.RLock() + defer nw.RUnlock() + rules := make([]model.Rule, 0) for _, rule := range nw.allRules { @@ -210,13 +229,15 @@ func (nw *reteNetworkImpl) GetRules() []model.Rule { return rules } -func removeRefsFromReteHandles(joinTableVar joinTable) { +func (nw *reteNetworkImpl) removeRefsFromReteHandles(joinTableVar types.JoinTable) { if joinTableVar == nil { return } - for tableRow := range joinTableVar.getMap() { - for _, handle := range tableRow.getHandles() { - handle.removeJoinTable(joinTableVar) + rIterator := joinTableVar.GetRowIterator(nil) + for rIterator.HasNext() { + tableRow := rIterator.Next() + for _, handle := range tableRow.GetHandles() { + nw.removeJoinTableRowRefs(nil, handle, nil) } } } @@ -279,7 +300,7 @@ func (nw *reteNetworkImpl) buildNetwork(rule model.Rule, nodesOfRule *list.List, lastNode = fNode } //Yoohoo! We have a Rule!! - ruleNode := newRuleNode(nw, rule) + ruleNode := newRuleNode(rule) newNodeLink(nw, lastNode, ruleNode, false) nodesOfRule.PushBack(ruleNode) } else { @@ -428,11 +449,9 @@ func (nw *reteNetworkImpl) createClassFilterNode(rule model.Rule, nodesOfRule *l } func (nw *reteNetworkImpl) createJoinNode(rule model.Rule, nodesOfRule *list.List, leftNode node, rightNode node, joinCondition model.Condition, conditionSet *list.List, nodeSet *list.List) { - //TODO handle equivJoins later.. joinNode := newJoinNode(nw, rule, leftNode.getIdentifiers(), rightNode.getIdentifiers(), joinCondition) - newNodeLink(nw, leftNode, joinNode, false) newNodeLink(nw, rightNode, joinNode, true) removeFromList(nodeSet, leftNode) @@ -490,16 +509,32 @@ func getClassNode(nw *reteNetworkImpl, name model.TupleType) classNode { } func (nw *reteNetworkImpl) String() string { + nw.RLock() + defer nw.RUnlock() str := "\n>>> Class View <<<\n" - for _, classNodeImpl := range nw.allClassNodes { str += classNodeImpl.String() + "\n" } - str += ">>>> Rule View <<<<\n" + str += ">>>> Rule View <<<<\n" for _, rule := range nw.allRules { - str += nw.PrintRule(rule) + str += "[Rule (" + rule.GetName() + ") Id()]\n" + nodesOfRule := nw.ruleNameNodesOfRule[rule.GetName()] + for e := nodesOfRule.Front(); e != nil; e = e.Next() { + n := e.Value.(abstractNode) + switch nodeImpl := n.(type) { + case *filterNodeImpl: + str += nodeImpl.String() + case *joinNodeImpl: + str += nodeImpl.String() + case *classNodeImpl: + str += nw.printClassNode(rule.GetName(), nodeImpl) + case *ruleNodeImpl: + str += nodeImpl.String() + } + str += "\n" + } } return str @@ -510,6 +545,9 @@ func pickIdentifier(idrs []model.TupleType) model.TupleType { } func (nw *reteNetworkImpl) PrintRule(rule model.Rule) string { + nw.RLock() + defer nw.RUnlock() + //str := "[Rule (" + rule.GetName() + ") Id(" + strconv.Itoa(rule.GetID()) + ")]\n" str := "[Rule (" + rule.GetName() + ") Id()]\n" @@ -544,73 +582,167 @@ func (nw *reteNetworkImpl) printClassNode(ruleName string, classNodeImpl *classN return "\t[ClassNode Class(" + classNodeImpl.getName() + ")" + links + "]\n" } -func (nw *reteNetworkImpl) Assert(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn) { - nw.assert(ctx, rs, tuple, changedProps, mode, "") +func (nw *reteNetworkImpl) Assert(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error { + return nw.assert(ctx, rs, tuple, changedProps, mode, "") } -func (nw *reteNetworkImpl) removeTupleFromRete(tuple model.Tuple) { - reteHandle, found := nw.allHandles[tuple.GetKey().String()] - if found && reteHandle != nil { - delete(nw.allHandles, tuple.GetKey().String()) - reteHandle.removeJoinTableRowRefs(nil) +func (nw *reteNetworkImpl) assert(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn, forRule string) error { + + if ctx == nil { + ctx = context.Background() } + + reteCtxVar, isRecursive, newCtx := getOrSetReteCtx(ctx, nw, rs) + + if !isRecursive { + nw.RLock() + defer nw.RUnlock() + if nw.lock != nil { + nw.lock.Lock() + defer nw.lock.Unlock() + } + + err := nw.assertInternal(newCtx, tuple, changedProps, mode, forRule) + if err != nil { + return err + } + + reteCtxVar.GetConflictResolver().ResolveConflict(newCtx) + //if Timeout is 0, remove it from rete + td := model.GetTupleDescriptor(tuple.GetTupleType()) + if td != nil { + if td.TTLInSeconds == 0 { //remove immediately. + nw.removeTupleFromRete(newCtx, tuple) + } else if td.TTLInSeconds > 0 { // TTL for the tuple type, after that, remove it from RETE + time.AfterFunc(time.Second*time.Duration(td.TTLInSeconds), func() { + nw.RLock() + defer nw.RUnlock() + if nw.lock != nil { + nw.lock.Lock() + defer nw.lock.Unlock() + } + nw.removeTupleFromRete(nil, tuple) + }) + } //else, its -ve and means, never expire + } + if nw.txnHandler != nil { + rtcTxn := newRtcTxn(reteCtxVar.GetRtcAdded(), reteCtxVar.GetRtcModified(), reteCtxVar.GetRtcDeleted()) + for i, txnHandler := range nw.txnHandler { + txnHandler(newCtx, rs, rtcTxn, nw.txnContext[i]) + } + } + return nil + } + + reteCtxVar.GetOpsList().PushBack(newAssertEntry(tuple, changedProps, mode)) + return nil } -func (nw *reteNetworkImpl) Retract(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn) { +func (nw *reteNetworkImpl) removeTupleFromRete(ctx context.Context, tuple model.Tuple) { + reteHandle := nw.handleService.RemoveHandle(tuple) + if reteHandle != nil { + nw.removeJoinTableRowRefs(ctx, reteHandle, nil) + } +} + +func (nw *reteNetworkImpl) Retract(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error { if ctx == nil { ctx = context.Background() } - reteCtxVar, isRecursive, _ := getOrSetReteCtx(ctx, nw, nil) + reteCtxVar, isRecursive, ctx := getOrSetReteCtx(ctx, nw, rs) if !isRecursive { - nw.assertLock.Lock() - defer nw.assertLock.Unlock() - nw.retractInternal(ctx, tuple, changedProps, mode) - if nw.txnHandler != nil && mode == DELETE { - rtcTxn := newRtcTxn(reteCtxVar.getRtcAdded(), reteCtxVar.getRtcModified(), reteCtxVar.getRtcDeleted()) - nw.txnHandler(ctx, reteCtxVar.getRuleSession(), rtcTxn, nw.txnContext) + nw.RLock() + defer nw.RUnlock() + if nw.lock != nil { + nw.lock.Lock() + defer nw.lock.Unlock() + } + + err := nw.RetractInternal(ctx, tuple, changedProps, mode) + if err != nil { + return err + } + if nw.txnHandler != nil && mode == common.DELETE { + rtcTxn := newRtcTxn(reteCtxVar.GetRtcAdded(), reteCtxVar.GetRtcModified(), reteCtxVar.GetRtcDeleted()) + for i, txnHandler := range nw.txnHandler { + txnHandler(ctx, rs, rtcTxn, nw.txnContext[i]) + } } } else { - reteCtxVar.getOpsList().PushBack(newDeleteEntry(tuple, mode, changedProps)) + reteCtxVar.GetOpsList().PushBack(newDeleteEntry(tuple, mode, changedProps)) } + + return nil } -func (nw *reteNetworkImpl) retractInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn) { +func (nw *reteNetworkImpl) RetractInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error { + handle, locked, dne := nw.handleService.GetLockedHandle(nw, tuple) + if locked { + return fmt.Errorf("Tuple with key [%s] is locked", tuple.GetKey().String()) + } else if dne { + return fmt.Errorf("Tuple with key [%s] doesn't exist", tuple.GetKey().String()) + } else if handle.GetStatus() != types.ReteHandleStatusCreated { + handle.Unlock() + return fmt.Errorf("Tuple with key [%s] is not created: %d", tuple.GetKey().String(), handle.GetStatus()) + } if ctx == nil { ctx = context.Background() } - rCtx, _, _ := getOrSetReteCtx(ctx, nw, nil) - - reteHandle := nw.allHandles[tuple.GetKey().String()] - if reteHandle != nil { - reteHandle.removeJoinTableRowRefs(changedProps) + rCtx, _, newCtx := getOrSetReteCtx(ctx, nw, nil) - //add it to the delete list - if mode == DELETE { - rCtx.addToRtcDeleted(tuple) - } - delete(nw.allHandles, tuple.GetKey().String()) + if mode == common.DELETE { + handle.SetStatus(types.ReteHandleStatusDeleting) + defer func() { + rCtx.AddToRtcDeleted(tuple) + nw.handleService.RemoveHandle(tuple) + }() + } else if mode == common.RETRACT || mode == common.MODIFY { + handle.SetStatus(types.ReteHandleStatusRetracting) + defer func() { + handle.SetStatus(types.ReteHandleStatusRetracted) + handle.Unlock() + }() } -} -func (nw *reteNetworkImpl) GetAssertedTuple(key model.TupleKey) model.Tuple { - reteHandle, found := nw.allHandles[key.String()] - if found { - return reteHandle.getTuple() - } + nw.removeJoinTableRowRefs(newCtx, handle, changedProps) + return nil } -func (nw *reteNetworkImpl) GetAssertedTupleByStringKey(key string) model.Tuple { - reteHandle, found := nw.allHandles[key] - if found { - return reteHandle.getTuple() +func (nw *reteNetworkImpl) GetAssertedTuple(ctx context.Context, rs model.RuleSession, key model.TupleKey) model.Tuple { + _, _, newCtx := getOrSetReteCtx(ctx, nw, rs) + reteHandle := nw.handleService.GetHandleByKey(newCtx, key) + if reteHandle != nil { + return reteHandle.GetTuple() } return nil } -func (nw *reteNetworkImpl) assertInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn, forRule string) { +func (nw *reteNetworkImpl) AssertInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn) error { + return nw.assertInternal(ctx, tuple, changedProps, mode, "") +} + +func (nw *reteNetworkImpl) assertInternal(ctx context.Context, tuple model.Tuple, changedProps map[string]bool, mode common.RtcOprn, forRule string) error { + if mode == common.ADD || mode == common.MODIFY { + handle, locked := nw.handleService.GetOrCreateLockedHandle(nw, tuple) + if locked { + return fmt.Errorf("Tuple with key [%s] is locked", tuple.GetKey().String()) + } else if handle.GetStatus() == types.ReteHandleStatusRetracted { + handle.SetStatus(types.ReteHandleStatusCreating) + } else if handle.GetStatus() == types.ReteHandleStatusCreated { + if len(forRule) == 0 { + handle.Unlock() + return fmt.Errorf("Tuple with key [%s] already asserted", tuple.GetKey().String()) + } + } + defer func() { + handle.SetStatus(types.ReteHandleStatusCreated) + handle.Unlock() + }() + } + tupleType := tuple.GetTupleType() listItem := nw.allClassNodes[string(tupleType)] if listItem != nil { @@ -619,72 +751,136 @@ func (nw *reteNetworkImpl) assertInternal(ctx context.Context, tuple model.Tuple } td := model.GetTupleDescriptor(tuple.GetTupleType()) if td != nil { - if td.TTLInSeconds != 0 && mode == ADD { + if td.TTLInSeconds != 0 && mode == common.ADD { rCtx := getReteCtx(ctx) if rCtx != nil { - rCtx.addToRtcAdded(tuple) + rCtx.AddToRtcAdded(tuple) } } } + return nil } -func (nw *reteNetworkImpl) getOrCreateHandle(ctx context.Context, tuple model.Tuple) reteHandle { - h := nw.allHandles[tuple.GetKey().String()] - if h == nil { - h1 := handleImpl{} - h1.initHandleImpl() - h1.setTuple(tuple) - h = &h1 - nw.allHandles[tuple.GetKey().String()] = h - } +func (nw *reteNetworkImpl) GetHandleWithTuple(ctx context.Context, tuple model.Tuple) types.ReteHandle { + return nw.handleService.GetHandleWithTuple(nw, tuple) +} + +func (nw *reteNetworkImpl) getHandle(ctx context.Context, tuple model.Tuple) types.ReteHandle { + h := nw.handleService.GetHandleByKey(ctx, tuple.GetKey()) return h } -func (nw *reteNetworkImpl) getHandle(tuple model.Tuple) reteHandle { - h := nw.allHandles[tuple.GetKey().String()] +func (nw *reteNetworkImpl) RegisterRtcTransactionHandler(txnHandler model.RtcTransactionHandler, txnContext interface{}) { + nw.txnHandler = append(nw.txnHandler, txnHandler) + nw.txnContext = append(nw.txnContext, txnContext) +} - return h +func (nw *reteNetworkImpl) GetConfigValue(key string) string { + val, _ := nw.config[key] + return val } -func (nw *reteNetworkImpl) incrementAndGetId() int { - nw.currentId++ - return nw.currentId +func (nw *reteNetworkImpl) GetConfig() map[string]string { + return nw.config } -func (nw *reteNetworkImpl) RegisterRtcTransactionHandler(txnHandler model.RtcTransactionHandler, txnContext interface{}) { - nw.txnHandler = txnHandler - nw.txnContext = txnContext +func (nw *reteNetworkImpl) getFactory() *TypeFactory { + return nw.factory } -func (nw *reteNetworkImpl) assert(ctx context.Context, rs model.RuleSession, tuple model.Tuple, changedProps map[string]bool, mode RtcOprn, forRule string) { +func (nw *reteNetworkImpl) SetTupleStore(tupleStore model.TupleStore) { + nw.tupleStore = tupleStore +} +func (nw *reteNetworkImpl) GetTupleStore() model.TupleStore { + return nw.tupleStore +} - if ctx == nil { - ctx = context.Background() - } +func getHandleWithTuple(ctx context.Context, tuple model.Tuple) types.ReteHandle { + reteCtxVar := getReteCtx(ctx) + return reteCtxVar.GetNetwork().GetHandleWithTuple(ctx, tuple) +} - reteCtxVar, isRecursive, newCtx := getOrSetReteCtx(ctx, nw, rs) +func (nw *reteNetworkImpl) removeJoinTableRowRefs(ctx context.Context, hdl types.ReteHandle, changedProps map[string]bool) { + tuple := hdl.GetTuple() + alias := tuple.GetTupleType() - if !isRecursive { - nw.assertLock.Lock() - defer nw.assertLock.Unlock() - nw.assertInternal(newCtx, tuple, changedProps, mode, forRule) - reteCtxVar.getConflictResolver().resolveConflict(newCtx) - //if Timeout is 0, remove it from rete - td := model.GetTupleDescriptor(tuple.GetTupleType()) - if td != nil { - if td.TTLInSeconds == 0 { //remove immediately. - nw.removeTupleFromRete(tuple) - } else if td.TTLInSeconds > 0 { // TTL for the tuple type, after that, remove it from RETE - go time.AfterFunc(time.Second*time.Duration(td.TTLInSeconds), func() { - nw.removeTupleFromRete(tuple) - }) - } //else, its -ve and means, never expire + hdlTblIter := nw.jtRefsService.GetRowIterator(ctx, hdl) + for hdlTblIter.HasNext() { + row, joinTable := hdlTblIter.Next() + if row == nil || joinTable == nil { + continue } - if nw.txnHandler != nil { - rtcTxn := newRtcTxn(reteCtxVar.getRtcAdded(), reteCtxVar.getRtcModified(), reteCtxVar.getRtcDeleted()) - nw.txnHandler(ctx, rs, rtcTxn, nw.txnContext) + + toDelete := false + if changedProps != nil { + rule := joinTable.GetRule() + depProps, found := rule.GetDeps()[alias] + if found { // rule depends on this type + for changedProp := range changedProps { + _, foundProp := depProps[changedProp] + if foundProp { + toDelete = true + break + } + } + } + } else { + toDelete = true } + + if !toDelete { + continue + } + + joinTable.RemoveRow(row.GetID()) + for _, otherHdl := range row.GetHandles() { + nw.jtRefsService.RemoveEntry(otherHdl, joinTable.GetName(), row.GetID()) + } + hdlTblIter.Remove() + } +} + +func (nw *reteNetworkImpl) getJoinNodeName() string { + name := strconv.Itoa(nw.joinNodeNames) + nw.joinNodeNames++ + return name +} + +func (nw *reteNetworkImpl) GetIdGenService() types.IdGen { + return nw.idGen +} + +func (nw *reteNetworkImpl) GetLockService() types.LockService { + return nw.lock +} + +func (nw *reteNetworkImpl) GetJtService() types.JtService { + return nw.jtService +} + +func (nw *reteNetworkImpl) GetJtRefService() types.JtRefsService { + return nw.jtRefsService +} + +func (nw *reteNetworkImpl) GetHandleService() types.HandleService { + return nw.handleService +} + +func (nw *reteNetworkImpl) GetPrefix() string { + return nw.prefix +} + +func (nw *reteNetworkImpl) ReplayTuplesForRule(ruleName string, rs model.RuleSession) error { + if rule, exists := nw.allRules[ruleName]; !exists { + return fmt.Errorf("Rule not found [%s]", ruleName) } else { - reteCtxVar.getOpsList().PushBack(newAssertEntry(tuple, changedProps, mode)) + for _, h := range nw.handleService.GetAllHandles(nw) { + tt := h.GetTuple() + if ContainedByFirst(rule.GetIdentifiers(), []model.TupleType{tt.GetTupleType()}) { + //assert it but only for this rule. + nw.assert(nil, rs, h.GetTuple(), nil, common.ADD, ruleName) + } + } } + return nil } diff --git a/rete/node.go b/rete/node.go index 36a69f8..8522ea8 100644 --- a/rete/node.go +++ b/rete/node.go @@ -7,35 +7,27 @@ import ( "strconv" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" ) //node a building block of the rete network type node interface { abstractNode + types.NwElemId getIdentifiers() []model.TupleType - getID() int addNodeLink(nodeLink) - assertObjects(ctx context.Context, handles []reteHandle, isRight bool) + assertObjects(ctx context.Context, handles []types.ReteHandle, isRight bool) } type nodeImpl struct { + types.NwElemIdImpl identifiers []model.TupleType nodeLinkVar nodeLink - id int rule model.Rule } -//NewNode ... returns a new node -//func newNode(nw Network, rule model.Rule, identifiers []model.TupleType) node { -// n := nodeImpl{} -// n.initNodeImpl(nw, rule, identifiers) -// return &n -//} - -func (n *nodeImpl) initNodeImpl(nw Network, rule model.Rule, identifiers []model.TupleType) { - - n.id = nw.incrementAndGetId() - +func (n *nodeImpl) initNodeImpl(nw *reteNetworkImpl, rule model.Rule, identifiers []model.TupleType) { + n.SetID(nw) n.identifiers = identifiers n.rule = rule } @@ -44,16 +36,12 @@ func (n *nodeImpl) getIdentifiers() []model.TupleType { return n.identifiers } -func (n *nodeImpl) getID() int { - return n.id -} - func (n *nodeImpl) addNodeLink(nl nodeLink) { n.nodeLinkVar = nl } func (n *nodeImpl) String() string { - str := "id:" + strconv.Itoa(n.id) + ", idrs:" + str := "id:" + strconv.Itoa(n.GetID()) + ", idrs:" for _, nodeIdentifier := range n.identifiers { str += string(nodeIdentifier) + "," } @@ -87,6 +75,6 @@ func findSimilarNodes(nodeSet *list.List) []node { return similarNodes } -func (n *nodeImpl) assertObjects(ctx context.Context, handles []reteHandle, isRight bool) { +func (n *nodeImpl) assertObjects(ctx context.Context, handles []types.ReteHandle, isRight bool) { fmt.Println("Abstract method here.., see filterNodeImpl and joinNodeImpl") } diff --git a/rete/nodelink.go b/rete/nodelink.go index 011a702..0a36be0 100644 --- a/rete/nodelink.go +++ b/rete/nodelink.go @@ -5,41 +5,40 @@ import ( "strconv" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" ) //nodelink connects 2 nodes, a rete building block type nodeLink interface { + types.NwElemId String() string getChild() node isRightNode() bool setChild(child node) setIsRightChild(isRight bool) - propagateObjects(ctx context.Context, handles []reteHandle) + propagateObjects(ctx context.Context, handles []types.ReteHandle) } type nodeLinkImpl struct { + types.NwElemIdImpl convert []int numIdentifiers int - - parent node - parentIds []model.TupleType - - child node - childIds []model.TupleType - - isRight bool - id int + parent node + parentIds []model.TupleType + child node + childIds []model.TupleType + isRight bool } -func newNodeLink(nw Network, parent node, child node, isRight bool) nodeLink { +func newNodeLink(nw types.Network, parent node, child node, isRight bool) nodeLink { nl := nodeLinkImpl{} nl.initNodeLink(nw, parent, child, isRight) return &nl } -func (nl *nodeLinkImpl) initNodeLink(nw Network, parent node, child node, isRight bool) { - nl.id = nw.incrementAndGetId() +func (nl *nodeLinkImpl) initNodeLink(nw types.Network, parent node, child node, isRight bool) { + nl.SetID(nw) nl.child = child nl.isRight = isRight @@ -60,8 +59,8 @@ func (nl *nodeLinkImpl) initNodeLink(nw Network, parent node, child node, isRigh } //initialize node link : for use with ClassNodeLink -func initClassNodeLink(nw Network, nl *nodeLinkImpl, child node) { - nl.id = nw.incrementAndGetId() +func (nl *nodeLinkImpl) initClassNodeLink(nw types.Network, child node) { + nl.SetID(nw) nl.child = child nl.childIds = child.getIdentifiers() } @@ -109,12 +108,12 @@ func (nl *nodeLinkImpl) String() string { switch nl.child.(type) { case *joinNodeImpl: if nl.isRight { - nextNode += "j" + strconv.Itoa(nl.child.getID()) + "R" + nextNode += "j" + strconv.Itoa(nl.child.GetID()) + "R" } else { - nextNode += "j" + strconv.Itoa(nl.child.getID()) + "L" + nextNode += "j" + strconv.Itoa(nl.child.GetID()) + "L" } case *filterNodeImpl: - nextNode += "f" + strconv.Itoa(nl.child.getID()) + nextNode += "f" + strconv.Itoa(nl.child.GetID()) } return "link (" + nextNode + ")" } @@ -130,9 +129,9 @@ func (nl *nodeLinkImpl) setIsRightChild(isRight bool) { nl.isRight = isRight } -func (nl *nodeLinkImpl) propagateObjects(ctx context.Context, handles []reteHandle) { +func (nl *nodeLinkImpl) propagateObjects(ctx context.Context, handles []types.ReteHandle) { if nl.convert != nil { - convertedHandles := make([]reteHandle, nl.numIdentifiers) + convertedHandles := make([]types.ReteHandle, nl.numIdentifiers) for i := 0; i < nl.numIdentifiers; i++ { convertedHandles[nl.convert[i]] = handles[i] } diff --git a/rete/opsList.go b/rete/opsList.go index 722b630..994d8ef 100644 --- a/rete/opsList.go +++ b/rete/opsList.go @@ -4,6 +4,7 @@ import ( "context" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/common" ) type opsEntry interface { @@ -23,10 +24,10 @@ type assertEntry interface { type assertEntryImpl struct { opsEntryImpl - mode RtcOprn + mode common.RtcOprn } -func newAssertEntry(tuple model.Tuple, changeProps map[string]bool, mode RtcOprn) assertEntry { +func newAssertEntry(tuple model.Tuple, changeProps map[string]bool, mode common.RtcOprn) assertEntry { aEntry := assertEntryImpl{} aEntry.tuple = tuple aEntry.changeProps = changeProps @@ -36,7 +37,7 @@ func newAssertEntry(tuple model.Tuple, changeProps map[string]bool, mode RtcOprn func (ai *assertEntryImpl) execute(ctx context.Context) { reteCtx := getReteCtx(ctx) - reteCtx.getNetwork().assertInternal(ctx, ai.tuple, ai.changeProps, ai.mode, "") + reteCtx.GetNetwork().AssertInternal(ctx, ai.tuple, ai.changeProps, ai.mode) } //Modify Entry @@ -58,9 +59,9 @@ func newModifyEntry(tuple model.Tuple, changeProps map[string]bool) modifyEntry func (me *modifyEntryImpl) execute(ctx context.Context) { reteCtx := getReteCtx(ctx) - reteCtx.getConflictResolver().deleteAgendaFor(ctx, me.tuple, me.changeProps) - reteCtx.getNetwork().Retract(ctx, me.tuple, me.changeProps, MODIFY) - reteCtx.getNetwork().Assert(ctx, reteCtx.getRuleSession(), me.tuple, me.changeProps, MODIFY) + reteCtx.GetConflictResolver().DeleteAgendaFor(ctx, me.tuple, me.changeProps) + reteCtx.GetNetwork().Retract(ctx, reteCtx.GetRuleSession(), me.tuple, me.changeProps, common.MODIFY) + reteCtx.GetNetwork().Assert(ctx, reteCtx.GetRuleSession(), me.tuple, me.changeProps, common.MODIFY) } //Delete Entry @@ -71,10 +72,10 @@ type deleteEntry interface { type deleteEntryImpl struct { opsEntryImpl - mode RtcOprn + mode common.RtcOprn } -func newDeleteEntry(tuple model.Tuple, mode RtcOprn, changeProps map[string]bool) deleteEntry { +func newDeleteEntry(tuple model.Tuple, mode common.RtcOprn, changeProps map[string]bool) deleteEntry { dEntry := deleteEntryImpl{} dEntry.tuple = tuple dEntry.mode = mode @@ -84,6 +85,6 @@ func newDeleteEntry(tuple model.Tuple, mode RtcOprn, changeProps map[string]bool func (de *deleteEntryImpl) execute(ctx context.Context) { reteCtx := getReteCtx(ctx) - reteCtx.getConflictResolver().deleteAgendaFor(ctx, de.tuple, de.changeProps) - reteCtx.getNetwork().retractInternal(ctx, de.tuple, de.changeProps, de.mode) + reteCtx.GetConflictResolver().DeleteAgendaFor(ctx, de.tuple, de.changeProps) + reteCtx.GetNetwork().RetractInternal(ctx, de.tuple, de.changeProps, de.mode) } diff --git a/rete/retehandle.go b/rete/retehandle.go deleted file mode 100644 index 05b734b..0000000 --- a/rete/retehandle.go +++ /dev/null @@ -1,106 +0,0 @@ -package rete - -import ( - "container/list" - "context" - - "github.com/project-flogo/rules/common/model" -) - -//Holds a tuple reference and related state -type reteHandle interface { - setTuple(tuple model.Tuple) - getTuple() model.Tuple - addJoinTableRowRef(joinTableRowVar joinTableRow, joinTableVar joinTable) - removeJoinTableRowRefs(changedProps map[string]bool) - removeJoinTable(joinTableVar joinTable) -} - -type handleImpl struct { - tuple model.Tuple - tablesAndRows map[joinTable]*list.List - - rtcStatus uint8 -} - -func (hdl *handleImpl) setTuple(tuple model.Tuple) { - hdl.tuple = tuple -} - -func (hdl *handleImpl) initHandleImpl() { - hdl.tablesAndRows = make(map[joinTable]*list.List) - hdl.rtcStatus = 0x00 -} - -func (hdl *handleImpl) getTuple() model.Tuple { - return hdl.tuple -} - -func getOrCreateHandle(ctx context.Context, tuple model.Tuple) reteHandle { - reteCtxVar := getReteCtx(ctx) - return reteCtxVar.getNetwork().getOrCreateHandle(ctx, tuple) -} - -func (hdl *handleImpl) addJoinTableRowRef(joinTableRowVar joinTableRow, joinTableVar joinTable) { - - rowsForJoinTable := hdl.tablesAndRows[joinTableVar] - if rowsForJoinTable == nil { - rowsForJoinTable = list.New() - hdl.tablesAndRows[joinTableVar] = rowsForJoinTable - } - rowsForJoinTable.PushBack(joinTableRowVar) - -} - -func (hdl *handleImpl) removeJoinTableRowRefs(changedProps map[string]bool) { - - tuple := hdl.tuple - alias := tuple.GetTupleType() - - emptyJoinTables := list.New() - - for joinTable, listOfRows := range hdl.tablesAndRows { - - toDelete := false - if changedProps != nil { - rule := joinTable.getRule() - depProps, found := rule.GetDeps()[alias] - if found { // rule depends on this type - for changedProp := range changedProps { - _, foundProp := depProps[changedProp] - if foundProp { - toDelete = true - break - } - } - } - } else { - toDelete = true - } - - if !toDelete { - continue - } - - for e := listOfRows.Front(); e != nil; e = e.Next() { - row := e.Value.(joinTableRow) - joinTable.removeRow(row) - } - if joinTable.len() == 0 { - emptyJoinTables.PushBack(joinTable) - } - } - - for e := emptyJoinTables.Front(); e != nil; e = e.Next() { - emptyJoinTable := e.Value.(joinTable) - delete(hdl.tablesAndRows, emptyJoinTable) - } -} - -//Used when a rule is deleted. See Network.RemoveRule -func (hdl *handleImpl) removeJoinTable(joinTableVar joinTable) { - _, ok := hdl.tablesAndRows[joinTableVar] - if ok { - delete(hdl.tablesAndRows, joinTableVar) - } -} diff --git a/rete/rtcmodified.go b/rete/rtcmodified.go index 3282dd1..8c9efd5 100644 --- a/rete/rtcmodified.go +++ b/rete/rtcmodified.go @@ -7,7 +7,7 @@ type rtcModifiedImpl struct { props map[string]bool } -func NewRtcModified (tuple model.Tuple) model.RtcModified { +func NewRtcModified(tuple model.Tuple) model.RtcModified { rm := rtcModifiedImpl{} rm.tuple = tuple rm.props = make(map[string]bool) @@ -24,4 +24,4 @@ func (rm *rtcModifiedImpl) GetModifiedProps() map[string]bool { func (rm *rtcModifiedImpl) addProp(prop string) { rm.props[prop] = true -} \ No newline at end of file +} diff --git a/rete/rtctxn.go b/rete/rtctxn.go index 4e0fcb6..f0756ba 100644 --- a/rete/rtctxn.go +++ b/rete/rtctxn.go @@ -50,7 +50,7 @@ func (tx *rtcTxnImpl) groupModifiedByType(modifiedTxn map[string]model.RtcModifi } } -func (tx *rtcTxnImpl) groupAddedByType(addedTxn map[string]model.Tuple) { +func (tx *rtcTxnImpl) groupAddedByType(addedTxn map[string]model.Tuple) { for key, tuple := range addedTxn { tdType := tuple.GetTupleDescriptor().Name tupleMap, found := tx.added[tdType] diff --git a/rete/rulenode.go b/rete/rulenode.go index 68d5603..8e42743 100644 --- a/rete/rulenode.go +++ b/rete/rulenode.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" ) //ruleNode the leaf node of the rule network for a Rule @@ -18,27 +19,26 @@ type ruleNodeImpl struct { rule model.Rule } -func newRuleNode(nw Network, rule model.Rule) ruleNode { +func newRuleNode(rule model.Rule) ruleNode { rn := ruleNodeImpl{} - rn.nodeImpl.initNodeImpl(nw, rule, rule.GetIdentifiers()) rn.identifiers = rule.GetIdentifiers() rn.rule = rule return &rn } func (rn *ruleNodeImpl) String() string { - return "\t[RuleNode id(" + strconv.Itoa(rn.id) + "): \n" + + return "\t[RuleNode id(" + strconv.Itoa(rn.GetID()) + "): \n" + "\t\tIdentifier = " + model.IdentifiersToString(rn.identifiers) + " ;\n" + "\t\tRule = " + rn.rule.GetName() + "]\n" } -func (rn *ruleNodeImpl) assertObjects(ctx context.Context, handles []reteHandle, isRight bool) { +func (rn *ruleNodeImpl) assertObjects(ctx context.Context, handles []types.ReteHandle, isRight bool) { tupleMap := copyIntoTupleMap(handles) - cr := getReteCtx(ctx).getConflictResolver() + cr := getReteCtx(ctx).GetConflictResolver() - cr.addAgendaItem(rn.getRule(), tupleMap) + cr.AddAgendaItem(rn.getRule(), tupleMap) } diff --git a/rete/utils.go b/rete/utils.go index 399f011..a0b34d3 100644 --- a/rete/utils.go +++ b/rete/utils.go @@ -2,21 +2,24 @@ package rete //translation utilities between handles/tuples to pass to user conditions and actions -import "github.com/project-flogo/rules/common/model" +import ( + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/internal/types" +) -func copyIntoTupleArray(handles []reteHandle) []model.Tuple { +func copyIntoTupleArray(handles []types.ReteHandle) []model.Tuple { tuples := make([]model.Tuple, len(handles)) for i := 0; i < len(handles); i++ { - tuples[i] = handles[i].getTuple() + tuples[i] = handles[i].GetTuple() } return tuples } -func copyIntoTupleMap(handles []reteHandle) map[model.TupleType]model.Tuple { +func copyIntoTupleMap(handles []types.ReteHandle) map[model.TupleType]model.Tuple { tupleMap := map[model.TupleType]model.Tuple{} tuples := make([]model.Tuple, len(handles)) for i := 0; i < len(handles); i++ { - tuples[i] = handles[i].getTuple() + tuples[i] = handles[i].GetTuple() tupleMap[tuples[i].GetTupleType()] = tuples[i] //assuming no self-joins! need to correct this! } return tupleMap diff --git a/ruleaction/action.go b/ruleaction/action.go index 6ee587d..64bdae1 100644 --- a/ruleaction/action.go +++ b/ruleaction/action.go @@ -96,6 +96,8 @@ func (f *ActionFactory) New(cfg *action.Config) (action.Action, error) { } ruleAction := &RuleAction{} + + // create rule session ruleSessionDescriptor, err := manager.GetRuleSessionDescriptor(settings.RuleSessionURI) if err != nil { return nil, fmt.Errorf("failed to get RuleSessionDescriptor for %s\n%s", settings.RuleSessionURI, err.Error()) @@ -105,7 +107,7 @@ func (f *ActionFactory) New(cfg *action.Config) (action.Action, error) { if err != nil { return nil, fmt.Errorf("failed to marshall RuleSessionDescriptor : %s", err.Error()) } - ruleAction.rs, err = ruleapi.GetOrCreateRuleSessionFromConfig(settings.RuleSessionURI, string(ruleCollectionJSON)) + ruleAction.rs, err = ruleapi.GetOrCreateRuleSessionFromConfig(settings.RuleSessionURI, "", string(ruleCollectionJSON)) if err != nil { return nil, fmt.Errorf("failed to create rulesession for %s\n %s", settings.RuleSessionURI, err.Error()) diff --git a/ruleapi/actionservice.go b/ruleapi/actionservice.go new file mode 100644 index 0000000..f15da91 --- /dev/null +++ b/ruleapi/actionservice.go @@ -0,0 +1,229 @@ +package ruleapi + +import ( + "context" + "fmt" + + "github.com/project-flogo/core/action" + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data" + "github.com/project-flogo/core/data/mapper" + "github.com/project-flogo/core/data/resolve" + "github.com/project-flogo/core/support" + "github.com/project-flogo/core/support/log" + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" +) + +var logger = log.ChildLogger(log.RootLogger(), "rules") + +// rule action service +type ruleActionService struct { + Name string + Type string + Function model.ActionFunction + Act activity.Activity + Action action.Action + DTable DecisionTable + Input map[string]interface{} +} + +// NewActionService creates new rule action service +func NewActionService(serviceCfg *config.ServiceDescriptor) (model.ActionService, error) { + + raService := &ruleActionService{ + Name: serviceCfg.Name, + Type: serviceCfg.Type, + Input: make(map[string]interface{}), + } + + switch serviceCfg.Type { + default: + return nil, fmt.Errorf("service type - '%s' is not supported", serviceCfg.Type) + case "": + return nil, fmt.Errorf("service type can't be empty") + case config.TypeServiceFunction: + if serviceCfg.Function == nil { + return nil, fmt.Errorf("service function can't empty") + } + raService.Function = serviceCfg.Function + case config.TypeServiceActivity: + // inflate activity from ref + if serviceCfg.Ref[0] == '#' { + var ok bool + activityRef := serviceCfg.Ref + serviceCfg.Ref, ok = support.GetAliasRef("activity", activityRef) + if !ok { + return nil, fmt.Errorf("activity '%s' not imported", activityRef) + } + } + + act := activity.Get(serviceCfg.Ref) + if act == nil { + return nil, fmt.Errorf("unsupported Activity:" + serviceCfg.Ref) + } + + f := activity.GetFactory(serviceCfg.Ref) + + if f != nil { + initCtx := newInitContext(serviceCfg.Name, serviceCfg.Settings, log.ChildLogger(log.RootLogger(), "ruleaction")) + pa, err := f(initCtx) + if err != nil { + return nil, fmt.Errorf("unable to create rule action service '%s' : %s", serviceCfg.Name, err.Error()) + } + act = pa + } + + raService.Act = act + + case config.TypeServiceAction: + if serviceCfg.Ref[0] == '#' { + var ok bool + actionRef := serviceCfg.Ref + serviceCfg.Ref, ok = support.GetAliasRef("action", actionRef) + if !ok { + return nil, fmt.Errorf("action - '%s' not imported", actionRef) + } + } + + actionFactory := action.GetFactory(serviceCfg.Ref) + if actionFactory == nil { + return nil, fmt.Errorf("factory not found for the action - '%s'", serviceCfg.Ref) + } + + actionCfg := &action.Config{Settings: serviceCfg.Settings} + var err error + raService.Action, err = actionFactory.New(actionCfg) + if err != nil { + return nil, fmt.Errorf("not able create action - %s", err) + } + + case config.TypeDecisionTable: + fileName := serviceCfg.Settings["filename"].(string) + if len(fileName) != 0 { + var err error + raService.DTable, err = LoadDecisionTableFromFile(fileName) + if err != nil { + return nil, fmt.Errorf("Unable to load Decison Table - %s", err) + } + } else { + return nil, fmt.Errorf("Decision Table filename not specified") + } + } + + return raService, nil +} + +// SetInput sets input +func (raService *ruleActionService) SetInput(input map[string]interface{}) { + for k, v := range input { + raService.Input[k] = v + } +} + +func resolveExpFromTupleScope(tuples map[model.TupleType]model.Tuple, exprs map[string]interface{}) (map[string]interface{}, error) { + // resolve inputs from tuple scope + mFactory := mapper.NewFactory(resolve.GetBasicResolver()) + mapper, err := mFactory.NewMapper(exprs) + if err != nil { + return nil, err + } + + tupleScope := make(map[string]interface{}) + for tk, t := range tuples { + tupleScope[string(tk)] = t.ToMap() + } + + scope := data.NewSimpleScope(tupleScope, nil) + return mapper.Apply(scope) +} + +// Execute execute rule action service +func (raService *ruleActionService) Execute(ctx context.Context, rs model.RuleSession, rName string, tuples map[model.TupleType]model.Tuple, rCtx model.RuleContext) (done bool, err error) { + + switch raService.Type { + + default: + return false, fmt.Errorf("unsupported service type - '%s'", raService.Type) + + case config.TypeServiceFunction: + // invoke function and return, if available + if raService.Function != nil { + raService.Function(ctx, rs, rName, tuples, rCtx) + return true, nil + } + + case config.TypeServiceActivity: + // resolve inputs from tuple scope + resolvedInputs, err := resolveExpFromTupleScope(tuples, raService.Input) + if err != nil { + return false, err + } + // create activity context and set resolved inputs + sContext := newServiceContext(raService.Act.Metadata()) + for k, v := range resolvedInputs { + sContext.SetInput(k, v) + } + // set rule context + sContext.SetInput("ctx", ctx) + sContext.SetInput("rulesession", rs) + sContext.SetInput("rulename", rName) + sContext.SetInput("tuples", tuples) + sContext.SetInput("rulecontext", rCtx) + // run activities Eval + return raService.Act.Eval(sContext) + + case config.TypeServiceAction: + // resolve inputs from tuple scope + resolvedInputs, err := resolveExpFromTupleScope(tuples, raService.Input) + if err != nil { + return false, err + } + + // check whether the action is sync action + syncAction, syncOk := raService.Action.(action.SyncAction) + if syncOk && syncAction != nil { + // sync action + results, err := syncAction.Run(ctx, resolvedInputs) + if err != nil { + return false, fmt.Errorf("error while running the action service[%s] - %s", raService.Name, err) + } + logger.Infof("service[%s] executed successfully. Service outputs: %s \n", raService.Name, results) + return true, nil + } + + // check whether the action is async action + asyncAction, asyncOk := raService.Action.(action.AsyncAction) + if asyncOk && asyncAction != nil { + err := asyncAction.Run(ctx, resolvedInputs, &actionResultHandler{name: raService.Name}) + if err != nil { + return false, fmt.Errorf("error while running the action service[%s] - %s", raService.Name, err) + } + return true, nil + } + + case config.TypeDecisionTable: + err := raService.DTable.Compile() + if err != nil { + return false, fmt.Errorf("unable to compile decision table while running the action service[%s] - %s", raService.Name, err) + } + raService.DTable.Apply(ctx, tuples) + return true, nil + } + + return false, fmt.Errorf("service not executed, something went wrong") +} + +type actionResultHandler struct { + name string +} + +// HandleResult is invoked when there are results available +func (arh *actionResultHandler) HandleResult(results map[string]interface{}, err error) { + logger.Infof("service[%s] outputs: %s \n", arh.name, results) +} + +// Done indicates that the action has completed +func (arh *actionResultHandler) Done() { + logger.Infof("service[%s] executed successfully asynchronously\n", arh.name) +} diff --git a/ruleapi/actionservice_test.go b/ruleapi/actionservice_test.go new file mode 100644 index 0000000..2899e7b --- /dev/null +++ b/ruleapi/actionservice_test.go @@ -0,0 +1,52 @@ +package ruleapi + +import ( + "context" + "testing" + + _ "github.com/project-flogo/contrib/activity/log" + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" + "github.com/stretchr/testify/assert" +) + +func TestNewActionService(t *testing.T) { + cfg := &config.ServiceDescriptor{ + Name: "TestActionService", + Description: "test action service", + } + aService, err := NewActionService(cfg) + assert.NotNil(t, err) + assert.Equal(t, "service type can't be empty", err.Error()) + assert.Nil(t, aService) + + // unsupported service type + cfg.Type = "unknowntype" + aService, err = NewActionService(cfg) + assert.NotNil(t, err) + assert.Equal(t, "service type - 'unknowntype' is not supported", err.Error()) + + // action service with function + cfg.Function = emptyAction + cfg.Type = config.TypeServiceFunction + aService, err = NewActionService(cfg) + assert.Nil(t, err) + assert.NotNil(t, aService) + cfg.Function = nil //clear for next test scenario + + // action service with activity + cfg.Ref = "github.com/project-flogo/contrib/activity/log" + cfg.Type = config.TypeServiceActivity + aService, err = NewActionService(cfg) + assert.Nil(t, err) + assert.NotNil(t, aService) + + // set input + input := map[string]interface{}{"message": "=$.n1.name"} + aService.SetInput(input) + + // TODO: test aService.Execute() +} + +func emptyAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { +} diff --git a/ruleapi/condition.go b/ruleapi/condition.go index 7ae71e3..df49f43 100644 --- a/ruleapi/condition.go +++ b/ruleapi/condition.go @@ -2,6 +2,7 @@ package ruleapi import ( "github.com/project-flogo/rules/common/model" + "strconv" ) type conditionImpl struct { @@ -20,6 +21,10 @@ func newCondition(name string, rule model.Rule, identifiers []model.TupleType, c } func (cnd *conditionImpl) initConditionImpl(name string, rule model.Rule, identifiers []model.TupleType, cfn model.ConditionEvaluator, ctx model.RuleContext) { + if name == "" { + cndIdx := len(rule.GetConditions()) + 1 + name = "c_" + strconv.Itoa(cndIdx) + } cnd.name = name cnd.rule = rule cnd.identifiers = append(cnd.identifiers, identifiers...) @@ -34,9 +39,9 @@ func (cnd *conditionImpl) GetContext() model.RuleContext { return cnd.ctx } -func (cnd *conditionImpl) GetEvaluator() model.ConditionEvaluator { - return cnd.cfn -} +//func (cnd *conditionImpl) GetEvaluator() model.ConditionEvaluator { +// return cnd.cfn +//} func (cnd *conditionImpl) String() string { return "[Condition: name:" + cnd.name + ", idrs: TODO]" @@ -52,3 +57,12 @@ func (cnd *conditionImpl) GetRule() model.Rule { func (cnd *conditionImpl) GetTupleTypeAlias() []model.TupleType { return cnd.identifiers } + +func (cnd *conditionImpl) Evaluate(condName string, ruleNm string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) (bool, error) { + result := false + if cnd.cfn != nil { + result = cnd.cfn(condName, ruleNm, tuples, ctx) + } + + return result, nil +} diff --git a/ruleapi/dtable.go b/ruleapi/dtable.go new file mode 100644 index 0000000..2dfb6a8 --- /dev/null +++ b/ruleapi/dtable.go @@ -0,0 +1,369 @@ +package ruleapi + +import ( + "context" + "encoding/csv" + "fmt" + "os" + "strconv" + "strings" + + excelize "github.com/360EntSecGroup-Skylar/excelize/v2" + "github.com/project-flogo/core/data" + "github.com/project-flogo/rules/common/model" +) + +// Decision table column types +type dtColType int8 + +const ( + ctID dtColType = iota // ID + ctCondition // condition + ctAction // action + ctDescription // Description + ctPriority // priority +) + +type decisionTableImpl struct { + titleRow1 []genCell + titleRow2 []genCell + metaRow []metaCell + rows [][]*genCell +} + +// DecisionTable interface +type DecisionTable interface { + Apply(ctx context.Context, tuples map[model.TupleType]model.Tuple) + Compile() error + Print() +} + +type metaCell struct { + ColType dtColType + tupleDesc *model.TupleDescriptor + propDesc *model.TuplePropertyDescriptor +} + +type genCell struct { + *metaCell + rawValue string + cdExpr string +} + +func (cell *genCell) compileExpr() { + rawValue := cell.rawValue + if len(rawValue) == 0 { + return + } + lhsToken := fmt.Sprintf("$.%s.%s", cell.tupleDesc.Name, cell.propDesc.Name) + expression := &Expr{Buffer: rawValue} + expression.Init() + expression.Expression.Init(rawValue) + if err := expression.Parse(); err != nil { + panic(err) + } + expression.Execute() + cell.cdExpr = expression.Evaluate(lhsToken) +} + +// LoadDecisionTableFromFile returns dtable from file +func LoadDecisionTableFromFile(fileName string) (DecisionTable, error) { + if fileName == "" { + return nil, fmt.Errorf("file name can't be empty") + } + tokens := strings.Split(fileName, ".") + fileExtension := tokens[len(tokens)-1] + + if fileExtension == "csv" || fileExtension == "CSV" { + return loadFromCSVFile(fileName) + } else if fileExtension == "xls" || fileExtension == "XLS" || fileExtension == "xlsx" || fileExtension == "XLSX" { + return loadFromXLSFile(fileName) + } + + return nil, fmt.Errorf("file[%s] extension not supported", fileName) +} + +// loadFromCSVFile loads decision table from CSV file +func loadFromCSVFile(fileName string) (DecisionTable, error) { + file, err := os.Open(fileName) + if err != nil { + return nil, fmt.Errorf("not able open the file [%s] - %s", fileName, err) + } + defer file.Close() + + lines, err := csv.NewReader(file).ReadAll() + if err != nil { + return nil, fmt.Errorf("not able read the file [%s] - %s", fileName, err) + } + + dtable := &decisionTableImpl{} + dtable.rows = make([][]*genCell, len(lines)-2) + for i, line := range lines { + if i == 0 { + // title row 1 + dtable.titleRow1 = make([]genCell, len(line)) + for j, val := range line { + dtable.titleRow1[j].rawValue = val + } + continue + } + if i == 1 { + // title row 2 + dtable.titleRow2 = make([]genCell, len(line)) + for j, val := range line { + dtable.titleRow2[j].rawValue = val + } + continue + } + // other rows + row := make([]*genCell, len(line)) + for j, val := range line { + row[j] = &genCell{ + rawValue: val, + } + } + dtable.rows[i-2] = row + } + return dtable, nil +} + +// loadFromXLSFile loads decision table from Excel file +func loadFromXLSFile(fileName string) (DecisionTable, error) { + file, err := excelize.OpenFile(fileName) + if err != nil { + return nil, fmt.Errorf("not able open the file [%s] - %s", fileName, err) + } + rows, err := file.GetRows("DecisionTable") + if err != nil { + return nil, fmt.Errorf("DecisionTable worksheet not available in %s", fileName) + } + + // find titleRowIndex + titleRowIndex := 0 + for i, r := range rows { + if len(r) > 0 { + if r[0] == "DecisionTable" { + titleRowIndex = i + 2 + } + } + } + titleRowSize := len(rows[titleRowIndex]) + + dtable := &decisionTableImpl{ + titleRow1: make([]genCell, titleRowSize), + titleRow2: make([]genCell, titleRowSize), + rows: make([][]*genCell, 1), + } + // title row 1 + for i, val := range rows[titleRowIndex] { + dtable.titleRow1[i].rawValue = val + } + // title row 2 + for i, val := range rows[titleRowIndex+1] { + dtable.titleRow2[i].rawValue = val + } + // other rows + for _, r := range rows[titleRowIndex+2:] { + if len(r) == 0 { + break + } + dtrow := make([]*genCell, titleRowSize) + for i, cell := range r { + dtrow[i] = &genCell{ + rawValue: cell, + } + } + dtable.rows = append(dtable.rows, dtrow) + } + + return dtable, nil +} + +func (dtable *decisionTableImpl) Compile() error { + // compute meta row from titleRow1 & titleRow2 + metaRow := make([]metaCell, len(dtable.titleRow1)) + dtable.metaRow = metaRow + // titleRow1 determines column type + for colIndex, cell := range dtable.titleRow1 { + if strings.Contains(cell.rawValue, "Id") { + metaRow[colIndex].ColType = ctID + } else if strings.Contains(cell.rawValue, "Condition") { + metaRow[colIndex].ColType = ctCondition + } else if strings.Contains(cell.rawValue, "Action") { + metaRow[colIndex].ColType = ctAction + } else if strings.Contains(cell.rawValue, "Description") { + metaRow[colIndex].ColType = ctDescription + } else if strings.Contains(cell.rawValue, "Priority") { + metaRow[colIndex].ColType = ctPriority + } else { + return fmt.Errorf("unknown column type - %s", cell.rawValue) + } + } + // titleRow2 determines tuple type & property + for colIndex, cell := range dtable.titleRow2 { + if cell.rawValue == "" { + continue + } + tokens := strings.Split(cell.rawValue, ".") + if len(tokens) != 2 { + return fmt.Errorf("[%s] is not a valid tuple property representation", cell.rawValue) + } + tupleType := tokens[0] + propName := tokens[1] + tupleDesc := model.GetTupleDescriptor(model.TupleType(tupleType)) + if tupleDesc == nil { + return fmt.Errorf("tuple type[%s] is not recognised", tupleType) + } + propDesc := tupleDesc.GetProperty(propName) + if propDesc == nil { + return fmt.Errorf("property[%s] is not a valid property for the tuple type[%s]", propName, tupleType) + } + metaRow[colIndex].tupleDesc = tupleDesc + metaRow[colIndex].propDesc = propDesc + } + // process all rows + for _, row := range dtable.rows { + for colIndex, cell := range row { + if cell == nil { + continue + } + cell.metaCell = &metaRow[colIndex] + if cell.ColType == ctCondition { + cell.compileExpr() + } + } + } + return nil +} + +func (dtable *decisionTableImpl) Apply(ctx context.Context, tuples map[model.TupleType]model.Tuple) { + // process all rows + for _, row := range dtable.rows { + // process row conditions + rowTruthiness := true + for _, cell := range row { + if cell == nil { + continue + } + if cell.ColType == ctCondition { + cellTruthiness := evaluateExpression(cell.cdExpr, tuples) + rowTruthiness = rowTruthiness && cellTruthiness + if !rowTruthiness { + break + } + } + } + // process row actions if all row conditions are evaluated to true + if rowTruthiness { + for _, cell := range row { + if cell == nil { + continue + } + if cell.ColType == ctAction { + updateTuple(ctx, tuples, cell.tupleDesc.Name, cell.propDesc.Name, cell.rawValue) + } + } + } + } +} + +// print prints decision table into stdout - TO BE REMOVED +func (dtable *decisionTableImpl) Print() { + // title + for _, v := range dtable.titleRow1 { + fmt.Printf("| %v |", v.rawValue) + } + fmt.Println() + // meta title + for _, v := range dtable.titleRow2 { + fmt.Printf("| %v |", v.rawValue) + } + fmt.Println() + // data + for _, row := range dtable.rows { + for _, rv := range row { + // fmt.Printf("| %v--%v |", rv.cdExpr, rv.metaCell) + fmt.Print(rv) + } + fmt.Println() + } +} + +// evaluateExpression evaluates expr into boolean value in tuples scope +func evaluateExpression(expr string, tuples map[model.TupleType]model.Tuple) bool { + condExpr := NewExprCondition(expr) + result, err := condExpr.Evaluate("", "", tuples, "") + if err != nil { + return false + } + return result +} + +// updateTuple updates tuple's prop with toValue +func updateTuple(context context.Context, tuples map[model.TupleType]model.Tuple, tupleType string, prop string, toVlaue interface{}) { + tuple := tuples[model.TupleType(tupleType)] + if tuple == nil { + return + } + mutableTuple := tuple.(model.MutableTuple) + tds := mutableTuple.GetTupleDescriptor() + strVal := fmt.Sprintf("%v", toVlaue) + switch tds.GetProperty(prop).PropType { + case data.TypeString: + if strings.Compare(strVal, "") == 0 { + strVal = "" + } + mutableTuple.SetString(context, prop, strVal) + case data.TypeBool: + if strings.Compare(strVal, "") == 0 { + strVal = "false" + } + b, err := strconv.ParseBool(strVal) + if err == nil { + mutableTuple.SetBool(context, prop, b) + } + case data.TypeInt: + if strings.Compare(strVal, "") == 0 { + strVal = "0" + } + i, err := strconv.ParseInt(strVal, 10, 64) + if err == nil { + mutableTuple.SetInt(context, prop, int(i)) + } + case data.TypeInt32: + if strings.Compare(strVal, "") == 0 { + strVal = "0" + } + i, err := strconv.ParseInt(strVal, 10, 64) + if err == nil { + mutableTuple.SetInt(context, prop, int(i)) + } + case data.TypeInt64: + if strings.Compare(strVal, "") == 0 { + strVal = "0" + } + i, err := strconv.ParseInt(strVal, 10, 64) + if err == nil { + mutableTuple.SetLong(context, prop, i) + } + case data.TypeFloat32: + if strings.Compare(strVal, "") == 0 { + strVal = "0.0" + } + f, err := strconv.ParseFloat(strVal, 32) + if err == nil { + mutableTuple.SetDouble(context, prop, f) + } + case data.TypeFloat64: + if strings.Compare(strVal, "") == 0 { + strVal = "0.0" + } + f, err := strconv.ParseFloat(strVal, 64) + if err == nil { + mutableTuple.SetDouble(context, prop, f) + } + default: + mutableTuple.SetValue(context, prop, toVlaue) + + } +} diff --git a/ruleapi/dtable_test.go b/ruleapi/dtable_test.go new file mode 100644 index 0000000..6469a35 --- /dev/null +++ b/ruleapi/dtable_test.go @@ -0,0 +1,214 @@ +package ruleapi + +import ( + "testing" + + "github.com/project-flogo/rules/common/model" + "github.com/stretchr/testify/assert" +) + +const tupleDescriptor = `[ + { + "name":"applicant", + "properties":[ + { + "name":"name", + "pk-index":0, + "type":"string" + }, + { + "name":"gender", + "type":"string" + }, + { + "name":"age", + "type":"int" + }, + { + "name":"address", + "type":"string" + }, + { + "name":"hasDL", + "type":"bool" + }, + { + "name":"ssn", + "type":"long" + }, + { + "name":"income", + "type":"double" + }, + { + "name":"maritalStatus", + "type":"string" + }, + { + "name":"creditScore", + "type":"int" + }, + { + "name":"status", + "type":"string" + }, + { + "name":"eligible", + "type":"bool" + }, + { + "name":"creditLimit", + "type":"double" + } + ] + }, + { + "name":"processapplication", + "ttl":0, + "properties":[ + { + "name":"ssn", + "pk-index":0, + "type":"long" + }, + { + "name":"start", + "type":"bool" + } + ] + } +]` + +func TestCellCompileExpr(t *testing.T) { + err := model.RegisterTupleDescriptors(string(tupleDescriptor)) + assert.Nil(t, err) + + // test cases input + tupleType := "applicant" + propName := "name" + testcases := make(map[string]string) + testcases["foo"] = "$.applicant.name == foo" + testcases["123foo"] = "$.applicant.name == 123foo" + testcases["foo123"] = "$.applicant.name == foo123" + testcases["123"] = "$.applicant.name == 123" + testcases["123.123"] = "$.applicant.name == 123.123" + testcases[".123"] = "$.applicant.name == .123" + testcases["!foo"] = "!($.applicant.name == foo)" + testcases["==foo"] = "$.applicant.name == foo" + testcases["!=foo"] = "$.applicant.name != foo" + testcases[">foo"] = "$.applicant.name > foo" + testcases["!>foo"] = "!($.applicant.name > foo)" + testcases[">=foo"] = "$.applicant.name >= foo" + testcases["=foo&&bar"] = "($.applicant.name >= foo && $.applicant.name == bar)" + testcases["car&&jeep&&bus"] = "(($.applicant.name == car && $.applicant.name == jeep) && $.applicant.name == bus)" + testcases["foo||bar"] = "($.applicant.name == foo || $.applicant.name == bar)" + testcases["car||jeep||bus"] = "(($.applicant.name == car || $.applicant.name == jeep) || $.applicant.name == bus)" + testcases["car&&jeep||bus"] = "(($.applicant.name == car && $.applicant.name == jeep) || $.applicant.name == bus)" + testcases["!(car&&(jeep||bus))"] = "!(($.applicant.name == car && ($.applicant.name == jeep || $.applicant.name == bus)))" + testcases["car||jeep&&bus"] = "($.applicant.name == car || ($.applicant.name == jeep && $.applicant.name == bus))" + + // prepare cell + tupleDesc := model.GetTupleDescriptor(model.TupleType(tupleType)) + assert.NotNil(t, tupleDesc) + propDesc := tupleDesc.GetProperty(propName) + assert.NotNil(t, propDesc) + cell := &genCell{ + metaCell: &metaCell{ + ColType: ctCondition, + tupleDesc: tupleDesc, + propDesc: propDesc, + }, + } + + // run test cases + for k, v := range testcases { + t.Log(k, v) + cell.rawValue = k + cell.compileExpr() + assert.Equal(t, v, cell.cdExpr) + } +} + +func TestDtableXLSX(t *testing.T) { + err := model.RegisterTupleDescriptors(string(tupleDescriptor)) + assert.Nil(t, err) + + dtable, err := LoadDecisionTableFromFile("test_dtable.xlsx") + assert.Nil(t, err) + + err = dtable.Compile() + assert.Nil(t, err) + + vls := make(map[string]interface{}) + + vls["name"] = "test" + vls["age"] = 20 + vls["hasDL"] = true + vls["creditScore"] = 600 + vls["maritalStatus"] = "single" + vls["income"] = 11000 + vls["eligible"] = false + vls["creditLimit"] = 0 + + tpl, err := model.NewTuple("applicant", vls) + assert.Nil(t, err) + assert.NotNil(t, tpl) + + presentCreditLimit, err := tpl.GetDouble("creditLimit") + assert.Nil(t, err) + assert.Equal(t, float64(0), presentCreditLimit) + + tuples := make(map[model.TupleType]model.Tuple) + tuples[tpl.GetTupleType()] = tpl + + dtable.Apply(nil, tuples) + + newCreditLimit, err := tpl.GetDouble("creditLimit") + assert.Nil(t, err) + assert.NotEqual(t, newCreditLimit, presentCreditLimit) + +} + +func TestDtableCSV(t *testing.T) { + err := model.RegisterTupleDescriptors(string(tupleDescriptor)) + assert.Nil(t, err) + + dtable, err := LoadDecisionTableFromFile("test_dtable.csv") + assert.Nil(t, err) + + err = dtable.Compile() + assert.Nil(t, err) + + vls := make(map[string]interface{}) + + vls["name"] = "test" + vls["age"] = 20 + vls["hasDL"] = true + vls["creditScore"] = 600 + vls["maritalStatus"] = "single" + vls["income"] = 11000 + vls["eligible"] = false + vls["creditLimit"] = 0 + + tpl, err := model.NewTuple("applicant", vls) + assert.Nil(t, err) + assert.NotNil(t, tpl) + + presentCreditLimit, err := tpl.GetDouble("creditLimit") + assert.Nil(t, err) + assert.Equal(t, float64(0), presentCreditLimit) + + tuples := make(map[model.TupleType]model.Tuple) + tuples[tpl.GetTupleType()] = tpl + + dtable.Apply(nil, tuples) + + newCreditLimit, err := tpl.GetDouble("creditLimit") + assert.Nil(t, err) + assert.NotEqual(t, newCreditLimit, presentCreditLimit) + +} diff --git a/ruleapi/exprcondition.go b/ruleapi/exprcondition.go new file mode 100644 index 0000000..1708efa --- /dev/null +++ b/ruleapi/exprcondition.go @@ -0,0 +1,166 @@ +package ruleapi + +import ( + "reflect" + "strconv" + + "github.com/project-flogo/core/data" + "github.com/project-flogo/core/data/expression" + "github.com/project-flogo/core/data/expression/script" + "github.com/project-flogo/core/data/resolve" + "github.com/project-flogo/rules/common/model" +) + +var td tuplePropertyResolver +var resolver resolve.CompositeResolver +var factory expression.Factory + +func init() { + td = tuplePropertyResolver{} + //resolver = resolve.NewCompositeResolver(map[string]resolve.Resolver{".": &td}) + resolver = resolve.NewCompositeResolver(map[string]resolve.Resolver{ + ".": &td, + "env": &resolve.EnvResolver{}, + "loop": &resolve.LoopResolver{}, + }) + factory = script.NewExprFactory(resolver) +} + +type exprConditionImpl struct { + name string + rule model.Rule + identifiers []model.TupleType + cExpr string + ctx model.RuleContext +} + +func newExprCondition(name string, rule model.Rule, identifiers []model.TupleType, cExpr string, ctx model.RuleContext) model.Condition { + c := exprConditionImpl{} + c.initExprConditionImpl(name, rule, identifiers, cExpr, ctx) + return &c +} + +func (cnd *exprConditionImpl) initExprConditionImpl(name string, rule model.Rule, identifiers []model.TupleType, cExpr string, ctx model.RuleContext) { + if name == "" { + cndIdx := len(rule.GetConditions()) + 1 + name = "c_" + strconv.Itoa(cndIdx) + } + cnd.name = name + cnd.rule = rule + cnd.identifiers = append(cnd.identifiers, identifiers...) + cnd.cExpr = cExpr + cnd.ctx = ctx +} + +func (cnd *exprConditionImpl) GetIdentifiers() []model.TupleType { + return cnd.identifiers +} +func (cnd *exprConditionImpl) GetContext() model.RuleContext { + return cnd.ctx +} + +func (cnd *exprConditionImpl) GetEvaluator() model.ConditionEvaluator { + return nil +} + +func (cnd *exprConditionImpl) String() string { + return "[Condition: name:" + cnd.name + ", idrs: TODO]" +} + +func (cnd *exprConditionImpl) GetName() string { + return cnd.name +} + +func (cnd *exprConditionImpl) GetRule() model.Rule { + return cnd.rule +} +func (cnd *exprConditionImpl) GetTupleTypeAlias() []model.TupleType { + return cnd.identifiers +} + +func (cnd *exprConditionImpl) Evaluate(condName string, ruleNm string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) (bool, error) { + result := false + if cnd.cExpr != "" { + //e, err := expression.ParseExpression(cnd.cExpr) + exprn, err := factory.NewExpr(cnd.cExpr) + if err != nil { + return result, err + } + + scope := tupleScope{tuples} + res, err := exprn.Eval(&scope) + if err != nil { + return false, err + } else if reflect.TypeOf(res).Kind() == reflect.Bool { + result = res.(bool) + } + } + + return result, nil +} + +////////////////////////////////////////////////////////// +type tupleScope struct { + tuples map[model.TupleType]model.Tuple +} + +func (ts *tupleScope) GetValue(name string) (value interface{}, exists bool) { + return false, true +} + +func (ts *tupleScope) SetValue(name string, value interface{}) error { + return nil +} + +// SetAttrValue sets the value of the specified attribute +func (ts *tupleScope) SetAttrValue(name string, value interface{}) error { + return nil +} + +/////////////////////////////////////////////////////////// +type tuplePropertyResolver struct { +} + +//func (t *tuplePropertyResolver) Resolve(toResolve string, scope data.Scope) (value interface{}, err error) { +func (t *tuplePropertyResolver) Resolve(scope data.Scope, item string, field string) (interface{}, error) { + //toResolve = toResolve[1:] + //aliasAndProp := strings.Split(toResolve, ".") + // + //var v interface{} + //if ts != nil { + // tuple := ts.tuples[model.TupleType(aliasAndProp[0])].(model.Tuple) + // if tuple != nil { + // + // p := tuple.GetTupleDescriptor().GetProperty(aliasAndProp[1]) + // switch p.PropType { + // case data.TypeString: + // v, err = tuple.GetString(aliasAndProp[1]) + // case data.TypeInteger: + // v, err = tuple.GetInt(aliasAndProp[1]) + // case data.TypeLong: + // v, err = tuple.GetLong(aliasAndProp[1]) + // case data.TypeDouble: + // v, err = tuple.GetDouble(aliasAndProp[1]) + // case data.TypeBoolean: + // v, err = tuple.GetBool(aliasAndProp[1]) + // } + // }` + //} + //return v, err + ts := scope.(*tupleScope) + tuple := ts.tuples[model.TupleType(field)] + m := tuple.ToMap() + return m, nil + +} + +func (*tuplePropertyResolver) GetResolverInfo() *resolve.ResolverInfo { + return resolve.NewResolverInfo(false, false) +} + +// NewExprCondition creates exprsn condition +func NewExprCondition(cExpr string) model.Condition { + c := exprConditionImpl{} + c.initExprConditionImpl("InstantCondition", nil, nil, cExpr, nil) + return &c +} diff --git a/ruleapi/expression.go b/ruleapi/expression.go new file mode 100644 index 0000000..3ada8a8 --- /dev/null +++ b/ruleapi/expression.go @@ -0,0 +1,128 @@ +package ruleapi + +// Type is a type of byte code +type Type uint8 + +const ( + // TypeLiteral a literal type + TypeLiteral Type = iota + // TypeNot a logical not operation + TypeNot + // TypeOr a logical or operation + TypeOr + // TypeAnd a logical and operation + TypeAnd + // TypeEq == operator + TypeEq + // TypeNEq != operator + TypeNEq + // TypeGt > operator + TypeGt + // TypeGte >= operator + TypeGte + // TypeLt < operator + TypeLt + // TypeLte <= operator + TypeLte +) + +// ByteCode is a expression byte code +type ByteCode struct { + T Type + Literal string +} + +// String prints out the string representation of the code +func (code *ByteCode) String() string { + switch code.T { + case TypeLiteral: + return code.Literal + case TypeNot: + return "!" + case TypeOr: + return "||" + case TypeAnd: + return "&&" + case TypeEq: + return "==" + case TypeNEq: + return "!=" + case TypeGt: + return ">" + case TypeGte: + return ">=" + case TypeLt: + return "<" + case TypeLte: + return "<=" + } + return "" +} + +// Expression is a decision table expression +type Expression struct { + Code []ByteCode + Top int +} + +// Init initializes the expression +func (e *Expression) Init(expression string) { + e.Code = make([]ByteCode, len(expression)) +} + +// AddOperator adds an operator to the stack +func (e *Expression) AddOperator(operator Type) { + code, top := e.Code, e.Top + e.Top++ + code[top].T = operator +} + +// AddLiteral adds a literal to the stack +func (e *Expression) AddLiteral(literal string) { + code, top := e.Code, e.Top + e.Top++ + code[top].T = TypeLiteral + code[top].Literal = literal +} + +// Evaluate evaluates the expression +func (e *Expression) Evaluate(lhsToken string) string { + stack, top := make([]string, len(e.Code)), 0 + for _, code := range e.Code[0:e.Top] { + switch code.T { + case TypeLiteral: + stack[top] = code.Literal + top++ + case TypeNot: + a := &stack[top-1] + *a = "!(" + *a + ")" + case TypeOr: + a, b := &stack[top-2], &stack[top-1] + top-- + *a = "(" + *a + " || " + *b + ")" + case TypeAnd: + a, b := &stack[top-2], &stack[top-1] + top-- + *a = "(" + *a + " && " + *b + ")" + case TypeEq: + a := &stack[top-1] + *a = lhsToken + " == " + *a + case TypeNEq: + a := &stack[top-1] + *a = lhsToken + " != " + *a + case TypeGt: + a := &stack[top-1] + *a = lhsToken + " > " + *a + case TypeGte: + a := &stack[top-1] + *a = lhsToken + " >= " + *a + case TypeLt: + a := &stack[top-1] + *a = lhsToken + " < " + *a + case TypeLte: + a := &stack[top-1] + *a = lhsToken + " <= " + *a + } + } + return stack[0] +} diff --git a/ruleapi/expression.peg b/ruleapi/expression.peg new file mode 100644 index 0000000..c753e59 --- /dev/null +++ b/ruleapi/expression.peg @@ -0,0 +1,38 @@ +package ruleapi + +type Expr Peg { + Expression +} + +e <- sp e1 !. +e1 <- e2 ( or e2 { p.AddOperator(TypeOr) } )* +e2 <- e3 ( and e3 { p.AddOperator(TypeAnd) } )* +e3 <- not e4 { p.AddOperator(TypeNot) } + / e4 +e4 <- eq e5 { p.AddOperator(TypeEq) } + / neq e5 { p.AddOperator(TypeNEq) } + / gte e5 { p.AddOperator(TypeGte) } + / gt e5 { p.AddOperator(TypeGt) } + / lte e5 { p.AddOperator(TypeLte) } + / lt e5 { p.AddOperator(TypeLt) } + / open e1 close + / e5 { p.AddOperator(TypeEq) } +e5 <- < value > sp { p.AddLiteral(buffer[begin:end]) } +value <- [0-9]+ [.] [0-9]* + / [.] [0-9]+ + / ["] (!["] .)* ["] + / ['] (!['] .)* ['] + / [a-zA-Z_0-9]+ + +not <- '!' sp +and <- '&&' sp +or <- '||' sp +eq <- '==' sp +neq <- '!=' sp +gt <- '>' sp +gte <- '>=' sp +lt <- '<' sp +lte <- '<=' sp +open <- '(' sp +close <- ')' sp +sp <- ( ' ' / '\t' )* diff --git a/ruleapi/expression.peg.go b/ruleapi/expression.peg.go new file mode 100644 index 0000000..8875c08 --- /dev/null +++ b/ruleapi/expression.peg.go @@ -0,0 +1,1206 @@ +package ruleapi + +// Code generated by peg expression.peg DO NOT EDIT. + +import ( + "fmt" + "io" + "os" + "sort" + "strconv" +) + +const endSymbol rune = 1114112 + +/* The rule types inferred from the grammar are below. */ +type pegRule uint8 + +const ( + ruleUnknown pegRule = iota + rulee + rulee1 + rulee2 + rulee3 + rulee4 + rulee5 + rulevalue + rulenot + ruleand + ruleor + ruleeq + ruleneq + rulegt + rulegte + rulelt + rulelte + ruleopen + ruleclose + rulesp + ruleAction0 + ruleAction1 + ruleAction2 + ruleAction3 + ruleAction4 + ruleAction5 + ruleAction6 + ruleAction7 + ruleAction8 + ruleAction9 + rulePegText + ruleAction10 +) + +var rul3s = [...]string{ + "Unknown", + "e", + "e1", + "e2", + "e3", + "e4", + "e5", + "value", + "not", + "and", + "or", + "eq", + "neq", + "gt", + "gte", + "lt", + "lte", + "open", + "close", + "sp", + "Action0", + "Action1", + "Action2", + "Action3", + "Action4", + "Action5", + "Action6", + "Action7", + "Action8", + "Action9", + "PegText", + "Action10", +} + +type token32 struct { + pegRule + begin, end uint32 +} + +func (t *token32) String() string { + return fmt.Sprintf("\x1B[34m%v\x1B[m %v %v", rul3s[t.pegRule], t.begin, t.end) +} + +type node32 struct { + token32 + up, next *node32 +} + +func (node *node32) print(w io.Writer, pretty bool, buffer string) { + var print func(node *node32, depth int) + print = func(node *node32, depth int) { + for node != nil { + for c := 0; c < depth; c++ { + fmt.Fprintf(w, " ") + } + rule := rul3s[node.pegRule] + quote := strconv.Quote(string(([]rune(buffer)[node.begin:node.end]))) + if !pretty { + fmt.Fprintf(w, "%v %v\n", rule, quote) + } else { + fmt.Fprintf(w, "\x1B[34m%v\x1B[m %v\n", rule, quote) + } + if node.up != nil { + print(node.up, depth+1) + } + node = node.next + } + } + print(node, 0) +} + +func (node *node32) Print(w io.Writer, buffer string) { + node.print(w, false, buffer) +} + +func (node *node32) PrettyPrint(w io.Writer, buffer string) { + node.print(w, true, buffer) +} + +type tokens32 struct { + tree []token32 +} + +func (t *tokens32) Trim(length uint32) { + t.tree = t.tree[:length] +} + +func (t *tokens32) Print() { + for _, token := range t.tree { + fmt.Println(token.String()) + } +} + +func (t *tokens32) AST() *node32 { + type element struct { + node *node32 + down *element + } + tokens := t.Tokens() + var stack *element + for _, token := range tokens { + if token.begin == token.end { + continue + } + node := &node32{token32: token} + for stack != nil && stack.node.begin >= token.begin && stack.node.end <= token.end { + stack.node.next = node.up + node.up = stack.node + stack = stack.down + } + stack = &element{node: node, down: stack} + } + if stack != nil { + return stack.node + } + return nil +} + +func (t *tokens32) PrintSyntaxTree(buffer string) { + t.AST().Print(os.Stdout, buffer) +} + +func (t *tokens32) WriteSyntaxTree(w io.Writer, buffer string) { + t.AST().Print(w, buffer) +} + +func (t *tokens32) PrettyPrintSyntaxTree(buffer string) { + t.AST().PrettyPrint(os.Stdout, buffer) +} + +func (t *tokens32) Add(rule pegRule, begin, end, index uint32) { + tree, i := t.tree, int(index) + if i >= len(tree) { + t.tree = append(tree, token32{pegRule: rule, begin: begin, end: end}) + return + } + tree[i] = token32{pegRule: rule, begin: begin, end: end} +} + +func (t *tokens32) Tokens() []token32 { + return t.tree +} + +type Expr struct { + Expression + + Buffer string + buffer []rune + rules [32]func() bool + parse func(rule ...int) error + reset func() + Pretty bool + tokens32 +} + +func (p *Expr) Parse(rule ...int) error { + return p.parse(rule...) +} + +func (p *Expr) Reset() { + p.reset() +} + +type textPosition struct { + line, symbol int +} + +type textPositionMap map[int]textPosition + +func translatePositions(buffer []rune, positions []int) textPositionMap { + length, translations, j, line, symbol := len(positions), make(textPositionMap, len(positions)), 0, 1, 0 + sort.Ints(positions) + +search: + for i, c := range buffer { + if c == '\n' { + line, symbol = line+1, 0 + } else { + symbol++ + } + if i == positions[j] { + translations[positions[j]] = textPosition{line, symbol} + for j++; j < length; j++ { + if i != positions[j] { + continue search + } + } + break search + } + } + + return translations +} + +type parseError struct { + p *Expr + max token32 +} + +func (e *parseError) Error() string { + tokens, err := []token32{e.max}, "\n" + positions, p := make([]int, 2*len(tokens)), 0 + for _, token := range tokens { + positions[p], p = int(token.begin), p+1 + positions[p], p = int(token.end), p+1 + } + translations := translatePositions(e.p.buffer, positions) + format := "parse error near %v (line %v symbol %v - line %v symbol %v):\n%v\n" + if e.p.Pretty { + format = "parse error near \x1B[34m%v\x1B[m (line %v symbol %v - line %v symbol %v):\n%v\n" + } + for _, token := range tokens { + begin, end := int(token.begin), int(token.end) + err += fmt.Sprintf(format, + rul3s[token.pegRule], + translations[begin].line, translations[begin].symbol, + translations[end].line, translations[end].symbol, + strconv.Quote(string(e.p.buffer[begin:end]))) + } + + return err +} + +func (p *Expr) PrintSyntaxTree() { + if p.Pretty { + p.tokens32.PrettyPrintSyntaxTree(p.Buffer) + } else { + p.tokens32.PrintSyntaxTree(p.Buffer) + } +} + +func (p *Expr) WriteSyntaxTree(w io.Writer) { + p.tokens32.WriteSyntaxTree(w, p.Buffer) +} + +func (p *Expr) Execute() { + buffer, _buffer, text, begin, end := p.Buffer, p.buffer, "", 0, 0 + for _, token := range p.Tokens() { + switch token.pegRule { + + case rulePegText: + begin, end = int(token.begin), int(token.end) + text = string(_buffer[begin:end]) + + case ruleAction0: + p.AddOperator(TypeOr) + case ruleAction1: + p.AddOperator(TypeAnd) + case ruleAction2: + p.AddOperator(TypeNot) + case ruleAction3: + p.AddOperator(TypeEq) + case ruleAction4: + p.AddOperator(TypeNEq) + case ruleAction5: + p.AddOperator(TypeGte) + case ruleAction6: + p.AddOperator(TypeGt) + case ruleAction7: + p.AddOperator(TypeLte) + case ruleAction8: + p.AddOperator(TypeLt) + case ruleAction9: + p.AddOperator(TypeEq) + case ruleAction10: + p.AddLiteral(buffer[begin:end]) + + } + } + _, _, _, _, _ = buffer, _buffer, text, begin, end +} + +func Pretty(pretty bool) func(*Expr) error { + return func(p *Expr) error { + p.Pretty = pretty + return nil + } +} + +func Size(size int) func(*Expr) error { + return func(p *Expr) error { + p.tokens32 = tokens32{tree: make([]token32, 0, size)} + return nil + } +} +func (p *Expr) Init(options ...func(*Expr) error) error { + var ( + max token32 + position, tokenIndex uint32 + buffer []rune + ) + for _, option := range options { + err := option(p) + if err != nil { + return err + } + } + p.reset = func() { + max = token32{} + position, tokenIndex = 0, 0 + + p.buffer = []rune(p.Buffer) + if len(p.buffer) == 0 || p.buffer[len(p.buffer)-1] != endSymbol { + p.buffer = append(p.buffer, endSymbol) + } + buffer = p.buffer + } + p.reset() + + _rules := p.rules + tree := p.tokens32 + p.parse = func(rule ...int) error { + r := 1 + if len(rule) > 0 { + r = rule[0] + } + matches := p.rules[r]() + p.tokens32 = tree + if matches { + p.Trim(tokenIndex) + return nil + } + return &parseError{p, max} + } + + add := func(rule pegRule, begin uint32) { + tree.Add(rule, begin, position, tokenIndex) + tokenIndex++ + if begin != position && position > max.end { + max = token32{rule, begin, position} + } + } + + matchDot := func() bool { + if buffer[position] != endSymbol { + position++ + return true + } + return false + } + + /*matchChar := func(c byte) bool { + if buffer[position] == c { + position++ + return true + } + return false + }*/ + + /*matchRange := func(lower byte, upper byte) bool { + if c := buffer[position]; c >= lower && c <= upper { + position++ + return true + } + return false + }*/ + + _rules = [...]func() bool{ + nil, + /* 0 e <- <(sp e1 !.)> */ + func() bool { + position0, tokenIndex0 := position, tokenIndex + { + position1 := position + if !_rules[rulesp]() { + goto l0 + } + if !_rules[rulee1]() { + goto l0 + } + { + position2, tokenIndex2 := position, tokenIndex + if !matchDot() { + goto l2 + } + goto l0 + l2: + position, tokenIndex = position2, tokenIndex2 + } + add(rulee, position1) + } + return true + l0: + position, tokenIndex = position0, tokenIndex0 + return false + }, + /* 1 e1 <- <(e2 (or e2 Action0)*)> */ + func() bool { + position3, tokenIndex3 := position, tokenIndex + { + position4 := position + if !_rules[rulee2]() { + goto l3 + } + l5: + { + position6, tokenIndex6 := position, tokenIndex + if !_rules[ruleor]() { + goto l6 + } + if !_rules[rulee2]() { + goto l6 + } + if !_rules[ruleAction0]() { + goto l6 + } + goto l5 + l6: + position, tokenIndex = position6, tokenIndex6 + } + add(rulee1, position4) + } + return true + l3: + position, tokenIndex = position3, tokenIndex3 + return false + }, + /* 2 e2 <- <(e3 (and e3 Action1)*)> */ + func() bool { + position7, tokenIndex7 := position, tokenIndex + { + position8 := position + if !_rules[rulee3]() { + goto l7 + } + l9: + { + position10, tokenIndex10 := position, tokenIndex + if !_rules[ruleand]() { + goto l10 + } + if !_rules[rulee3]() { + goto l10 + } + if !_rules[ruleAction1]() { + goto l10 + } + goto l9 + l10: + position, tokenIndex = position10, tokenIndex10 + } + add(rulee2, position8) + } + return true + l7: + position, tokenIndex = position7, tokenIndex7 + return false + }, + /* 3 e3 <- <((not e4 Action2) / e4)> */ + func() bool { + position11, tokenIndex11 := position, tokenIndex + { + position12 := position + { + position13, tokenIndex13 := position, tokenIndex + if !_rules[rulenot]() { + goto l14 + } + if !_rules[rulee4]() { + goto l14 + } + if !_rules[ruleAction2]() { + goto l14 + } + goto l13 + l14: + position, tokenIndex = position13, tokenIndex13 + if !_rules[rulee4]() { + goto l11 + } + } + l13: + add(rulee3, position12) + } + return true + l11: + position, tokenIndex = position11, tokenIndex11 + return false + }, + /* 4 e4 <- <((eq e5 Action3) / (neq e5 Action4) / (gte e5 Action5) / (gt e5 Action6) / (lte e5 Action7) / (lt e5 Action8) / (open e1 close) / (e5 Action9))> */ + func() bool { + position15, tokenIndex15 := position, tokenIndex + { + position16 := position + { + position17, tokenIndex17 := position, tokenIndex + if !_rules[ruleeq]() { + goto l18 + } + if !_rules[rulee5]() { + goto l18 + } + if !_rules[ruleAction3]() { + goto l18 + } + goto l17 + l18: + position, tokenIndex = position17, tokenIndex17 + if !_rules[ruleneq]() { + goto l19 + } + if !_rules[rulee5]() { + goto l19 + } + if !_rules[ruleAction4]() { + goto l19 + } + goto l17 + l19: + position, tokenIndex = position17, tokenIndex17 + if !_rules[rulegte]() { + goto l20 + } + if !_rules[rulee5]() { + goto l20 + } + if !_rules[ruleAction5]() { + goto l20 + } + goto l17 + l20: + position, tokenIndex = position17, tokenIndex17 + if !_rules[rulegt]() { + goto l21 + } + if !_rules[rulee5]() { + goto l21 + } + if !_rules[ruleAction6]() { + goto l21 + } + goto l17 + l21: + position, tokenIndex = position17, tokenIndex17 + if !_rules[rulelte]() { + goto l22 + } + if !_rules[rulee5]() { + goto l22 + } + if !_rules[ruleAction7]() { + goto l22 + } + goto l17 + l22: + position, tokenIndex = position17, tokenIndex17 + if !_rules[rulelt]() { + goto l23 + } + if !_rules[rulee5]() { + goto l23 + } + if !_rules[ruleAction8]() { + goto l23 + } + goto l17 + l23: + position, tokenIndex = position17, tokenIndex17 + if !_rules[ruleopen]() { + goto l24 + } + if !_rules[rulee1]() { + goto l24 + } + if !_rules[ruleclose]() { + goto l24 + } + goto l17 + l24: + position, tokenIndex = position17, tokenIndex17 + if !_rules[rulee5]() { + goto l15 + } + if !_rules[ruleAction9]() { + goto l15 + } + } + l17: + add(rulee4, position16) + } + return true + l15: + position, tokenIndex = position15, tokenIndex15 + return false + }, + /* 5 e5 <- <( sp Action10)> */ + func() bool { + position25, tokenIndex25 := position, tokenIndex + { + position26 := position + { + position27 := position + if !_rules[rulevalue]() { + goto l25 + } + add(rulePegText, position27) + } + if !_rules[rulesp]() { + goto l25 + } + if !_rules[ruleAction10]() { + goto l25 + } + add(rulee5, position26) + } + return true + l25: + position, tokenIndex = position25, tokenIndex25 + return false + }, + /* 6 value <- <(([0-9]+ '.' [0-9]*) / ('.' [0-9]+) / ('"' (!'"' .)* '"') / ('\'' (!'\'' .)* '\'') / ([a-z] / [A-Z] / '_' / [0-9])+)> */ + func() bool { + position28, tokenIndex28 := position, tokenIndex + { + position29 := position + { + position30, tokenIndex30 := position, tokenIndex + if c := buffer[position]; c < rune('0') || c > rune('9') { + goto l31 + } + position++ + l32: + { + position33, tokenIndex33 := position, tokenIndex + if c := buffer[position]; c < rune('0') || c > rune('9') { + goto l33 + } + position++ + goto l32 + l33: + position, tokenIndex = position33, tokenIndex33 + } + if buffer[position] != rune('.') { + goto l31 + } + position++ + l34: + { + position35, tokenIndex35 := position, tokenIndex + if c := buffer[position]; c < rune('0') || c > rune('9') { + goto l35 + } + position++ + goto l34 + l35: + position, tokenIndex = position35, tokenIndex35 + } + goto l30 + l31: + position, tokenIndex = position30, tokenIndex30 + if buffer[position] != rune('.') { + goto l36 + } + position++ + if c := buffer[position]; c < rune('0') || c > rune('9') { + goto l36 + } + position++ + l37: + { + position38, tokenIndex38 := position, tokenIndex + if c := buffer[position]; c < rune('0') || c > rune('9') { + goto l38 + } + position++ + goto l37 + l38: + position, tokenIndex = position38, tokenIndex38 + } + goto l30 + l36: + position, tokenIndex = position30, tokenIndex30 + if buffer[position] != rune('"') { + goto l39 + } + position++ + l40: + { + position41, tokenIndex41 := position, tokenIndex + { + position42, tokenIndex42 := position, tokenIndex + if buffer[position] != rune('"') { + goto l42 + } + position++ + goto l41 + l42: + position, tokenIndex = position42, tokenIndex42 + } + if !matchDot() { + goto l41 + } + goto l40 + l41: + position, tokenIndex = position41, tokenIndex41 + } + if buffer[position] != rune('"') { + goto l39 + } + position++ + goto l30 + l39: + position, tokenIndex = position30, tokenIndex30 + if buffer[position] != rune('\'') { + goto l43 + } + position++ + l44: + { + position45, tokenIndex45 := position, tokenIndex + { + position46, tokenIndex46 := position, tokenIndex + if buffer[position] != rune('\'') { + goto l46 + } + position++ + goto l45 + l46: + position, tokenIndex = position46, tokenIndex46 + } + if !matchDot() { + goto l45 + } + goto l44 + l45: + position, tokenIndex = position45, tokenIndex45 + } + if buffer[position] != rune('\'') { + goto l43 + } + position++ + goto l30 + l43: + position, tokenIndex = position30, tokenIndex30 + { + position49, tokenIndex49 := position, tokenIndex + if c := buffer[position]; c < rune('a') || c > rune('z') { + goto l50 + } + position++ + goto l49 + l50: + position, tokenIndex = position49, tokenIndex49 + if c := buffer[position]; c < rune('A') || c > rune('Z') { + goto l51 + } + position++ + goto l49 + l51: + position, tokenIndex = position49, tokenIndex49 + if buffer[position] != rune('_') { + goto l52 + } + position++ + goto l49 + l52: + position, tokenIndex = position49, tokenIndex49 + if c := buffer[position]; c < rune('0') || c > rune('9') { + goto l28 + } + position++ + } + l49: + l47: + { + position48, tokenIndex48 := position, tokenIndex + { + position53, tokenIndex53 := position, tokenIndex + if c := buffer[position]; c < rune('a') || c > rune('z') { + goto l54 + } + position++ + goto l53 + l54: + position, tokenIndex = position53, tokenIndex53 + if c := buffer[position]; c < rune('A') || c > rune('Z') { + goto l55 + } + position++ + goto l53 + l55: + position, tokenIndex = position53, tokenIndex53 + if buffer[position] != rune('_') { + goto l56 + } + position++ + goto l53 + l56: + position, tokenIndex = position53, tokenIndex53 + if c := buffer[position]; c < rune('0') || c > rune('9') { + goto l48 + } + position++ + } + l53: + goto l47 + l48: + position, tokenIndex = position48, tokenIndex48 + } + } + l30: + add(rulevalue, position29) + } + return true + l28: + position, tokenIndex = position28, tokenIndex28 + return false + }, + /* 7 not <- <('!' sp)> */ + func() bool { + position57, tokenIndex57 := position, tokenIndex + { + position58 := position + if buffer[position] != rune('!') { + goto l57 + } + position++ + if !_rules[rulesp]() { + goto l57 + } + add(rulenot, position58) + } + return true + l57: + position, tokenIndex = position57, tokenIndex57 + return false + }, + /* 8 and <- <('&' '&' sp)> */ + func() bool { + position59, tokenIndex59 := position, tokenIndex + { + position60 := position + if buffer[position] != rune('&') { + goto l59 + } + position++ + if buffer[position] != rune('&') { + goto l59 + } + position++ + if !_rules[rulesp]() { + goto l59 + } + add(ruleand, position60) + } + return true + l59: + position, tokenIndex = position59, tokenIndex59 + return false + }, + /* 9 or <- <('|' '|' sp)> */ + func() bool { + position61, tokenIndex61 := position, tokenIndex + { + position62 := position + if buffer[position] != rune('|') { + goto l61 + } + position++ + if buffer[position] != rune('|') { + goto l61 + } + position++ + if !_rules[rulesp]() { + goto l61 + } + add(ruleor, position62) + } + return true + l61: + position, tokenIndex = position61, tokenIndex61 + return false + }, + /* 10 eq <- <('=' '=' sp)> */ + func() bool { + position63, tokenIndex63 := position, tokenIndex + { + position64 := position + if buffer[position] != rune('=') { + goto l63 + } + position++ + if buffer[position] != rune('=') { + goto l63 + } + position++ + if !_rules[rulesp]() { + goto l63 + } + add(ruleeq, position64) + } + return true + l63: + position, tokenIndex = position63, tokenIndex63 + return false + }, + /* 11 neq <- <('!' '=' sp)> */ + func() bool { + position65, tokenIndex65 := position, tokenIndex + { + position66 := position + if buffer[position] != rune('!') { + goto l65 + } + position++ + if buffer[position] != rune('=') { + goto l65 + } + position++ + if !_rules[rulesp]() { + goto l65 + } + add(ruleneq, position66) + } + return true + l65: + position, tokenIndex = position65, tokenIndex65 + return false + }, + /* 12 gt <- <('>' sp)> */ + func() bool { + position67, tokenIndex67 := position, tokenIndex + { + position68 := position + if buffer[position] != rune('>') { + goto l67 + } + position++ + if !_rules[rulesp]() { + goto l67 + } + add(rulegt, position68) + } + return true + l67: + position, tokenIndex = position67, tokenIndex67 + return false + }, + /* 13 gte <- <('>' '=' sp)> */ + func() bool { + position69, tokenIndex69 := position, tokenIndex + { + position70 := position + if buffer[position] != rune('>') { + goto l69 + } + position++ + if buffer[position] != rune('=') { + goto l69 + } + position++ + if !_rules[rulesp]() { + goto l69 + } + add(rulegte, position70) + } + return true + l69: + position, tokenIndex = position69, tokenIndex69 + return false + }, + /* 14 lt <- <('<' sp)> */ + func() bool { + position71, tokenIndex71 := position, tokenIndex + { + position72 := position + if buffer[position] != rune('<') { + goto l71 + } + position++ + if !_rules[rulesp]() { + goto l71 + } + add(rulelt, position72) + } + return true + l71: + position, tokenIndex = position71, tokenIndex71 + return false + }, + /* 15 lte <- <('<' '=' sp)> */ + func() bool { + position73, tokenIndex73 := position, tokenIndex + { + position74 := position + if buffer[position] != rune('<') { + goto l73 + } + position++ + if buffer[position] != rune('=') { + goto l73 + } + position++ + if !_rules[rulesp]() { + goto l73 + } + add(rulelte, position74) + } + return true + l73: + position, tokenIndex = position73, tokenIndex73 + return false + }, + /* 16 open <- <('(' sp)> */ + func() bool { + position75, tokenIndex75 := position, tokenIndex + { + position76 := position + if buffer[position] != rune('(') { + goto l75 + } + position++ + if !_rules[rulesp]() { + goto l75 + } + add(ruleopen, position76) + } + return true + l75: + position, tokenIndex = position75, tokenIndex75 + return false + }, + /* 17 close <- <(')' sp)> */ + func() bool { + position77, tokenIndex77 := position, tokenIndex + { + position78 := position + if buffer[position] != rune(')') { + goto l77 + } + position++ + if !_rules[rulesp]() { + goto l77 + } + add(ruleclose, position78) + } + return true + l77: + position, tokenIndex = position77, tokenIndex77 + return false + }, + /* 18 sp <- <(' ' / '\t')*> */ + func() bool { + { + position80 := position + l81: + { + position82, tokenIndex82 := position, tokenIndex + { + position83, tokenIndex83 := position, tokenIndex + if buffer[position] != rune(' ') { + goto l84 + } + position++ + goto l83 + l84: + position, tokenIndex = position83, tokenIndex83 + if buffer[position] != rune('\t') { + goto l82 + } + position++ + } + l83: + goto l81 + l82: + position, tokenIndex = position82, tokenIndex82 + } + add(rulesp, position80) + } + return true + }, + /* 20 Action0 <- <{ p.AddOperator(TypeOr) }> */ + func() bool { + { + add(ruleAction0, position) + } + return true + }, + /* 21 Action1 <- <{ p.AddOperator(TypeAnd) }> */ + func() bool { + { + add(ruleAction1, position) + } + return true + }, + /* 22 Action2 <- <{ p.AddOperator(TypeNot) }> */ + func() bool { + { + add(ruleAction2, position) + } + return true + }, + /* 23 Action3 <- <{ p.AddOperator(TypeEq) }> */ + func() bool { + { + add(ruleAction3, position) + } + return true + }, + /* 24 Action4 <- <{ p.AddOperator(TypeNEq) }> */ + func() bool { + { + add(ruleAction4, position) + } + return true + }, + /* 25 Action5 <- <{ p.AddOperator(TypeGte) }> */ + func() bool { + { + add(ruleAction5, position) + } + return true + }, + /* 26 Action6 <- <{ p.AddOperator(TypeGt) }> */ + func() bool { + { + add(ruleAction6, position) + } + return true + }, + /* 27 Action7 <- <{ p.AddOperator(TypeLte) }> */ + func() bool { + { + add(ruleAction7, position) + } + return true + }, + /* 28 Action8 <- <{ p.AddOperator(TypeLt) }> */ + func() bool { + { + add(ruleAction8, position) + } + return true + }, + /* 29 Action9 <- <{ p.AddOperator(TypeEq) }> */ + func() bool { + { + add(ruleAction9, position) + } + return true + }, + nil, + /* 31 Action10 <- <{ p.AddLiteral(buffer[begin:end]) }> */ + func() bool { + { + add(ruleAction10, position) + } + return true + }, + } + p.rules = _rules + return nil +} diff --git a/ruleapi/internal/store/mem/mstore.go b/ruleapi/internal/store/mem/mstore.go new file mode 100644 index 0000000..a4c5f51 --- /dev/null +++ b/ruleapi/internal/store/mem/mstore.go @@ -0,0 +1,70 @@ +package mem + +import ( + "fmt" + "sync" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/rete/common" +) + +type storeImpl struct { + allTuples map[string]model.Tuple + sync.RWMutex +} + +func NewStore(config common.Config) model.TupleStore { + ms := storeImpl{ + allTuples: make(map[string]model.Tuple), + } + return &ms +} + +func (ms *storeImpl) Init() { + +} + +func (ms *storeImpl) GetTupleByKey(key model.TupleKey) model.Tuple { + ms.RLock() + defer ms.RUnlock() + return ms.allTuples[key.String()] +} + +func (ms *storeImpl) SaveTuple(tuple model.Tuple) { + ms.Lock() + defer ms.Unlock() + ms.allTuples[tuple.GetKey().String()] = tuple +} + +func (ms *storeImpl) DeleteTuple(key model.TupleKey) { + ms.Lock() + defer ms.Unlock() + delete(ms.allTuples, key.String()) +} + +func (ms *storeImpl) SaveTuples(added map[string]map[string]model.Tuple) { + for tupleType, tuples := range added { + for key, tuple := range tuples { + fmt.Printf("Saving tuple. Type [%s] Key [%s], Val [%v]\n", tupleType, key, tuple) + ms.SaveTuple(tuple) + } + } +} + +func (ms *storeImpl) SaveModifiedTuples(modified map[string]map[string]model.RtcModified) { + for tupleType, mmap := range modified { + for key, mdfd := range mmap { + fmt.Printf("Saving tuple. Type [%s] Key [%s], Val [%v]\n", tupleType, key, mdfd.GetTuple()) + ms.SaveTuple(mdfd.GetTuple()) + } + } +} + +func (ms *storeImpl) DeleteTuples(deleted map[string]map[string]model.Tuple) { + for tupleType, tuples := range deleted { + for key, tuple := range tuples { + fmt.Printf("Deleting tuple. Type [%s] Key [%s], Val [%v]\n", tupleType, key, tuple) + ms.DeleteTuple(tuple.GetKey()) + } + } +} diff --git a/ruleapi/internal/store/redis/rstore.go b/ruleapi/internal/store/redis/rstore.go new file mode 100644 index 0000000..77a9f10 --- /dev/null +++ b/ruleapi/internal/store/redis/rstore.go @@ -0,0 +1,83 @@ +package redis + +import ( + "fmt" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/redisutils" + "github.com/project-flogo/rules/rete/common" +) + +type storeImpl struct { + //allTuples map[string]model.Tuple + prefix string + config common.Config + redisutils.RedisHdl +} + +func NewStore(config common.Config) model.TupleStore { + ms := storeImpl{ + config: config, + RedisHdl: redisutils.NewRedisHdl(config.Stores.Redis), + } + return &ms +} + +func (ms *storeImpl) Init() { + +} + +func (ms *storeImpl) GetTupleByKey(key model.TupleKey) model.Tuple { + strKey := ms.prefix + key.String() + vals := ms.HGetAll(strKey) + + tuple, err := model.NewTuple(model.TupleType(key.GetTupleDescriptor().Name), vals) + + if err == nil { + return tuple + } + return nil +} + +func (ms *storeImpl) SaveTuple(tuple model.Tuple) { + m := tuple.ToMap() + + strKey := ms.prefix + tuple.GetKey().String() + + ms.HSetAll(strKey, m) +} + +func (ms *storeImpl) DeleteTuple(key model.TupleKey) { + strKey := ms.prefix + key.String() + ms.Del(strKey) +} + +func (ms *storeImpl) SaveTuples(added map[string]map[string]model.Tuple) { + for tupleType, tuples := range added { + for key, tuple := range tuples { + fmt.Printf("Saving tuple. Type [%s] Key [%s]\n", tupleType, key) + strKey := ms.prefix + key + ms.HSetAll(strKey, tuple.ToMap()) + } + } +} + +func (ms *storeImpl) SaveModifiedTuples(modified map[string]map[string]model.RtcModified) { + for tupleType, mmap := range modified { + for key, mdfd := range mmap { + fmt.Printf("Saving tuple. Type [%s] Key [%s]\n", tupleType, key) + strKey := ms.prefix + key + ms.HSetAll(strKey, mdfd.GetTuple().ToMap()) + } + } +} + +func (ms *storeImpl) DeleteTuples(deleted map[string]map[string]model.Tuple) { + for tupleType, tuples := range deleted { + for key, _ := range tuples { + fmt.Printf("Deleting tuple. Type [%s] Key [%s]\n", tupleType, key) + strKey := ms.prefix + key + ms.Del(strKey) + } + } +} diff --git a/ruleapi/rule.go b/ruleapi/rule.go index c4fc1af..6d53a2f 100644 --- a/ruleapi/rule.go +++ b/ruleapi/rule.go @@ -2,19 +2,20 @@ package ruleapi import ( "fmt" + "regexp" "strings" "github.com/project-flogo/rules/common/model" ) type ruleImpl struct { - name string - identifiers []model.TupleType - conditions []model.Condition - actionFn model.ActionFunction - priority int - deps map[model.TupleType]map[string]bool - ctx model.RuleContext + name string + identifiers []model.TupleType + conditions []model.Condition + actionService model.ActionService + priority int + deps map[model.TupleType]map[string]bool + ctx model.RuleContext } func (rule *ruleImpl) GetContext() model.RuleContext { @@ -43,16 +44,16 @@ func (rule *ruleImpl) GetName() string { return rule.name } -func (rule *ruleImpl) GetActionFn() model.ActionFunction { - return rule.actionFn +func (rule *ruleImpl) GetActionService() model.ActionService { + return rule.actionService } func (rule *ruleImpl) GetConditions() []model.Condition { return rule.conditions } -func (rule *ruleImpl) SetActionFn(actionFn model.ActionFunction) { - rule.actionFn = actionFn +func (rule *ruleImpl) SetActionService(actionService model.ActionService) { + rule.actionService = actionService } func (rule *ruleImpl) addCond(conditionName string, idrs []model.TupleType, cfn model.ConditionEvaluator, ctx model.RuleContext, setIdr bool) { @@ -74,6 +75,47 @@ func (rule *ruleImpl) addCond(conditionName string, idrs []model.TupleType, cfn } +func (rule *ruleImpl) AddIdrsToRule(idrs []model.TupleType) { + for _, cidr := range idrs { + //TODO: configure the rulesession + if model.GetTupleDescriptor(cidr) == nil { + return + } + if len(rule.identifiers) == 0 { + rule.identifiers = append(rule.identifiers, cidr) + } else { + found := false + for _, ridr := range rule.identifiers { + if cidr == ridr { + found = true + break + } + } + if !found { + rule.identifiers = append(rule.identifiers, cidr) + } + } + } +} + +func (rule *ruleImpl) addExprCond(conditionName string, idrs []model.TupleType, cExpr string, ctx model.RuleContext) { + condition := newExprCondition(conditionName, rule, idrs, cExpr, ctx) + rule.conditions = append(rule.conditions, condition) + + for _, cidr := range idrs { + if len(rule.identifiers) == 0 { + rule.identifiers = append(rule.identifiers, cidr) + } else { + for _, ridr := range rule.identifiers { + if cidr != ridr { + rule.identifiers = append(rule.identifiers, cidr) + break + } + } + } + } +} + func (rule *ruleImpl) GetPriority() int { return rule.priority } @@ -99,11 +141,7 @@ func (rule *ruleImpl) GetIdentifiers() []model.TupleType { return rule.identifiers } -func (rule *ruleImpl) SetAction(actionFn model.ActionFunction) { - rule.actionFn = actionFn -} - -func (rule *ruleImpl) AddCondition(conditionName string, idrs []string, cFn model.ConditionEvaluator, ctx model.RuleContext) (err error) { +func (rule *ruleImpl) AddCondition2(conditionName string, idrs []string, cFn model.ConditionEvaluator, ctx model.RuleContext) (err error) { typeDeps := []model.TupleType{} for _, idr := range idrs { aliasProp := strings.Split(string(idr), ".") @@ -138,6 +176,149 @@ func (rule *ruleImpl) AddCondition(conditionName string, idrs []string, cFn mode return err } +func (rule *ruleImpl) AddCondition(conditionName string, idrs []string, cFn model.ConditionEvaluator, ctx model.RuleContext) (err error) { + typeDeps, err := rule.addDeps(idrs) + if err != nil { + return err + } + rule.addCond(conditionName, typeDeps, cFn, ctx, true) + return err +} + +func (rule *ruleImpl) addDeps(idrs []string) ([]model.TupleType, error) { + typeDeps := []model.TupleType{} + for _, idr := range idrs { + aliasProp := strings.Split(string(idr), ".") + alias := model.TupleType(aliasProp[0]) + + if model.GetTupleDescriptor(model.TupleType(alias)) == nil { + return typeDeps, fmt.Errorf("Tuple type not found [%s]", string(alias)) + } + + exists, _ := model.Contains(typeDeps, alias) + if !exists { + typeDeps = append(typeDeps, alias) + } + if len(aliasProp) == 2 { //specifically 2, else do not consider + prop := aliasProp[1] + + td := model.GetTupleDescriptor(model.TupleType(alias)) + if prop != "none" && td.GetProperty(prop) == nil { //"none" is a special case + return typeDeps, fmt.Errorf("TupleType property not found [%s]", prop) + } + + propMap, found := rule.deps[alias] + if !found { + propMap = map[string]bool{} + rule.deps[alias] = propMap + } + propMap[prop] = true + } + } + + return typeDeps, nil +} + func (rule *ruleImpl) GetDeps() map[model.TupleType]map[string]bool { return rule.deps } + +func (rule *ruleImpl) AddExprCondition(conditionName string, cstr string, ctx model.RuleContext) error { + + //e, err := expression.ParseExpression(cstr) + //if err != nil { + // return err + //} + //exprn := e.(*expr.Expression) + //refs, err := getRefs(exprn) + refs := getRefs(cstr) + + err := validateRefs(refs) + if err != nil { + return err + } + + typeDeps, err := rule.addDeps(refs) + if err != nil { + return err + } + rule.addExprCond(conditionName, typeDeps, cstr, ctx) + return nil + +} + +func validateRefs(refs []string) error { + for _, ref := range refs { + ref := strings.TrimPrefix(ref, "$.") + vals := strings.Split(ref, ".") + td := model.GetTupleDescriptor(model.TupleType(vals[0])) + if td == nil { + return fmt.Errorf("Invalid TupleType [%s]", vals[0]) + } + prop := td.GetProperty(vals[1]) + if prop == nil { + return fmt.Errorf("Property [%s] not found in TupleType [%s]", vals[1], vals[0]) + } + } + return nil +} + +// +//func getRefs(e *expr.Expression) ([]string, error) { +// refs := make(map[string]bool) +// keys := []string{} +// +// err := getRefRecursively(e, refs) +// if err != nil { +// return keys, err +// } +// +// for key, _ := range refs { +// keys = append (keys, key) +// } +// return keys, err +//} +// +//func getRefRecursively(e *expr.Expression, refs map[string]bool) error { +// +// if e == nil { +// return nil +// } +// err := getRefsInternal(e.Left, refs) +// if err != nil { +// return err +// } +// err = getRefsInternal(e.Right, refs) +// if err != nil { +// return err +// } +// return nil +//} +// +//func getRefsInternal(e *expr.Expression, refs map[string]bool) error { +// if e.Type == funcexprtype.EXPRESSION { +// getRefRecursively(e, refs) +// } else if e.Type == funcexprtype.REF || e.Type == funcexprtype.ARRAYREF { +// value := e.Value.(string) +// if strings.Index(value, "$") == 0 { +// value = value[1:len(value)] +// split := strings.Split(value, ".") +// if split != nil && len(split) != 2 { +// return fmt.Errorf("Invalid tokens [%s]", value) +// } +// +// refs[value] = true +// } +// } +// return nil +//} + +func getRefs(cstr string) []string { + keys2 := []string{} + re := regexp.MustCompile(`\$\.(\w+\.\w+)`) + keys := re.FindAllStringSubmatch(cstr, -1) + for _, k := range keys { + keys2 = append(keys2, k[1]) + } + return keys2 +} diff --git a/ruleapi/rulesession.go b/ruleapi/rulesession.go index 0a5ec42..8a8c041 100644 --- a/ruleapi/rulesession.go +++ b/ruleapi/rulesession.go @@ -5,12 +5,17 @@ import ( "encoding/json" "errors" "fmt" + "os" "sync" "time" + utils "github.com/project-flogo/rules/common" "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/config" "github.com/project-flogo/rules/rete" + "github.com/project-flogo/rules/rete/common" + "github.com/project-flogo/rules/ruleapi/internal/store/mem" + "github.com/project-flogo/rules/ruleapi/internal/store/redis" ) var ( @@ -18,26 +23,41 @@ var ( ) type rulesessionImpl struct { + sync.RWMutex + name string - reteNetwork rete.Network + reteNetwork common.Network + + timers map[interface{}]*time.Timer + startupFn model.StartupRSFunction + started bool + storeConfig string + tupleStore model.TupleStore + config common.Config +} - timers map[interface{}]*time.Timer - startupFn model.StartupRSFunction - started bool +func ClearSessions() { + sessionMap.Range(func(key, value interface{}) bool { + sessionMap.Delete(key) + return true + }) } -func GetOrCreateRuleSession(name string) (model.RuleSession, error) { +func GetOrCreateRuleSession(name, config string) (model.RuleSession, error) { if name == "" { return nil, errors.New("RuleSession name cannot be empty") } rs := rulesessionImpl{} + rs.loadStoreConfig(config) rs.initRuleSession(name) + rs1, _ := sessionMap.LoadOrStore(name, &rs) return rs1.(*rulesessionImpl), nil } -func GetOrCreateRuleSessionFromConfig(name string, jsonConfig string) (model.RuleSession, error) { - rs, err := GetOrCreateRuleSession(name) +// GetOrCreateRuleSessionFromConfig returns rule session from created from config +func GetOrCreateRuleSessionFromConfig(name, store, jsonConfig string) (model.RuleSession, error) { + rs, err := GetOrCreateRuleSession(name, store) if err != nil { return nil, err @@ -49,14 +69,44 @@ func GetOrCreateRuleSessionFromConfig(name string, jsonConfig string) (model.Rul return nil, err } + // inflate action services + aServices := make(map[string]model.ActionService) + for _, s := range ruleSessionDescriptor.Services { + aService, err := NewActionService(s) + if err != nil { + return nil, err + } + aServices[s.Name] = aService + } + for _, ruleCfg := range ruleSessionDescriptor.Rules { rule := NewRule(ruleCfg.Name) rule.SetContext("This is a test of context") - rule.SetAction(ruleCfg.ActionFunc) + // set action service to rule, if exist + if ruleCfg.ActionService != nil { + aService, found := aServices[ruleCfg.ActionService.Service] + if !found { + return nil, fmt.Errorf("rule action service[%s] not found", ruleCfg.ActionService.Service) + } + aService.SetInput(ruleCfg.ActionService.Input) + rule.SetActionService(aService) + } rule.SetPriority(ruleCfg.Priority) for _, condCfg := range ruleCfg.Conditions { - rule.AddCondition(condCfg.Name, condCfg.Identifiers, condCfg.Evaluator, nil) + if condCfg.Expression == "" { + rule.AddCondition(condCfg.Name, condCfg.Identifiers, condCfg.Evaluator, nil) + } else { + rule.AddExprCondition(condCfg.Name, condCfg.Expression, nil) + } + } + //now add explicit rule identifiers if any + if ruleCfg.Identifiers != nil { + idrs := []model.TupleType{} + for _, idr := range ruleCfg.Identifiers { + idrs = append(idrs, model.TupleType(idr)) + } + rule.AddIdrsToRule(idrs) } rs.AddRule(rule) @@ -67,11 +117,86 @@ func GetOrCreateRuleSessionFromConfig(name string, jsonConfig string) (model.Rul return rs, nil } -func (rs *rulesessionImpl) initRuleSession(name string) { - rs.reteNetwork = rete.NewReteNetwork() +const defaultConfig = `{ + "mode": "consistency", + "rs": { + "prefix": "x", + "store-ref": "mem" + }, + "rete": { + "jt-ref": "mem", + "idgen-ref": "mem", + "jt":"mem" + }, + "stores": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + }, + "idgens": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + }, + "jts": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + } +}` + +func (rs *rulesessionImpl) loadStoreConfig(name string) { + if value := os.Getenv("STORECONFIG"); value != "" { + name = value + } + + if _, err := os.Stat(name); err != nil { + rs.storeConfig = defaultConfig + return + } + + rs.storeConfig = utils.FileToString(name) +} + +func (rs *rulesessionImpl) initRuleSession(name string) error { + + err := json.Unmarshal([]byte(rs.storeConfig), &rs.config) + if err != nil { + return err + } + rs.name = name rs.timers = make(map[interface{}]*time.Timer) + rs.reteNetwork = rete.NewReteNetwork(rs.name, rs.storeConfig) + + //TODO: Configure it from jconsonfig + tupleStore := getTupleStore(rs.config) + if tupleStore != nil { + tupleStore.Init() + rs.SetStore(tupleStore) + } rs.started = false + return nil +} + +func getTupleStore(config common.Config) model.TupleStore { + switch config.Rs.StoreRef { + case common.ServiceTypeMem: + return mem.NewStore(config) + case common.ServiceTypeRedis: + return redis.NewStore(config) + default: + panic("invalid service type") + } } func (rs *rulesessionImpl) AddRule(rule model.Rule) (err error) { @@ -90,25 +215,19 @@ func (rs *rulesessionImpl) Assert(ctx context.Context, tuple model.Tuple) (err e if !rs.started { return fmt.Errorf("Cannot assert tuple. Rulesession [%s] not started", rs.name) } - assertedTuple := rs.GetAssertedTuple(tuple.GetKey()) - if assertedTuple == tuple { - return fmt.Errorf("Tuple with key [%s] already asserted", tuple.GetKey().String()) - } else if assertedTuple != nil { - return fmt.Errorf("Tuple with key [%s] already asserted", tuple.GetKey().String()) - } if ctx == nil { ctx = context.Context(context.Background()) } - rs.reteNetwork.Assert(ctx, rs, tuple, nil, rete.ADD) - return nil + + return rs.reteNetwork.Assert(ctx, rs, tuple, nil, common.ADD) } -func (rs *rulesessionImpl) Retract(ctx context.Context, tuple model.Tuple) { - rs.reteNetwork.Retract(ctx, tuple, nil, rete.RETRACT) +func (rs *rulesessionImpl) Retract(ctx context.Context, tuple model.Tuple) error { + return rs.reteNetwork.Retract(ctx, rs, tuple, nil, common.RETRACT) } -func (rs *rulesessionImpl) Delete(ctx context.Context, tuple model.Tuple) { - rs.reteNetwork.Retract(ctx, tuple, nil, rete.DELETE) +func (rs *rulesessionImpl) Delete(ctx context.Context, tuple model.Tuple) error { + return rs.reteNetwork.Retract(ctx, rs, tuple, nil, common.DELETE) } func (rs *rulesessionImpl) printNetwork() { @@ -126,17 +245,25 @@ func (rs *rulesessionImpl) Unregister() { func (rs *rulesessionImpl) ScheduleAssert(ctx context.Context, delayInMillis uint64, key interface{}, tuple model.Tuple) { timer := time.AfterFunc(time.Millisecond*time.Duration(delayInMillis), func() { + rs.Lock() + defer rs.Unlock() ctxNew := context.TODO() delete(rs.timers, key) rs.Assert(ctxNew, tuple) }) + rs.Lock() + defer rs.Unlock() rs.timers[key] = timer } func (rs *rulesessionImpl) CancelScheduledAssert(ctx context.Context, key interface{}) { + rs.RLock() timer, ok := rs.timers[key] + rs.RUnlock() if ok { + rs.Lock() + defer rs.Unlock() fmt.Printf("Cancelling timer attached to key [%v]\n", key) delete(rs.timers, key) timer.Stop() @@ -167,14 +294,41 @@ func (rs *rulesessionImpl) Start(startupCtx map[string]interface{}) error { return nil } -func (rs *rulesessionImpl) GetAssertedTuple(key model.TupleKey) model.Tuple { - return rs.reteNetwork.GetAssertedTuple(key) +func (rs *rulesessionImpl) GetAssertedTuple(ctx context.Context, key model.TupleKey) model.Tuple { + return rs.reteNetwork.GetAssertedTuple(ctx, rs, key) } func (rs *rulesessionImpl) RegisterRtcTransactionHandler(txnHandler model.RtcTransactionHandler, txnContext interface{}) { rs.reteNetwork.RegisterRtcTransactionHandler(txnHandler, txnContext) } +func (rs *rulesessionImpl) GetStore() model.TupleStore { + return rs.tupleStore +} + +func (rs *rulesessionImpl) SetStore(store model.TupleStore) error { + if store == nil { + return fmt.Errorf("Cannot set nil store") + } + if rs.tupleStore != nil { + return fmt.Errorf("TupleStore already set") + } + if rs.started { + return fmt.Errorf("RuleSession already started") + } + rs.tupleStore = store + rs.reteNetwork.SetTupleStore(store) + rs.reteNetwork.RegisterRtcTransactionHandler(internalTxnHandler, nil) + return nil +} + +func internalTxnHandler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + store := rs.GetStore() + store.DeleteTuples(rtxn.GetRtcDeleted()) + store.SaveTuples(rtxn.GetRtcAdded()) + store.SaveModifiedTuples(rtxn.GetRtcModified()) +} + func (rs *rulesessionImpl) ReplayTuplesForRule(ruleName string) (err error) { return rs.reteNetwork.ReplayTuplesForRule(ruleName, rs) } diff --git a/ruleapi/servicecontext.go b/ruleapi/servicecontext.go new file mode 100644 index 0000000..23fadec --- /dev/null +++ b/ruleapi/servicecontext.go @@ -0,0 +1,185 @@ +package ruleapi + +import ( + "github.com/project-flogo/core/activity" + "github.com/project-flogo/core/data" + "github.com/project-flogo/core/data/mapper" + "github.com/project-flogo/core/data/metadata" + "github.com/project-flogo/core/data/resolve" + "github.com/project-flogo/core/support/log" + "github.com/project-flogo/core/support/trace" +) + +// activity init context +type initContext struct { + settings map[string]interface{} + mapperFactory mapper.Factory + logger log.Logger +} + +func newInitContext(name string, settings map[string]interface{}, l log.Logger) *initContext { + return &initContext{ + settings: settings, + mapperFactory: mapper.NewFactory(resolve.GetBasicResolver()), + logger: log.ChildLogger(l, name), + } +} + +func (i *initContext) Settings() map[string]interface{} { + return i.settings +} + +func (i *initContext) MapperFactory() mapper.Factory { + return i.mapperFactory +} + +func (i *initContext) Logger() log.Logger { + return i.logger +} + +// ServiceContext context +type ServiceContext struct { + TaskName string + activityHost activity.Host + + metadata *activity.Metadata + settings map[string]interface{} + inputs map[string]interface{} + outputs map[string]interface{} + + shared map[string]interface{} +} + +func newServiceContext(md *activity.Metadata) *ServiceContext { + input := map[string]data.TypedValue{"Input1": data.NewTypedValue(data.TypeString, "")} + output := map[string]data.TypedValue{"Output1": data.NewTypedValue(data.TypeString, "")} + + // TBD: rule action's details (like: metadata, name, etc) to be used here + sHost := &ServiceHost{ + HostId: "1", + HostRef: "github.com/project-flogo/rules", + IoMetadata: &metadata.IOMetadata{Input: input, Output: output}, + HostData: data.NewSimpleScope(nil, nil), + } + sContext := &ServiceContext{ + metadata: md, + activityHost: sHost, + TaskName: "Rule action service", + inputs: make(map[string]interface{}, len(md.Input)), + outputs: make(map[string]interface{}, len(md.Output)), + settings: make(map[string]interface{}, len(md.Settings)), + } + + for name, tv := range md.Input { + sContext.inputs[name] = tv.Value() + } + for name, tv := range md.Output { + sContext.outputs[name] = tv.Value() + } + + return sContext +} + +// ActivityHost gets the "host" under with the activity is executing +func (sc *ServiceContext) ActivityHost() activity.Host { + return sc.activityHost +} + +//Name the name of the activity that is currently executing +func (sc *ServiceContext) Name() string { + return sc.TaskName +} + +// GetInput gets the value of the specified input attribute +func (sc *ServiceContext) GetInput(name string) interface{} { + val, found := sc.inputs[name] + if found { + return val + } + return nil +} + +// SetOutput sets the value of the specified output attribute +func (sc *ServiceContext) SetOutput(name string, value interface{}) error { + sc.outputs[name] = value + return nil +} + +// GetInputObject gets all the activity input as the specified object. +func (sc *ServiceContext) GetInputObject(input data.StructValue) error { + err := input.FromMap(sc.inputs) + return err +} + +// SetOutputObject sets the activity output as the specified object. +func (sc *ServiceContext) SetOutputObject(output data.StructValue) error { + sc.outputs = output.ToMap() + return nil +} + +// GetSharedTempData get shared temporary data for activity, lifespan +// of the data dependent on the activity host implementation +func (sc *ServiceContext) GetSharedTempData() map[string]interface{} { + if sc.shared == nil { + sc.shared = make(map[string]interface{}) + } + return sc.shared +} + +// Logger the logger for the activity +func (sc *ServiceContext) Logger() log.Logger { + return logger +} + +// SetInput sets input +func (sc *ServiceContext) SetInput(name string, value interface{}) { + sc.inputs[name] = value +} + +// GetTracingContext returns tracing context +func (sc *ServiceContext) GetTracingContext() trace.TracingContext { + return nil +} + +// ServiceHost hosts service +type ServiceHost struct { + HostId string + HostRef string + + IoMetadata *metadata.IOMetadata + HostData data.Scope + ReplyData map[string]interface{} + ReplyErr error +} + +// ID returns the ID of the Activity Host +func (ac *ServiceHost) ID() string { + return ac.HostId +} + +// Name the name of the Activity Host +func (ac *ServiceHost) Name() string { + return "" +} + +// IOMetadata get the input/output metadata of the activity host +func (ac *ServiceHost) IOMetadata() *metadata.IOMetadata { + return ac.IoMetadata +} + +// Reply is used to reply to the activity Host with the results of the execution +func (ac *ServiceHost) Reply(replyData map[string]interface{}, err error) { + ac.ReplyData = replyData + ac.ReplyErr = err +} + +// Return is used to indicate to the activity Host that it should complete and return the results of the execution +func (ac *ServiceHost) Return(returnData map[string]interface{}, err error) { + ac.ReplyData = returnData + ac.ReplyErr = err +} + +// Scope returns the scope for the Host's data +func (ac *ServiceHost) Scope() data.Scope { + return ac.HostData +} diff --git a/ruleapi/test_dtable.csv b/ruleapi/test_dtable.csv new file mode 100644 index 0000000..a887aee --- /dev/null +++ b/ruleapi/test_dtable.csv @@ -0,0 +1,3 @@ +Id,Condition (1),Condition (2),Condition (3),Condition (4),Condition (5),Action (6),Action (7),Action (8),Description,Priority +,applicant.age,applicant.creditScore,applicant.hasDL,applicant.maritalStatus,applicant.income,applicant.eligible,applicant.status,applicant.creditLimit,, +1,>= 16,>= 500,== true,== 'single',> 10000,true,VISA-Granted,2500.0,, \ No newline at end of file diff --git a/ruleapi/test_dtable.xlsx b/ruleapi/test_dtable.xlsx new file mode 100644 index 0000000..ae8aef9 Binary files /dev/null and b/ruleapi/test_dtable.xlsx differ diff --git a/ruleapi/tests/common.go b/ruleapi/tests/common.go index c1cf504..3eb14f4 100644 --- a/ruleapi/tests/common.go +++ b/ruleapi/tests/common.go @@ -1,20 +1,40 @@ package tests import ( + "bytes" "context" + "fmt" + "io" "io/ioutil" "log" + "net" + "os" + "os/exec" + "reflect" + "runtime" + "strings" + "sync" "testing" + "time" "github.com/project-flogo/rules/common" "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) -func createRuleSession() (model.RuleSession, error) { - rs, _ := ruleapi.GetOrCreateRuleSession("test") +var ( + store = "" + image = "" + done = false +) - tupleDescFileAbsPath := common.GetAbsPathForResource("src/github.com/project-flogo/rules/ruleapi/tests/tests.json") +func createRuleSession(t *testing.T) (model.RuleSession, error) { + t.Logf("createRuleSession with store=%s and docker image=%s", store, image) + done = false + tupleDescFileAbsPath := common.GetPathForResource("ruleapi/tests/tests.json", "./tests.json") dat, err := ioutil.ReadFile(tupleDescFileAbsPath) if err != nil { @@ -24,7 +44,21 @@ func createRuleSession() (model.RuleSession, error) { if err != nil { return nil, err } - return rs, nil + + return ruleapi.GetOrCreateRuleSession("test", store) +} + +func deleteRuleSession(t *testing.T, session model.RuleSession, tuples ...model.Tuple) { + done = true + defer session.Unregister() + rules := session.GetRules() + for _, rule := range rules { + session.DeleteRule(rule.GetName()) + } + for _, tuple := range tuples { + err := session.Delete(nil, tuple) + assert.Nil(t, err) + } } //conditions and actions @@ -35,6 +69,7 @@ func falseCondition(ruleName string, condName string, tuples map[model.TupleType return false } func emptyAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + } func printTuples(t *testing.T, oprn string, tupleMap map[string]map[string]model.Tuple) { @@ -60,3 +95,82 @@ type txnCtx struct { Testing *testing.T TxnCnt int } + +func CaptureStdOutput(f func()) string { + reader, writer, err := os.Pipe() + if err != nil { + panic(err) + } + stdout := os.Stdout + stderr := os.Stderr + defer func() { + os.Stdout = stdout + os.Stderr = stderr + log.SetOutput(os.Stderr) + }() + os.Stdout = writer + os.Stderr = writer + log.SetOutput(writer) + out := make(chan string) + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + var buf bytes.Buffer + wg.Done() + io.Copy(&buf, reader) + out <- buf.String() + }() + wg.Wait() + f() + writer.Close() + return <-out +} + +func Command(name string, arg ...string) { + fmt.Printf("%s %v\n", name, arg) + output, err := exec.Command(name, arg...).CombinedOutput() + if err != nil { + os.Stderr.WriteString(err.Error()) + } + if len(output) > 0 { + fmt.Printf("%s", string(output)) + } +} + +func Drain(port string) { + for { + conn, err := net.DialTimeout("tcp", net.JoinHostPort("", port), time.Second) + if conn != nil { + conn.Close() + } + if err != nil && strings.Contains(err.Error(), "connect: connection refused") { + break + } + } +} + +func Pour(port string) { + for { + conn, _ := net.Dial("tcp", net.JoinHostPort("", port)) + if conn != nil { + conn.Close() + break + } + } +} + +type TestKey struct{} + +func createActionServiceFromFunction(t *testing.T, actionFunction model.ActionFunction) model.ActionService { + fname := runtime.FuncForPC(reflect.ValueOf(actionFunction).Pointer()).Name() + cfg := &config.ServiceDescriptor{ + Name: fname, + Description: fname, + Type: config.TypeServiceFunction, + Function: actionFunction, + } + aService, err := ruleapi.NewActionService(cfg) + assert.Nil(t, err) + assert.NotNil(t, aService) + return aService +} diff --git a/ruleapi/tests/expr_1_test.go b/ruleapi/tests/expr_1_test.go new file mode 100644 index 0000000..646b2e6 --- /dev/null +++ b/ruleapi/tests/expr_1_test.go @@ -0,0 +1,69 @@ +package tests + +import ( + "context" + "testing" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" +) + +//1 condition, 1 expression +func Test_1_Expr(t *testing.T) { + actionCount := map[string]int{"count": 0} + rs, err := createRuleSession(t) + assert.Nil(t, err) + r1 := ruleapi.NewRule("r1") + err = r1.AddExprCondition("c1", "$.t2.p2 > $.t1.p1", nil) + assert.Nil(t, err) + r1.SetActionService(createActionServiceFromFunction(t, a1)) + r1.SetContext(actionCount) + err = rs.AddRule(r1) + assert.Nil(t, err) + err = rs.Start(nil) + assert.Nil(t, err) + var ctx context.Context + + t1, err := model.NewTupleWithKeyValues("t1", "t1") + assert.Nil(t, err) + err = t1.SetInt(nil, "p1", 1) + assert.Nil(t, err) + err = t1.SetDouble(nil, "p2", 1.3) + assert.Nil(t, err) + err = t1.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t1) + assert.Nil(t, err) + + t2, err := model.NewTupleWithKeyValues("t2", "t2") + assert.Nil(t, err) + err = t2.SetInt(nil, "p1", 1) + assert.Nil(t, err) + err = t2.SetDouble(nil, "p2", 1.0001) + assert.Nil(t, err) + err = t2.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t2) + assert.Nil(t, err) + + deleteRuleSession(t, rs, t1) + + count := actionCount["count"] + if count != 1 { + t.Errorf("expected [%d], got [%d]\n", 1, count) + } +} + +func a1(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + t := ctx.Value(TestKey{}).(*testing.T) + t.Logf("Test_1_Expr executed!") + actionCount := ruleCtx.(map[string]int) + count := actionCount["count"] + actionCount["count"] = count + 1 +} diff --git a/ruleapi/tests/expr_2_test.go b/ruleapi/tests/expr_2_test.go new file mode 100644 index 0000000..8f44bfb --- /dev/null +++ b/ruleapi/tests/expr_2_test.go @@ -0,0 +1,73 @@ +package tests + +import ( + "context" + "testing" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" +) + +//2 conditions, 1 expr each +func Test_2_Expr(t *testing.T) { + + actionCount := map[string]int{"count": 0} + rs, err := createRuleSession(t) + assert.Nil(t, err) + r1 := ruleapi.NewRule("r1") + err = r1.AddExprCondition("c1", "$.t1.p1 > $.t2.p1", nil) + assert.Nil(t, err) + err = r1.AddExprCondition("c2", "$.t1.p1 == 2", nil) + assert.Nil(t, err) + r1.SetActionService(createActionServiceFromFunction(t, a2)) + r1.SetContext(actionCount) + + err = rs.AddRule(r1) + assert.Nil(t, err) + + err = rs.Start(nil) + assert.Nil(t, err) + + var ctx context.Context + + t1, err := model.NewTupleWithKeyValues("t1", "t1") + assert.Nil(t, err) + err = t1.SetInt(nil, "p1", 2) + assert.Nil(t, err) + err = t1.SetDouble(nil, "p2", 1.3) + assert.Nil(t, err) + err = t1.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t1) + assert.Nil(t, err) + + t2, err := model.NewTupleWithKeyValues("t2", "t2") + assert.Nil(t, err) + err = t2.SetInt(nil, "p1", 1) + assert.Nil(t, err) + err = t2.SetDouble(nil, "p2", 1.1) + assert.Nil(t, err) + err = t2.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t2) + assert.Nil(t, err) + deleteRuleSession(t, rs, t1) + count := actionCount["count"] + if count != 1 { + t.Errorf("expected [%d], got [%d]\n", 1, count) + } +} + +func a2(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + t := ctx.Value(TestKey{}).(*testing.T) + t.Logf("Test_2_Expr executed!") + actionCount := ruleCtx.(map[string]int) + count := actionCount["count"] + actionCount["count"] = count + 1 +} diff --git a/ruleapi/tests/expr_3_test.go b/ruleapi/tests/expr_3_test.go new file mode 100644 index 0000000..54b1a74 --- /dev/null +++ b/ruleapi/tests/expr_3_test.go @@ -0,0 +1,71 @@ +package tests + +import ( + "context" + "testing" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" +) + +//1 conditions, 2 expr +func Test_3_Expr(t *testing.T) { + + actionCount := map[string]int{"count": 0} + rs, err := createRuleSession(t) + r1 := ruleapi.NewRule("r1") + assert.Nil(t, err) + err = r1.AddExprCondition("c1", "($.t1.p1 > $.t2.p1) && ($.t1.p2 > $.t2.p2)", nil) + assert.Nil(t, err) + r1.SetActionService(createActionServiceFromFunction(t, a3)) + r1.SetContext(actionCount) + + err = rs.AddRule(r1) + assert.Nil(t, err) + + err = rs.Start(nil) + assert.Nil(t, err) + + var ctx context.Context + + t1, err := model.NewTupleWithKeyValues("t1", "t1") + assert.Nil(t, err) + err = t1.SetInt(nil, "p1", 2) + assert.Nil(t, err) + err = t1.SetDouble(nil, "p2", 1.3) + assert.Nil(t, err) + err = t1.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t1) + assert.Nil(t, err) + + t2, err := model.NewTupleWithKeyValues("t2", "t2") + assert.Nil(t, err) + err = t2.SetInt(nil, "p1", 1) + assert.Nil(t, err) + err = t2.SetDouble(nil, "p2", 1.1) + assert.Nil(t, err) + err = t2.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t2) + assert.Nil(t, err) + deleteRuleSession(t, rs, t1) + count := actionCount["count"] + if count != 1 { + t.Errorf("expected [%d], got [%d]\n", 1, count) + } +} + +func a3(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + t := ctx.Value(TestKey{}).(*testing.T) + t.Logf("Test_3_Expr executed!") + actionCount := ruleCtx.(map[string]int) + count := actionCount["count"] + actionCount["count"] = count + 1 +} diff --git a/ruleapi/tests/expr_4_test.go b/ruleapi/tests/expr_4_test.go new file mode 100644 index 0000000..fb7345a --- /dev/null +++ b/ruleapi/tests/expr_4_test.go @@ -0,0 +1,71 @@ +package tests + +import ( + "context" + "testing" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" +) + +//1 conditions, 3 expr +func Test_4_Expr(t *testing.T) { + + actionCount := map[string]int{"count": 0} + rs, err := createRuleSession(t) + assert.Nil(t, err) + r1 := ruleapi.NewRule("r1") + err = r1.AddExprCondition("c1", "($.t1.p1 > $.t2.p1) && (($.t1.p2 > $.t2.p2) && ($.t1.p3 == $.t2.p3))", nil) + assert.Nil(t, err) + r1.SetActionService(createActionServiceFromFunction(t, a4)) + r1.SetContext(actionCount) + + err = rs.AddRule(r1) + assert.Nil(t, err) + + err = rs.Start(nil) + assert.Nil(t, err) + + var ctx context.Context + + t1, err := model.NewTupleWithKeyValues("t1", "t1") + assert.Nil(t, err) + err = t1.SetInt(nil, "p1", 2) + assert.Nil(t, err) + err = t1.SetDouble(nil, "p2", 1.3) + assert.Nil(t, err) + err = t1.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t1) + assert.Nil(t, err) + + t2, err := model.NewTupleWithKeyValues("t2", "t2") + assert.Nil(t, err) + err = t2.SetInt(nil, "p1", 1) + assert.Nil(t, err) + err = t2.SetDouble(nil, "p2", 1.1) + assert.Nil(t, err) + err = t2.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t2) + assert.Nil(t, err) + deleteRuleSession(t, rs, t1) + count := actionCount["count"] + if count != 1 { + t.Errorf("expected [%d], got [%d]\n", 1, count) + } +} + +func a4(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + t := ctx.Value(TestKey{}).(*testing.T) + t.Logf("Test_4_Expr executed!") + actionCount := ruleCtx.(map[string]int) + count := actionCount["count"] + actionCount["count"] = count + 1 +} diff --git a/ruleapi/tests/expr_5_test.go b/ruleapi/tests/expr_5_test.go new file mode 100644 index 0000000..75547ed --- /dev/null +++ b/ruleapi/tests/expr_5_test.go @@ -0,0 +1,71 @@ +package tests + +import ( + "context" + "testing" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" +) + +//1 arithmetic operation +func Test_5_Expr(t *testing.T) { + + actionCount := map[string]int{"count": 0} + rs, err := createRuleSession(t) + assert.Nil(t, err) + r1 := ruleapi.NewRule("r1") + err = r1.AddExprCondition("c1", "(($.t1.p1 + $.t2.p1) == 5) && (($.t1.p2 > $.t2.p2) && ($.t1.p3 == $.t2.p3))", nil) + assert.Nil(t, err) + r1.SetActionService(createActionServiceFromFunction(t, a5)) + r1.SetContext(actionCount) + + err = rs.AddRule(r1) + assert.Nil(t, err) + + err = rs.Start(nil) + assert.Nil(t, err) + + var ctx context.Context + + t1, err := model.NewTupleWithKeyValues("t1", "t1") + assert.Nil(t, err) + err = t1.SetInt(nil, "p1", 1) + assert.Nil(t, err) + err = t1.SetDouble(nil, "p2", 1.3) + assert.Nil(t, err) + err = t1.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t1) + assert.Nil(t, err) + + t2, err := model.NewTupleWithKeyValues("t2", "t2") + assert.Nil(t, err) + err = t2.SetInt(nil, "p1", 4) + assert.Nil(t, err) + err = t2.SetDouble(nil, "p2", 1.1) + assert.Nil(t, err) + err = t2.SetString(nil, "p3", "t3") + assert.Nil(t, err) + + ctx = context.WithValue(context.TODO(), TestKey{}, t) + err = rs.Assert(ctx, t2) + assert.Nil(t, err) + deleteRuleSession(t, rs, t1) + count := actionCount["count"] + if count != 1 { + t.Errorf("expected [%d], got [%d]\n", 1, count) + } +} + +func a5(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { + t := ctx.Value(TestKey{}).(*testing.T) + t.Logf("Test_5_Expr executed!") + actionCount := ruleCtx.(map[string]int) + count := actionCount["count"] + actionCount["count"] = count + 1 +} diff --git a/ruleapi/tests/identifier_1_test.go b/ruleapi/tests/identifier_1_test.go index 7dfc736..4a40022 100644 --- a/ruleapi/tests/identifier_1_test.go +++ b/ruleapi/tests/identifier_1_test.go @@ -2,53 +2,69 @@ package tests import ( "context" + "sync/atomic" "testing" "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) -var count int +var count uint64 //Check if all combination of tuples t1 and t3 are triggering actions func Test_I1(t *testing.T) { - - rs, _ := createRuleSession() + count = 0 + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("I1") - rule.AddCondition("I1_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) - rule.SetAction(i1_action) + err = rule.AddCondition("I1_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, i1_action)) + rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) - t2, _ := model.NewTupleWithKeyValues("t1", "t11") - rs.Assert(context.TODO(), t2) + t2, err := model.NewTupleWithKeyValues("t1", "t11") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t2) + assert.Nil(t, err) - t3, _ := model.NewTupleWithKeyValues("t3", "t12") - rs.Assert(context.TODO(), t3) + t3, err := model.NewTupleWithKeyValues("t3", "t12") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t3) + assert.Nil(t, err) //Check if the 2 combinations {t1=t10,t3=t12} and {t1=t11,t3=t12} triggers action twice - if count != 2 { - t.Errorf("Expecting [2] actions, got [%d]", count) + if cnt := atomic.LoadUint64(&count); cnt != 2 { + t.Errorf("Expecting [2] actions, got [%d]", cnt) t.FailNow() } - t4, _ := model.NewTupleWithKeyValues("t3", "t13") - rs.Assert(context.TODO(), t4) + t4, err := model.NewTupleWithKeyValues("t3", "t13") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t4) + assert.Nil(t, err) //Check if the 2 combinations {t1=t10,t3=t13} and {t1=t11,t3=t13} triggers action two more times making the total action count 4 - if count != 4 { - t.Errorf("Expecting [4] actions, got [%d]", count) + if cnt := atomic.LoadUint64(&count); cnt != 4 { + t.Errorf("Expecting [4] actions, got [%d]", cnt) t.FailNow() } - rs.Unregister() + deleteRuleSession(t, rs, t1, t2, t3, t4) } @@ -60,15 +76,15 @@ func i1_action(ctx context.Context, rs model.RuleSession, ruleName string, tuple id3, _ := t2.GetString("id") if id1 == "t11" && id3 == "t12" { - count++ + atomic.AddUint64(&count, 1) } if id1 == "t10" && id3 == "t12" { - count++ + atomic.AddUint64(&count, 1) } if id1 == "t11" && id3 == "t13" { - count++ + atomic.AddUint64(&count, 1) } if id1 == "t10" && id3 == "t13" { - count++ + atomic.AddUint64(&count, 1) } } diff --git a/ruleapi/tests/identifier_2_test.go b/ruleapi/tests/identifier_2_test.go index 7e82a51..0d61ce9 100644 --- a/ruleapi/tests/identifier_2_test.go +++ b/ruleapi/tests/identifier_2_test.go @@ -2,83 +2,104 @@ package tests import ( "context" + "sync/atomic" "testing" "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) -var cnt int +var cnt uint64 //Using 3 Identifiers, different Join conditions and triggering respective actions --->Verify order of actions and count. func Test_I2(t *testing.T) { - - rs, _ := createRuleSession() + cnt = 0 + rs, err := createRuleSession(t) + assert.Nil(t, err) //actionMap := make(map[string]string) rule := ruleapi.NewRule("I21") - rule.AddCondition("I2_c1", []string{"t1.none", "t2.none"}, trueCondition, nil) - rule.SetAction(i21_action) + err = rule.AddCondition("I2_c1", []string{"t1.none", "t2.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, i21_action)) rule.SetPriority(1) //rule.SetContext(actionMap) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rule1 := ruleapi.NewRule("I22") - rule1.AddCondition("I2_c2", []string{"t1.none", "t3.none"}, trueCondition, nil) - rule1.SetAction(i22_action) + err = rule1.AddCondition("I2_c2", []string{"t1.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule1.SetActionService(createActionServiceFromFunction(t, i22_action)) rule1.SetPriority(1) //rule.SetContext(actionMap) - rs.AddRule(rule1) + err = rs.AddRule(rule1) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule1.GetName()) rule2 := ruleapi.NewRule("I23") - rule2.AddCondition("I2_c3", []string{"t2.none", "t3.none"}, trueCondition, nil) - rule2.SetAction(i23_action) + err = rule2.AddCondition("I2_c3", []string{"t2.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule2.SetActionService(createActionServiceFromFunction(t, i23_action)) rule2.SetPriority(1) //rule.SetContext(actionMap) - rs.AddRule(rule2) + err = rs.AddRule(rule2) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule1.GetName()) rule3 := ruleapi.NewRule("I24") - rule3.AddCondition("I2_c4", []string{"t1.none", "t2.none", "t3.none"}, trueCondition, nil) - rule3.SetAction(i24_action) + err = rule3.AddCondition("I2_c4", []string{"t1.none", "t2.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule3.SetActionService(createActionServiceFromFunction(t, i24_action)) rule3.SetPriority(1) //rule.SetContext(actionMap) - rs.AddRule(rule3) + err = rs.AddRule(rule3) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule2.GetName()) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) - t2, _ := model.NewTupleWithKeyValues("t2", "t11") - rs.Assert(context.TODO(), t2) + t2, err := model.NewTupleWithKeyValues("t2", "t11") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t2) + assert.Nil(t, err) - if cnt != 1 { - t.Errorf("Expecting [1] actions, got [%d]", cnt) + if count := atomic.LoadUint64(&cnt); count != 1 { + t.Errorf("Expecting [1] actions, got [%d]", count) t.FailNow() } - t3, _ := model.NewTupleWithKeyValues("t3", "t12") - rs.Assert(context.TODO(), t3) + t3, err := model.NewTupleWithKeyValues("t3", "t12") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t3) + assert.Nil(t, err) - if cnt != 2 { - t.Errorf("Expecting [2] actions, got [%d]", cnt) + if count := atomic.LoadUint64(&cnt); count != 2 { + t.Errorf("Expecting [2] actions, got [%d]", count) t.FailNow() } - t4, _ := model.NewTupleWithKeyValues("t2", "t13") - rs.Assert(context.TODO(), t4) + t4, err := model.NewTupleWithKeyValues("t2", "t13") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t4) + assert.Nil(t, err) - if cnt != 5 { - t.Errorf("Expecting [5] actions, got [%d]", cnt) + if count := atomic.LoadUint64(&cnt); count != 5 { + t.Errorf("Expecting [5] actions, got [%d]", count) t.FailNow() } - rs.Unregister() + deleteRuleSession(t, rs, t1, t3) } @@ -89,13 +110,13 @@ func i21_action(ctx context.Context, rs model.RuleSession, ruleName string, tupl t2 := tuples[model.TupleType("t2")].(model.MutableTuple) id2, _ := t2.GetString("id") - if id1 == "t10" && id2 == "t11" && cnt == 0 { - cnt++ + if count := atomic.LoadUint64(&cnt); id1 == "t10" && id2 == "t11" && count == 0 { + atomic.AddUint64(&cnt, 1) } if id1 == "t10" && id2 == "t13" { - if cnt >= 2 && cnt <= 4 { - cnt++ + if count := atomic.LoadUint64(&cnt); count >= 2 && count <= 4 { + atomic.AddUint64(&cnt, 1) } } } @@ -107,8 +128,8 @@ func i22_action(ctx context.Context, rs model.RuleSession, ruleName string, tupl t2 := tuples[model.TupleType("t3")].(model.MutableTuple) id3, _ := t2.GetString("id") - if id1 == "t10" && id3 == "t12" && cnt == 1 { - cnt++ + if count := atomic.LoadUint64(&cnt); id1 == "t10" && id3 == "t12" && count == 1 { + atomic.AddUint64(&cnt, 1) } } @@ -120,8 +141,8 @@ func i23_action(ctx context.Context, rs model.RuleSession, ruleName string, tupl id3, _ := t2.GetString("id") if id1 == "t13" && id3 == "t12" { - if cnt >= 2 && cnt <= 4 { - cnt++ + if count := atomic.LoadUint64(&cnt); count >= 2 && count <= 4 { + atomic.AddUint64(&cnt, 1) } } } @@ -137,8 +158,8 @@ func i24_action(ctx context.Context, rs model.RuleSession, ruleName string, tupl id3, _ := t3.GetString("id") if id1 == "t10" && id2 == "t13" && id3 == "t12" { - if cnt >= 2 && cnt <= 4 { - cnt++ + if count := atomic.LoadUint64(&cnt); count >= 2 && count <= 4 { + atomic.AddUint64(&cnt, 1) } } } diff --git a/ruleapi/tests/main_test.go b/ruleapi/tests/main_test.go new file mode 100644 index 0000000..71b8de5 --- /dev/null +++ b/ruleapi/tests/main_test.go @@ -0,0 +1,68 @@ +package tests + +import ( + "os" + "os/exec" + "strings" + "testing" +) + +func TestMain(m *testing.M) { + image = "none" + code := m.Run() + if code != 0 { + os.Exit(code) + } + + store = "rsconfigmp.json" + code = m.Run() + if code != 0 { + os.Exit(code) + } + + run := func(img string) int { + image = img + command := exec.Command("docker", "run", "-p", "6380:6379", "-d", img) + hash, err := command.Output() + if err != nil { + panic(err) + } + Pour("6380") + + defer func() { + command := exec.Command("docker", "stop", strings.TrimSpace(string(hash))) + err := command.Run() + if err != nil { + panic(err) + } + command = exec.Command("docker", "rm", strings.TrimSpace(string(hash))) + err = command.Run() + if err != nil { + panic(err) + } + Drain("6380") + }() + + return m.Run() + } + store = "rsconfig.json" + code = run("redis") + if code != 0 { + os.Exit(code) + } + + store = "rsconfigp.json" + code = run("redis") + if code != 0 { + os.Exit(code) + } + + store = "rsconfig.json" + code = run("eqalpha/keydb") + if code != 0 { + os.Exit(code) + } + + store = "rsconfigp.json" + os.Exit(run("eqalpha/keydb")) +} diff --git a/ruleapi/tests/memory/memory.go b/ruleapi/tests/memory/memory.go new file mode 100644 index 0000000..0f0f675 --- /dev/null +++ b/ruleapi/tests/memory/memory.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + _ "net/http/pprof" + "reflect" + "runtime" + "time" + + "github.com/project-flogo/rules/common" + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/config" + "github.com/project-flogo/rules/ruleapi" +) + +func createRuleSession() (model.RuleSession, error) { + rs, _ := ruleapi.GetOrCreateRuleSession("test", "") + + tupleDescFileAbsPath := common.GetPathForResource("ruleapi/tests/tests.json", "./../tests.json") + + dat, err := ioutil.ReadFile(tupleDescFileAbsPath) + if err != nil { + log.Fatal(err) + } + err = model.RegisterTupleDescriptors(string(dat)) + if err != nil { + return nil, err + } + return rs, nil +} + +func trueCondition(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { + return true +} + +func emptyAction(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { +} + +var ( + adddelete = flag.Bool("adddelete", false, "add followed by delete mode") + addsdeletes = flag.Bool("addsdeletes", false, "adds followed by deletes mode") + ttl = flag.Bool("ttl", false, "add with ttl mode") + same = flag.Bool("same", false, "add same tuple") +) + +func main() { + flag.Parse() + + go func() { + log.Println(http.ListenAndServe("localhost:6060", nil)) + }() + + if *adddelete { + rs, _ := createRuleSession() + defer rs.Unregister() + rule := ruleapi.NewRule("R2") + rule.AddCondition("R2_c1", []string{"t3.none"}, trueCondition, nil) + rule.SetActionService(createActionServiceFromFunction(emptyAction)) + rule.SetPriority(1) + rs.AddRule(rule) + log.Printf("Rule added: [%s]\n", rule.GetName()) + rs.Start(nil) + + i := 0 + for { + t1, _ := model.NewTupleWithKeyValues("t3", fmt.Sprintf("tuple%d", i)) + err := rs.Assert(context.TODO(), t1) + if err != nil { + log.Fatalf("err should be nil: %v", err) + } + time.Sleep(10 * time.Millisecond) + rs.Delete(context.TODO(), t1) + time.Sleep(10 * time.Millisecond) + i++ + } + } else if *addsdeletes { + rs, _ := createRuleSession() + defer rs.Unregister() + rule := ruleapi.NewRule("R2") + rule.AddCondition("R2_c1", []string{"t3.none"}, trueCondition, nil) + rule.SetActionService(createActionServiceFromFunction(emptyAction)) + rule.SetPriority(1) + rs.AddRule(rule) + log.Printf("Rule added: [%s]\n", rule.GetName()) + rs.Start(nil) + + i := 0 + for { + tuples := make([]model.MutableTuple, 0, 10) + for j := 0; j < 10; j++ { + t1, _ := model.NewTupleWithKeyValues("t3", fmt.Sprintf("tuple%d_%d", i, j)) + err := rs.Assert(context.TODO(), t1) + if err != nil { + log.Fatalf("err should be nil: %v", err) + } + tuples = append(tuples, t1) + time.Sleep(10 * time.Millisecond) + } + + for _, tuple := range tuples { + rs.Delete(context.TODO(), tuple) + time.Sleep(10 * time.Millisecond) + } + i++ + } + } else if *ttl { + rs, _ := createRuleSession() + defer rs.Unregister() + rule := ruleapi.NewRule("R2") + rule.AddCondition("R2_c1", []string{"t4.none"}, trueCondition, nil) + rule.SetActionService(createActionServiceFromFunction(emptyAction)) + rule.SetPriority(1) + rs.AddRule(rule) + log.Printf("Rule added: [%s]\n", rule.GetName()) + rs.Start(nil) + + i := 0 + for { + t1, _ := model.NewTupleWithKeyValues("t4", fmt.Sprintf("tuple%d", i)) + err := rs.Assert(context.TODO(), t1) + if err != nil { + log.Fatalf("err should be nil: %v", err) + } + time.Sleep(time.Second) + i++ + } + } else if *same { + rs, _ := createRuleSession() + defer rs.Unregister() + rule := ruleapi.NewRule("R2") + rule.AddCondition("R2_c1", []string{"t3.none"}, trueCondition, nil) + rule.SetActionService(createActionServiceFromFunction(emptyAction)) + rule.SetPriority(1) + rs.AddRule(rule) + log.Printf("Rule added: [%s]\n", rule.GetName()) + rs.Start(nil) + + t1, _ := model.NewTupleWithKeyValues("t3", "t3") + err := rs.Assert(context.TODO(), t1) + if err != nil { + log.Fatalf("err should be nil: %v", err) + } + for { + rs.Assert(context.TODO(), t1) + time.Sleep(10 * time.Millisecond) + } + } +} + +func createActionServiceFromFunction(actionFunction model.ActionFunction) model.ActionService { + fname := runtime.FuncForPC(reflect.ValueOf(actionFunction).Pointer()).Name() + cfg := &config.ServiceDescriptor{ + Name: fname, + Description: fname, + Type: config.TypeServiceFunction, + Function: actionFunction, + } + aService, _ := ruleapi.NewActionService(cfg) + return aService +} diff --git a/ruleapi/tests/retract_1_test.go b/ruleapi/tests/retract_1_test.go index 43d3f61..91c9064 100644 --- a/ruleapi/tests/retract_1_test.go +++ b/ruleapi/tests/retract_1_test.go @@ -6,57 +6,49 @@ import ( "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) //Retract func Test_Retract_1(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) //create a rule joining t1 and t3 rule := ruleapi.NewRule("Retract_Test") - err := rule.AddCondition("R7_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) - if err != nil { - t.Logf("%s", err) - t.FailNow() - } + err = rule.AddCondition("R7_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) ruleActionCtx := make(map[string]string) rule.SetContext(ruleActionCtx) - rule.SetAction(assertAction) + rule.SetActionService(createActionServiceFromFunction(t, assertAction)) rule.SetPriority(1) err = rs.AddRule(rule) - if err != nil { - t.Logf("%s", err) - t.FailNow() - } + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) err = rs.Start(nil) - if err != nil { - t.Logf("%s", err) - t.FailNow() - } + assert.Nil(t, err) + tuples := []model.Tuple{} // Case1: assert a t1 { ctx := context.WithValue(context.TODO(), "key", t) - tuple, _ := model.NewTupleWithKeyValues("t1", "t1") - err := rs.Assert(ctx, tuple) - if err != nil { - t.Logf("%s", err) - t.FailNow() - } + tuple, err := model.NewTupleWithKeyValues("t1", "t1") + assert.Nil(t, err) + tuples = append(tuples, tuple) + err = rs.Assert(ctx, tuple) + assert.Nil(t, err) } // Case2: assert a t3 so that the rule fires for keys t1 and t3 { ctx := context.WithValue(context.TODO(), "key", t) - tuple, _ := model.NewTupleWithKeyValues("t3", "t3") - err := rs.Assert(ctx, tuple) - if err != nil { - t.Logf("%s", err) - t.FailNow() - } + tuple, err := model.NewTupleWithKeyValues("t3", "t3") + assert.Nil(t, err) + err = rs.Assert(ctx, tuple) + assert.Nil(t, err) // make sure that rule action got fired by inspecting the rule context isActionFired, ok := ruleActionCtx["isActionFired"] if !ok || isActionFired != "Fired" { @@ -69,8 +61,10 @@ func Test_Retract_1(t *testing.T) { // Case3: now retract t3 { ctx := context.WithValue(context.TODO(), "key", t) - tuple, _ := model.NewTupleWithKeyValues("t3", "t3") - rs.Retract(ctx, tuple) + tuple, err := model.NewTupleWithKeyValues("t3", "t3") + assert.Nil(t, err) + err = rs.Retract(ctx, tuple) + assert.Nil(t, err) } /** @@ -80,12 +74,10 @@ func Test_Retract_1(t *testing.T) { */ { ctx := context.WithValue(context.TODO(), "key", t) - tuple, _ := model.NewTupleWithKeyValues("t3", "t3") - err := rs.Assert(ctx, tuple) - if err != nil { - t.Logf("%s", err) - t.FailNow() - } + tuple, err := model.NewTupleWithKeyValues("t3", "t3") + assert.Nil(t, err) + err = rs.Assert(ctx, tuple) + assert.Nil(t, err) // make sure that rule action got fired by inspecting the rule context isActionFired, ok := ruleActionCtx["isActionFired"] if !ok || isActionFired != "Fired" { @@ -102,8 +94,10 @@ func Test_Retract_1(t *testing.T) { */ { ctx := context.WithValue(context.TODO(), "key", t) - tuple, _ := model.NewTupleWithKeyValues("t3", "t3") - rs.Retract(ctx, tuple) + tuple, err := model.NewTupleWithKeyValues("t3", "t3") + assert.Nil(t, err) + err = rs.Retract(ctx, tuple) + assert.Nil(t, err) } /** @@ -113,12 +107,11 @@ func Test_Retract_1(t *testing.T) { */ { ctx := context.WithValue(context.TODO(), "key", t) - tuple, _ := model.NewTupleWithKeyValues("t1", "t11") - err := rs.Assert(ctx, tuple) - if err != nil { - t.Logf("%s", err) - t.FailNow() - } + tuple, err := model.NewTupleWithKeyValues("t1", "t11") + assert.Nil(t, err) + tuples = append(tuples, tuple) + err = rs.Assert(ctx, tuple) + assert.Nil(t, err) // make sure that rule action doesn't fire by inspecting the rule context _, ok := ruleActionCtx["isActionFired"] if ok { @@ -126,7 +119,7 @@ func Test_Retract_1(t *testing.T) { t.FailNow() } } - rs.Unregister() + deleteRuleSession(t, rs, tuples...) } diff --git a/ruleapi/tests/rsconfig.json b/ruleapi/tests/rsconfig.json new file mode 100644 index 0000000..e5a49bf --- /dev/null +++ b/ruleapi/tests/rsconfig.json @@ -0,0 +1,36 @@ +{ + "mode": "consistency", + "rs": { + "prefix": "x", + "store-ref": "redis" + }, + "rete": { + "jt-ref": "redis", + "idgen-ref": "redis", + "jt":"redis" + }, + "stores": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6380" + } + }, + "idgens": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6380" + } + }, + "jts": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6380" + } + } +} diff --git a/ruleapi/tests/rsconfigmp.json b/ruleapi/tests/rsconfigmp.json new file mode 100644 index 0000000..caf347c --- /dev/null +++ b/ruleapi/tests/rsconfigmp.json @@ -0,0 +1,36 @@ +{ + "mode": "performance", + "rs": { + "prefix": "x", + "store-ref": "mem" + }, + "rete": { + "jt-ref": "mem", + "idgen-ref": "mem", + "jt":"mem" + }, + "stores": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + }, + "idgens": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + }, + "jts": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6379" + } + } +} diff --git a/ruleapi/tests/rsconfigp.json b/ruleapi/tests/rsconfigp.json new file mode 100644 index 0000000..d79588c --- /dev/null +++ b/ruleapi/tests/rsconfigp.json @@ -0,0 +1,36 @@ +{ + "mode": "performance", + "rs": { + "prefix": "x", + "store-ref": "redis" + }, + "rete": { + "jt-ref": "redis", + "idgen-ref": "redis", + "jt":"redis" + }, + "stores": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6380" + } + }, + "idgens": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6380" + } + }, + "jts": { + "mem": { + }, + "redis": { + "network": "tcp", + "address": ":6380" + } + } +} diff --git a/ruleapi/tests/rtctxn_10_test.go b/ruleapi/tests/rtctxn_10_test.go index ac95149..6187281 100644 --- a/ruleapi/tests/rtctxn_10_test.go +++ b/ruleapi/tests/rtctxn_10_test.go @@ -6,31 +6,41 @@ import ( "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) //1 rtc ->Delete multiple tuple types and verify count. func Test_T10(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R10") - rule.AddCondition("R10_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) - rule.SetAction(r10_action) + err = rule.AddCondition("R10_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r10_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t10Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) - t3, _ := model.NewTupleWithKeyValues("t3", "t11") - rs.Assert(context.TODO(), t3) + t3, err := model.NewTupleWithKeyValues("t3", "t11") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t3) + assert.Nil(t, err) - rs.Unregister() + deleteRuleSession(t, rs) } @@ -40,13 +50,13 @@ func r10_action(ctx context.Context, rs model.RuleSession, ruleName string, tupl id, _ := t3.GetString("id") if id == "t11" { tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") - t10 := rs.GetAssertedTuple(tk).(model.MutableTuple) + t10 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) if t10 != nil { rs.Delete(ctx, t10) } tk1, _ := model.NewTupleKeyWithKeyValues("t3", "t11") - t11 := rs.GetAssertedTuple(tk1).(model.MutableTuple) + t11 := rs.GetAssertedTuple(ctx, tk1).(model.MutableTuple) if t11 != nil { rs.Delete(ctx, t11) } @@ -54,6 +64,9 @@ func r10_action(ctx context.Context, rs model.RuleSession, ruleName string, tupl } func t10Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt = txnCtx.TxnCnt + 1 diff --git a/ruleapi/tests/rtctxn_11_test.go b/ruleapi/tests/rtctxn_11_test.go index aeb9b84..d1b9db9 100644 --- a/ruleapi/tests/rtctxn_11_test.go +++ b/ruleapi/tests/rtctxn_11_test.go @@ -6,35 +6,50 @@ import ( "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) //1 rtc->one assert triggers two rule actions each rule action asserts 2 more tuples.Verify Tuple type and Tuples count. func Test_T11(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R11") - rule.AddCondition("R11_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(r11_action) + err = rule.AddCondition("R11_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r11_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rule1 := ruleapi.NewRule("R112") - rule1.AddCondition("R112_c1", []string{"t1.none"}, trueCondition, nil) - rule1.SetAction(r112_action) + err = rule1.AddCondition("R112_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule1.SetActionService(createActionServiceFromFunction(t, r112_action)) rule1.SetPriority(1) - rs.AddRule(rule1) + err = rs.AddRule(rule1) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule1.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t11Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) + + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) + t2, err := model.NewTupleWithKeyValues("t3", "t2") + assert.Nil(t, err) + t3, err := model.NewTupleWithKeyValues("t3", "t1") + assert.Nil(t, err) - rs.Unregister() + deleteRuleSession(t, rs, t1, t2, t3) } @@ -59,6 +74,9 @@ func r112_action(ctx context.Context, rs model.RuleSession, ruleName string, tup } func t11Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt = txnCtx.TxnCnt + 1 diff --git a/ruleapi/tests/rtctxn_12_test.go b/ruleapi/tests/rtctxn_12_test.go index 3404b62..b24deb5 100644 --- a/ruleapi/tests/rtctxn_12_test.go +++ b/ruleapi/tests/rtctxn_12_test.go @@ -6,44 +6,60 @@ import ( "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) //1 rtc->one assert triggers two rule actions each rule action modifies tuples.Verify Tuple type and Tuples count. func Test_T12(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R12") - rule.AddCondition("R12_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) - rule.SetAction(r122_action) + err = rule.AddCondition("R12_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r122_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rule1 := ruleapi.NewRule("R122") - rule1.AddCondition("R122_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) - rule1.SetAction(r12_action) + err = rule1.AddCondition("R122_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule1.SetActionService(createActionServiceFromFunction(t, r12_action)) rule1.SetPriority(1) - rs.AddRule(rule1) + err = rs.AddRule(rule1) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule1.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t12Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) - t2, _ := model.NewTupleWithKeyValues("t1", "t11") - rs.Assert(context.TODO(), t2) + t2, err := model.NewTupleWithKeyValues("t1", "t11") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t2) + assert.Nil(t, err) - t3, _ := model.NewTupleWithKeyValues("t3", "t12") - rs.Assert(context.TODO(), t3) + t3, err := model.NewTupleWithKeyValues("t3", "t12") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t3) + assert.Nil(t, err) - t4, _ := model.NewTupleWithKeyValues("t3", "t13") - rs.Assert(context.TODO(), t4) + t4, err := model.NewTupleWithKeyValues("t3", "t13") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t4) + assert.Nil(t, err) - rs.Unregister() + deleteRuleSession(t, rs, t1, t2, t3, t4) } @@ -53,7 +69,7 @@ func r12_action(ctx context.Context, rs model.RuleSession, ruleName string, tupl if id == "t13" { tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") - t10 := rs.GetAssertedTuple(tk).(model.MutableTuple) + t10 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) t10.SetDouble(ctx, "p2", 11.11) } } @@ -64,16 +80,19 @@ func r122_action(ctx context.Context, rs model.RuleSession, ruleName string, tup if id == "t13" { tk1, _ := model.NewTupleKeyWithKeyValues("t1", "t11") - t11 := rs.GetAssertedTuple(tk1).(model.MutableTuple) + t11 := rs.GetAssertedTuple(ctx, tk1).(model.MutableTuple) t11.SetDouble(ctx, "p2", 11.11) tk2, _ := model.NewTupleKeyWithKeyValues("t3", "t12") - t12 := rs.GetAssertedTuple(tk2).(model.MutableTuple) + t12 := rs.GetAssertedTuple(ctx, tk2).(model.MutableTuple) t12.SetDouble(ctx, "p2", 11.11) } } func t12Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt = txnCtx.TxnCnt + 1 diff --git a/ruleapi/tests/rtctxn_13_test.go b/ruleapi/tests/rtctxn_13_test.go index 0b2775b..9c9a628 100644 --- a/ruleapi/tests/rtctxn_13_test.go +++ b/ruleapi/tests/rtctxn_13_test.go @@ -6,38 +6,50 @@ import ( "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) //1 rtc->one assert triggers two rule actions each rule action deletes tuples.Verify Deleted Tuple types and Tuples count. func Test_T13(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R13") - rule.AddCondition("R13_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) - rule.SetAction(r13_action) + err = rule.AddCondition("R13_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r13_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rule1 := ruleapi.NewRule("R132") - rule1.AddCondition("R132_c1", []string{"t3.none"}, trueCondition, nil) - rule1.SetAction(r132_action) + err = rule1.AddCondition("R132_c1", []string{"t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule1.SetActionService(createActionServiceFromFunction(t, r132_action)) rule1.SetPriority(2) - rs.AddRule(rule1) + err = rs.AddRule(rule1) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule1.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t13Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) - t2, _ := model.NewTupleWithKeyValues("t3", "t12") - rs.Assert(context.TODO(), t2) + t2, err := model.NewTupleWithKeyValues("t3", "t12") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t2) + assert.Nil(t, err) - rs.Unregister() + deleteRuleSession(t, rs) } @@ -47,7 +59,7 @@ func r13_action(ctx context.Context, rs model.RuleSession, ruleName string, tupl if id == "t12" { tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") - t11 := rs.GetAssertedTuple(tk).(model.MutableTuple) + t11 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) if t11 != nil { rs.Delete(ctx, t11) } @@ -60,7 +72,7 @@ func r132_action(ctx context.Context, rs model.RuleSession, ruleName string, tup if id == "t12" { tk, _ := model.NewTupleKeyWithKeyValues("t3", "t12") - t12 := rs.GetAssertedTuple(tk).(model.MutableTuple) + t12 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) if t12 != nil { rs.Delete(ctx, t12) } @@ -68,6 +80,9 @@ func r132_action(ctx context.Context, rs model.RuleSession, ruleName string, tup } func t13Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt = txnCtx.TxnCnt + 1 diff --git a/ruleapi/tests/rtctxn_14_test.go b/ruleapi/tests/rtctxn_14_test.go index f2fe006..40ca1aa 100644 --- a/ruleapi/tests/rtctxn_14_test.go +++ b/ruleapi/tests/rtctxn_14_test.go @@ -6,42 +6,60 @@ import ( "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) //1 rtc->Redundant add and modify on same tuple->Verify added and modified count func Test_T14(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R14") - rule.AddCondition("R14_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(r14_action) + err = rule.AddCondition("R14_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r14_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rule1 := ruleapi.NewRule("R142") - rule1.AddCondition("R142_c1", []string{"t3.none"}, trueCondition, nil) - rule1.SetAction(r142_action) + err = rule1.AddCondition("R142_c1", []string{"t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule1.SetActionService(createActionServiceFromFunction(t, r142_action)) rule1.SetPriority(2) - rs.AddRule(rule1) + err = rs.AddRule(rule1) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule1.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t14Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) + + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = t1.SetDouble(context.TODO(), "p2", 11.11) + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - t1.SetDouble(context.TODO(), "p2", 11.11) - rs.Assert(context.TODO(), t1) + t2, err := model.NewTupleWithKeyValues("t3", "t12") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t2) + assert.Nil(t, err) - t2, _ := model.NewTupleWithKeyValues("t3", "t12") - rs.Assert(context.TODO(), t2) + t3, err := model.NewTupleWithKeyValues("t3", "t13") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t3) + assert.Nil(t, err) - t3, _ := model.NewTupleWithKeyValues("t3", "t13") - rs.Assert(context.TODO(), t3) + t4, err := model.NewTupleWithKeyValues("t1", "t2") + assert.Nil(t, err) - rs.Unregister() + deleteRuleSession(t, rs, t1, t2, t3, t4) } @@ -64,18 +82,21 @@ func r142_action(ctx context.Context, rs model.RuleSession, ruleName string, tup if id == "t12" { //Modifing p2 with the same value tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") - t10 := rs.GetAssertedTuple(tk).(model.MutableTuple) + t10 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) t10.SetDouble(ctx, "p2", 11.11) } if id == "t13" { //Modifing p2 value tk1, _ := model.NewTupleKeyWithKeyValues("t1", "t10") - t11 := rs.GetAssertedTuple(tk1).(model.MutableTuple) + t11 := rs.GetAssertedTuple(ctx, tk1).(model.MutableTuple) t11.SetDouble(ctx, "p2", 12.11) } } func t14Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt = txnCtx.TxnCnt + 1 diff --git a/ruleapi/tests/rtctxn_15_test.go b/ruleapi/tests/rtctxn_15_test.go index 86e5aff..d7fe2b5 100644 --- a/ruleapi/tests/rtctxn_15_test.go +++ b/ruleapi/tests/rtctxn_15_test.go @@ -2,47 +2,55 @@ package tests import ( "context" + "sync/atomic" "testing" "time" "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) -var actionCnt int +var actionCnt uint64 //1 rtc->Scheduled assert, Action should be fired after the delay time. func Test_T15(t *testing.T) { - - rs, _ := createRuleSession() + actionCnt = 0 + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R15") - rule.AddCondition("R15_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(r15_action) + err = rule.AddCondition("R15_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r15_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) rs.ScheduleAssert(context.TODO(), 1000, "1", t1) - if actionCnt != 0 { - t.Errorf("Expecting [0] actions, got [%d]", actionCnt) + if count := atomic.LoadUint64(&actionCnt); count != 0 { + t.Errorf("Expecting [0] actions, got [%d]", count) t.FailNow() } time.Sleep(2000 * time.Millisecond) - if actionCnt != 1 { - t.Errorf("Expecting [1] actions, got [%d]", actionCnt) + if count := atomic.LoadUint64(&actionCnt); count != 1 { + t.Errorf("Expecting [1] actions, got [%d]", count) t.FailNow() } - rs.Unregister() + deleteRuleSession(t, rs, t1) } func r15_action(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - actionCnt++ + atomic.AddUint64(&actionCnt, 1) } diff --git a/ruleapi/tests/rtctxn_16_test.go b/ruleapi/tests/rtctxn_16_test.go index b89dac7..353dc50 100644 --- a/ruleapi/tests/rtctxn_16_test.go +++ b/ruleapi/tests/rtctxn_16_test.go @@ -2,44 +2,52 @@ package tests import ( "context" + "sync/atomic" "testing" "time" "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) -var actionCnt1 int +var actionCnt1 uint64 //1 rtc->Schedule assert, Cancel scheduled assert and action should not be fired func Test_T16(t *testing.T) { - - rs, _ := createRuleSession() + actionCnt1 = 0 + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R16") - rule.AddCondition("R16_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(r16_action) + err = rule.AddCondition("R16_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r16_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) rs.ScheduleAssert(context.TODO(), 1000, "1", t1) rs.CancelScheduledAssert(context.TODO(), "1") time.Sleep(2000 * time.Millisecond) - if actionCnt1 != 0 { - t.Errorf("Expecting [0] actions, got [%d]", actionCnt1) + if count := atomic.LoadUint64(&actionCnt1); count != 0 { + t.Errorf("Expecting [0] actions, got [%d]", count) t.FailNow() } - rs.Unregister() + deleteRuleSession(t, rs) } func r16_action(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - actionCnt1++ + atomic.AddUint64(&actionCnt1, 1) } diff --git a/ruleapi/tests/rtctxn_1_test.go b/ruleapi/tests/rtctxn_1_test.go index 28ac038..dc5d6da 100644 --- a/ruleapi/tests/rtctxn_1_test.go +++ b/ruleapi/tests/rtctxn_1_test.go @@ -1,44 +1,50 @@ package tests import ( + "context" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "context" - "testing" + "github.com/stretchr/testify/assert" ) //TTL != 0 asserted func Test_T1(t *testing.T) { - - rs, _ := createRuleSession() - + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R1") - rule.AddCondition("R1_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(emptyAction) + err = rule.AddCondition("R1_c1", []string{"t1.none"}, trueCondition, t) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, emptyAction)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rs.RegisterRtcTransactionHandler(t1Handler, t) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t1") - rs.Assert(context.TODO(), t1) - rs.Unregister() + t1, err := model.NewTupleWithKeyValues("t1", "t1") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + deleteRuleSession(t, rs, t1) } - - func t1Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { - + if done { + return + } t := handlerCtx.(*testing.T) lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } lM := len(rtxn.GetRtcModified()) if lM != 0 { @@ -48,6 +54,6 @@ func t1Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } diff --git a/ruleapi/tests/rtctxn_2_test.go b/ruleapi/tests/rtctxn_2_test.go index 1eb4415..5df2a1e 100644 --- a/ruleapi/tests/rtctxn_2_test.go +++ b/ruleapi/tests/rtctxn_2_test.go @@ -2,40 +2,51 @@ package tests import ( "context" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "testing" + + "github.com/stretchr/testify/assert" ) //TTL = 0, asserted func Test_T2(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R2") - rule.AddCondition("R2_c1", []string{"t2.none"}, trueCondition, nil) - rule.SetAction(emptyAction) + err = rule.AddCondition("R2_c1", []string{"t2.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, emptyAction)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rs.RegisterRtcTransactionHandler(t2Handler, t) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t2", "t2") - rs.Assert(context.TODO(), t1) - rs.Unregister() + t1, err := model.NewTupleWithKeyValues("t2", "t2") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + deleteRuleSession(t, rs) } func t2Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { - + if done { + return + } t := handlerCtx.(*testing.T) lA := len(rtxn.GetRtcAdded()) if lA != 0 { t.Errorf("RtcAdded: Expected [%d], got [%d]\n", 0, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } lM := len(rtxn.GetRtcModified()) if lM != 0 { @@ -46,6 +57,6 @@ func t2Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } diff --git a/ruleapi/tests/rtctxn_3_test.go b/ruleapi/tests/rtctxn_3_test.go index 2281e1f..d1b563e 100644 --- a/ruleapi/tests/rtctxn_3_test.go +++ b/ruleapi/tests/rtctxn_3_test.go @@ -2,29 +2,42 @@ package tests import ( "context" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "testing" + + "github.com/stretchr/testify/assert" ) //New asserted in action (forward chain) func Test_T3(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R3") - rule.AddCondition("R3_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(R3_action) + err = rule.AddCondition("R3_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, R3_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rs.RegisterRtcTransactionHandler(t3Handler, t) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) - rs.Unregister() + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + + t2, err := model.NewTupleWithKeyValues("t1", "t11") + assert.Nil(t, err) + + deleteRuleSession(t, rs, t1, t2) } func R3_action(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { @@ -33,13 +46,16 @@ func R3_action(ctx context.Context, rs model.RuleSession, ruleName string, tuple } func t3Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } t := handlerCtx.(*testing.T) lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Types expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } else { //ok @@ -60,6 +76,6 @@ func t3Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } diff --git a/ruleapi/tests/rtctxn_4_test.go b/ruleapi/tests/rtctxn_4_test.go index dbbb303..18e2588 100644 --- a/ruleapi/tests/rtctxn_4_test.go +++ b/ruleapi/tests/rtctxn_4_test.go @@ -2,29 +2,38 @@ package tests import ( "context" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "testing" + + "github.com/stretchr/testify/assert" ) // modified in action (forward chain) func Test_T4(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R4") - rule.AddCondition("R4_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(r4_action) + err = rule.AddCondition("R4_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r4_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rs.RegisterRtcTransactionHandler(t4Handler, t) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) - rs.Unregister() + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + deleteRuleSession(t, rs, t1) } @@ -34,13 +43,16 @@ func r4_action(ctx context.Context, rs model.RuleSession, ruleName string, tuple } func t4Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } t := handlerCtx.(*testing.T) lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Types expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } else { //ok @@ -59,6 +71,6 @@ func t4Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } diff --git a/ruleapi/tests/rtctxn_5_test.go b/ruleapi/tests/rtctxn_5_test.go index 5c65fa0..70aac69 100644 --- a/ruleapi/tests/rtctxn_5_test.go +++ b/ruleapi/tests/rtctxn_5_test.go @@ -2,37 +2,50 @@ package tests import ( "context" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "testing" + + "github.com/stretchr/testify/assert" ) //3 rtcs, 1st rtc ->asserted, 2nd rtc ->modified the 1st one, 3rd rtc ->deleted the 2nd one func Test_T5(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R5") - rule.AddCondition("R5_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(r5_action) + err = rule.AddCondition("R5_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r5_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t5Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - i1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), i1) + i1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), i1) + assert.Nil(t, err) - i2, _ := model.NewTupleWithKeyValues("t1", "t11") - rs.Assert(context.TODO(), i2) + i2, err := model.NewTupleWithKeyValues("t1", "t11") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), i2) + assert.Nil(t, err) - i3, _ := model.NewTupleWithKeyValues("t1", "t13") - rs.Assert(context.TODO(), i3) + i3, err := model.NewTupleWithKeyValues("t1", "t13") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), i3) + assert.Nil(t, err) - rs.Unregister() + deleteRuleSession(t, rs, i1, i3) } @@ -40,17 +53,17 @@ func r5_action(ctx context.Context, rs model.RuleSession, ruleName string, tuple t1 := tuples[model.TupleType("t1")].(model.MutableTuple) //t1.SetString(ctx, "p3", "v3") id, _ := t1.GetString("id") - if id == "t11" { - tk, _:= model.NewTupleKeyWithKeyValues("t1", "t10") - t10 := rs.GetAssertedTuple (tk).(model.MutableTuple) + if id == "t11" { + tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") + t10 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) if t10 != nil { t10.SetString(ctx, "p3", "v3") t10.SetDouble(ctx, "p2", 11.11) } - } else if (id == "t13") { + } else if id == "t13" { //delete t11 - tk, _:= model.NewTupleKeyWithKeyValues("t1", "t11") - t11 := rs.GetAssertedTuple (tk).(model.MutableTuple) + tk, _ := model.NewTupleKeyWithKeyValues("t1", "t11") + t11 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) if t11 != nil { rs.Delete(ctx, t11) } @@ -58,6 +71,9 @@ func r5_action(ctx context.Context, rs model.RuleSession, ruleName string, tuple } func t5Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt = txnCtx.TxnCnt + 1 @@ -66,7 +82,7 @@ func t5Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Types expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } lM := len(rtxn.GetRtcModified()) if lM != 0 { @@ -76,13 +92,13 @@ func t5Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } else if txnCtx.TxnCnt == 2 { lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Types expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } lM := len(rtxn.GetRtcModified()) if lM != 1 { @@ -92,13 +108,13 @@ func t5Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } else if txnCtx.TxnCnt == 3 { lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Types expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } lM := len(rtxn.GetRtcModified()) if lM != 0 { @@ -108,7 +124,7 @@ func t5Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 1 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 1, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } -} \ No newline at end of file +} diff --git a/ruleapi/tests/rtctxn_6_test.go b/ruleapi/tests/rtctxn_6_test.go index 3fc6ad4..f68d8a4 100644 --- a/ruleapi/tests/rtctxn_6_test.go +++ b/ruleapi/tests/rtctxn_6_test.go @@ -2,53 +2,66 @@ package tests import ( "context" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "testing" + + "github.com/stretchr/testify/assert" ) //Same as Test_T5, but in 3rd rtc, assert a TTL=0 based and a TTL=1 based func Test_T6(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R6") - rule.AddCondition("R6_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(r6_action) + err = rule.AddCondition("R6_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r6_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t6Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - i1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), i1) + i1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), i1) + assert.Nil(t, err) - i2, _ := model.NewTupleWithKeyValues("t1", "t11") - rs.Assert(context.TODO(), i2) + i2, err := model.NewTupleWithKeyValues("t1", "t11") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), i2) + assert.Nil(t, err) - i3, _ := model.NewTupleWithKeyValues("t1", "t13") - rs.Assert(context.TODO(), i3) - rs.Unregister() + i3, err := model.NewTupleWithKeyValues("t1", "t13") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), i3) + assert.Nil(t, err) + deleteRuleSession(t, rs, i1, i3) } func r6_action(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { t1 := tuples[model.TupleType("t1")].(model.MutableTuple) id, _ := t1.GetString("id") - if id == "t11" { - tk, _:= model.NewTupleKeyWithKeyValues("t1", "t10") - t10 := rs.GetAssertedTuple (tk).(model.MutableTuple) + if id == "t11" { + tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") + t10 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) if t10 != nil { t10.SetString(ctx, "p3", "v3") t10.SetDouble(ctx, "p2", 11.11) } - } else if (id == "t13") { + } else if id == "t13" { //delete t11 - tk, _:= model.NewTupleKeyWithKeyValues("t1", "t11") - t11 := rs.GetAssertedTuple (tk).(model.MutableTuple) + tk, _ := model.NewTupleKeyWithKeyValues("t1", "t11") + t11 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) if t11 != nil { rs.Delete(ctx, t11) } @@ -62,6 +75,9 @@ func r6_action(ctx context.Context, rs model.RuleSession, ruleName string, tuple } func t6Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt++ @@ -70,7 +86,7 @@ func t6Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } lM := len(rtxn.GetRtcModified()) if lM != 0 { @@ -80,13 +96,13 @@ func t6Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } else if txnCtx.TxnCnt == 2 { lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Types expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } lM := len(rtxn.GetRtcModified()) if lM != 1 { @@ -96,18 +112,18 @@ func t6Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } else if txnCtx.TxnCnt == 3 { lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Types expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } else { - added, _ := rtxn.GetRtcAdded() ["t1"] + added, _ := rtxn.GetRtcAdded()["t1"] if len(added) != 2 { t.Errorf("RtcAdded: Tuples expected [%d], got [%d]\n", 2, len(added)) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } } lM := len(rtxn.GetRtcModified()) @@ -118,7 +134,7 @@ func t6Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 1 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 1, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } -} \ No newline at end of file +} diff --git a/ruleapi/tests/rtctxn_7_test.go b/ruleapi/tests/rtctxn_7_test.go index 6eb03ca..40d001b 100644 --- a/ruleapi/tests/rtctxn_7_test.go +++ b/ruleapi/tests/rtctxn_7_test.go @@ -2,43 +2,55 @@ package tests import ( "context" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "testing" + + "github.com/stretchr/testify/assert" ) //add and delete in the same rtc func Test_T7(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R7") - rule.AddCondition("R7_c1", []string{"t1.none"}, trueCondition, nil) - rule.SetAction(r7_action) + err = rule.AddCondition("R7_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r7_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t7Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - i1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), i1) + i1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), i1) + assert.Nil(t, err) - rs.Unregister() + deleteRuleSession(t, rs) } func r7_action(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { t1 := tuples[model.TupleType("t1")].(model.MutableTuple) id, _ := t1.GetString("id") - if id == "t10" { + if id == "t10" { rs.Delete(ctx, t1) } } func t7Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt++ @@ -47,7 +59,7 @@ func t7Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lA := len(rtxn.GetRtcAdded()) if lA != 1 { t.Errorf("RtcAdded: Expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } lM := len(rtxn.GetRtcModified()) if lM != 0 { @@ -57,7 +69,7 @@ func t7Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 1 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 1, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } -} \ No newline at end of file +} diff --git a/ruleapi/tests/rtctxn_8_test.go b/ruleapi/tests/rtctxn_8_test.go index 350929c..477af3c 100644 --- a/ruleapi/tests/rtctxn_8_test.go +++ b/ruleapi/tests/rtctxn_8_test.go @@ -1,49 +1,61 @@ package tests import ( + "context" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "context" - "testing" + "github.com/stretchr/testify/assert" ) //no-identifier condition func Test_T8(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R1") - rule.AddCondition("R1_c1", []string{"t1.none"}, trueCondition, nil) - rule.AddCondition("R1_c2", []string{}, falseCondition, nil) - rule.SetAction(assertTuple) + err = rule.AddCondition("R1_c1", []string{"t1.none"}, trueCondition, nil) + assert.Nil(t, err) + err = rule.AddCondition("R1_c2", []string{}, falseCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, assertTuple)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rs.RegisterRtcTransactionHandler(t8Handler, t) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t1") - rs.Assert(context.TODO(), t1) - rs.Unregister() + t1, err := model.NewTupleWithKeyValues("t1", "t1") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) -} + deleteRuleSession(t, rs, t1) +} func assertTuple(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - t, _:= model.NewTupleWithKeyValues("t1", "t2") + t, _ := model.NewTupleWithKeyValues("t1", "t2") rs.Assert(ctx, t) } func t8Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } t := handlerCtx.(*testing.T) if m, found := rtxn.GetRtcAdded()["t1"]; found { lA := len(m) if lA != 1 { t.Errorf("RtcAdded: Expected [%d], got [%d]\n", 1, lA) - printTuples(t,"Added", rtxn.GetRtcAdded()) + printTuples(t, "Added", rtxn.GetRtcAdded()) } } lM := len(rtxn.GetRtcModified()) @@ -54,6 +66,6 @@ func t8Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, han lD := len(rtxn.GetRtcDeleted()) if lD != 0 { t.Errorf("RtcDeleted: Expected [%d], got [%d]\n", 0, lD) - printTuples(t,"Deleted", rtxn.GetRtcDeleted()) + printTuples(t, "Deleted", rtxn.GetRtcDeleted()) } } diff --git a/ruleapi/tests/rtctxn_9_test.go b/ruleapi/tests/rtctxn_9_test.go index b5a242c..a550a5c 100644 --- a/ruleapi/tests/rtctxn_9_test.go +++ b/ruleapi/tests/rtctxn_9_test.go @@ -6,37 +6,51 @@ import ( "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) //2 rtcs, 1st rtc ->Asserted multiple tuple types and verify count, 2nd rtc -> Modified multiple tuple types and verify count. func Test_T9(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) rule := ruleapi.NewRule("R9") - rule.AddCondition("R9_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) - rule.SetAction(r9_action) + err = rule.AddCondition("R9_c1", []string{"t1.none", "t3.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r9_action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) txnCtx := txnCtx{t, 0} rs.RegisterRtcTransactionHandler(t9Handler, &txnCtx) - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - rs.Assert(context.TODO(), t1) + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) - t2, _ := model.NewTupleWithKeyValues("t1", "t11") - rs.Assert(context.TODO(), t2) + t2, err := model.NewTupleWithKeyValues("t1", "t11") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t2) + assert.Nil(t, err) - t3, _ := model.NewTupleWithKeyValues("t3", "t12") - rs.Assert(context.TODO(), t3) + t3, err := model.NewTupleWithKeyValues("t3", "t12") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t3) + assert.Nil(t, err) - t4, _ := model.NewTupleWithKeyValues("t3", "t13") - rs.Assert(context.TODO(), t4) + t4, err := model.NewTupleWithKeyValues("t3", "t13") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t4) + assert.Nil(t, err) - rs.Unregister() + deleteRuleSession(t, rs, t1, t2, t3, t4) } @@ -53,21 +67,24 @@ func r9_action(ctx context.Context, rs model.RuleSession, ruleName string, tuple rs.Assert(ctx, t3) } else if id == "t13" { tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") - t10 := rs.GetAssertedTuple(tk).(model.MutableTuple) + t10 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) t10.SetDouble(ctx, "p2", 11.11) tk1, _ := model.NewTupleKeyWithKeyValues("t1", "t11") - t11 := rs.GetAssertedTuple(tk1).(model.MutableTuple) + t11 := rs.GetAssertedTuple(ctx, tk1).(model.MutableTuple) t11.SetDouble(ctx, "p2", 11.11) tk2, _ := model.NewTupleKeyWithKeyValues("t3", "t12") - t12 := rs.GetAssertedTuple(tk2).(model.MutableTuple) + t12 := rs.GetAssertedTuple(ctx, tk2).(model.MutableTuple) t12.SetDouble(ctx, "p2", 11.11) } } func t9Handler(ctx context.Context, rs model.RuleSession, rtxn model.RtcTxn, handlerCtx interface{}) { + if done { + return + } txnCtx := handlerCtx.(*txnCtx) txnCtx.TxnCnt = txnCtx.TxnCnt + 1 diff --git a/ruleapi/tests/rules_1_test.go b/ruleapi/tests/rules_1_test.go index ac28026..3006680 100644 --- a/ruleapi/tests/rules_1_test.go +++ b/ruleapi/tests/rules_1_test.go @@ -3,9 +3,12 @@ package tests import ( "context" "fmt" + "testing" + "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" - "testing" + + "github.com/stretchr/testify/assert" ) /** @@ -16,47 +19,57 @@ the expected outcome is that all three rules should fire func Test_One(t *testing.T) { - rs, _ := createRuleSession() + rs, err := createRuleSession(t) + assert.Nil(t, err) actionMap := make(map[string]string) //// rule 1 r1 := ruleapi.NewRule("R1") - r1.AddCondition("C1", []string{"t1"}, checkC1, nil) - r1.SetAction(actionA1) + err = r1.AddCondition("C1", []string{"t1"}, checkC1, nil) + assert.Nil(t, err) + r1.SetActionService(createActionServiceFromFunction(t, actionA1)) r1.SetPriority(1) r1.SetContext(actionMap) - rs.AddRule(r1) + err = rs.AddRule(r1) + assert.Nil(t, err) // rule 2 r2 := ruleapi.NewRule("R2") - r2.AddCondition("C2", []string{"t1"}, checkC2, nil) - r2.SetAction(actionA2) + err = r2.AddCondition("C2", []string{"t1"}, checkC2, nil) + assert.Nil(t, err) + r2.SetActionService(createActionServiceFromFunction(t, actionA2)) r2.SetPriority(2) r2.SetContext(actionMap) - rs.AddRule(r2) + err = rs.AddRule(r2) + assert.Nil(t, err) // rule 3 r3 := ruleapi.NewRule("R3") - r3.AddCondition("C3", []string{"t1"}, checkC3, nil) - r3.SetAction(actionA3) + err = r3.AddCondition("C3", []string{"t1"}, checkC3, nil) + assert.Nil(t, err) + r3.SetActionService(createActionServiceFromFunction(t, actionA3)) r3.SetPriority(3) r3.SetContext(actionMap) - rs.AddRule(r3) + err = rs.AddRule(r3) + assert.Nil(t, err) //Start the rule session - rs.Start(nil) + err = rs.Start(nil) + assert.Nil(t, err) //Now assert a "t1" tuple - t1, _ := model.NewTupleWithKeyValues("t1", "Tom") + t1, err := model.NewTupleWithKeyValues("t1", "Tom") + assert.Nil(t, err) t1.SetString(nil, "p3", "test") - rs.Assert(nil, t1) + err = rs.Assert(nil, t1) + assert.Nil(t, err) //unregister the session, i.e; cleanup - rs.Unregister() + deleteRuleSession(t, rs, t1) if len(actionMap) != 3 { t.Errorf("Expecting [3] actions, got [%d]", len(actionMap)) diff --git a/ruleapi/tests/rules_2_test.go b/ruleapi/tests/rules_2_test.go index 4cca838..049ab99 100644 --- a/ruleapi/tests/rules_2_test.go +++ b/ruleapi/tests/rules_2_test.go @@ -8,6 +8,8 @@ import ( "github.com/project-flogo/rules/common" "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) func Test_Two(t *testing.T) { @@ -15,7 +17,7 @@ func Test_Two(t *testing.T) { // fmt.Println("** rulesapp: Example usage of the Rules module/API **") //Load the tuple descriptor file (relative to GOPATH) - tupleDescAbsFileNm := common.GetAbsPathForResource("src/github.com/project-flogo/rules/examples/rulesapp/rulesapp.json") + tupleDescAbsFileNm := common.GetPathForResource("examples/rulesapp/rulesapp.json", "../../examples/rulesapp/rulesapp.json") tupleDescriptor := common.FileToString(tupleDescAbsFileNm) // fmt.Printf("Loaded tuple descriptor: \n%s\n", tupleDescriptor) @@ -27,30 +29,41 @@ func Test_Two(t *testing.T) { } //Create a RuleSession - rs, _ := ruleapi.GetOrCreateRuleSession("asession") + rs, err := ruleapi.GetOrCreateRuleSession("asession", store) + assert.Nil(t, err) actionFireCount := make(map[string]int) //// check for name "Bob" in n1 rule := ruleapi.NewRule("rule1") - rule.AddCondition("c1", []string{"n1"}, checkForBob, nil) - rule.AddCondition("c2", []string{"n1"}, checkForName, nil) + err = rule.AddCondition("c1", []string{"n1"}, checkForBob, nil) + assert.Nil(t, err) + err = rule.AddCondition("c2", []string{"n1"}, checkForName, nil) + assert.Nil(t, err) - rule.SetAction(checkForBobAction) + rule.SetActionService(createActionServiceFromFunction(t, checkForBobAction)) rule.SetContext(actionFireCount) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) fmt.Printf("Rule added: [%s]\n", rule.GetName()) //Start the rule session - rs.Start(nil) - t1, _ := model.NewTupleWithKeyValues("n1", "Tom") - rs.Assert(nil, t1) - - t2, _ := model.NewTupleWithKeyValues("n1", "Bob") - rs.Assert(nil, t2) + err = rs.Start(nil) + assert.Nil(t, err) + t1, err := model.NewTupleWithKeyValues("n1", "Tom") + assert.Nil(t, err) + err = rs.Assert(nil, t1) + assert.Nil(t, err) + + t2, err := model.NewTupleWithKeyValues("n1", "Bob") + assert.Nil(t, err) + err = rs.Assert(nil, t2) + assert.Nil(t, err) //Retract tuples - rs.Retract(nil, t1) - rs.Retract(nil, t2) + err = rs.Retract(nil, t1) + assert.Nil(t, err) + err = rs.Retract(nil, t2) + assert.Nil(t, err) if cnt, found := actionFireCount["count"]; found { if cnt > 1 { @@ -63,7 +76,7 @@ func Test_Two(t *testing.T) { rs.DeleteRule(rule.GetName()) //unregister the session, i.e; cleanup - rs.Unregister() + deleteRuleSession(t, rs) } func checkForName(ruleName string, condName string, tuples map[model.TupleType]model.Tuple, ctx model.RuleContext) bool { diff --git a/ruleapi/tests/rules_3_test.go b/ruleapi/tests/rules_3_test.go index 0b54fe6..83e07e5 100644 --- a/ruleapi/tests/rules_3_test.go +++ b/ruleapi/tests/rules_3_test.go @@ -2,52 +2,67 @@ package tests import ( "context" + "sync/atomic" "testing" "github.com/project-flogo/rules/common/model" "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" ) -var actCnt int +var actCnt uint64 //Forward chain-Data change in r3action and r32action triggers the r32action. func Test_Three(t *testing.T) { - - rs, _ := createRuleSession() + actCnt = 0 + rs, err := createRuleSession(t) + assert.Nil(t, err) actionMap := make(map[string]string) rule := ruleapi.NewRule("R3") - rule.AddCondition("R3c1", []string{"t1.id"}, trueCondition, nil) - rule.SetAction(r3action) + err = rule.AddCondition("R3c1", []string{"t1.id"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, r3action)) rule.SetPriority(1) - rs.AddRule(rule) + err = rs.AddRule(rule) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule.GetName()) rule1 := ruleapi.NewRule("R32") - rule1.AddCondition("R32c1", []string{"t1.p1"}, r3Condition, nil) - rule1.SetAction(r32action) + err = rule1.AddCondition("R32c1", []string{"t1.p1"}, r3Condition, nil) + assert.Nil(t, err) + rule1.SetActionService(createActionServiceFromFunction(t, r32action)) rule1.SetPriority(1) rule1.SetContext(actionMap) - rs.AddRule(rule1) + err = rs.AddRule(rule1) + assert.Nil(t, err) t.Logf("Rule added: [%s]\n", rule1.GetName()) - rs.Start(nil) - - t1, _ := model.NewTupleWithKeyValues("t1", "t10") - t1.SetInt(context.TODO(), "p1", 2000) - rs.Assert(context.TODO(), t1) - - t2, _ := model.NewTupleWithKeyValues("t1", "t11") - t2.SetInt(context.TODO(), "p1", 2000) - rs.Assert(context.TODO(), t2) - - if actCnt != 2 { - t.Errorf("Expecting [2] actions, got [%d]", actCnt) + err = rs.Start(nil) + assert.Nil(t, err) + + t1, err := model.NewTupleWithKeyValues("t1", "t10") + assert.Nil(t, err) + err = t1.SetInt(context.TODO(), "p1", 2000) + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + + t2, err := model.NewTupleWithKeyValues("t1", "t11") + assert.Nil(t, err) + err = t2.SetInt(context.TODO(), "p1", 2000) + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t2) + assert.Nil(t, err) + + if count := atomic.LoadUint64(&actCnt); count != 2 { + t.Errorf("Expecting [2] actions, got [%d]", count) t.FailNow() } - rs.Unregister() + deleteRuleSession(t, rs, t1, t2) } @@ -63,22 +78,20 @@ func r3Condition(ruleName string, condName string, tuples map[model.TupleType]mo } func r3action(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - //fmt.Println("r13_action triggered") t1 := tuples[model.TupleType("t1")].(model.MutableTuple) id, _ := t1.GetString("id") if id == "t11" { tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") - t10 := rs.GetAssertedTuple(tk).(model.MutableTuple) + t10 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) t10.SetInt(ctx, "p1", 100) } } func r32action(ctx context.Context, rs model.RuleSession, ruleName string, tuples map[model.TupleType]model.Tuple, ruleCtx model.RuleContext) { - //fmt.Println("r132_action triggered") - actCnt++ + atomic.AddUint64(&actCnt, 1) tk, _ := model.NewTupleKeyWithKeyValues("t1", "t10") - t10 := rs.GetAssertedTuple(tk).(model.MutableTuple) + t10 := rs.GetAssertedTuple(ctx, tk).(model.MutableTuple) t10.SetInt(ctx, "p1", 500) } diff --git a/ruleapi/tests/rules_4_test.go b/ruleapi/tests/rules_4_test.go index fc1eeb2..df6759a 100644 --- a/ruleapi/tests/rules_4_test.go +++ b/ruleapi/tests/rules_4_test.go @@ -11,12 +11,12 @@ import ( // Add previously removed rule func Test_Four(t *testing.T) { - rs, _ := createRuleSession() + rs, _ := createRuleSession(t) // create rule r1 := ruleapi.NewRule("Rule1") r1.AddCondition("r1c1", []string{"t1.none", "t2.none"}, trueCondition, nil) - r1.SetAction(r1Action) + r1.SetActionService(createActionServiceFromFunction(t, r1Action)) // create tuples t1, _ := model.NewTupleWithKeyValues("t1", "one") // No TTL t2, _ := model.NewTupleWithKeyValues("t2", "two") // TTL is 0 @@ -31,8 +31,8 @@ func Test_Four(t *testing.T) { // Test1: add r1 and assert t1 & t2; rule action SHOULD be fired addRule(t, rs, r1) - assert(assertCtx, rs, t1) - assert(assertCtx, rs, t2) + assertWithCtx(assertCtx, rs, t1) + assertWithCtx(assertCtx, rs, t2) actionFired, _ := assertCtxValues["actionFired"].(string) if actionFired != "FIRED" { t.Log("rule action SHOULD be fired") @@ -42,7 +42,7 @@ func Test_Four(t *testing.T) { // Test2: remove r1 and assert t2; rule action SHOULD NOT be fired deleteRule(t, rs, r1) assertCtxValues["actionFired"] = "NOTFIRED" - assert(assertCtx, rs, t2) + assertWithCtx(assertCtx, rs, t2) actionFired, _ = assertCtxValues["actionFired"].(string) if actionFired != "NOTFIRED" { t.Log("rule action SHOULD NOT be fired") @@ -53,7 +53,7 @@ func Test_Four(t *testing.T) { addRule(t, rs, r1) rs.ReplayTuplesForRule(r1.GetName()) assertCtxValues["actionFired"] = "NOTFIRED" - assert(assertCtx, rs, t2) + assertWithCtx(assertCtx, rs, t2) actionFired, _ = assertCtxValues["actionFired"].(string) if actionFired != "FIRED" { t.Log("rule action SHOULD be fired") @@ -77,7 +77,7 @@ func deleteRule(t *testing.T, rs model.RuleSession, rule model.Rule) { t.Logf("[%s] Rule[%s] deleted. \n", time.Now().Format("15:04:05.999999"), rule.GetName()) } -func assert(ctx context.Context, rs model.RuleSession, tuple model.Tuple) { +func assertWithCtx(ctx context.Context, rs model.RuleSession, tuple model.Tuple) { assertCtxValues := ctx.Value("values").(map[string]interface{}) t, _ := assertCtxValues["test"].(*testing.T) err := rs.Assert(ctx, tuple) diff --git a/ruleapi/tests/sessions_test.go b/ruleapi/tests/sessions_test.go new file mode 100644 index 0000000..9f9f5e2 --- /dev/null +++ b/ruleapi/tests/sessions_test.go @@ -0,0 +1,104 @@ +package tests + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/project-flogo/rules/common/model" + "github.com/project-flogo/rules/ruleapi" + + "github.com/stretchr/testify/assert" +) + +func TestClearSessions(t *testing.T) { + _, err := ruleapi.GetOrCreateRuleSession("test", "") + assert.Nil(t, err) + ruleapi.ClearSessions() +} + +func TestAssert(t *testing.T) { + rs, err := createRuleSession(t) + assert.Nil(t, err) + rule := ruleapi.NewRule("R2") + err = rule.AddCondition("R2_c1", []string{"t4.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, emptyAction)) + rule.SetPriority(1) + err = rs.AddRule(rule) + assert.Nil(t, err) + t.Logf("Rule added: [%s]\n", rule.GetName()) + err = rs.Start(nil) + assert.Nil(t, err) + + t1, err := model.NewTupleWithKeyValues("t4", "t4") + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.NotNil(t, err) + + time.Sleep(2 * time.Second) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + deleteRuleSession(t, rs, t1) +} + +func TestRace(t *testing.T) { + rs, err := createRuleSession(t) + assert.Nil(t, err) + defer rs.Unregister() + rule := ruleapi.NewRule("R2") + err = rule.AddCondition("R2_c1", []string{"t4.none"}, trueCondition, nil) + assert.Nil(t, err) + rule.SetActionService(createActionServiceFromFunction(t, emptyAction)) + rule.SetPriority(1) + err = rs.AddRule(rule) + assert.Nil(t, err) + t.Logf("Rule added: [%s]\n", rule.GetName()) + err = rs.Start(nil) + assert.Nil(t, err) + + done := make(chan bool, 8) + withTTL := func() { + for i := 0; i < 10; i++ { + t1, err := model.NewTupleWithKeyValues("t4", fmt.Sprintf("ttl%d", i)) + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + time.Sleep(10 * time.Millisecond) + } + done <- true + } + withDelete := func() { + for i := 0; i < 10; i++ { + t1, err := model.NewTupleWithKeyValues("t3", fmt.Sprintf("delete%d", i)) + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + time.Sleep(10 * time.Millisecond) + err = rs.Delete(context.TODO(), t1) + assert.Nil(t, err) + time.Sleep(10 * time.Millisecond) + } + done <- true + } + addOnly := func() { + for i := 0; i < 10; i++ { + t1, err := model.NewTupleWithKeyValues("t3", fmt.Sprintf("add%d", i)) + assert.Nil(t, err) + err = rs.Assert(context.TODO(), t1) + assert.Nil(t, err) + time.Sleep(10 * time.Millisecond) + } + done <- true + } + go withTTL() + go withDelete() + go addOnly() + for i := 0; i < 3; i++ { + <-done + } + time.Sleep(3 * time.Second) +} diff --git a/ruleapi/tests/tests.json b/ruleapi/tests/tests.json index 40a4d82..3496558 100644 --- a/ruleapi/tests/tests.json +++ b/ruleapi/tests/tests.json @@ -65,5 +65,28 @@ "type":"string" } ] + }, + { + "name":"t4", + "ttl": 1, + "properties":[ + { + "name":"id", + "type":"string", + "pk-index":0 + }, + { + "name":"p1", + "type":"int" + }, + { + "name":"p2", + "type":"double" + }, + { + "name":"p3", + "type":"string" + } + ] } -] \ No newline at end of file +] diff --git a/scripts/README.md b/scripts/README.md deleted file mode 100755 index 6cdeff0..0000000 --- a/scripts/README.md +++ /dev/null @@ -1,37 +0,0 @@ -## Sanity Testing - -* There is a shell script file `run_sanitytest.sh` that performs sanity testing against rules/examples and generates html report. - -* This script file checks for all available `sanity.sh` files inside rules/examples and run tests against individual `sanity.sh` file - - -* To run sanity tests - -``` -cd $GOPATH/src/github.com/project-flogo/rules/scripts -./run_sanitytest.sh -``` - -* Testcase status of each example is updated in the html report and test report is made available in scripts folder. - - -### Contributing - -If you're adding a new rules example, optionally you can add sanity test file with name `sanity.sh`. Below is the template used for creating test file. - -``` -#!/bin/bash - -function get_test_cases { - local my_list=( testcase1 ) - echo "${my_list[@]}" -} - -function testcase1 { -# Add detailed steps to execute the test case -} -``` -Sample sanity test file can be found at -``` -$GOPATH/src/github.com/project-flogo/rules/examples/flogo/simple/sanity.sh -``` \ No newline at end of file diff --git a/scripts/run_sanitytest.sh b/scripts/run_sanitytest.sh deleted file mode 100755 index f10de76..0000000 --- a/scripts/run_sanitytest.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - -RULESPATH=$GOPATH/src/github.com/project-flogo/rules -export FILENAME="RulesSanityReport.html" -HTML=" -

Rules Sanity Report

Summary

Number of test cases passed
Number of test cases failed
Total test cases
Recipe Testcase Status
" - -echo $HTML >> $RULESPATH/scripts/$FILENAME -PASS_COUNT=0 FAIL_COUNT=0 - -# Fetch list of sanity.sh files in examples folder -function get_sanitylist() -{ - cd $RULESPATH/examples - find | grep sanity.sh > file.txt - readarray -t array < file.txt - for EXAMPLE in "${array[@]}" - do - echo "$EXAMPLE" - RECIPE=$(echo $EXAMPLE | sed -e 's/\/sanity.sh//g' | sed -e 's/\.\///g' | sed -e 's/\//-/g') - execute_testcase - done -} - -# Execute and obtain testcase status (pass/fail) -function execute_testcase() -{ - echo $RECIPE - source $EXAMPLE - TESTCASE_LIST=($(get_test_cases)) - sleep 10 - for ((i=0;i < ${#TESTCASE_LIST[@]};i++)) - do - TESTCASE=$(${TESTCASE_LIST[i]}) - sleep 10 - if [[ $TESTCASE == *"PASS"* ]]; then - echo "$RECIPE":"Passed" - PASS_COUNT=$((PASS_COUNT+1)) - sed -i "s/<\/tr> <\/table>/$RECIPE<\/td>${TESTCASE_LIST[i]}<\/td>PASS<\/td><\/tr><\/tr> <\/table>/g" $RULESPATH/scripts/$FILENAME - else - echo "$RECIPE":"Failed" - FAIL_COUNT=$((FAIL_COUNT+1)) - sed -i "s/<\/tr> <\/table>/$RECIPE<\/td>${TESTCASE_LIST[i]}<\/td>FAIL<\/td><\/tr><\/tr> <\/table>/g" $RULESPATH/scripts/$FILENAME - fi - done -} - -get_sanitylist - -# Update testcase count in html report -sed -i s/"passed <\/td> "/"passed <\/td> $PASS_COUNT"/g $RULESPATH/scripts/$FILENAME -sed -i s/"failed <\/td> "/"failed <\/td> $FAIL_COUNT"/g $RULESPATH/scripts/$FILENAME -sed -i s/"cases<\/td>"/"cases<\/td>$((PASS_COUNT+FAIL_COUNT))"/g $RULESPATH/scripts/$FILENAME \ No newline at end of file