From dc76526d84bbf5b868a74a02c49cf2cacdd9a6f8 Mon Sep 17 00:00:00 2001 From: Andreas Bichinger Date: Tue, 3 May 2022 14:07:01 +0200 Subject: [PATCH 1/3] add map support --- dummies_test.go | 11 +++++++++-- evaluationStage.go | 45 +++++++++++++++++++++++++++++++-------------- evaluation_test.go | 16 +++++++++++++++- 3 files changed, 55 insertions(+), 17 deletions(-) diff --git a/dummies_test.go b/dummies_test.go index e3a1a2e..6f4bf56 100644 --- a/dummies_test.go +++ b/dummies_test.go @@ -3,6 +3,7 @@ package govaluate import ( "errors" "fmt" + "strings" ) /* @@ -14,6 +15,7 @@ type dummyParameter struct { BoolFalse bool Nil interface{} Nested dummyNestedParameter + Map map[string]interface{} } func (this dummyParameter) Func() string { @@ -33,9 +35,9 @@ func (this dummyParameter) FuncArgStr(arg1 string) string { } func (this dummyParameter) TestArgs(str string, ui uint, ui8 uint8, ui16 uint16, ui32 uint32, ui64 uint64, i int, i8 int8, i16 int16, i32 int32, i64 int64, f32 float32, f64 float64, b bool) string { - + var sum float64 - + sum = float64(ui) + float64(ui8) + float64(ui16) + float64(ui32) + float64(ui64) sum += float64(i) + float64(i8) + float64(i16) + float64(i32) + float64(i64) sum += float64(f32) @@ -67,6 +69,11 @@ var dummyParameterInstance = dummyParameter{ Nested: dummyNestedParameter{ Funk: "funkalicious", }, + Map: map[string]interface{}{ + "String": "string!", + "Int": 101, + "StringCompare": strings.Compare, + }, } var fooParameter = EvaluationParameter{ diff --git a/evaluationStage.go b/evaluationStage.go index 7017b7f..4bb16b2 100644 --- a/evaluationStage.go +++ b/evaluationStage.go @@ -313,6 +313,7 @@ func makeAccessorStage(pair []string) evaluationOperator { } }() + LOOP: for i := 1; i < len(pair); i++ { coreValue := reflect.ValueOf(value) @@ -325,24 +326,40 @@ func makeAccessorStage(pair []string) evaluationOperator { coreValue = coreValue.Elem() } - if coreValue.Kind() != reflect.Struct { - return nil, errors.New("Unable to access '" + pair[i] + "', '" + pair[i-1] + "' is not a struct") - } - - field := coreValue.FieldByName(pair[i]) - if field != (reflect.Value{}) { - value = field.Interface() - continue - } + var field reflect.Value + var method reflect.Value - method := coreValue.MethodByName(pair[i]) - if method == (reflect.Value{}) { - if corePtrVal.IsValid() { - method = corePtrVal.MethodByName(pair[i]) + switch coreValue.Kind() { + case reflect.Struct: + field = coreValue.FieldByName(pair[i]) + if field != (reflect.Value{}) { + value = field.Interface() + continue LOOP } + + method = coreValue.MethodByName(pair[i]) if method == (reflect.Value{}) { - return nil, errors.New("No method or field '" + pair[i] + "' present on parameter '" + pair[i-1] + "'") + if corePtrVal.IsValid() { + method = corePtrVal.MethodByName(pair[i]) + } } + case reflect.Map: + field = coreValue.MapIndex(reflect.ValueOf(pair[i])) + if field != (reflect.Value{}) { + inter := field.Interface() + if reflect.TypeOf(inter).Kind() == reflect.Func { + method = reflect.ValueOf(inter) + } else { + value = inter + continue LOOP + } + } + default: + return nil, errors.New("Unable to access '" + pair[i] + "', '" + pair[i-1] + "' is not a struct or map") + } + + if method == (reflect.Value{}) { + return nil, errors.New("No method or field '" + pair[i] + "' present on parameter '" + pair[i-1] + "'") } switch right := right.(type) { diff --git a/evaluation_test.go b/evaluation_test.go index a2b65e8..56c1272 100644 --- a/evaluation_test.go +++ b/evaluation_test.go @@ -710,7 +710,7 @@ func TestNoParameterEvaluation(test *testing.T) { Expected: true, }, EvaluationTest{ - + Name: "Ternary/Java EL ambiguity", Input: "false ? foo:length()", Functions: map[string]ExpressionFunction{ @@ -1391,6 +1391,20 @@ func TestParameterizedEvaluation(test *testing.T) { Parameters: []EvaluationParameter{fooParameter}, Expected: "funkalicious", }, + EvaluationTest{ + + Name: "Nested Map string", + Input: "foo.Map.String", + Parameters: []EvaluationParameter{fooParameter}, + Expected: "string!", + }, + EvaluationTest{ + + Name: "Nested Map function", + Input: `foo.Map.StringCompare("foo", "bar")`, + Parameters: []EvaluationParameter{fooParameter}, + Expected: 1.0, + }, EvaluationTest{ Name: "Parameter call with + modifier", From 1b408622c27ef77a58ce9d394fd111970fc92cc8 Mon Sep 17 00:00:00 2001 From: Andreas Bichinger Date: Mon, 20 Nov 2023 17:21:18 +0100 Subject: [PATCH 2/3] check unexported fields in evaluation stage --- evaluationFailure_test.go | 13 +++++++++++++ evaluationStage.go | 8 ++++++++ parsing.go | 11 ----------- parsingFailure_test.go | 8 -------- 4 files changed, 21 insertions(+), 19 deletions(-) diff --git a/evaluationFailure_test.go b/evaluationFailure_test.go index 44ede95..d1b08d6 100644 --- a/evaluationFailure_test.go +++ b/evaluationFailure_test.go @@ -34,6 +34,7 @@ const ( TOO_FEW_ARGS = "Too few arguments to parameter call" TOO_MANY_ARGS = "Too many arguments to parameter call" MISMATCHED_PARAMETERS = "Argument type conversion failed" + UNEXPORTED_ACCESSOR = "Unable to access unexported" ) // preset parameter map of types that can be used in an evaluation failure test to check typing. @@ -488,6 +489,18 @@ func TestInvalidParameterCalls(test *testing.T) { Parameters: fooFailureParameters, Expected: MISMATCHED_PARAMETERS, }, + EvaluationFailureTest{ + Name: "Unexported parameter access", + Input: "foo.bar", + Parameters: map[string]interface{}{ + "foo": struct { + bar string + }{ + bar: "baz", + }, + }, + Expected: UNEXPORTED_ACCESSOR, + }, } runEvaluationFailureTests(evaluationTests, test) diff --git a/evaluationStage.go b/evaluationStage.go index 4bb16b2..2ea68df 100644 --- a/evaluationStage.go +++ b/evaluationStage.go @@ -7,6 +7,7 @@ import ( "reflect" "regexp" "strings" + "unicode" ) const ( @@ -331,6 +332,13 @@ func makeAccessorStage(pair []string) evaluationOperator { switch coreValue.Kind() { case reflect.Struct: + // check if field is exported + firstCharacter := getFirstRune(pair[i]) + if unicode.ToUpper(firstCharacter) != firstCharacter { + errorMsg := fmt.Sprintf("Unable to access unexported field '%s' in '%s'", pair[i], pair[i-1]) + return nil, errors.New(errorMsg) + } + field = coreValue.FieldByName(pair[i]) if field != (reflect.Value{}) { value = field.Interface() diff --git a/parsing.go b/parsing.go index c707bc8..dae78f7 100644 --- a/parsing.go +++ b/parsing.go @@ -186,17 +186,6 @@ func readToken(stream *lexerStream, state lexerState, functions map[string]Expre kind = ACCESSOR splits := strings.Split(tokenString, ".") tokenValue = splits - - // check that none of them are unexported - for i := 1; i < len(splits); i++ { - - firstCharacter := getFirstRune(splits[i]) - - if unicode.ToUpper(firstCharacter) != firstCharacter { - errorMsg := fmt.Sprintf("Unable to access unexported field '%s' in token '%s'", splits[i], tokenString) - return ExpressionToken{}, errors.New(errorMsg), false - } - } } break } diff --git a/parsingFailure_test.go b/parsingFailure_test.go index 2e375bc..2411196 100644 --- a/parsingFailure_test.go +++ b/parsingFailure_test.go @@ -17,7 +17,6 @@ const ( INVALID_NUMERIC = "Unable to parse numeric value" UNDEFINED_FUNCTION = "Undefined function" HANGING_ACCESSOR = "Hanging accessor on token" - UNEXPORTED_ACCESSOR = "Unable to access unexported" INVALID_HEX = "Unable to parse hex value" ) @@ -178,13 +177,6 @@ func TestParsingFailure(test *testing.T) { Input: "foo.Bar.", Expected: HANGING_ACCESSOR, }, - ParsingFailureTest{ - - // this is expected to change once there are structtags in place that allow aliasing of fields - Name: "Unexported parameter access", - Input: "foo.bar", - Expected: UNEXPORTED_ACCESSOR, - }, ParsingFailureTest{ Name: "Incomplete Hex", Input: "0x", From 81528c1abd4ec0e9fa4dfe0c11d24bbce00457ef Mon Sep 17 00:00:00 2001 From: Andreas Bichinger Date: Mon, 20 Nov 2023 17:36:45 +0100 Subject: [PATCH 3/3] Update README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dabc37d..576a9df 100644 --- a/README.md +++ b/README.md @@ -161,13 +161,13 @@ Fields are accessed in a similar way. Assuming `foo` has a field called "Length" "foo.Length > 9000" -Accessors can be nested to any depth, like the following +The values of a `map` are accessed in the same way. Assuming the parameter `foo` is `map[string]int{ "bar": 1 }` - "foo.Bar.Baz.SomeFunction()" + "foo.bar == 1" -However it is not _currently_ supported to access values in `map`s. So the following will not work +Accessors can be nested to any depth, like the following - "foo.SomeMap['key']" + "foo.Bar.Baz.SomeFunction()" This may be convenient, but note that using accessors involves a _lot_ of reflection. This makes the expression about four times slower than just using a parameter (consult the benchmarks for more precise measurements on your system). If at all reasonable, the author recommends extracting the values you care about into a parameter map beforehand, or defining a struct that implements the `Parameters` interface, and which grabs fields as required. If there are functions you want to use, it's better to pass them as expression functions (see the above section). These approaches use no reflection, and are designed to be fast and clean.