Skip to content

Commit

Permalink
Custom js names (#4)
Browse files Browse the repository at this point in the history
* added support for anonymous or custom named functions;
code refactoring

* updated change log for v.0.2.0
  • Loading branch information
aleybovich authored May 5, 2024
1 parent 44ded9d commit 03e41c5
Show file tree
Hide file tree
Showing 8 changed files with 201 additions and 159 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[0.2.0]
- Added: `ExportMermaid` function accepts business rules in YAML and returns Mermaid code to display the rules in a flow disagram
- Added: `WithDecisionCallback` option in RulesRunner constructor; when provided, used to receive the decision steps from rules engine; to be used for rules troubleshooting or audit
- Added: support for anonymous or custom named javascript functions
- Changed: code refactoring/cleanup

[0.1.0]
- Added: business rules engine
162 changes: 69 additions & 93 deletions condition.go
Original file line number Diff line number Diff line change
@@ -1,131 +1,107 @@
package main
package yabre

import (
"fmt"

"github.com/dop251/goja"
)

type Designation string

const (
True Designation = "true"
False Designation = "false"
)

type Condition struct {
Default bool `yaml:"default"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Check string `yaml:"check"`
True *ConditionResult `yaml:"true"`
False *ConditionResult `yaml:"false"`
Default bool `yaml:"default"`
Name string `yaml:"name"`
Description string `yaml:"description"`
Check string `yaml:"check"`
True *Decision `yaml:"true"`
False *Decision `yaml:"false"`
}

type ConditionResult struct {
Name string `yaml:"-"`
Description string `yaml:"description"`
Action string `yaml:"action"`
Next string `yaml:"next"`
Terminate bool `yaml:"terminate"`
Designation Designation `yaml:"-"` // true or false
type Decision struct {
Name string `yaml:"-"`
Description string `yaml:"description"`
Action string `yaml:"action"`
Next string `yaml:"next"`
Terminate bool `yaml:"terminate"`
Value bool `yaml:"-"`
}

func (cr *ConditionResult) UnmarshalYAML(unmarshal func(interface{}) error) error {
type conditionResult ConditionResult // we need to create an intermediate type to avoid infinite recursion
var ccr conditionResult
if err := unmarshal(&ccr); err != nil {
func (cr *Decision) UnmarshalYAML(unmarshal func(interface{}) error) error {
type decision Decision // we need to create an intermediate type to avoid infinite recursion
var dsn decision
if err := unmarshal(&dsn); err != nil {
return err
}

if ccr.Next != "" && ccr.Terminate {
if dsn.Next != "" && dsn.Terminate {
return fmt.Errorf("next and terminate cannot be used together")
}

*cr = ConditionResult(ccr)
*cr = Decision(dsn)
return nil
}

// Run the conditions recursively
func (runner *RulesRunner[Context]) runCondition(vm *goja.Runtime, rules *Rules, condition *Condition) error {
runner.DecisionCallback("Running condition: %s\n", condition.Name)
runner.decisionCallback("Running condition: [%s] %s\n", condition.Name, condition.Description)

// Get the custom function name for the check function
checkFuncName := runner.getFunctionName(condition.Name)

// Evaluate the check function
checkFunc, ok := goja.AssertFunction(vm.Get(condition.Name))
checkFunc, ok := goja.AssertFunction(vm.Get(checkFuncName))
if !ok {
return fmt.Errorf("check function not found: %s", condition.Name)
return fmt.Errorf("check function not found: %s", checkFuncName)
}
checkResult, err := checkFunc(goja.Undefined())
if err != nil {
return fmt.Errorf("error evaluating check function %s: %v", condition.Name, err)
return fmt.Errorf("error evaluating check function %s: %v", checkFuncName, err)
}

if checkResult.ToBoolean() {
if condition.True == nil {
return nil
}
return runner.runAction(vm, rules, condition.True)
} else {
if condition.False == nil {
return nil
}
return runner.runAction(vm, rules, condition.False)
}
}

// Helper function to run the action
func (runner *RulesRunner[Context]) runAction(vm *goja.Runtime, rules *Rules, result *Decision) error {
if result.Action != "" {
actionFuncName := runner.getFunctionName(result.Name)
runner.decisionCallback("\tRunning action: [%s] %s\n", actionFuncName, result.Description)
actionFunc, ok := goja.AssertFunction(vm.Get(actionFuncName))
if !ok {
return fmt.Errorf("action function not found: %s", actionFuncName)
}
_, err := actionFunc(goja.Undefined())
if err != nil {
return fmt.Errorf("error running action: %v", err)
}
}

if checkResult.ToBoolean() { // If check was true
if condition.True != nil {
runner.DecisionCallback("\tRunning TRUE check: %s\n", condition.True.Description)
if condition.True.Action != "" {
// Run the action function
actionFuncName := condition.Name + "_true"
runner.DecisionCallback("\t\tRunning action function: %s\n", actionFuncName)
actionFunc, ok := goja.AssertFunction(vm.Get(actionFuncName))
if !ok {
return fmt.Errorf("action function not found: %s", actionFuncName)
}
_, err := actionFunc(goja.Undefined())
if err != nil {
return fmt.Errorf("error running action function: %v", err)
}
}
if condition.True.Next != "" {
nextCondition, err := findConditionByName(rules, condition.True.Next)
if err != nil {
return fmt.Errorf("unexpected error: condition '%s' not found", condition.True.Next)
}
err = runner.runCondition(vm, rules, nextCondition)
if err != nil {
return fmt.Errorf("error while running condition '%s': %v", condition.True.Next, err)
}
}
if condition.True.Terminate {
runner.DecisionCallback("Terminating\n")
return nil
}
if result.Next != "" {
nextCondition, err := findConditionByName(rules, result.Next)
if err != nil {
return fmt.Errorf("unexpected error: condition '%s' not found", result.Next)
}
} else { // if check was false
if condition.False != nil {
runner.DecisionCallback("\tRunning FALSE check: %s\n", condition.False.Description)
if condition.False.Action != "" {
// Run the action function
actionFuncName := condition.Name + "_false"
runner.DecisionCallback("\t\tRunning action function: %s\n", actionFuncName)
actionFunc, ok := goja.AssertFunction(vm.Get(actionFuncName))
if !ok {
return fmt.Errorf("action function not found: %s", actionFuncName)
}
_, err := actionFunc(goja.Undefined())
if err != nil {
return fmt.Errorf("error running action function: %v", err)
}
}
if condition.False.Next != "" {
nextCondition, err := findConditionByName(rules, condition.False.Next)
if err != nil {
return fmt.Errorf("unexpected error: condition '%s' not found", condition.False.Next)
}

err = runner.runCondition(vm, rules, nextCondition)
if err != nil {
return fmt.Errorf("error while running condition '%s': %v", condition.False.Next, err)
}
}
if condition.False.Terminate {
runner.DecisionCallback("Terminating\n")
return nil
}
runner.decisionCallback("\tMoving to next condition: %s\n", nextCondition.Name)
err = runner.runCondition(vm, rules, nextCondition)
if err != nil {
return fmt.Errorf("error while running condition '%s': %v", result.Next, err)
}
}

return nil // return from runCondition
if result.Terminate {
runner.decisionCallback("Terminating\n")
return nil
}

return nil
}

func findConditionByName(rule *Rules, name string) (*Condition, error) {
Expand Down
30 changes: 15 additions & 15 deletions mermaid.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package yabre

import (
"fmt"
Expand Down Expand Up @@ -53,42 +53,42 @@ func ExportMermaid(yamlString []byte, defaultConditionName string) (string, erro

func renderCondition(condition *Condition, mermaid *strings.Builder) error {
if condition.True != nil {
renderConditionResult(condition, condition.True, mermaid)
renderDecision(condition, condition.True, mermaid)
}
if condition.False != nil {
renderConditionResult(condition, condition.False, mermaid)
renderDecision(condition, condition.False, mermaid)
}

return nil
}

func renderConditionResult(
func renderDecision(
condition *Condition,
conditionResult *ConditionResult,
decision *Decision,
mermaid *strings.Builder) error {

if conditionResult.Action != "" {
if decision.Action != "" {
// connection from condition to True/False action
mermaid.WriteString(fmt.Sprintf(" %s --> |%s| %s\n", condition.Name, conditionResult.Designation, conditionResult.Name))
mermaid.WriteString(fmt.Sprintf(" %s --> |%t| %s\n", condition.Name, decision.Value, decision.Name))

if conditionResult.Next != "" {
if decision.Next != "" {
// connection from True/False action to next condition
mermaid.WriteString(fmt.Sprintf(" %s --> %s\n", conditionResult.Name, conditionResult.Next))
mermaid.WriteString(fmt.Sprintf(" %s --> %s\n", decision.Name, decision.Next))
}

if conditionResult.Terminate {
if decision.Terminate {
// terminator from True/False action
mermaid.WriteString(fmt.Sprintf(" %s --> %s_end\n", conditionResult.Name, conditionResult.Name))
mermaid.WriteString(fmt.Sprintf(" %s --> %s_end\n", decision.Name, decision.Name))
}
} else {
if conditionResult.Next != "" {
if decision.Next != "" {
// connection from condition to next condition
mermaid.WriteString(fmt.Sprintf(" %s --> |%s| %s\n", condition.Name, conditionResult.Designation, conditionResult.Next))
mermaid.WriteString(fmt.Sprintf(" %s --> |%t| %s\n", condition.Name, decision.Value, decision.Next))
}

if conditionResult.Terminate {
if decision.Terminate {
// terminator from condition
mermaid.WriteString(fmt.Sprintf(" %s --> |%s| %s_end\n", condition.Name, conditionResult.Designation, conditionResult.Name))
mermaid.WriteString(fmt.Sprintf(" %s --> |%t| %s_end\n", condition.Name, decision.Value, decision.Name))
}
}
return nil
Expand Down
2 changes: 1 addition & 1 deletion mermaid_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package yabre

import (
"fmt"
Expand Down
43 changes: 31 additions & 12 deletions rules.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package main
package yabre

import (
"fmt"
"os"
"regexp"

"github.com/dop251/goja"
"gopkg.in/yaml.v2"
Expand All @@ -29,12 +30,12 @@ func (r *Rules) UnmarshalYAML(unmarshal func(interface{}) error) error {

if condition.True != nil {
condition.True.Name = condition.Name + "_true"
condition.True.Designation = True
condition.True.Value = true
}

if condition.False != nil {
condition.False.Name = condition.Name + "_false"
condition.False.Designation = False
condition.False.Value = false
}

rr.Conditions[name] = condition
Expand Down Expand Up @@ -71,28 +72,46 @@ func (rr *RulesRunner[Context]) loadRulesFromYaml(fileName string) (*Rules, erro
return &rules, nil
}

func (rr *RulesRunner[Context]) addJsFunctions(vm *goja.Runtime) error {
func (runner *RulesRunner[Context]) addJsFunctions(vm *goja.Runtime) error {
// add all js functions to the vm
for _, condition := range rr.Rules.Conditions {
for _, condition := range runner.Rules.Conditions {
if condition.Check != "" {
_, err := vm.RunString(condition.Check)
if err != nil {
return fmt.Errorf("error injecting check function into vm: %v", err)
checkName := condition.Name
if err := runner.injectJSFunction(vm, checkName, condition.Check); err != nil {
return fmt.Errorf("error injecting condition function into vm: %v", err)
}
}
if condition.True != nil && condition.True.Action != "" {
_, err := vm.RunString(condition.True.Action)
if err != nil {
actionName := fmt.Sprintf("%s_%t", condition.Name, condition.True.Value)
if err := runner.injectJSFunction(vm, actionName, condition.True.Action); err != nil {
return fmt.Errorf("error injecting action function into vm: %v", err)
}
}
if condition.False != nil && condition.False.Action != "" {
_, err := vm.RunString(condition.False.Action)
if err != nil {
actionName := fmt.Sprintf("%s_%t", condition.Name, condition.False.Value)
if err := runner.injectJSFunction(vm, actionName, condition.True.Action); err != nil {
return fmt.Errorf("error injecting action function into vm: %v", err)
}
}
}

return nil
}

func (runner *RulesRunner[Context]) injectJSFunction(vm *goja.Runtime, defaultName, funcCode string) error {
funcName := defaultName

re := regexp.MustCompile(`function\s+(\w+)\s*\(`)
matches := re.FindStringSubmatch(funcCode)
if len(matches) > 1 {
funcName = matches[1]
}

runner.functionNames[defaultName] = funcName // Store the function name mapping
_, err := vm.RunString(fmt.Sprintf("%s = %s", funcName, funcCode))
if err != nil {
return fmt.Errorf("error injecting function into vm: %v", err)
}

return nil
}
Loading

0 comments on commit 03e41c5

Please sign in to comment.