Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental: CEL testing. #20

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Test application
run: go test ./...
- name: Release application to Github
uses: goreleaser/goreleaser-action@v3
uses: goreleaser/goreleaser-action@v4
with:
distribution: goreleaser
version: latest
Expand Down
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ require (
)

require (
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect
github.com/google/cel-go v0.13.0 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/stoewer/go-strcase v1.2.0 // indirect
golang.org/x/text v0.3.8 // indirect
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
google.golang.org/protobuf v1.28.1 // indirect
)
21 changes: 21 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg=
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves=
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/cel-go v0.13.0 h1:z+8OBOcmh7IeKyqwT/6IlnMvy621fYUqnTVPEdegGlU=
github.com/google/cel-go v0.13.0/go.mod h1:K2hpQgEjDp18J76a2DKFRlPBPpgRZgi6EbnpDgIhJ8s=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
5 changes: 4 additions & 1 deletion root.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,10 @@ func run(r io.Reader, w io.Writer, opts settings) error {
}

if len(output) == 0 {
return fmt.Errorf("input had no columns to handle")
if b.Len() == 0 {
return fmt.Errorf("input had no columns to handle")
}
return fmt.Errorf("no columns matched the given criteria")
}

t := tabwriter.NewWriter(w, 0, 0, 3, ' ', 0)
Expand Down
129 changes: 124 additions & 5 deletions tabloid/exprfuncs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,137 @@ package tabloid

import (
"fmt"
"log"
"regexp"
"strings"
"time"

"github.com/Knetic/govaluate"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
str2duration "github.com/xhit/go-str2duration/v2"
)

var celFunctions = []cel.EnvOption{
defineUnaryFunction("isready", celIsReady),
defineUnaryMethod("isready", celIsReady),

defineUnaryFunction("hasrestarts", celHasRestarts),
defineUnaryMethod("hasrestarts", celHasRestarts),

defineBinaryFunction("olderthan", celOlderThan),
defineBinaryMethod("olderthan", celOlderThan),

defineBinaryFunction("olderthanEq", celOlderThanEqual),
defineBinaryMethod("olderthanEq", celOlderThanEqual),

defineBinaryFunction("newerthan", celNewerThan),
defineBinaryMethod("newerthan", celNewerThan),

defineBinaryFunction("newerthanEq", celNewerThanEqual),
defineBinaryMethod("newerthanEq", celNewerThanEqual),

defineBinaryFunction("eqduration", celEqualDuration),
defineBinaryMethod("eqduration", celEqualDuration),
}

func celIsReady(val ref.Val) ref.Val {
v, ok := val.Value().(string)
if !ok {
return types.ValOrErr(val, "isready function only works with strings")
}

pieces := strings.FieldsFunc(v, func(r rune) bool {
return r == '/'
})

return types.Bool(pieces[0] == pieces[1])
}

func celHasRestarts(val ref.Val) ref.Val {
log.Printf("celHasRestarts: %v", val)
v, ok := val.Value().(string)
if !ok {
return types.ValOrErr(val, "hasrestarts function only works with strings")
}

pieces := strings.FieldsFunc(v, func(r rune) bool {
return r == '/'
})

return types.Bool(pieces[0] != pieces[1])
}

func celParseDurations(val1, val2 ref.Val) (time.Duration, time.Duration, error) {
v1, ok := val1.Value().(string)
if !ok {
return time.Duration(0), time.Duration(0), fmt.Errorf("olderthan function only accepts string arguments")
}

v2, ok := val2.Value().(string)
if !ok {
return time.Duration(0), time.Duration(0), fmt.Errorf("olderthan function only accepts string arguments")
}

d1, err := str2duration.ParseDuration(v1)
if err != nil {
return time.Duration(0), time.Duration(0), fmt.Errorf("unable to parse duration: %s", err)
}

d2, err := str2duration.ParseDuration(v2)
if err != nil {
return time.Duration(0), time.Duration(0), fmt.Errorf("unable to parse duration: %s", err)
}

return d1, d2, nil
}

func celOlderThan(val1, val2 ref.Val) ref.Val {
d1, d2, err := celParseDurations(val1, val2)
if err != nil {
return types.ValOrErr(val1, err.Error())
}

return types.Bool(d1 > d2)
}

func celOlderThanEqual(val1, val2 ref.Val) ref.Val {
d1, d2, err := celParseDurations(val1, val2)
if err != nil {
return types.ValOrErr(val1, err.Error())
}

return types.Bool(d1 >= d2)
}

func celNewerThan(val1, val2 ref.Val) ref.Val {
d1, d2, err := celParseDurations(val1, val2)
if err != nil {
return types.ValOrErr(val1, err.Error())
}

return types.Bool(d1 < d2)
}

