Skip to content

Commit

Permalink
Merge pull request #1051 from dgeorgievski/1047-expose-addfunction-ap…
Browse files Browse the repository at this point in the history
…i-cesql

#1047 Expose AddFunction API for CESQL Parser
  • Loading branch information
lionelvillard authored Jun 11, 2024
2 parents cbba3fd + b877ab4 commit 4a007a1
Show file tree
Hide file tree
Showing 6 changed files with 305 additions and 2 deletions.
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

- 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)
}
})
}
})
}
}

0 comments on commit 4a007a1

Please sign in to comment.