Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 70 additions & 1 deletion builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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()
}
Comment thread
nitram509 marked this conversation as resolved.
}
}
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,
Expand Down
132 changes: 81 additions & 51 deletions builtins_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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) {
Expand All @@ -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) {
Expand Down
4 changes: 1 addition & 3 deletions builtins_misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
36 changes: 20 additions & 16 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,29 @@ 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
Comment thread
nitram509 marked this conversation as resolved.
} 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 dereferencePtr(v), nil
} else {
if v, ok := argsByKey.Context[argsByKey.Key]; ok {
return dereferencePtr(v), nil
} else {
return null, nil
}
return null, nil
}
}).Required("context", "key"))

Expand Down
Loading