diff --git a/builtin/builtin.go b/builtin/builtin.go index cc6f197c..1b1f7ff9 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -103,6 +103,20 @@ var Builtins = []*Function{ Predicate: true, Types: types(new(func([]any, func(any, any) any, any) any)), }, + { + Name: "flatten", + Func: func(args ...any) (any, error) { + if len(args) != 1 { + return nil, fmt.Errorf("invalid number of arguments for flatten (expected 1, got %d)", len(args)) + } + res, err := flatten(args[0]) + + return any(res), err + }, + Validate: func(args []reflect.Type) (reflect.Type, error) { + return validateAggregateFunc("flatten", args) + }, + }, { Name: "len", Fast: Len, diff --git a/builtin/builtin_test.go b/builtin/builtin_test.go index 97f24989..a61c026f 100644 --- a/builtin/builtin_test.go +++ b/builtin/builtin_test.go @@ -152,6 +152,10 @@ func TestBuiltin(t *testing.T) { {`reduce([], 5, 0)`, 0}, {`concat(ArrayOfString, ArrayOfInt)`, []any{"foo", "bar", "baz", 1, 2, 3}}, {`concat(PtrArrayWithNil, [nil])`, []any{42, nil}}, + {`flatten([[1, 2], [3, 4]])`, []any{1, 2, 3, 4}}, + {`flatten([[1, 2], [3, 4], "5"])`, []any{1, 2, 3, 4, "5"}}, + {`flatten([1, 2, 3, 4])`, []any{1, 2, 3, 4}}, + {`flatten([1,[2,[3,4]]])`, []any{1, 2, 3, 4}}, } for _, test := range tests { @@ -236,6 +240,8 @@ func TestBuiltin_errors(t *testing.T) { {`now(nil)`, "invalid number of arguments (expected 0, got 1)"}, {`date(nil)`, "interface {} is nil, not string (1:1)"}, {`timezone(nil)`, "cannot use nil as argument (type string) to call timezone (1:10)"}, + {`flatten(nil)`, "invalid argument for flatten (type %!s()) (1:1)"}, + {`flatten([1, 2], [3, 4])`, "invalid number of arguments for flatten (expected 1, got 2) (1:1)"}, } for _, test := range errorTests { t.Run(test.input, func(t *testing.T) { diff --git a/builtin/lib.go b/builtin/lib.go index e3cd61b9..88f75cd0 100644 --- a/builtin/lib.go +++ b/builtin/lib.go @@ -359,3 +359,25 @@ func median(args ...any) ([]float64, error) { } return values, nil } + +func flatten(args ...any) ([]any, error) { + var values []any + + for _, arg := range args { + rv := reflect.ValueOf(deref.Deref(arg)) + switch rv.Kind() { + case reflect.Array, reflect.Slice: + size := rv.Len() + for i := 0; i < size; i++ { + elems, err := flatten(rv.Index(i).Interface()) + if err != nil { + return nil, err + } + values = append(values, elems...) + } + default: + values = append(values, arg) + } + } + return values, nil +} diff --git a/checker/checker_test.go b/checker/checker_test.go index fad56eb2..be66db61 100644 --- a/checker/checker_test.go +++ b/checker/checker_test.go @@ -100,6 +100,7 @@ func TestCheck(t *testing.T) { {"count(1..30, {# % 3 == 0}) > 0"}, {"map(1..3, {#}) == [1,2,3]"}, {"map(1..3, #index) == [0,1,2]"}, + {"map(1..3, [#index]) == [[0],[1],[2]]"}, {"map(filter(ArrayOfFoo, {.Bar.Baz != ''}), {.Bar}) == []"}, {"filter(Any, {.AnyMethod()})[0] == ''"}, {"Time == Time"}, diff --git a/docgen/docgen.go b/docgen/docgen.go index 1844f23b..66209e20 100644 --- a/docgen/docgen.go +++ b/docgen/docgen.go @@ -38,16 +38,17 @@ type Type struct { var ( Operators = []string{"matches", "contains", "startsWith", "endsWith"} Builtins = map[Identifier]*Type{ - "true": {Kind: "bool"}, - "false": {Kind: "bool"}, - "len": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "int"}}, - "all": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}}, - "none": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}}, - "any": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}}, - "one": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}}, - "filter": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}}, - "map": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}}, - "count": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "int"}}, + "true": {Kind: "bool"}, + "false": {Kind: "bool"}, + "len": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "int"}}, + "all": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}}, + "none": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}}, + "any": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}}, + "one": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "bool"}}, + "filter": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}}, + "map": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}}, + "count": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}, {Kind: "func"}}, Return: &Type{Kind: "int"}}, + "flatten": {Kind: "func", Arguments: []*Type{{Kind: "array", Type: &Type{Kind: "any"}}}, Return: &Type{Kind: "array", Type: &Type{Kind: "any"}}}, "trim": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "cutstr", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}}, "trimPrefix": {Kind: "func", Arguments: []*Type{{Name: "string", Kind: "string"}, {Name: "cutstr", Kind: "string"}}, Return: &Type{Name: "string", Kind: "string"}},