Skip to content

Commit

Permalink
Add bindConstants() builtin function (#178)
Browse files Browse the repository at this point in the history
* add bindConstants() builtin fn

* add HandleTypeSchemaCombine() fn

* add schemaName() and BuildSchemaNames() fns

* add tests
  • Loading branch information
mfleader committed May 3, 2024
1 parent d27afd7 commit 8a4265e
Show file tree
Hide file tree
Showing 2 changed files with 285 additions and 0 deletions.
92 changes: 92 additions & 0 deletions internal/builtinfunctions/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ func GetFunctions() map[string]schema.CallableFunction {
toUpperFunction := getToUpperFunction()
splitStringFunction := getSplitStringFunction()
loadFileFunction := getReadFileFunction()
// Data transformation functions
bindConstantsFunction := getBindConstantsFunction()

// Combine in a map
allFunctions := map[string]schema.CallableFunction{
Expand All @@ -55,6 +57,7 @@ func GetFunctions() map[string]schema.CallableFunction {
toUpperFunction.ID(): toUpperFunction,
splitStringFunction.ID(): splitStringFunction,
loadFileFunction.ID(): loadFileFunction,
bindConstantsFunction.ID(): bindConstantsFunction,
}

return allFunctions
Expand Down Expand Up @@ -525,3 +528,92 @@ func getReadFileFunction() schema.CallableFunction {
}
return funcSchema
}

// CombinedObjPropertyConstantName is the identifier string key that points to the container of constant or repeated values.
const CombinedObjPropertyConstantName = "constant"

// CombinedObjPropertyItemName is the identifier string key that points to the container of a list of objects.
const CombinedObjPropertyItemName = "item"

// CombinedObjIDDelimiter is the delimiter for joining Object ID strings and/or TypeID strings.
const CombinedObjIDDelimiter = "__"

// ListSchemaNameDelimiter is the delimiter for joining the names of the types nested within a list.
const ListSchemaNameDelimiter = "_"

func getBindConstantsFunction() schema.CallableFunction {
funcSchema, err := schema.NewDynamicCallableFunction(
"bindConstants",
[]schema.Type{
schema.NewListSchema(schema.NewAnySchema(), nil, nil),
schema.NewAnySchema()},
schema.NewDisplayValue(
schema.PointerTo("Bind Constants"),
schema.PointerTo(
"Creates a list of objects with ID `CombinedObject`. "+
fmt.Sprintf("Each object has two properties `%s` and `%s`.\n", CombinedObjPropertyItemName, CombinedObjPropertyConstantName)+
fmt.Sprintf("Param 1: Value(s) to be included in the `%s` field \n", CombinedObjPropertyItemName)+
fmt.Sprintf("Param 2: Value(s) to populate the field `%s` with every output item", CombinedObjPropertyConstantName)),
nil),
func(items []any, columnValues any) (any, error) {
combinedItems := make([]any, len(items))
for k, itemValue := range items {
combinedItems[k] = map[string]any{
CombinedObjPropertyItemName: itemValue,
CombinedObjPropertyConstantName: columnValues,
}
}
return combinedItems, nil
},
HandleTypeSchemaCombine,
)
if err != nil {
panic(err)
}
return funcSchema
}

// HandleTypeSchemaCombine returns the type that is output by the 'bindConstants' function.
// Its parameter list requires a ListSchema and one other schema of any type.
// A new ListSchema of ObjectSchemas is created from the two input types.
func HandleTypeSchemaCombine(inputType []schema.Type) (schema.Type, error) {
if len(inputType) != 2 {
return nil, fmt.Errorf("expected exactly two input types")
}
itemsType, isList := inputType[0].(*schema.ListSchema)
if !isList {
return nil, fmt.Errorf("expected first input type to be a list schema")
}
itemType := itemsType.ItemsValue
constantsTypeArg := inputType[1]
combinedObjectName := schemaName(itemType) + CombinedObjIDDelimiter + schemaName(constantsTypeArg)
return schema.NewListSchema(
schema.NewObjectSchema(
combinedObjectName,
map[string]*schema.PropertySchema{
CombinedObjPropertyItemName: schema.NewPropertySchema(itemType, nil, false, nil, nil, nil, nil, nil),
CombinedObjPropertyConstantName: schema.NewPropertySchema(constantsTypeArg, nil, false, nil, nil, nil, nil, nil),
}),
nil, nil), nil
}

func schemaName(typeSchema schema.Type) string {
return strings.Join(BuildSchemaNames(typeSchema, []string{}), ListSchemaNameDelimiter)
}

// BuildSchemaNames appends the name or constituent names of a schema, the Object ID
// for ObjectSchemas and the stringified TypeID for all other schemas, to
// a list of strings. ListSchema names start with the ListSchema TypeID, and
// recursively append the name of list's ItemValue schema.
func BuildSchemaNames(typeSchema schema.Type, names []string) []string {
listSchema, isList := typeSchema.(*schema.ListSchema)
if isList {
names = append(names, string(listSchema.TypeID()))
return BuildSchemaNames(listSchema.ItemsValue, names)
}
objItemType, itemIsObject := schema.ConvertToObjectSchema(typeSchema)
if itemIsObject {
return append(names, objItemType.ID())
}
return append(names, string(typeSchema.TypeID()))
}
193 changes: 193 additions & 0 deletions internal/builtinfunctions/functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import (
"fmt"
"go.arcalot.io/assert"
"go.flow.arcalot.io/engine/internal/builtinfunctions"
"go.flow.arcalot.io/pluginsdk/schema"
"math"
"reflect"
"regexp"
"strings"
"testing"
)

Expand Down Expand Up @@ -819,3 +822,193 @@ func Test_floatToFormattedString_success(t *testing.T) {
}
}
}

