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

#1047 Expose AddFunction API for CESQL Parser #1051

Merged
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
48 changes: 48 additions & 0 deletions sql/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,54 @@ expression, err := cesqlparser.Parse("subject = 'Hello world'")
res, err := expression.Evaluate(event)
```

Add a user defined function
```go
import (
cesql "github.com/cloudevents/sdk-go/sql/v2"
cefn "github.com/cloudevents/sdk-go/sql/v2/function"
cesqlparser "github.com/cloudevents/sdk-go/sql/v2/parser"
ceruntime "github.com/cloudevents/sdk-go/sql/v2/runtime"
cloudevents "github.com/cloudevents/sdk-go/v2"
)

// Create a test event
event := cloudevents.NewEvent()
event.SetID("aaaa-bbbb-dddd")
event.SetSource("https://my-source")
event.SetType("dev.tekton.event")

// Create and add a new user defined function
var HasPrefixFunction cesql.Function = cefn.NewFunction(
"HASPREFIX",
[]cesql.Type{cesql.StringType, cesql.StringType},
nil,
func(event cloudevents.Event, i []interface{}) (interface{}, error) {
str := i[0].(string)
prefix := i[1].(string)

return strings.HasPrefix(str, prefix), nil
},
)

err := ceruntime.AddFunction(HasPrefixFunction)

// parse the expression
expression, err := cesqlparser.Parse("HASPREFIX(type, 'dev.tekton.event')")
if err != nil {
fmt.Println("parser err: ", err)
os.Exit(1)
}

// Evalute the expression with the test event
res, err := expression.Evaluate(event)

