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

Oneof Outputs for Graceful Handling of Disabled Steps #207

Merged
merged 11 commits into from
Sep 12, 2024
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
23 changes: 14 additions & 9 deletions internal/infer/infer.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ 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) {
Expand All @@ -76,6 +76,13 @@ func Type(
}
return expressionType, nil
}
if oneOfExpression, ok := data.(*OneOfExpression); ok {
oneOfType, err := oneOfExpression.Type(internalDataModel, functions, workflowContext)
if err != nil {
return nil, fmt.Errorf("failed to evaluate type of expression %s (%w)", oneOfExpression.String(), err)
}
return oneOfType, nil
}
v := reflect.ValueOf(data)
switch v.Kind() {
case reflect.Map:
Expand Down Expand Up @@ -132,7 +139,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 +148,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 +191,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 +212,7 @@ func objectType(
nil,
)
}
return schema.NewObjectSchema(
return schema.NewUnenforcedIDObjectSchema(
generateRandomObjectID("inferred_schema"),
properties,
), nil
Expand All @@ -216,7 +221,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 +242,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")),
},
},
Node: "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
Node string
jaredoconnell marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading