Skip to content

Commit

Permalink
Adding support for evaluating dependencies on Any types (#10)
Browse files Browse the repository at this point in the history
* Adding support for evaluating dependencies on Any types

* Remove orphaned comment

---------

Co-authored-by: jaredoconnell <[email protected]>
  • Loading branch information
Janos Bonic and jaredoconnell authored Apr 21, 2023
1 parent dd56a64 commit d07d69f
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 57 deletions.
4 changes: 2 additions & 2 deletions expression.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ type Expression interface {
Type(schema schema.Scope, workflowContext map[string][]byte) (schema.Type, error)
// Dependencies traverses the passed scope and evaluates the items this expression depends on. This is useful to
// construct a dependency tree based on expressions.
Dependencies(schema schema.Scope, workflowContext map[string][]byte) ([]Path, error)
Dependencies(schema schema.Type, workflowContext map[string][]byte) ([]Path, error)
// Evaluate evaluates the expression on the given data set regardless of any
// schema. The caller is responsible for validating the expected schema.
Evaluate(data any, workflowContext map[string][]byte) (any, error)
Expand Down Expand Up @@ -65,7 +65,7 @@ func (e expression) Type(scope schema.Scope, workflowContext map[string][]byte)
return result, nil
}

func (e expression) Dependencies(scope schema.Scope, workflowContext map[string][]byte) ([]Path, error) {
func (e expression) Dependencies(scope schema.Type, workflowContext map[string][]byte) ([]Path, error) {
tree := &PathTree{
PathItem: "$",
Subtrees: nil,
Expand Down
15 changes: 12 additions & 3 deletions expression_dependencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
// dependencyContext holds the root data for a dependency evaluation in an expression. This is useful so that we
// don't need to pass the root type, path, and workflow context along with each function call.
type dependencyContext struct {
rootType schema.Scope
rootType schema.Type
rootPath *PathTree
workflowContext map[string][]byte
}
Expand Down Expand Up @@ -95,7 +95,9 @@ func (c *dependencyContext) bracketAccessorDependencies(
case schema.TypeIDMap:
return c.bracketSubExprMapDependencies(keyType, leftType, leftPath)
case schema.TypeIDList:
return c.bracketSubExprListDependencies(keyType, leftType, path)
return c.bracketSubExprListDependencies(keyType, leftType, leftPath)
case schema.TypeIDAny:
return schema.NewAnySchema(), leftPath, nil
default:
// We don't support subexpressions to pick a property on an object type since that would result in
// unpredictable behavior and runtime errors. Furthermore, we would not be able to perform type
Expand Down Expand Up @@ -131,7 +133,7 @@ func (c *dependencyContext) bracketSubExprMapDependencies(
}

// bracketSubExprListDependencies is used to resolve dependencies when a bracket accessor has a subexpression,
// with the left type being a list. So format `list[index]`
// with the left type being a list.
func (c *dependencyContext) bracketSubExprListDependencies(
keyType schema.Type,
leftType schema.Type,
Expand Down Expand Up @@ -237,6 +239,13 @@ func dependenciesBracketKey(currentType schema.Type, key any, path *PathTree) (s
}
path.Subtrees = append(path.Subtrees, pathItem)
return property.Type(), pathItem, nil
case schema.TypeIDAny:
pathItem := &PathTree{
PathItem: key,
Subtrees: nil,
}
path.Subtrees = append(path.Subtrees, pathItem)
return currentType, pathItem, nil
default:
return nil, nil, fmt.Errorf("cannot evaluate expression identifier %s on data type %s", key, currentType.TypeID())
}
Expand Down
128 changes: 76 additions & 52 deletions expression_dependencies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,83 @@ import (

"go.arcalot.io/assert"
"go.flow.arcalot.io/expressions"
"go.flow.arcalot.io/pluginsdk/schema"
)

func TestDependencyResolution(t *testing.T) {
t.Run("object", func(t *testing.T) {
expr, err := expressions.New("$.foo.bar")
assert.NoError(t, err)
path, err := expr.Dependencies(testScope, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 1)
assert.Equals(t, path[0].String(), "$.foo.bar")
})

t.Run("map-accessor", func(t *testing.T) {
expr, err := expressions.New("$[\"foo\"].bar")
assert.NoError(t, err)
path, err := expr.Dependencies(testScope, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 1)
assert.Equals(t, path[0].String(), "$.foo.bar")
})

t.Run("map", func(t *testing.T) {
expr, err := expressions.New("$.faz")
assert.NoError(t, err)
path, err := expr.Dependencies(testScope, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 1)
assert.Equals(t, path[0].String(), "$.faz")
})

t.Run("map-subkey", func(t *testing.T) {
expr, err := expressions.New("$.faz.foo")
assert.NoError(t, err)
path, err := expr.Dependencies(testScope, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 1)
assert.Equals(t, path[0].String(), "$.faz.foo")
})

t.Run("subexpression-invalid", func(t *testing.T) {
expr, err := expressions.New("$.foo[($.faz.foo)]")
assert.NoError(t, err)
_, err = expr.Dependencies(testScope, nil)
assert.Error(t, err)
})

t.Run("subexpression", func(t *testing.T) {
expr, err := expressions.New("$.faz[($.foo.bar)]")
assert.NoError(t, err)
path, err := expr.Dependencies(testScope, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 2)
assert.Equals(t, path[0].String(), "$.faz.*")
assert.Equals(t, path[1].String(), "$.foo.bar")
})
scopes := map[string]schema.Type{
"scope": testScope,
"any": schema.NewAnySchema(),
}
for name, schemaType := range scopes {
name := name
schemaType := schemaType
t.Run(name, func(t *testing.T) {
t.Run("object", func(t *testing.T) {
expr, err := expressions.New("$.foo.bar")
assert.NoError(t, err)
path, err := expr.Dependencies(schemaType, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 1)
assert.Equals(t, path[0].String(), "$.foo.bar")
})

t.Run("map-accessor", func(t *testing.T) {
expr, err := expressions.New("$[\"foo\"].bar")
assert.NoError(t, err)
path, err := expr.Dependencies(schemaType, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 1)
assert.Equals(t, path[0].String(), "$.foo.bar")
})

t.Run("map", func(t *testing.T) {
expr, err := expressions.New("$.faz")
assert.NoError(t, err)
path, err := expr.Dependencies(schemaType, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 1)
assert.Equals(t, path[0].String(), "$.faz")
})

t.Run("map-subkey", func(t *testing.T) {
expr, err := expressions.New("$.faz.foo")
assert.NoError(t, err)
path, err := expr.Dependencies(schemaType, nil)
assert.NoError(t, err)
assert.Equals(t, len(path), 1)
assert.Equals(t, path[0].String(), "$.faz.foo")
})
t.Run("subexpression-invalid", func(t *testing.T) {
expr, err := expressions.New("$.foo[($.faz.foo)]")
assert.NoError(t, err)
path, err := expr.Dependencies(schemaType, nil)
if name == "scope" {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equals(t, path[0].String(), "$.foo")
assert.Equals(t, path[1].String(), "$.faz.foo")
}
})

t.Run("subexpression", func(t *testing.T) {
expr, err := expressions.New("$.faz[($.foo.bar)]")
assert.NoError(t, err)
path, err := expr.Dependencies(schemaType, nil)
if name == "scope" {
assert.NoError(t, err)
assert.Equals(t, len(path), 2)
assert.Equals(t, path[0].String(), "$.faz.*")
assert.Equals(t, path[1].String(), "$.foo.bar")
} else {
assert.NoError(t, err)
assert.Equals(t, len(path), 2)
assert.Equals(t, path[0].String(), "$.faz")
assert.Equals(t, path[1].String(), "$.foo.bar")
}
})
})
}

}

0 comments on commit d07d69f

Please sign in to comment.