feat: parseVariables

This commit is contained in:
debugtalk
2021-10-04 10:37:22 +08:00
parent 7fc352b26d
commit c741825bab
2 changed files with 255 additions and 1 deletions

131
parser.go
View File

@@ -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
}

View File

@@ -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()
}
}
}