From b54e39a5e7f7bb10219f7593a7da61c1a611beda Mon Sep 17 00:00:00 2001 From: debugtalk Date: Mon, 4 Oct 2021 10:37:22 +0800 Subject: [PATCH] feat: parseVariables --- parser.go | 131 ++++++++++++++++++++++++++++++++++++++++++++++++- parser_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 255 insertions(+), 1 deletion(-) diff --git a/parser.go b/parser.go index 6ebe6ed7..51f816f6 100644 --- a/parser.go +++ b/parser.go @@ -316,5 +316,134 @@ func parseFunctionArguments(argsStr string) ([]interface{}, error) { } func parseVariables(variables map[string]interface{}) (map[string]interface{}, error) { - return variables, nil + parsedVariables := make(map[string]interface{}) + var traverseRounds int + + for len(parsedVariables) != len(variables) { + for varName, varValue := range variables { + // skip parsed variables + if _, ok := parsedVariables[varName]; ok { + continue + } + + // extract variables from current value + extractVarsSet := extractVariables(varValue) + + // check if reference variable itself + // e.g. + // variables = {"token": "abc$token"} + // variables = {"key": ["$key", 2]} + if _, ok := extractVarsSet[varName]; ok { + log.Printf("[parseVariables] variable self reference error: %v", variables) + return variables, fmt.Errorf("variable self reference: %v", varName) + } + + // check if reference variable not in variables mapping + // e.g. + // {"varA": "123$varB", "varB": "456$varC"} => $varC not defined + // {"varC": "${sum_two($a, $b)}"} => $a, $b not defined + var undefinedVars []string + for extractVar := range extractVarsSet { + if _, ok := variables[extractVar]; !ok { // not in variables mapping + undefinedVars = append(undefinedVars, extractVar) + } + } + if len(undefinedVars) > 0 { + log.Printf("[parseVariables] variable not defined error: %v", undefinedVars) + return variables, fmt.Errorf("variable not defined: %v", undefinedVars) + } + + parsedValue := parseData(varValue, parsedVariables) + if parsedValue == nil { + continue + } + parsedVariables[varName] = parsedValue + } + traverseRounds += 1 + // check if circular reference exists + if traverseRounds > len(variables) { + log.Printf("[parseVariables] circular reference error, break infinite loop!") + return variables, fmt.Errorf("circular reference") + } + } + + return parsedVariables, nil +} + +type variableSet map[string]struct{} + +func extractVariables(raw interface{}) variableSet { + rawValue := reflect.ValueOf(raw) + switch rawValue.Kind() { + case reflect.String: + return findallVariables(rawValue.String()) + case reflect.Slice: + varSet := make(variableSet) + for i := 0; i < rawValue.Len(); i++ { + for extractVar := range extractVariables(rawValue.Index(i).Interface()) { + varSet[extractVar] = struct{}{} + } + } + return varSet + case reflect.Map: + varSet := make(variableSet) + for _, key := range rawValue.MapKeys() { + value := rawValue.MapIndex(key) + for extractVar := range extractVariables(value.Interface()) { + varSet[extractVar] = struct{}{} + } + } + return varSet + default: + // other types, e.g. nil, int, float, bool + return make(variableSet) + } +} + +func findallVariables(raw string) variableSet { + matchStartPosition := 0 + remainedString := raw + varSet := make(variableSet) + + for matchStartPosition < len(raw) { + // locate $ char position + startPosition := strings.Index(remainedString, "$") + if startPosition == -1 { // no $ found + return varSet + } + + // found $, check if variable or function + matchStartPosition += startPosition + remainedString = remainedString[startPosition:] + + // Notice: notation priority + // $$ > $var + + // search $$, use $$ to escape $ notation + if strings.HasPrefix(remainedString, "$$") { // found $$ + matchStartPosition += 2 + remainedString = remainedString[2:] + continue + } + + // search variable like ${var} or $var + varMatched := regexCompileVariable.FindStringSubmatch(remainedString) + if len(varMatched) == 3 { + var varName string + if varMatched[1] != "" { + varName = varMatched[1] // match ${var} + } else { + varName = varMatched[2] // match $var + } + varSet[varName] = struct{}{} + + matchStartPosition += len(varMatched[0]) + remainedString = raw[matchStartPosition:] + continue + } + + break + } + + return varSet } diff --git a/parser_test.go b/parser_test.go index a0d3345c..aacb5c64 100644 --- a/parser_test.go +++ b/parser_test.go @@ -1,6 +1,7 @@ package httpboomer import ( + "sort" "testing" "time" @@ -428,3 +429,127 @@ func TestConvertString(t *testing.T) { } } } + +func TestParseVariables(t *testing.T) { + testData := []struct { + rawVars map[string]interface{} + expectVars map[string]interface{} + }{ + { + map[string]interface{}{"varA": "$varB", "varB": "$varC", "varC": "123", "a": 1, "b": 2}, + map[string]interface{}{"varA": "123", "varB": "123", "varC": "123", "a": 1, "b": 2}, + }, + } + + for _, data := range testData { + value, err := parseVariables(data.rawVars) + if !assert.Nil(t, err) { + t.Fail() + } + if !assert.Equal(t, data.expectVars, value) { + t.Fail() + } + } +} + +func TestParseVariablesAbnormal(t *testing.T) { + testData := []struct { + rawVars map[string]interface{} + expectVars map[string]interface{} + }{ + { // self referenced variable $varA + map[string]interface{}{"varA": "$varA"}, + map[string]interface{}{"varA": "$varA"}, + }, + { // undefined variable $varC + map[string]interface{}{"varA": "$varB", "varB": "$varC", "a": 1, "b": 2}, + map[string]interface{}{"varA": "$varB", "varB": "$varC", "a": 1, "b": 2}, + }, + { // circular reference + map[string]interface{}{"varA": "$varB", "varB": "$varA"}, + map[string]interface{}{"varA": "$varB", "varB": "$varA"}, + }, + } + + for _, data := range testData { + value, err := parseVariables(data.rawVars) + if !assert.NotNil(t, err) { + t.Fail() + } + if !assert.Equal(t, data.expectVars, value) { + t.Fail() + } + } +} + +func TestExtractVariables(t *testing.T) { + testData := []struct { + raw interface{} + expectVars []string + }{ + {nil, nil}, + {"/$var1/$var1", []string{"var1"}}, + { + map[string]interface{}{"varA": "$varB", "varB": "$varC", "varC": "123"}, + []string{"varB", "varC"}, + }, + { + []interface{}{"varA", "$varB", 123, "$varC", "123"}, + []string{"varB", "varC"}, + }, + { // nested map and slice + map[string]interface{}{"varA": "$varB", "varB": map[string]interface{}{"C": "$varC", "D": []string{"$varE"}}}, + []string{"varB", "varC", "varE"}, + }, + } + + for _, data := range testData { + var varList []string + for varName := range extractVariables(data.raw) { + varList = append(varList, varName) + } + sort.Strings(varList) + if !assert.Equal(t, data.expectVars, varList) { + t.Fail() + } + } +} + +func TestFindallVariables(t *testing.T) { + testData := []struct { + raw string + expectVars []string + }{ + {"", nil}, + {"$variable", []string{"variable"}}, + {"${variable}123", []string{"variable"}}, + {"/blog/$postid", []string{"postid"}}, + {"/$var1/$var2", []string{"var1", "var2"}}, + {"/$var1/$var1", []string{"var1"}}, + {"abc", nil}, + {"Z:2>1*0*1+1$a", []string{"a"}}, + {"Z:2>1*0*1+1$$a", nil}, + {"Z:2>1*0*1+1$$$a", []string{"a"}}, + {"Z:2>1*0*1+1$$$$a", nil}, + {"Z:2>1*0*1+1$$a$b", []string{"b"}}, + {"Z:2>1*0*1+1$$a$$b", nil}, + {"Z:2>1*0*1+1$a$b", []string{"a", "b"}}, + {"Z:2>1*0*1+1$$1", nil}, + {"a$var", []string{"var"}}, + {"a$v b", []string{"v"}}, + {"${func()}", nil}, + {"a${func(1,2)}b", nil}, + {"${gen_md5($TOKEN, $data, $random)}", []string{"TOKEN", "data", "random"}}, + } + + for _, data := range testData { + var varList []string + for varName := range findallVariables(data.raw) { + varList = append(varList, varName) + } + sort.Strings(varList) + if !assert.Equal(t, data.expectVars, varList) { + t.Fail() + } + } +}