if res.(bool) {
fmt.Println("Event type has the prefix")
} else {
fmt.Println("Event type doesn't have the prefix")
}
```

## Development guide

To regenerate the parser, make sure you have [ANTLR4 installed](https://github.com/antlr/antlr4/blob/master/doc/getting-started.md) and then run:
Expand Down
16 changes: 15 additions & 1 deletion sql/v2/function/function.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (
cloudevents "github.com/cloudevents/sdk-go/v2"
)

type FuncType func(cloudevents.Event, []interface{}) (interface{}, error)

type function struct {
name string
fixedArgs []cesql.Type
variadicArgs *cesql.Type
fn func(cloudevents.Event, []interface{}) (interface{}, error)
fn FuncType
}

func (f function) Name() string {
Expand All @@ -39,3 +41,15 @@ func (f function) ArgType(index int) *cesql.Type {
func (f function) Run(event cloudevents.Event, arguments []interface{}) (interface{}, error) {
return f.fn(event, arguments)
}

func NewFunction(name string,
fixedargs []cesql.Type,
variadicArgs *cesql.Type,
fn FuncType) cesql.Function {
return function{
name: name,
fixedArgs: fixedargs,
variadicArgs: variadicArgs,
fn: fn,
}
}
2 changes: 1 addition & 1 deletion sql/v2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/antlr/antlr4/runtime/Go/antlr v1.4.10
github.com/cloudevents/sdk-go/v2 v2.5.0
github.com/stretchr/testify v1.8.0
gopkg.in/yaml.v2 v2.4.0
sigs.k8s.io/yaml v1.3.0
)

Expand All @@ -20,7 +21,6 @@ require (
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.10.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

Expand Down
5 changes: 5 additions & 0 deletions sql/v2/runtime/functions_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ func (table functionTable) AddFunction(function cesql.Function) error {
}
}

// Adds user defined function
func AddFunction(fn cesql.Function) error {
return globalFunctionTable.AddFunction(fn)
}

func (table functionTable) ResolveFunction(name string, args int) cesql.Function {
item := table[strings.ToUpper(name)]
if item == nil {
Expand Down
27 changes: 27 additions & 0 deletions sql/v2/runtime/test/tck/user_defined_functions.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: User defined functions
tests:
- name: HASPREFIX (1)
expression: "HASPREFIX('abcdef', 'ab')"
result: true
- name: HASPREFIX (2)
expression: "HASPREFIX('abcdef', 'abcdef')"
result: true
- name: HASPREFIX (3)
expression: "HASPREFIX('abcdef', '')"
result: true
- name: HASPREFIX (4)
expression: "HASPREFIX('abcdef', 'gh')"
result: false
- name: HASPREFIX (5)
expression: "HASPREFIX('abcdef', 'abcdefg')"
result: false

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove newline?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing sql tck yaml files use new line to separate tests cases.
https://github.com/cloudevents/sdk-go/blob/main/sql/v2/test/tck/string_builtin_functions.yaml#L12-L18

I was following their format. I think the new line between consecutive tests cases should be kept but the new line at the end of the file should be removed.

- name: KONKAT (1)
expression: "KONKAT('a', 'b', 'c')"
result: abc
- name: KONKAT (2)
expression: "KONKAT()"
result: ""
- name: KONKAT (3)
expression: "KONKAT('a')"
result: "a"
209 changes: 209 additions & 0 deletions sql/v2/runtime/test/user_defined_functions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
Copyright 2024 The CloudEvents Authors
SPDX-License-Identifier: Apache-2.0
*/

package runtime_test

import (
"io"
"os"
"path"
"runtime"
"strings"
"testing"

cesql "github.com/cloudevents/sdk-go/sql/v2"
"github.com/cloudevents/sdk-go/sql/v2/function"
"github.com/cloudevents/sdk-go/sql/v2/parser"
ceruntime "github.com/cloudevents/sdk-go/sql/v2/runtime"
cloudevents "github.com/cloudevents/sdk-go/v2"
"github.com/cloudevents/sdk-go/v2/binding/spec"
"github.com/cloudevents/sdk-go/v2/event"
"github.com/cloudevents/sdk-go/v2/test"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)

var TCKFileNames = []string{
"user_defined_functions",
}

var TCKUserDefinedFunctions = []cesql.Function{
function.NewFunction(
"HASPREFIX",
[]cesql.Type{cesql.StringType, cesql.StringType},
nil,
func(event cloudevents.Event, i []interface{}) (interface{}, error) {
str := i[0].(string)
prefix := i[1].(string)

return strings.HasPrefix(str, prefix), nil
},
),
function.NewFunction(
"KONKAT",
[]cesql.Type{},
cesql.TypePtr(cesql.StringType),
func(event cloudevents.Event, i []interface{}) (interface{}, error) {
var sb strings.Builder
for _, v := range i {
sb.WriteString(v.(string))
}
return sb.String(), nil
},
),
}

type ErrorType string

const (
ParseError ErrorType = "parse"
MathError ErrorType = "math"
CastError ErrorType = "cast"
MissingAttributeError ErrorType = "missingAttribute"
MissingFunctionError ErrorType = "missingFunction"
FunctionEvaluationError ErrorType = "functionEvaluation"
)

type TckFile struct {
Name string `json:"name"`
Tests []TckTestCase `json:"tests"`
}

type TckTestCase struct {
Name string `json:"name"`
Expression string `json:"expression"`

Result interface{} `json:"result"`
Error ErrorType `json:"error"`

Event *cloudevents.Event `json:"event"`
EventOverrides map[string]interface{} `json:"eventOverrides"`
}

func (tc TckTestCase) InputEvent(t *testing.T) cloudevents.Event {
var inputEvent cloudevents.Event
if tc.Event != nil {
inputEvent = *tc.Event
} else {
inputEvent = test.FullEvent()
}

// Make sure the event is v1
inputEvent.SetSpecVersion(event.CloudEventsVersionV1)

for k, v := range tc.EventOverrides {
require.NoError(t, spec.V1.SetAttribute(inputEvent.Context, k, v))
}

return inputEvent
}

func (tc TckTestCase) ExpectedResult() interface{} {
switch tc.Result.(type) {
case int:
return int32(tc.Result.(int))
case float64:
return int32(tc.Result.(float64))
case bool:
return tc.Result.(bool)
}
return tc.Result
}

func TestFunctionTableAddFunction(t *testing.T) {

type args struct {
functions []cesql.Function
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "Add user functions to global table",

args: args{
functions: TCKUserDefinedFunctions,
},
wantErr: false,
},
{
name: "Fail add user functions to global table",
args: args{
functions: TCKUserDefinedFunctions,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, fn := range tt.args.functions {
if err := ceruntime.AddFunction(fn); (err != nil) != tt.wantErr {
t.Errorf("AddFunction() error = %v, wantErr %v", err, tt.wantErr)
}
}
})
}
}

func TestUserFunctions(t *testing.T) {
tckFiles := make([]TckFile, 0, len(TCKFileNames))

_, basePath, _, _ := runtime.Caller(0)
basePath, _ = path.Split(basePath)

for _, testFile := range TCKFileNames {
testFilePath := path.Join(basePath, "tck", testFile+".yaml")

t.Logf("Loading file %s", testFilePath)
file, err := os.Open(testFilePath)
require.NoError(t, err)

fileBytes, err := io.ReadAll(file)
require.NoError(t, err)

tckFileModel := TckFile{}
require.NoError(t, yaml.Unmarshal(fileBytes, &tckFileModel))

tckFiles = append(tckFiles, tckFileModel)
}

for i, file := range tckFiles {
i := i
t.Run(file.Name, func(t *testing.T) {
for j, testCase := range tckFiles[i].Tests {
j := j
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
testCase := tckFiles[i].Tests[j]

t.Logf("Test expression: '%s'", testCase.Expression)

if testCase.Error == ParseError {
_, err := parser.Parse(testCase.Expression)
require.NotNil(t, err)
return
}

expr, err := parser.Parse(testCase.Expression)
require.NoError(t, err)
require.NotNil(t, expr)

inputEvent := testCase.InputEvent(t)
result, err := expr.Evaluate(inputEvent)

if testCase.Error != "" {
require.NotNil(t, err)
} else {
require.NoError(t, err)
require.Equal(t, testCase.ExpectedResult(), result)
}
})
}
})
}
}