func celNewerThanEqual(val1, val2 ref.Val) ref.Val {
d1, d2, err := celParseDurations(val1, val2)
if err != nil {
return types.ValOrErr(val1, err.Error())
}

return types.Bool(d1 <= d2)
}

func celEqualDuration(val1, val2 ref.Val) ref.Val {
d1, d2, err := celParseDurations(val1, val2)
if err != nil {
return types.ValOrErr(val1, err.Error())
}

return types.Bool(d1 == d2)
}

// isready checks if a string is in the form of <current>/<total> and if the
// current value is equal to the total value, false otherwise.
func isready(args ...interface{}) (interface{}, error) {
Expand All @@ -30,11 +153,7 @@ func isready(args ...interface{}) (interface{}, error) {
return nil, fmt.Errorf("isready function only accepts string arguments in the form of <current>/<total>")
}

if pieces[0] != pieces[1] {
return false, nil
}

return true, nil
return pieces[0] == pieces[1], nil
}

var reRestart = regexp.MustCompile(`[1-9]\d*( \([^\)]+\))?`)
Expand Down
File renamed without changes.
43 changes: 37 additions & 6 deletions tabloid/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"fmt"
"strings"

"github.com/Knetic/govaluate"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)

func (t *Tabloid) Filter(columns []Column, expression string) ([]Column, error) {
Expand All @@ -15,28 +17,57 @@ func (t *Tabloid) Filter(columns []Column, expression string) ([]Column, error)
return columns, nil
}

expr, err := govaluate.NewEvaluableExpressionWithFunctions(expression, funcs)
vars := make([]*expr.Decl, 0, len(columns))
for _, column := range columns {
t.logger.Printf("adding column %q to CEL environment", column.ExprTitle)
vars = append(vars, decls.NewVar(column.ExprTitle, decls.Dyn))
}

var opts []cel.EnvOption
opts = append(opts, cel.ClearMacros())
opts = append(opts, cel.Declarations(vars...))
opts = append(opts, celFunctions...)

env, err := cel.NewEnv(opts...)
if err != nil {
return nil, fmt.Errorf("unable to create common expression language environment: %w", err)
}

ast, issues := env.Parse(expression)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("unable to parse expression %q: %w", expression, issues.Err())
}

check, issues := env.Check(ast)
if issues != nil && issues.Err() != nil {
return nil, fmt.Errorf("unable to check expression %q: %w", expression, issues.Err())
}

prg, err := env.Program(check)
if err != nil {
return nil, fmt.Errorf("unable to process expression %q: %w", expression, err)
return nil, fmt.Errorf("unable to create program from expression %q: %w", expression, err)
}

newColumns := make([]Column, 0, len(columns))
for _, column := range columns {
for pos := range column.Values {
// Create a map of information to pass to the evaluation
row := make(map[string]interface{})
for _, column := range columns {
row[column.ExprTitle] = column.Values[pos]
}

result, err := expr.Evaluate(row)
out, det, err := prg.Eval(row)
t.logger.Printf("row: %v", row)
t.logger.Printf("details: %v", det)
if err != nil {
t.logger.Printf("error type: %T", err)
return nil, fmt.Errorf("unable to evaluate expression for row %d: %w", pos+1, err)
}

chosen, ok := result.(bool)
chosen, ok := out.Value().(bool)
if !ok {
return nil, fmt.Errorf("expression %q must return a boolean value", expression)
return nil, fmt.Errorf("expression %q must return a boolean value, but instead it returned type \"%T\"", expression, out.Value())
}

if chosen {
Expand Down
19 changes: 19 additions & 0 deletions tabloid/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import (
"fmt"
"strings"
"unicode"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
)

type DuplicateColumnTitleError struct {
Expand Down Expand Up @@ -32,3 +35,19 @@ func fnKey(s string) string {

return string(out)
}

func defineUnaryFunction(name string, unaryFn func(ref.Val) ref.Val) cel.EnvOption {
return cel.Function(name, cel.Overload(name+"_global_unary_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(unaryFn)))
}

func defineUnaryMethod(name string, unaryFn func(ref.Val) ref.Val) cel.EnvOption {
return cel.Function(name, cel.MemberOverload(name+"_method_unary_string", []*cel.Type{cel.StringType}, cel.BoolType, cel.UnaryBinding(unaryFn)))
}

func defineBinaryFunction(name string, binaryFn func(ref.Val, ref.Val) ref.Val) cel.EnvOption {
return cel.Function(name, cel.Overload(name+"_global_binary_string_string", []*cel.Type{cel.StringType, cel.StringType}, cel.BoolType, cel.BinaryBinding(binaryFn)))
}

func defineBinaryMethod(name string, binaryFn func(ref.Val, ref.Val) ref.Val) cel.EnvOption {
return cel.Function(name, cel.MemberOverload(name+"_method_binary_string_string", []*cel.Type{cel.StringType, cel.StringType}, cel.BoolType, cel.BinaryBinding(binaryFn)))
}