diff --git a/builtins.go b/builtins.go index 0e2a954..ebcb0b1 100644 --- a/builtins.go +++ b/builtins.go @@ -3,11 +3,12 @@ package feel import ( "errors" "fmt" - "github.com/mitchellh/mapstructure" "math" "reflect" "sort" "strings" + + "github.com/mitchellh/mapstructure" ) func toFEELIndex(idx int) int { @@ -18,6 +19,74 @@ func fromFEELIndex(idx int) int { return idx - 1 } +// getContextMap takes any input type and puts each value in the output map. +// If the input is a struct it will use the json tag if available, else the field name will remain as defined on the struct +// If the input is a map, each key value of the map will be copied to the output map +func getContextMap(input any, output map[string]any) error { + if output == nil { + return errors.New("output map cannot be nil") + } + + v := reflect.ValueOf(input) + if !v.IsValid() { + return errors.New("input is invalid") + } + + // Dereference pointer if needed + if v.Kind() == reflect.Ptr { + if v.IsNil() { + return errors.New("input is a nil pointer") + } + v = v.Elem() + } + + if v.Kind() == reflect.Map { + // Add all key-value pairs from the input map to the output map + for iter := v.MapRange(); iter.Next(); { + key := iter.Key() + value := iter.Value() + if keyStr, ok := key.Interface().(string); ok { + output[keyStr] = value.Interface() + } + } + } + if v.Kind() == reflect.Struct { + + t := v.Type() + for i := 0; i < v.NumField(); i++ { + fieldType := t.Field(i) + if fieldType.PkgPath != "" { + continue // skip unexported fields + } + + fieldVal := v.Field(i) + if !fieldVal.CanInterface() { + continue // skip if value can't be interfaced + } + + // Use json tag if available + jsonTag := fieldType.Tag.Get("json") + if jsonTag == "-" { + continue // skip fields explicitly ignored + } + + key := fieldType.Name + if jsonTag != "" { + // Handle tag options like "name,omitempty" + if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 { + key = jsonTag[:commaIdx] + } else { + key = jsonTag + } + } + + output[key] = fieldVal.Interface() + } + } + + return nil +} + func decodeKWArgs(input map[string]any, output any) error { config := &mapstructure.DecoderConfig{ Metadata: nil, diff --git a/builtins_list_test.go b/builtins_list_test.go index 9da3ffc..89f1374 100644 --- a/builtins_list_test.go +++ b/builtins_list_test.go @@ -28,19 +28,19 @@ func Test_builtin_list_functions_list_replace_function(t *testing.T) { } func Test_builtin_list_functions_count(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`count( [1,2,3] )`) assert.NoError(t, err) - assert.Equal(t, 3, actual) + assert.Equal(t, float64(3), actual) } func Test_builtin_list_functions_min(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`min( [1,2,3] )`) assert.NoError(t, err) - assert.Equal(t, 1, actual) + assert.Equal(t, float64(1), actual) + + actual, err = EvalString(`min( [1.1,2.2,3.3] )`) + assert.NoError(t, err) + assert.Equal(t, 1.1, actual) actual, err = EvalString(`min( ["a","b","c"] )`) assert.NoError(t, err) @@ -52,11 +52,13 @@ func Test_builtin_list_functions_min(t *testing.T) { } func Test_builtin_list_functions_max(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`max( [1,2,3] )`) assert.NoError(t, err) - assert.Equal(t, 3, actual) + assert.Equal(t, float64(3), actual) + + actual, err = EvalString(`max( [1.1,2.2,3.3] )`) + assert.NoError(t, err) + assert.Equal(t, 3.3, actual) actual, err = EvalString(`max( ["a","b","c"] )`) assert.NoError(t, err) @@ -68,19 +70,19 @@ func Test_builtin_list_functions_max(t *testing.T) { } func Test_builtin_list_functions_sum(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`sum( [1,2,3] )`) assert.NoError(t, err) - assert.Equal(t, 6, actual) + assert.Equal(t, float64(6), actual) + + actual, err = EvalString(`sum( [1.1,2.2,3.3] )`) + assert.NoError(t, err) + assert.Equal(t, 6.6, actual) } func Test_builtin_list_functions_mean(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`mean( [1,2,3] )`) assert.NoError(t, err) - assert.Equal(t, 2, actual) + assert.Equal(t, float64(2), actual) } func Test_builtin_list_functions_all(t *testing.T) { @@ -96,99 +98,127 @@ func Test_builtin_list_functions_any(t *testing.T) { } func Test_builtin_list_functions_sublist(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`sublist( [4,5,6], 1, 2 )`) assert.NoError(t, err) - assert.Equal(t, []int{4, 5}, actual) + assert.Equal(t, []any{float64(4), float64(5)}, actual) + + actual, err = EvalString(`sublist( [4.4,5.5,6.6], 1, 2 )`) + assert.NoError(t, err) + assert.Equal(t, []any{4.4, 5.5}, actual) } func Test_builtin_list_functions_append(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`append( [1], 2, 3 )`) assert.NoError(t, err) - assert.Equal(t, []int{1, 2, 3}, actual) + assert.Equal(t, []any{float64(1), float64(2), float64(3)}, actual) + + actual, err = EvalString(`append( [1.1], 2.2, 3.3 )`) + assert.NoError(t, err) + assert.Equal(t, []any{1.1, 2.2, 3.3}, actual) } func Test_builtin_list_functions_concatenate(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`concatenate( ["a","b"],["c"] )`) assert.NoError(t, err) - assert.Equal(t, []string{"a", "b", "c"}, actual) + assert.Equal(t, []any{"a", "b", "c"}, actual) } func Test_builtin_list_functions_insert_before(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`insert before( ["a","c"],1,"b")`) assert.NoError(t, err) - assert.Equal(t, []string{"b", "a", "c"}, actual) + assert.Equal(t, []any{"b", "a", "c"}, actual) } func Test_builtin_list_functions_remove(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`remove( ["a","b", "c"], 2 )`) assert.NoError(t, err) - assert.Equal(t, []string{"a", "c"}, actual) + assert.Equal(t, []any{"a", "c"}, actual) } func Test_builtin_list_functions_reverse(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`reverse( ["a", "b", "c"])`) assert.NoError(t, err) - assert.Equal(t, []string{"c", "b", "a"}, actual) + assert.Equal(t, []any{"c", "b", "a"}, actual) } func Test_builtin_list_functions_index_of(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`index of( [1,2,3,2],2 )`) assert.NoError(t, err) - assert.Equal(t, []int{2, 4}, actual) + assert.Equal(t, []any{float64(2), float64(4)}, actual) + + actual, err = EvalString(`index of( [1.2,2.2,3.3,2.2],2.2 )`) + assert.NoError(t, err) + assert.Equal(t, []any{float64(2), float64(4)}, actual) } func Test_builtin_list_functions_union(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`union( [1,2],[2,3] )`) assert.NoError(t, err) - assert.Equal(t, []int{1, 2, 3}, actual) + assert.Equal(t, []any{float64(1), float64(2), float64(3)}, actual) + + actual, err = EvalString(`union( [1.1,2.2],[2.2,3.3] )`) + assert.NoError(t, err) + assert.Equal(t, []any{1.1, 2.2, 3.3}, actual) + + actual, err = EvalString(`union( [1,2],[2.2,3.3] )`) + assert.NoError(t, err) + assert.Equal(t, []any{float64(1), float64(2), 2.2, 3.3}, actual) } func Test_builtin_list_functions_distinct_values(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`distinct values( [1,2,3,2,1] )`) assert.NoError(t, err) - assert.Equal(t, []int{1, 2, 3}, actual) + assert.Equal(t, []any{float64(1), float64(2), float64(3)}, actual) + + actual, err = EvalString(`distinct values( [1.1,2.2,3.3,2.2,1.1] )`) + assert.NoError(t, err) + assert.Equal(t, []any{1.1, 2.2, 3.3}, actual) + + actual, err = EvalString(`distinct values( [1,2.2,3.3,2.2,1] )`) + assert.NoError(t, err) + assert.Equal(t, []any{float64(1), 2.2, 3.3}, actual) } func Test_builtin_list_functions_flatten(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`flatten( [[1,2],[[3]], 4] )`) assert.NoError(t, err) - assert.Equal(t, []int{1, 2, 3, 4}, actual) + assert.Equal(t, []any{float64(1), float64(2), float64(3), float64(4)}, actual) + + actual, err = EvalString(`flatten( [[1.1,2.2],[[3.3]], 4.4] )`) + assert.NoError(t, err) + assert.Equal(t, []any{1.1, 2.2, 3.3, 4.4}, actual) + + actual, err = EvalString(`flatten( [[1,2.2],[[3]], 4.4] )`) + assert.NoError(t, err) + assert.Equal(t, []any{float64(1), 2.2, float64(3), 4.4}, actual) } func Test_builtin_list_functions_product(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`product( [2, 3, 4] )`) assert.NoError(t, err) - assert.Equal(t, []int{24}, actual) + assert.Equal(t, float64(24), actual) + + actual, err = EvalString(`product( [2.2, 3.3, 4.4] )`) + assert.NoError(t, err) + assert.Equal(t, 31.944, actual) + + actual, err = EvalString(`product( [2, 3.3, 4] )`) + assert.NoError(t, err) + assert.Equal(t, 26.4, actual) } func Test_builtin_list_functions_median(t *testing.T) { - t.Skip("method implemented, but return type should be native Go type") - actual, err := EvalString(`median( 8, 2, 5, 3, 4 )`) assert.NoError(t, err) - assert.Equal(t, 4, actual) + assert.Equal(t, float64(4), actual) + + actual, err = EvalString(`median( 8.8, 2.2, 5.5, 3.3, 4.4 )`) + assert.NoError(t, err) + assert.Equal(t, 4.4, actual) + + actual, err = EvalString(`median( 8, 2.2, 5, 3.3, 4 )`) + assert.NoError(t, err) + assert.Equal(t, float64(4), actual) } func Test_builtin_list_functions_stddev(t *testing.T) { diff --git a/builtins_misc_test.go b/builtins_misc_test.go index 0e6b35f..470bcc6 100644 --- a/builtins_misc_test.go +++ b/builtins_misc_test.go @@ -12,7 +12,5 @@ func Test_builtin_misc_today_just_returns_a_date(t *testing.T) { now := time.Now() date := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) - expected := FEELDate{t: date} - - assert.Equal(t, &expected, actual) + assert.Equal(t, date, actual) } diff --git a/context.go b/context.go index 3a63f9e..ec8c868 100644 --- a/context.go +++ b/context.go @@ -85,19 +85,27 @@ func installContextFunctions(prelude *Prelude) { Keys []string `json:"key"` } - argsByKey := getvalueByKey{} - - if err := decodeKWArgs(kwargs, &argsByKey); err != nil { - argsByKeys := getvalueByKeys{} - if err := decodeKWArgs(kwargs, &argsByKeys); err != nil { - return nil, err + if key, ok := kwargs["key"].(string); ok { + argsByKey := getvalueByKey{ + Context: map[string]any{}, + Key: key, } - - if v, ok := contextGetByKeys(argsByKeys.Context, argsByKeys.Keys); ok { - return v, nil - } else { - return null, nil + if err := getContextMap(kwargs["context"], argsByKey.Context); err == nil { + if v, ok := argsByKey.Context[argsByKey.Key]; ok { + return dereferencePtr(v), nil + } else { + return null, nil + } } + } + + argsByKeys := getvalueByKeys{} + if err := decodeKWArgs(kwargs, &argsByKeys); err != nil { + return nil, err + } + + if v, ok := contextGetByKeys(argsByKeys.Context, argsByKeys.Keys); ok { + return v, nil } else { if v, ok := argsByKey.Context[argsByKey.Key]; ok { return dereferencePtr(v), nil diff --git a/eval.go b/eval.go index a39eb9c..05d1380 100644 --- a/eval.go +++ b/eval.go @@ -3,6 +3,7 @@ package feel import ( "encoding/json" "errors" + "reflect" ) // values @@ -613,5 +614,61 @@ func EvalStringWithScope(input string, scope Scope) (any, error) { intp.Push(scope) } r, err := ast.Eval(intp) - return r, err + if err != nil || r == nil { + return r, err + } + return unwrapFEELValue(r) +} + +func unwrapFEELValue(v any) (any, error) { + switch val := v.(type) { + case *Number: + return val.Float64(), nil + case *FEELDate: + return val.Date(), nil + case *FEELTime: + return val.Time(), nil + case *FEELDatetime: + return val.Date(), nil + case *FEELDuration: + return val.Duration(), nil + case *NullValue: + return nil, nil + default: + rv := reflect.ValueOf(val) + switch rv.Kind() { + case reflect.Slice, reflect.Array: + result := make([]any, rv.Len()) + for i := 0; i < rv.Len(); i++ { + uv, err := unwrapFEELValue(rv.Index(i).Interface()) + if err != nil { + return nil, err + } + result[i] = uv + } + return result, nil + case reflect.Map: + result := make(map[string]any, rv.Len()) + for _, key := range rv.MapKeys() { + mapVal, err := unwrapFEELValue(rv.MapIndex(key).Interface()) + if err != nil { + return nil, err + } + + if uk, err := unwrapFEELValue(key.Interface()); err == nil { + if sk, ok := uk.(string); ok { + result[sk] = mapVal + } else { + return nil, errors.New("map keys must be strings") + } + } else { + return nil, err + } + + } + return result, nil + default: + return val, nil + } + } } diff --git a/eval_test.go b/eval_test.go index 3430bb4..9d60cc8 100644 --- a/eval_test.go +++ b/eval_test.go @@ -9,14 +9,14 @@ import ( "github.com/stretchr/testify/assert" ) -func Test_N(t *testing.T) { +func Test_float64(t *testing.T) { input := "5 + 6" res, err := EvalString(input) if err != nil { fmt.Printf("bad input %s\n", input) } assert.NoError(t, err) - assert.Empty(t, cmp.Diff(N(11), res)) + assert.Empty(t, cmp.Diff(float64(11), res)) } func Test_unknown_symbols_raise_error(t *testing.T) { @@ -36,9 +36,9 @@ func Test_EvalString(t *testing.T) { // empty input outputs nil {"", nil}, - {"5 + -6", N(-1)}, - {"5 + 6", N(11)}, - {"(function(a) 2 * a)(5)", N(10)}, + {"5 + -6", float64(-1)}, + {"5 + 6", float64(11)}, + {"(function(a) 2 * a)(5)", float64(10)}, {"true", true}, {"false", false}, {`"hello" + " world"`, "hello world"}, @@ -68,24 +68,24 @@ func Test_EvalString(t *testing.T) { {`not( 5 > 6)`, true}, // loop functions - {`some x in [3, 4, 5] satisfies x >= 4`, N(4)}, - {`every y in [3, 4, 5] satisfies y >= 4`, []any{N(4), N(5)}}, + {`some x in [3, 4, 5] satisfies x >= 4`, float64(4)}, + {`every y in [3, 4, 5] satisfies y >= 4`, []any{float64(4), float64(5)}}, // null check {`a != null and a.b > 10`, false}, {`a = null or a.b > 10`, true}, // keyword arguments - {`bind("sub", function(a, b) a - b); sub(a: 4, b: 2)`, N(2)}, + {`bind("sub", function(a, b) a - b); sub(a: 4, b: 2)`, float64(2)}, // temporal expressions - {`last day of month(@"2020-02-11")`, N(29)}, - {`last day of month(@"2021-01-07")`, N(31)}, - {`last day of month(@"2023-06-11")`, N(30)}, - {`last day of month(@"2023-07-11")`, N(31)}, + {`last day of month(@"2020-02-11")`, float64(29)}, + {`last day of month(@"2021-01-07")`, float64(31)}, + {`last day of month(@"2023-06-11")`, float64(30)}, + {`last day of month(@"2023-07-11")`, float64(31)}, - {`@"2023-07-21T13:57:32@CST" - @"PT2H3M"`, MustParseDatetime("2023-07-21T11:54:32@CST")}, // test day/hour/min duration - {`@"2023-06-01T10:33:20@CST" + @"P3Y11M"`, MustParseDatetime("2027-05-01T10:33:20@CST")}, // test year/month duration + {`@"2023-07-21T13:57:32@CST" - @"PT2H3M"`, MustParseDatetime("2023-07-21T11:54:32@CST").Time()}, // test day/hour/min duration + {`@"2023-06-01T10:33:20@CST" + @"P3Y11M"`, MustParseDatetime("2027-05-01T10:33:20@CST").Time()}, // test year/month duration // builtin functions {`is defined(x)`, false}, @@ -102,21 +102,21 @@ func Test_EvalString(t *testing.T) { {`not({a: 1})`, false}, // list functions - {`median([3, 5, 9, 1, "hello", -2])`, N(3)}, + {`median([3, 5, 9, 1, "hello", -2])`, float64(3)}, {`append(["hello"], " ", "world")`, []any{"hello", " ", "world"}}, - {`concatenate([2, 1], [3])`, []any{N(2), N(1), N(3)}}, + {`concatenate([2, 1], [3])`, []any{float64(2), float64(1), float64(3)}}, {`insert before(["hello", "world"], 2, "another")`, []any{"hello", "another", "world"}}, {`remove(["hello", "a", "world"], 2)`, []any{"hello", "world"}}, - {`index of([1,2,3,2],2)`, []any{N(2), N(4)}}, + {`index of([1,2,3,2],2)`, []any{float64(2), float64(4)}}, - {`distinct values([1, 2, 1, 2, 3, 2, 1])`, []any{N(1), N(2), N(3)}}, + {`distinct values([1, 2, 1, 2, 3, 2, 1])`, []any{float64(1), float64(2), float64(3)}}, {`flatten([["a"], [["b", ["c"]]], ["d"]])`, []any{"a", "b", "c", "d"}}, {`union(["a", "b"], ["b", "c"], ["d"])`, []any{"a", "b", "c", "d"}}, {`sort(["hello", "a", "world"], function(x, y) x < y)`, []any{"a", "hello", "world"}}, - {`sort([8, -1, 3], function(x, y) x > y)`, []any{N(8), N(3), N(-1)}}, + {`sort([8, -1, 3], function(x, y) x > y)`, []any{float64(8), float64(3), float64(-1)}}, {`string join(["hello", "world"])`, "helloworld"}, {`string join(["hello", "world"], " ", "[", "]")`, "[hello world]"}, @@ -126,13 +126,13 @@ func Test_EvalString(t *testing.T) { {`and([true, 1, true, "ok"])`, true}, // context/map functions - {`get value({a: 2}, "b")`, null}, - {`get value({a: 2}, "a")`, N(2)}, - {`get value({a: {b: {c: 4}}}, ["a", "b", "c"])`, N(4)}, - {`get value({a: {b: {c: 4}}}, ["a", "b"])`, map[string]any{"c": N(4)}}, - {`get value({a: {b: {c: 4}}}, ["a", "k"])`, null}, - {`get value(context put({a: false}, ["b", "c", "d"], 4), ["b", "c"])`, map[string]any{"d": N(4)}}, - {`context merge([{x:1, y: 0}, {y:2}])`, map[string]any{"x": N(1), "y": N(2)}}, + {`get value({a: 2}, "b")`, nil}, + {`get value({a: 2}, "a")`, float64(2)}, + {`get value({a: {b: {c: 4}}}, ["a", "b", "c"])`, float64(4)}, + {`get value({a: {b: {c: 4}}}, ["a", "b"])`, map[string]any{"c": float64(4)}}, + {`get value({a: {b: {c: 4}}}, ["a", "k"])`, nil}, + {`get value(context put({a: false}, ["b", "c", "d"], 4), ["b", "c"])`, map[string]any{"d": float64(4)}}, + {`context merge([{x:1, y: 0}, {y:2}])`, map[string]any{"x": float64(1), "y": float64(2)}}, // range functions {`before(1, 10)`, true}, @@ -245,7 +245,96 @@ func Test_EvalStringWithScope(t *testing.T) { input := `foo + bar` v, err := EvalStringWithScope(input, Scope{"foo": 5, "bar": 7}) assert.NoError(t, err) - assert.True(t, N(12).Equal(*v.(*Number))) + assert.Equal(t, float64(12), v) +} + +func Test_EvalStringWithScopeStruct(t *testing.T) { + type ScopeStruct struct { + Str string + StrPtr *string + Int int + IntPtr *int + Bool bool + BoolPtr *bool + } + + type ScopeObj struct { + Str string + StrPtr *string + Int int + IntPtr *int + Bool bool + BoolPtr *bool + Nested ScopeStruct + NestedPtr *ScopeStruct + List []any + ListPtr *[]any + Map map[any]any + } + + ptrString := "bar" + ptrInt := 7 + ptrBoolTrue := true + ptrBoolFalse := false + + tests := []struct { + name string + expression string + scope Scope + expect any + }{ + // ScopeObj fields + {name: "Access ScopeObj.Str", expression: `get value(struct, "Str")`, scope: Scope{"struct": ScopeObj{Str: "foo"}}, expect: "foo"}, + {name: "Access ScopeObj.StrPtr", expression: `get value(struct, "StrPtr")`, scope: Scope{"struct": ScopeObj{StrPtr: &ptrString}}, expect: "bar"}, + {name: "Access ScopeObj.Int", expression: `get value(struct, "Int")`, scope: Scope{"struct": ScopeObj{Int: 5}}, expect: float64(5)}, + {name: "Access ScopeObj.IntPtr", expression: `get value(struct, "IntPtr")`, scope: Scope{"struct": ScopeObj{IntPtr: &ptrInt}}, expect: float64(7)}, + {name: "Access ScopeObj.Bool", expression: `get value(struct, "Bool")`, scope: Scope{"struct": ScopeObj{Bool: true}}, expect: true}, + {name: "Access ScopeObj.BoolPtr true", expression: `get value(struct, "BoolPtr")`, scope: Scope{"struct": ScopeObj{BoolPtr: &ptrBoolTrue}}, expect: true}, + {name: "Access ScopeObj.BoolPtr false", expression: `get value(struct, "BoolPtr")`, scope: Scope{"struct": ScopeObj{BoolPtr: &ptrBoolFalse}}, expect: false}, + {name: "Access ScopeObj.List", expression: `get value(struct, "List")`, scope: Scope{"struct": ScopeObj{List: []any{1, 2}}}, expect: []any{1, 2}}, + {name: "Access ScopeObj.ListPtr", expression: `get value(struct, "ListPtr")`, scope: Scope{"struct": ScopeObj{ListPtr: &[]any{1, 2}}}, expect: []any{1, 2}}, + {name: "Access ScopeObj.Nested", expression: `get value(struct, "Nested")`, scope: Scope{"struct": ScopeObj{Nested: ScopeStruct{Str: "1"}}}, expect: ScopeStruct{Str: "1"}}, + {name: "Access ScopeObj.NestedPtr", expression: `get value(struct, "NestedPtr")`, scope: Scope{"struct": ScopeObj{NestedPtr: &ScopeStruct{Str: "1"}}}, expect: ScopeStruct{Str: "1"}}, + {name: "Access ScopeObj.Map", expression: `get value(struct, "Map")`, scope: Scope{"struct": ScopeObj{Map: map[any]any{"a": "b"}}}, expect: map[string]any{"a": "b"}}, + {name: "Access ScopeObj.Map.a", expression: `get value( get value(struct, "Map"), "a")`, scope: Scope{"struct": ScopeObj{Map: map[any]any{"a": "b"}}}, expect: "b"}, + + // Pointer to ScopeObj tests + {name: "Access *ScopeObj.Str", expression: `get value(struct, "Str")`, scope: Scope{"struct": &ScopeObj{Str: "foo"}}, expect: "foo"}, + {name: "Access *ScopeObj.StrPtr", expression: `get value(struct, "StrPtr")`, scope: Scope{"struct": &ScopeObj{StrPtr: &ptrString}}, expect: "bar"}, + {name: "Access *ScopeObj.Int", expression: `get value(struct, "Int")`, scope: Scope{"struct": &ScopeObj{Int: 5}}, expect: float64(5)}, + {name: "Access *ScopeObj.IntPtr", expression: `get value(struct, "IntPtr")`, scope: Scope{"struct": &ScopeObj{IntPtr: &ptrInt}}, expect: float64(7)}, + {name: "Access *ScopeObj.Bool", expression: `get value(struct, "Bool")`, scope: Scope{"struct": &ScopeObj{Bool: true}}, expect: true}, + {name: "Access *ScopeObj.BoolPtr true", expression: `get value(struct, "BoolPtr")`, scope: Scope{"struct": &ScopeObj{BoolPtr: &ptrBoolTrue}}, expect: true}, + {name: "Access *ScopeObj.BoolPtr false", expression: `get value(struct, "BoolPtr")`, scope: Scope{"struct": &ScopeObj{BoolPtr: &ptrBoolFalse}}, expect: false}, + {name: "Access *ScopeObj.List false", expression: `get value(struct, "List")`, scope: Scope{"struct": &ScopeObj{List: []any{1, 2}}}, expect: []any{1, 2}}, + {name: "Access *ScopeObj.ListPtr false", expression: `get value(struct, "ListPtr")`, scope: Scope{"struct": &ScopeObj{ListPtr: &[]any{1, 2}}}, expect: []any{1, 2}}, + + // ScopeStruct fields via Nested + {name: "Access Nested.Str", expression: `get value(get value(struct, "Nested"), "Str")`, scope: Scope{"struct": ScopeObj{Nested: ScopeStruct{Str: "foo"}}}, expect: "foo"}, + {name: "Access Nested.StrPtr", expression: `get value(get value(struct, "Nested"), "StrPtr")`, scope: Scope{"struct": ScopeObj{Nested: ScopeStruct{StrPtr: &ptrString}}}, expect: "bar"}, + {name: "Access Nested.Int", expression: `get value(get value(struct, "Nested"), "Int")`, scope: Scope{"struct": ScopeObj{Nested: ScopeStruct{Int: 1}}}, expect: float64(1)}, + {name: "Access Nested.IntPtr", expression: `get value(get value(struct, "Nested"), "IntPtr")`, scope: Scope{"struct": ScopeObj{Nested: ScopeStruct{IntPtr: &ptrInt}}}, expect: float64(7)}, + {name: "Access Nested.Bool", expression: `get value(get value(struct, "Nested"), "Bool")`, scope: Scope{"struct": ScopeObj{Nested: ScopeStruct{Bool: true}}}, expect: true}, + {name: "Access Nested.BoolPtr true", expression: `get value(get value(struct, "Nested"), "BoolPtr")`, scope: Scope{"struct": ScopeObj{Nested: ScopeStruct{BoolPtr: &ptrBoolTrue}}}, expect: true}, + {name: "Access Nested.BoolPtr false", expression: `get value(get value(struct, "Nested"), "BoolPtr")`, scope: Scope{"struct": ScopeObj{Nested: ScopeStruct{BoolPtr: &ptrBoolFalse}}}, expect: false}, + + // ScopeStruct fields via NestedPtr + {name: "Access NestedPtr.Str", expression: `get value(get value(struct, "NestedPtr"), "Str")`, scope: Scope{"struct": ScopeObj{NestedPtr: &ScopeStruct{Str: "foo"}}}, expect: "foo"}, + {name: "Access NestedPtr.StrPtr", expression: `get value(get value(struct, "NestedPtr"), "StrPtr")`, scope: Scope{"struct": ScopeObj{NestedPtr: &ScopeStruct{StrPtr: &ptrString}}}, expect: "bar"}, + {name: "Access NestedPtr.Int", expression: `get value(get value(struct, "NestedPtr"), "Int")`, scope: Scope{"struct": ScopeObj{NestedPtr: &ScopeStruct{Int: 1}}}, expect: float64(1)}, + {name: "Access NestedPtr.IntPtr", expression: `get value(get value(struct, "NestedPtr"), "IntPtr")`, scope: Scope{"struct": ScopeObj{NestedPtr: &ScopeStruct{IntPtr: &ptrInt}}}, expect: float64(7)}, + {name: "Access NestedPtr.Bool", expression: `get value(get value(struct, "NestedPtr"), "Bool")`, scope: Scope{"struct": ScopeObj{NestedPtr: &ScopeStruct{Bool: true}}}, expect: true}, + {name: "Access NestedPtr.BoolPtr true", expression: `get value(get value(struct, "NestedPtr"), "BoolPtr")`, scope: Scope{"struct": ScopeObj{NestedPtr: &ScopeStruct{BoolPtr: &ptrBoolTrue}}}, expect: true}, + {name: "Access NestedPtr.BoolPtr false", expression: `get value(get value(struct, "NestedPtr"), "BoolPtr")`, scope: Scope{"struct": ScopeObj{NestedPtr: &ScopeStruct{BoolPtr: &ptrBoolFalse}}}, expect: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res, err := EvalStringWithScope(tc.expression, tc.scope) + assert.NoError(t, err) + assert.Empty(t, cmp.Diff(tc.expect, res)) + }) + } } func Test_EvalStringWithScopeStruct(t *testing.T) { @@ -359,17 +448,17 @@ func TestTemporalValue(t *testing.T) { input := `@"2023-06-07".day` v, err := EvalString(input) assert.NoError(t, err) - assert.Empty(t, cmp.Diff(v, N(7))) + assert.Empty(t, cmp.Diff(v, float64(7))) input1 := `@"2023-06-07T15:08:39".second` v1, err := EvalString(input1) assert.NoError(t, err) - assert.Empty(t, cmp.Diff(v1, N(39))) + assert.Empty(t, cmp.Diff(v1, float64(39))) input2 := `@"P1DT3H25M60S".minutes` v2, err := EvalString(input2) assert.NoError(t, err) - assert.Empty(t, cmp.Diff(v2, N(25))) + assert.Empty(t, cmp.Diff(v2, float64(25))) dt, err := ParseDatetime(`2023-06-07T15:04:05`) assert.NoError(t, err) diff --git a/tests/feel_test_helper.go b/tests/feel_test_helper.go index 7f1fa36..63994f3 100644 --- a/tests/feel_test_helper.go +++ b/tests/feel_test_helper.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "testing" + "time" ) // SafeCall wraps a function to recover from panics and return errors @@ -54,33 +55,48 @@ func CreateExpected(t *testing.T, result testconfig.ExpectedResult) interface{} } return b case "dateTime": - datetime, err := feel.ParseDatetime(value) - if err != nil { - t.Fatalf("Cannot parse expected value as FEEL datetime: %v", err) + formats := []string{ + "2006-01-02T15:04:05", + "2006-01-02T15:04:05-07:00", + "2006-01-02T15:04:05@MST", + } + for _, format := range formats { + if datetime, err := time.Parse(format, value); err == nil { + return datetime + } } - return datetime + t.Fatalf("Cannot parse expected value as datetime: %s", value) case "date": - date, err := feel.ParseDate(value) + date, err := time.Parse(time.DateOnly, value) if err != nil { - t.Fatalf("Cannot parse expected value as FEEL date: %v", err) + t.Fatalf("Cannot parse expected value as date: %v", err) } return date case "decimal", "double": - return feel.N(value) + f, err := strconv.ParseFloat(value, 64) + if err != nil { + t.Fatalf("Cannot parse expected value as date: %v", err) + } + return f case "duration": dur, err := feel.ParseDuration(value) if err != nil { - t.Fatalf("Cannot parse expected value as FEEL duration: %v", err) + t.Fatalf("Cannot parse expected value as duration: %v", err) } - return dur + return dur.Duration() // A bit of a cheat here because go does have a std lib that can read ISO duration case "string": return value case "time": - time, err := feel.ParseTime(value) - if err != nil { - t.Fatalf("Cannot parse expected value as FEEL time: %v", err) + formats := []string{ + "15:04:05Z07:00", + time.TimeOnly, + } + for _, format := range formats { + if datetime, err := time.Parse(format, value); err == nil { + return datetime + } } - return time + t.Fatalf("Cannot parse expected value as datetime: %s", value) default: t.Fatalf( diff --git a/tests/tck/tck_test.go b/tests/tck/tck_test.go index 3c37dfb..9a773f4 100644 --- a/tests/tck/tck_test.go +++ b/tests/tck/tck_test.go @@ -44,7 +44,7 @@ func runFeelTests(t *testing.T, testConfigFile string) { var err error switch { - case test.Context != nil: + case test.Context != nil && len(*test.Context) > 0: result, err = tests.SafeCall( func() (any, error) { resultMap := make(map[string]any)