func expectedOutputBindConstants(repInp any, items []any) []any {
return []any{
map[string]any{builtinfunctions.CombinedObjPropertyItemName: items[0], builtinfunctions.CombinedObjPropertyConstantName: repInp},
map[string]any{builtinfunctions.CombinedObjPropertyItemName: items[1], builtinfunctions.CombinedObjPropertyConstantName: repInp},
map[string]any{builtinfunctions.CombinedObjPropertyItemName: items[2], builtinfunctions.CombinedObjPropertyConstantName: repInp},
}
}

func Test_bindConstants(t *testing.T) {
items := []any{
map[string]any{"loop_id": 1},
map[string]any{"loop_id": 2},
map[string]any{"loop_id": 3},
}
repeatedInputsMap := map[string]any{
"a": "A", "b": "B",
}
repeatedInputsInt := 2
repeatedInputsRegexPattern := regexp.MustCompile("p([a-z]+)ch")
testItems := map[string]any{
"combine with map": repeatedInputsMap,
"combine with list": items,
"combine with int": repeatedInputsInt,
"combine with regex": repeatedInputsRegexPattern,
}
functionToTest := builtinfunctions.GetFunctions()["bindConstants"]

for testName, testValue := range testItems {
tvLocal := testValue
t.Run(testName, func(t *testing.T) {
output, err := functionToTest.Call([]any{items, tvLocal})
assert.NoError(t, err)
assert.Equals(t, output.([]any), expectedOutputBindConstants(tvLocal, items))
})
}

t.Run("no items in input list", func(t *testing.T) {
output, err := functionToTest.Call([]any{[]any{}, repeatedInputsMap})
assert.NoError(t, err)
assert.Equals(t, output.([]any), []any{})
})
}

func defaultPropertySchema(t schema.Type) *schema.PropertySchema {
return schema.NewPropertySchema(t, nil, false, nil, nil, nil, nil, nil)
}

func joinStrs(s1, s2 string) string {
return strings.Join([]string{s1, s2}, builtinfunctions.CombinedObjIDDelimiter)
}

