From af22ec8020f075e5f222dce3a170258e4d084535 Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Wed, 16 Oct 2024 17:34:18 +1030 Subject: [PATCH] lib: add sprintf helper function --- lib/strings.go | 69 +++++++++++++++++++++++++++++++++++++++++++++ lib/types.go | 3 ++ mito.go | 1 + testdata/printf.txt | 14 +++++++++ 4 files changed, 87 insertions(+) create mode 100644 testdata/printf.txt diff --git a/lib/strings.go b/lib/strings.go index c35c05b..1f9e79f 100644 --- a/lib/strings.go +++ b/lib/strings.go @@ -19,12 +19,14 @@ package lib import ( "bytes" + "fmt" "strings" "unicode/utf8" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/common/types/traits" ) // Strings returns a cel.EnvOption to configure extended functions for @@ -1043,3 +1045,70 @@ func (l stringLib) validString(arg ref.Val) ref.Val { } return types.DefaultTypeAdapter.NativeToValue(utf8.Valid([]byte(s))) } + +// Printf returns a cel.EnvOption to configure an extended function for +// formatting strings and associated arguments. +// +// # Sprintf +// +// Formats a string from a format string and a set of arguments using the +// Go fmt syntax: +// +// .sprintf(>) -> +// sprintf(, >) -> +// +// Since CEL does not support variadic functions, arguments are provided +// as an array corresponding to the normal Go variadic slice. +// +// Examples: +// +// "Hello, %s".sprintf(["World!"]) // return "Hello, World!" +// sprintf("Hello, %s", ["World!"]) // return "Hello, World!" +func Printf() cel.EnvOption { + return cel.Lib(printfLib{}) +} + +type printfLib struct{} + +func (l printfLib) CompileOptions() []cel.EnvOption { + return []cel.EnvOption{ + cel.Function("sprintf", + cel.MemberOverload( + "sprintf_string_list_string", + []*cel.Type{types.StringType, listDyn}, + types.StringType, + cel.BinaryBinding(l.sprintf), + ), + cel.Overload( + "string_sprintf_list_string", + []*cel.Type{types.StringType, listDyn}, + types.StringType, + cel.BinaryBinding(l.sprintf), + ), + ), + } +} + +func (l printfLib) ProgramOptions() []cel.ProgramOption { return nil } + +func (l printfLib) sprintf(arg0, arg1 ref.Val) ref.Val { + format, ok := arg0.(types.String) + if !ok { + return types.ValOrErr(format, "no such overload for sprintf: format is not a string") + } + argList, ok := arg1.(traits.Lister) + if !ok { + return types.ValOrErr(argList, "no such overload for sprintf: args is not a list") + } + n, _ := argList.Size().(types.Int) // Continue since this is an optimisation. + args := make([]any, 0, n) + it := argList.Iterator() + for i := 1; it.HasNext() == types.True; i++ { + v, err := it.Next().ConvertToNative(reflectAnyType) + if err != nil { + return types.NewErr("failed to convert argument %d: %v", i, err) + } + args = append(args, v) + } + return types.String(fmt.Sprintf(string(format), args...)) +} diff --git a/lib/types.go b/lib/types.go index f30b54c..34a39a2 100644 --- a/lib/types.go +++ b/lib/types.go @@ -75,6 +75,9 @@ var ( // Types used for reflect conversion. var ( + anyVal any + + reflectAnyType = reflect.TypeOf(&anyVal).Elem() reflectBoolType = reflect.TypeOf(true) reflectByteSliceType = reflect.TypeOf([]byte(nil)) reflectIntType = reflect.TypeOf(0) diff --git a/mito.go b/mito.go index d21ee10..2e45870 100644 --- a/mito.go +++ b/mito.go @@ -299,6 +299,7 @@ var ( "http": nil, // This will be populated by Main. "limit": lib.Limit(limitPolicies), "strings": lib.Strings(), + "printf": lib.Printf(), } mimetypes = map[string]interface{}{ diff --git a/testdata/printf.txt b/testdata/printf.txt new file mode 100644 index 0000000..4e9ec4e --- /dev/null +++ b/testdata/printf.txt @@ -0,0 +1,14 @@ +mito -use printf src.cel +! stderr . +cmp stdout want.txt + +-- src.cel -- +[ + "Hello, %s".sprintf(["World!"]), + sprintf("Hello, %s", ["World!"]), +] +-- want.txt -- +[ + "Hello, World!", + "Hello, World!" +]