diff --git a/converter/converter.go b/converter/converter.go new file mode 100644 index 0000000..127ea51 --- /dev/null +++ b/converter/converter.go @@ -0,0 +1,212 @@ +package converter + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" +) + +// JSONToExpression parses jsonlogic in string format to an expression +// compatible with nikunjy/rules engine +func JSONToExpression(jsonLogic string) (string, error) { + var logicTree map[string]interface{} + if err := json.Unmarshal([]byte(jsonLogic), &logicTree); err != nil { + return "", err + } + return parseLogicTree(logicTree), nil +} + +func parseLogicTree(node map[string]interface{}) string { + for _, key := range []string{"and", "or"} { + if rulesList, ok := node[key].([]interface{}); ok { + rules := make([]string, len(rulesList)) + for i, r := range rulesList { + rules[i] = parseLogicTree(r.(map[string]interface{})) + } + return "(" + strings.Join(rules, " "+key+" ") + ")" + } + } + + for op, value := range node { + values := value.([]interface{}) + left := values[0] + if m, ok := left.(map[string]interface{}); ok { + left = m["var"] + } + return "(" + fmt.Sprintf("%v %s %v", left, op, values[1]) + ")" + } + + return "" +} + + +// ExpressionToJSON parses a nikunjy/rules expression to jsonLogic +func ExpressionToJSON(expression string) (string, error) { + tokens := strings.Fields(expression) + + logicTree, err := buildLogicTree(tokens) + if err != nil { + return "", err + } + + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + enc.SetEscapeHTML(false) + + if err := enc.Encode(logicTree); err != nil { + return "", err + } + + return strings.TrimSpace(buf.String()), nil +} + +func buildLogicTree(tokens []string) (map[string]interface{}, error) { + var nodes []interface{} + var logicalOperator string + + for i := 0; i < len(tokens); i++ { + token := tokens[i] + + switch token { + case "(": + expression, tokensProcessed, err := parseParenExpression(tokens[i:]) + if err != nil { + return nil, err + } + nodes = append(nodes, expression) + i += tokensProcessed + case "and", "or": + logicalOperator = token + case "not": + expression, tokensProcessed, err := parseNotExpression(tokens[i:]) + if err != nil { + return nil, err + } + nodes = append(nodes, expression) + i += tokensProcessed + default: + expression, err := parseComparisonExpression(tokens[i:]) + if err != nil { + return nil, err + } + nodes = append(nodes, expression) + i += 2 + } + } + + if len(nodes) == 1 { + return nodes[0].(map[string]interface{}), nil + } + + return map[string]interface{}{ + logicalOperator: nodes, + }, nil +} + +func parseParenExpression(tokens []string) (map[string]interface{}, int, error) { + level := 1 + end := 1 + + for level > 0 && end < len(tokens) { + if tokens[end] == "(" { + level++ + } else if tokens[end] == ")" { + level-- + } + end++ + } + if level != 0 { + return nil, 0, fmt.Errorf("invalid expression") + } + + expression, err := buildLogicTree(tokens[1 : end-1]) + if err != nil { + return nil, 0, err + } + + return expression, end - 1, nil +} + +func parseNotExpression(tokens []string) (map[string]interface{}, int, error) { + if len(tokens) < 2 { + return nil, 0, fmt.Errorf("invalid expression") + } + + if tokens[1] == "(" { + subExpr, newIndex, err := parseParenExpression(tokens[1:]) + if err != nil { + return nil, 0, err + } + return map[string]interface{}{"!": subExpr}, newIndex + 1, nil + } + + if len(tokens) >= 4 && isComparisonOperator(tokens[2]) { + return map[string]interface{}{ + "!": map[string]interface{}{ + tokens[2]: []interface{}{ + map[string]interface{}{"var": tokens[1]}, + parseValue(tokens[3]), + }, + }, + }, 3, nil + } + + return map[string]interface{}{ + "!": map[string]interface{}{"var": tokens[1]}, + }, 1, nil +} +func isComparisonOperator(op string) bool { + switch op { + case "eq", "==", + "ne", "!=", + "lt", "<", + "gt", ">", + "le", "<=", + "ge", ">=", + "co", "sw", "ew", + "in", "pr": + return true + } + return false +} + +func parseComparisonExpression(tokens []string) (map[string]interface{}, error) { + if len(tokens) < 3 { + return nil, fmt.Errorf("invalid expression") + } + + varName := strings.Trim(tokens[0], "()") + value := strings.Trim(tokens[2], "()") + + return map[string]interface{}{ + tokens[1]: []interface{}{ + map[string]interface{}{"var": varName}, + parseValue(value), + }, + }, nil +} + +func parseValue(val string) interface{} { + if strings.HasPrefix(val, "[") && strings.HasSuffix(val, "]") { + items := strings.Split(strings.Trim(val, "[]"), ",") + var result []interface{} + + for _, item := range items { + item = strings.TrimSpace(item) + if num, err := strconv.Atoi(item); err == nil { + result = append(result, num) + } else { + result = append(result, strings.Trim(item, "\"'")) + } + } + return result + } + + if num, err := strconv.Atoi(val); err == nil { + return num + } + + return strings.Trim(val, "\"'") +} \ No newline at end of file diff --git a/test/converter/converter_test.go b/test/converter/converter_test.go new file mode 100644 index 0000000..6244f41 --- /dev/null +++ b/test/converter/converter_test.go @@ -0,0 +1,154 @@ +package test + +import ( + "testing" + + "github.com/ahuangg/json-rules/converter" +) + + +func TestJSONToExpression(t *testing.T) { + tests := []struct { + name string + input string + expected string + wantErr bool + }{ + { + name: "equal test", + input: `{"eq": [{"var": "x"}, 1]}`, + expected: "(x eq 1)", + }, + { + name: "equal test 2", + input: `{"==": [{"var": "x"}, 2]}`, + expected: "(x == 2)", + }, + { + name: "less than test", + input: `{"<": [{"var": "x"}, 2]}`, + expected: "(x < 2)", + }, + { + name: "less than test 2", + input: `{"<": [{"var": "x"}, 1]}`, + expected: "(x < 1)", + }, + { + name: "greater than test", + input: `{">": [{"var": "x"}, 0]}`, + expected: "(x > 0)", + }, + { + name: "greater than test 2", + input: `{">": [{"var": "x"}, 2]}`, + expected: "(x > 2)", + }, + { + name: "equal and less than equal test", + input: `{"and": [{"==": [{"var": "x.a"}, 1]}, {"<=": [{"var": "x.b.c"}, 2]}]}`, + expected: "((x.a == 1) and (x.b.c <= 2))", + }, + { + name: "equal and greater than test", + input: `{"and": [{"==": [{"var": "y"}, 4]}, {">": [{"var": "x"}, 1]}]}`, + expected: "((y == 4) and (x > 1))", + }, + { + name: "in test", + input: `{"and": [{"==": [{"var": "y"}, 4]}, {"in": [{"var": "x"}, [1, 2, 3]]}]}`, + expected: "((y == 4) and (x in [1 2 3]))", + }, + { + name: "equal string test", + input: `{"and": [{"==": [{"var": "y"}, 4]}, {"eq": [{"var": "x"}, "1.2.3"]}]}`, + expected: `((y == 4) and (x eq 1.2.3))`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := converter.JSONToExpression(tt.input) + if err != nil { + return + } + + if got != tt.expected { + t.Errorf("JSONToExpression() = %v, want %v", got, tt.expected) + } + }) + } +} + +func TestExpressionToJSON(t *testing.T) { + tests := []struct { + name string + input string + expected string + wantErr bool + }{ + { + name: "equal test", + input: "x eq 1", + expected: `{"eq":[{"var":"x"},1]}`, + }, + { + name: "equal test 2", + input: "x == 2", + expected: `{"==":[{"var":"x"},2]}`, + }, + { + name: "less than test", + input: "x < 2", + expected: `{"<":[{"var":"x"},2]}`, + }, + { + name: "less than test 2", + input: "x < 1", + expected: `{"<":[{"var":"x"},1]}`, + }, + { + name: "greater than test", + input: "x > 0", + expected: `{">":[{"var":"x"},0]}`, + }, + { + name: "greater than test 2", + input: "x > 2", + expected: `{">":[{"var":"x"},2]}`, + }, + { + name: "equal and less than equal test", + input: "((x.a == 1) and (x.b.c <= 2))", + expected: `{"and":[{"==":[{"var":"x.a"},1]},{"<=":[{"var":"x.b.c"},2]}]}`, + }, + { + name: "equal and greater than test", + input: "y == 4 and (x > 1)", + expected: `{"and":[{"==":[{"var":"y"},4]},{">":[{"var":"x"},1]}]}`, + }, + { + name: "in test", + input: "y == 4 and (x in [1 2 3])", + expected: `{"and":[{"==":[{"var":"y"},4]},{"in":[{"var":"x"},[1,2,3]]}]}`, + }, + { + name: "equal string test", + input: `y == 4 and (x eq 1.2.3)`, + expected: `{"and":[{"==":[{"var":"y"},4]},{"eq":[{"var":"x"},"1.2.3"]}]}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := converter.ExpressionToJSON(tt.input) + if err != nil { + return + } + + if got != tt.expected { + t.Errorf("ExpressionToJSON() = %v, want %v", got, tt.expected) + } + }) + } +} \ No newline at end of file diff --git a/test/examples/eq2_test.json b/test/parser/examples/eq2_test.json similarity index 100% rename from test/examples/eq2_test.json rename to test/parser/examples/eq2_test.json diff --git a/test/examples/eq_and_gt_test.json b/test/parser/examples/eq_and_gt_test.json similarity index 100% rename from test/examples/eq_and_gt_test.json rename to test/parser/examples/eq_and_gt_test.json diff --git a/test/examples/eq_and_lte_test.json b/test/parser/examples/eq_and_lte_test.json similarity index 100% rename from test/examples/eq_and_lte_test.json rename to test/parser/examples/eq_and_lte_test.json diff --git a/test/examples/eq_string_test.json b/test/parser/examples/eq_string_test.json similarity index 100% rename from test/examples/eq_string_test.json rename to test/parser/examples/eq_string_test.json diff --git a/test/examples/eq_test.json b/test/parser/examples/eq_test.json similarity index 100% rename from test/examples/eq_test.json rename to test/parser/examples/eq_test.json diff --git a/test/examples/gt2_test.json b/test/parser/examples/gt2_test.json similarity index 100% rename from test/examples/gt2_test.json rename to test/parser/examples/gt2_test.json diff --git a/test/examples/gt_test.json b/test/parser/examples/gt_test.json similarity index 100% rename from test/examples/gt_test.json rename to test/parser/examples/gt_test.json diff --git a/test/examples/in_test.json b/test/parser/examples/in_test.json similarity index 100% rename from test/examples/in_test.json rename to test/parser/examples/in_test.json diff --git a/test/examples/lt2_test.json b/test/parser/examples/lt2_test.json similarity index 100% rename from test/examples/lt2_test.json rename to test/parser/examples/lt2_test.json diff --git a/test/examples/lt_test.json b/test/parser/examples/lt_test.json similarity index 100% rename from test/examples/lt_test.json rename to test/parser/examples/lt_test.json diff --git a/test/parser_test.go b/test/parser/parser_test.go similarity index 100% rename from test/parser_test.go rename to test/parser/parser_test.go