diff --git a/workflow/yaml.go b/workflow/yaml.go index 5ed051a..735e08b 100644 --- a/workflow/yaml.go +++ b/workflow/yaml.go @@ -61,21 +61,31 @@ const YamlOneOfTag = "!oneof" func buildOneOfExpressions(data yaml.Node, path []string) (any, error) { if data.Type() != yaml.TypeIDMap { - return nil, fmt.Errorf("!oneof found on non-map node at %s; expected a map with a list of options and the discriminator ", strings.Join(path, " -> ")) + return nil, fmt.Errorf( + "!oneof found on non-map node at %s; expected a map with a list of options and the discriminator ", + strings.Join(path, " -> ")) } discriminatorNode, found := data.MapKey(YamlDiscriminatorKey) if !found { - return nil, fmt.Errorf("key %q not present within !oneof at %q", YamlDiscriminatorKey, strings.Join(path, " -> ")) + return nil, fmt.Errorf("key %q not present within %s at %q", + YamlDiscriminatorKey, YamlOneOfTag, strings.Join(path, " -> ")) } if discriminatorNode.Type() != yaml.TypeIDString { - return nil, fmt.Errorf("%q within !oneof should be a string; got %s", discriminatorNode.Type(), YamlDiscriminatorKey) + return nil, fmt.Errorf("%q within %s should be a string; got %s", + YamlDiscriminatorKey, YamlOneOfTag, discriminatorNode.Type()) + } + discriminator := discriminatorNode.Value() + if len(discriminator) == 0 { + return nil, fmt.Errorf("%q within %s is empty", YamlDiscriminatorKey, YamlOneOfTag) } oneOfOptionsNode, found := data.MapKey(YamlOneOfKey) if !found { - return nil, fmt.Errorf("key %q not present within !oneof at %q", YamlOneOfKey, strings.Join(path, " -> ")) + return nil, fmt.Errorf("key %q not present within %s at %q", + YamlOneOfKey, YamlOneOfTag, strings.Join(path, " -> ")) } if oneOfOptionsNode.Type() != yaml.TypeIDMap { - return nil, fmt.Errorf("%q within !oneof should be a map; got %s", YamlOneOfKey, discriminatorNode.Type()) + return nil, fmt.Errorf("%q within %q should be a map; got %s", + YamlOneOfKey, YamlOneOfTag, discriminatorNode.Type()) } options := map[string]any{} for _, optionNodeKey := range oneOfOptionsNode.MapKeys() { @@ -87,7 +97,6 @@ func buildOneOfExpressions(data yaml.Node, path []string) (any, error) { } } - discriminator := discriminatorNode.Value() return &infer.OneOfExpression{ Discriminator: discriminator, Options: options, diff --git a/workflow/yaml_test.go b/workflow/yaml_test.go new file mode 100644 index 0000000..8389f2a --- /dev/null +++ b/workflow/yaml_test.go @@ -0,0 +1,105 @@ +package workflow //nolint:testpackage // Tests internal functions for unit testing. + +import ( + "go.arcalot.io/assert" + "go.arcalot.io/lang" + "go.flow.arcalot.io/engine/internal/infer" + "go.flow.arcalot.io/engine/internal/yaml" + "go.flow.arcalot.io/expressions" + "testing" +) + +func TestBuildOneOfExpression_Simple(t *testing.T) { + yamlInput := []byte(` +!oneof + discriminator: d + one_of: + a: !expr some_expr + b: !expr some_other_expr +`) + input := assert.NoErrorR[yaml.Node](t)(yaml.New().Parse(yamlInput)) + result, err := buildOneOfExpressions(input, make([]string, 0)) + assert.NoError(t, err) + assert.InstanceOf[*infer.OneOfExpression](t, result) + oneofResult := result.(*infer.OneOfExpression) + assert.Equals(t, oneofResult.Discriminator, "d") + assert.Equals(t, oneofResult.Options, map[string]any{ + "a": lang.Must2(expressions.New("some_expr")), + "b": lang.Must2(expressions.New("some_other_expr")), + }) +} + +func TestBuildOneOfExpression_InputValidation(t *testing.T) { + // Not a map + yamlInput := []byte(` +!oneof "thisisastring"`) + input := assert.NoErrorR[yaml.Node](t)(yaml.New().Parse(yamlInput)) + _, err := buildOneOfExpressions(input, make([]string, 0)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "expected a map") + + // Wrong discriminator key + yamlInput = []byte(` +!oneof + wrong_key: "" + one_of: {}`) + input = assert.NoErrorR[yaml.Node](t)(yaml.New().Parse(yamlInput)) + _, err = buildOneOfExpressions(input, make([]string, 0)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "key \""+YamlDiscriminatorKey+"\" not present") + + // Discriminator not a string + yamlInput = []byte(` +!oneof + discriminator: {} + one_of: {}`) + input = assert.NoErrorR[yaml.Node](t)(yaml.New().Parse(yamlInput)) + _, err = buildOneOfExpressions(input, make([]string, 0)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "should be a string") + + // Empty discriminator + yamlInput = []byte(` +!oneof + discriminator: "" + one_of: {}`) + input = assert.NoErrorR[yaml.Node](t)(yaml.New().Parse(yamlInput)) + _, err = buildOneOfExpressions(input, make([]string, 0)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "is empty") + + // Missing or wrong oneof key + yamlInput = []byte(` +!oneof + discriminator: "valid" + one_of_wrong: {}`) + input = assert.NoErrorR[yaml.Node](t)(yaml.New().Parse(yamlInput)) + _, err = buildOneOfExpressions(input, make([]string, 0)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "key \"one_of\" not present") + + // Wrong type for oneof options node + yamlInput = []byte(` +!oneof + discriminator: "valid" + one_of: wrong`) + input = assert.NoErrorR[yaml.Node](t)(yaml.New().Parse(yamlInput)) + _, err = buildOneOfExpressions(input, make([]string, 0)) + assert.Error(t, err) + assert.Contains(t, err.Error(), "should be a map") + + // Non-object type as option in one_of section. + yamlInput = []byte(` +!oneof + discriminator: "valid" + one_of: + a: test`) + input = assert.NoErrorR[yaml.Node](t)(yaml.New().Parse(yamlInput)) + oneofResult, err := buildOneOfExpressions(input, make([]string, 0)) + assert.NoError(t, err) + assert.InstanceOf[*infer.OneOfExpression](t, oneofResult) + _, err = oneofResult.(*infer.OneOfExpression).Type(nil, nil, nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not an object") + +}