// TestHandleTypeSchemaCombine tests that the error cases for invalid
// and valid input cases create the expected error or type.
func TestHandleTypeSchemaCombine(t *testing.T) {
basicStringSchema := schema.NewStringSchema(nil, nil, nil)
basicIntSchema := schema.NewIntSchema(nil, nil, nil)
strTypeID := string(basicStringSchema.TypeID())
intTypeID := string(basicIntSchema.TypeID())
listInt := schema.NewListSchema(basicIntSchema, nil, nil)
listStr := schema.NewListSchema(basicStringSchema, nil, nil)
myFirstObj := schema.NewObjectSchema(
"ObjectTitle",
map[string]*schema.PropertySchema{
"a": defaultPropertySchema(basicStringSchema),
"b": defaultPropertySchema(basicStringSchema),
})
listMyFirstObj := schema.NewListSchema(myFirstObj, nil, nil)
constantsObj := schema.NewObjectSchema(
"Constants",
map[string]*schema.PropertySchema{
"p_str": defaultPropertySchema(basicStringSchema),
"p_int": defaultPropertySchema(basicIntSchema),
})
listPrefix := string(listInt.TypeID()) + builtinfunctions.ListSchemaNameDelimiter

// invalid inputs
t.Run("first argument incorrect type", func(t *testing.T) {
_, err := builtinfunctions.HandleTypeSchemaCombine(
[]schema.Type{basicStringSchema, basicIntSchema})
assert.Error(t, err)
assert.Contains(t, err.Error(), "expected first input type to be a list schema")
})
t.Run("incorrect argument quantity", func(t *testing.T) {
_, err := builtinfunctions.HandleTypeSchemaCombine(
[]schema.Type{listInt})
assert.Error(t, err)
assert.Contains(t, err.Error(), "expected exactly two input types")
})

// valid inputs
type testInput struct {
typeArgs []schema.Type
expectedResult string
}
testInputs := []testInput{
{ // Inputs: a list of int's and an object
typeArgs: []schema.Type{listInt, constantsObj},
expectedResult: joinStrs(intTypeID, constantsObj.ID()),
},
{ // Inputs: a list of objects and an object
typeArgs: []schema.Type{listMyFirstObj, constantsObj},
expectedResult: joinStrs(myFirstObj.ID(), constantsObj.ID()),
},
{ // Inputs: a list of objects and a string
typeArgs: []schema.Type{listMyFirstObj, basicStringSchema},
expectedResult: joinStrs(myFirstObj.ID(), strTypeID),
},
{ // Inputs: a list of strings and an int
typeArgs: []schema.Type{listStr, basicIntSchema},
expectedResult: joinStrs(strTypeID, intTypeID),
},
{ // Inputs: a list of strings and a list of objects
typeArgs: []schema.Type{listStr, listMyFirstObj},
expectedResult: joinStrs(strTypeID, listPrefix+myFirstObj.ID()),
},
}

for _, input := range testInputs {
lclInput := input
t.Run(lclInput.expectedResult, func(t *testing.T) {
outputType, err := builtinfunctions.HandleTypeSchemaCombine(lclInput.typeArgs)
assert.NoError(t, err)
listItemObj, isObj := schema.ConvertToObjectSchema(outputType.(*schema.ListSchema).ItemsValue)
assert.Equals(t, isObj, true)
assert.Equals(t, listItemObj.ID(), lclInput.expectedResult)
})
}
}

func TestBuildSchemaNames(t *testing.T) {
basicStringSchema := schema.NewStringSchema(nil, nil, nil)
basicIntSchema := schema.NewIntSchema(nil, nil, nil)
intTypeID := string(basicIntSchema.TypeID())
listInt := schema.NewListSchema(basicIntSchema, nil, nil)
myFirstObj := schema.NewObjectSchema(
"ObjectTitle",
map[string]*schema.PropertySchema{
"a": defaultPropertySchema(basicStringSchema),
"b": defaultPropertySchema(basicStringSchema),
})
listMyFirstObj := schema.NewListSchema(myFirstObj, nil, nil)

listTypeID := string(listMyFirstObj.TypeID())

listListInt := schema.NewListSchema(listInt, nil, nil)
listListListInt := schema.NewListSchema(listListInt, nil, nil)
listListObj := schema.NewListSchema(listMyFirstObj, nil, nil)
listListListObj := schema.NewListSchema(listListObj, nil, nil)

// valid inputs
type testInput struct {
typeArgs schema.Type
expectedResult []string
}
testInputs := []testInput{
{
typeArgs: listInt,
expectedResult: []string{listTypeID, intTypeID},
},
{
typeArgs: listMyFirstObj,
expectedResult: []string{listTypeID, myFirstObj.ID()},
},
{
typeArgs: listListInt,
expectedResult: []string{listTypeID, listTypeID, intTypeID},
},
{
typeArgs: listListObj,
expectedResult: []string{listTypeID, listTypeID, myFirstObj.ID()},
},
{
typeArgs: listListListInt,
expectedResult: []string{listTypeID, listTypeID, listTypeID, intTypeID},
},
{
typeArgs: listListListObj,
expectedResult: []string{listTypeID, listTypeID, listTypeID, myFirstObj.ID()},
},
}

for _, input := range testInputs {
lclInput := input
t.Run(strings.Join(lclInput.expectedResult, "_"), func(t *testing.T) {
outputNames := builtinfunctions.BuildSchemaNames(lclInput.typeArgs, []string{})
assert.Equals(t, outputNames, lclInput.expectedResult)
})
}
}

0 comments on commit 8a4265e

Please sign in to comment.