Skip to content

Commit

Permalink
Oneof Outputs for Graceful Handling of Disabled Steps (#207)
Browse files Browse the repository at this point in the history
* Progress towards oneof outputs

* Got oneof yaml logic working

* Finish graceful handling of disabled steps

It is only waiting for the dependency on the DAG to be updatd

* Added unit tests, and updated dependency

* Fix linter errors

* Remove stale comment

* Use type switch in infer

* Moved code out of large function

* Panic after failing to resolve OR group

* Remove unused nolint directive

* Improve field name
  • Loading branch information
jaredoconnell authored Sep 12, 2024
1 parent 0d85d08 commit 213a0df
Show file tree
Hide file tree
Showing 12 changed files with 1,151 additions and 98 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.21

require (
go.arcalot.io/assert v1.8.0
go.arcalot.io/dgraph v1.5.0
go.arcalot.io/dgraph v1.6.0
go.arcalot.io/lang v1.1.0
go.arcalot.io/log/v2 v2.2.0
go.flow.arcalot.io/deployer v0.6.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.arcalot.io/assert v1.8.0 h1:hGcHMPncQXwQvjj7MbyOu2gg8VIBB00crUJZpeQOjxs=
go.arcalot.io/assert v1.8.0/go.mod h1:nNmWPoNUHFyrPkNrD2aASm5yPuAfiWdB/4X7Lw3ykHk=
go.arcalot.io/dgraph v1.5.0 h1:6cGlxLzmmehJoD/nj0Hkql7uh90EU0A0GtZhGYkr28M=
go.arcalot.io/dgraph v1.5.0/go.mod h1:+Kxc81utiihMSmC1/ttSPGLDlWPpvgOpNxSFmIDPxFM=
go.arcalot.io/dgraph v1.6.0 h1:mJFZ1vdPEg3KtqyhNqYtWVAkxxWBWoJVUFZQ2Z4mbvE=
go.arcalot.io/dgraph v1.6.0/go.mod h1:+Kxc81utiihMSmC1/ttSPGLDlWPpvgOpNxSFmIDPxFM=
go.arcalot.io/exex v0.2.0 h1:u44pjwPwcH57TF8knhaqVZP/1V/KbnRe//pKzMwDpLw=
go.arcalot.io/exex v0.2.0/go.mod h1:5zlFr+7vOQNZKYCNOEDdsad+z/dlvXKs2v4kG+v+bQo=
go.arcalot.io/lang v1.1.0 h1:ugglRKpd3qIMkdghAjKJxsziIgHm8QpxrzZPSXoa08I=
Expand Down
30 changes: 18 additions & 12 deletions internal/infer/infer.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,25 @@ func Scope(
// Type attempts to infer the data model from the data, possibly evaluating expressions.
func Type(
data any,
internalDataModel *schema.ScopeSchema,
internalDataModel schema.Scope,
functions map[string]schema.Function,
workflowContext map[string][]byte,
) (schema.Type, error) {
if expression, ok := data.(expressions.Expression); ok {
expressionType, err := expression.Type(internalDataModel, functions, workflowContext)
switch expr := data.(type) {
case expressions.Expression:
expressionType, err := expr.Type(internalDataModel, functions, workflowContext)
if err != nil {
return nil, fmt.Errorf("failed to evaluate type of expression %s (%w)", expression.String(), err)
return nil, fmt.Errorf("failed to evaluate type of expression %s (%w)", expr.String(), err)
}
return expressionType, nil
case *OneOfExpression:
oneOfType, err := expr.Type(internalDataModel, functions, workflowContext)
if err != nil {
return nil, fmt.Errorf("failed to evaluate type of expression %s (%w)", expr.String(), err)
}
return oneOfType, nil
}

v := reflect.ValueOf(data)
switch v.Kind() {
case reflect.Map:
Expand Down Expand Up @@ -132,7 +140,7 @@ func Type(
// mapType infers the type of a map value.
func mapType(
v reflect.Value,
internalDataModel *schema.ScopeSchema,
internalDataModel schema.Scope,
functions map[string]schema.Function,
workflowContext map[string][]byte,
) (schema.Type, error) {
Expand All @@ -141,9 +149,7 @@ func mapType(
return nil, fmt.Errorf("failed to infer map key type (%w)", err)
}
switch keyType.TypeID() {
case schema.TypeIDString:
fallthrough
case schema.TypeIDStringEnum:
case schema.TypeIDString, schema.TypeIDStringEnum:
return objectType(v, internalDataModel, functions, workflowContext)
case schema.TypeIDInt:
case schema.TypeIDIntEnum:
Expand Down Expand Up @@ -186,7 +192,7 @@ func mapType(

func objectType(
value reflect.Value,
internalDataModel *schema.ScopeSchema,
internalDataModel schema.Scope,
functions map[string]schema.Function,
workflowContext map[string][]byte,
) (schema.Type, error) {
Expand All @@ -207,7 +213,7 @@ func objectType(
nil,
)
}
return schema.NewObjectSchema(
return schema.NewUnenforcedIDObjectSchema(
generateRandomObjectID("inferred_schema"),
properties,
), nil
Expand All @@ -216,7 +222,7 @@ func objectType(
// sliceType tries to infer the type of a slice.
func sliceType(
v reflect.Value,
internalDataModel *schema.ScopeSchema,
internalDataModel schema.Scope,
functions map[string]schema.Function,
workflowContext map[string][]byte,
) (schema.Type, error) {
Expand All @@ -237,7 +243,7 @@ func sliceType(

func sliceItemType(
values []reflect.Value,
internalDataModel *schema.ScopeSchema,
internalDataModel schema.Scope,
functions map[string]schema.Function,
workflowContext map[string][]byte,
) (schema.Type, error) {
Expand Down
100 changes: 98 additions & 2 deletions internal/infer/infer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package infer_test

import (
"fmt"
"go.arcalot.io/assert"
"go.arcalot.io/lang"
"go.flow.arcalot.io/expressions"
"testing"

"go.flow.arcalot.io/engine/internal/infer"
Expand All @@ -11,16 +14,31 @@ import (
type testEntry struct {
name string
input any
dataModel schema.Scope
expectedOutputType schema.TypeID
validate func(t schema.Type) error
}

var testOneOf = infer.OneOfExpression{
Discriminator: "option",
Options: map[string]any{
"a": map[string]any{
"value-1": 1,
},
"b": map[string]any{
"value-2": lang.Must2(expressions.New("$.a")),
},
},
NodePath: "n/a",
}

var testData = []testEntry{
{
"string",
"foo",
nil,
schema.TypeIDString,
func(t schema.Type) error {
func(_ schema.Type) error {
return nil
},
},
Expand All @@ -30,6 +48,7 @@ var testData = []testEntry{
"foo": "bar",
"baz": 42,
},
nil,
schema.TypeIDObject,
func(t schema.Type) error {
objectSchema := t.(*schema.ObjectSchema)
Expand All @@ -46,6 +65,7 @@ var testData = []testEntry{
{
"slice",
[]string{"foo"},
nil,
schema.TypeIDList,
func(t schema.Type) error {
listType := t.(*schema.ListSchema)
Expand All @@ -55,19 +75,95 @@ var testData = []testEntry{
return nil
},
},
{
"expression-1",
lang.Must2(expressions.New("$.a")),
schema.NewScopeSchema(
schema.NewObjectSchema("root", map[string]*schema.PropertySchema{
"a": schema.NewPropertySchema(
schema.NewStringSchema(nil, nil, nil),
nil,
true,
nil,
nil,
nil,
nil,
nil,
),
}),
),
schema.TypeIDString,
func(_ schema.Type) error {
return nil
},
},
{
"oneof-expression",
&testOneOf,
schema.NewScopeSchema(
schema.NewObjectSchema("root", map[string]*schema.PropertySchema{
"a": schema.NewPropertySchema(
schema.NewStringSchema(nil, nil, nil),
nil,
true,
nil,
nil,
nil,
nil,
nil,
),
}),
),
schema.TypeIDOneOfString,
func(t schema.Type) error {
return t.ValidateCompatibility(
schema.NewOneOfStringSchema[any](
map[string]schema.Object{
"a": schema.NewObjectSchema("n/a", map[string]*schema.PropertySchema{
"value-1": schema.NewPropertySchema(
schema.NewIntSchema(nil, nil, nil),
nil,
true,
nil,
nil,
nil,
nil,
nil,
),
}),
"b": schema.NewObjectSchema("n/a", map[string]*schema.PropertySchema{
"value-2": schema.NewPropertySchema(
schema.NewStringSchema(nil, nil, nil),
nil,
true,
nil,
nil,
nil,
nil,
nil,
),
}),
},
"option",
false,
),
)
},
},
}

func TestInfer(t *testing.T) {
for _, entry := range testData {
entry := entry
t.Run(entry.name, func(t *testing.T) {
inferredType, err := infer.Type(entry.input, nil, nil, nil)
inferredType, err := infer.Type(entry.input, entry.dataModel, nil, nil)
if err != nil {
t.Fatalf("%v", err)
}
if inferredType.TypeID() != entry.expectedOutputType {
t.Fatalf("Incorrect type inferred: %s", inferredType.TypeID())
}
assert.NoError(t, entry.validate(inferredType))
})
}

Expand Down
41 changes: 41 additions & 0 deletions internal/infer/oneof_expression.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package infer

import (
"fmt"
"go.flow.arcalot.io/pluginsdk/schema"
)

// OneOfExpression stores the discriminator, and a key-value pair of all possible oneof values.
// The keys are the value for the discriminator, and the values are the YAML inputs, which can be
// inferred within the infer class.
type OneOfExpression struct {
Discriminator string
Options map[string]any
NodePath string
}

func (o *OneOfExpression) String() string {
return fmt.Sprintf("{OneOf Expression; Discriminator: %s; Options: %v}", o.Discriminator, o.Options)
}

// Type returns the OneOf type. Calculates the types of all possible oneof options for this.
func (o *OneOfExpression) Type(
internalDataModel schema.Scope,
functions map[string]schema.Function,
workflowContext map[string][]byte,
) (schema.Type, error) {
schemas := map[string]schema.Object{}
// Gets the type for all options.
for optionID, data := range o.Options {
inferredType, err := Type(data, internalDataModel, functions, workflowContext)
if err != nil {
return nil, err
}
inferredObjectType, isObject := inferredType.(schema.Object)
if !isObject {
return nil, fmt.Errorf("type of OneOf option is not an object; got %T", inferredType)
}
schemas[optionID] = inferredObjectType
}
return schema.NewOneOfStringSchema[any](schemas, o.Discriminator, false), nil
}
3 changes: 2 additions & 1 deletion internal/yaml/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ type Node interface {
// Contents returns the contents as further Node items. For maps, this will contain exactly two nodes, while
// for sequences this will contain as many nodes as there are items. For strings, this will contain no items.
Contents() []Node
// MapKey selects a specific map key. If the node is not a map, this function panics.
// MapKey selects a specific map key. Returns the node and a bool that represents whether the key was present.
// If the node is not a map, this function panics.
MapKey(key string) (Node, bool)
// MapKeys lists all keys of a map. If the node is not a map, this function panics.
MapKeys() []string
Expand Down
4 changes: 3 additions & 1 deletion workflow/any.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package workflow

import (
"fmt"
"go.flow.arcalot.io/engine/internal/infer"
"reflect"

"go.flow.arcalot.io/expressions"
Expand Down Expand Up @@ -43,7 +44,8 @@ func (a *anySchemaWithExpressions) Serialize(data any) (any, error) {
}

func (a *anySchemaWithExpressions) checkAndConvert(data any) (any, error) {
if _, ok := data.(expressions.Expression); ok {
switch data.(type) {
case expressions.Expression, infer.OneOfExpression, *infer.OneOfExpression:
return data, nil
}
t := reflect.ValueOf(data)
Expand Down
Loading

0 comments on commit 213a0df

Please sign in to comment.