Skip to content

Commit

Permalink
feat: support sum types on ingress
Browse files Browse the repository at this point in the history
fixes #1388
  • Loading branch information
worstell committed May 14, 2024
1 parent de6a500 commit fce8afd
Show file tree
Hide file tree
Showing 21 changed files with 337 additions and 273 deletions.
60 changes: 48 additions & 12 deletions backend/controller/ingress/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,55 @@ func transformAliasedFields(sch *schema.Schema, t schema.Type, obj any, aliaser
}
switch t := t.(type) {
case *schema.Ref:
data, err := sch.ResolveRefMonomorphised(t)
if err != nil {
return fmt.Errorf("%s: failed to resolve data type: %w", t.Pos, err)
}
m, ok := obj.(map[string]any)
if !ok {
return fmt.Errorf("%s: expected map, got %T", t.Pos, obj)
}
for _, field := range data.Fields {
name := aliaser(m, field)
if err := transformAliasedFields(sch, field.Type, m[name], aliaser); err != nil {
return err
switch decl := sch.ResolveRef(t).(type) {
case *schema.Data:
data, err := sch.ResolveRefMonomorphised(t)
if err != nil {
return fmt.Errorf("%s: failed to resolve data type: %w", t.Pos, err)
}
m, ok := obj.(map[string]any)
if !ok {
return fmt.Errorf("%s: expected map, got %T", t.Pos, obj)
}
for _, field := range data.Fields {
name := aliaser(m, field)
if err := transformAliasedFields(sch, field.Type, m[name], aliaser); err != nil {
return err
}
}
case *schema.Enum:
if decl.IsValueEnum() {
return nil
}

// type enum
m, ok := obj.(map[string]any)
if !ok {
return fmt.Errorf("%s: expected map, got %T", t.Pos, obj)
}
name, ok := m["name"]
if !ok {
return fmt.Errorf("%s: expected type enum request to have 'name' field", t.Pos)
}
nameStr, ok := name.(string)
if !ok {
return fmt.Errorf("%s: expected 'name' field to be a string, got %T", t.Pos, name)
}

value, ok := m["value"]
if !ok {
return fmt.Errorf("%s: expected type enum request to have 'value' field", t.Pos)
}

for _, v := range decl.Variants {
if v.Name == nameStr {
if err := transformAliasedFields(sch, v.Value.(*schema.TypeValue).Value, value, aliaser); err != nil { //nolint:forcetypeassert
return err
}
}
}
case *schema.Config, *schema.Database, *schema.FSM, *schema.Secret, *schema.Verb:
return fmt.Errorf("%s: unsupported ref type %T", t.Pos, decl)
}

case *schema.Array:
Expand Down
30 changes: 30 additions & 0 deletions backend/controller/ingress/alias_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
func TestTransformFromAliasedFields(t *testing.T) {
schemaText := `
module test {
enum TypeEnum {
A test.Inner
B String
}
data Inner {
waz String +alias json "foo"
}
Expand All @@ -21,9 +26,11 @@ func TestTransformFromAliasedFields(t *testing.T) {
array [test.Inner]
map {String: test.Inner}
optional test.Inner
typeEnum test.TypeEnum
}
}
`

sch, err := schema.ParseString("test", schemaText)
assert.NoError(t, err)
actual, err := transformFromAliasedFields(&schema.Ref{Module: "test", Name: "Test"}, sch, map[string]any{
Expand All @@ -44,6 +51,10 @@ func TestTransformFromAliasedFields(t *testing.T) {
"optional": map[string]any{
"foo": "value",
},
"typeEnum": map[string]any{
"name": "A",
"value": map[string]any{"foo": "value"},
},
})
expected := map[string]any{
"scalar": "value",
Expand All @@ -63,6 +74,10 @@ func TestTransformFromAliasedFields(t *testing.T) {
"optional": map[string]any{
"waz": "value",
},
"typeEnum": map[string]any{
"name": "A",
"value": map[string]any{"waz": "value"},
},
}
assert.NoError(t, err)
assert.Equal(t, expected, actual)
Expand All @@ -71,6 +86,11 @@ func TestTransformFromAliasedFields(t *testing.T) {
func TestTransformToAliasedFields(t *testing.T) {
schemaText := `
module test {
enum TypeEnum {
A test.Inner
B String
}
data Inner {
waz String +alias json "foo"
}
Expand All @@ -81,9 +101,11 @@ func TestTransformToAliasedFields(t *testing.T) {
array [test.Inner]
map {String: test.Inner}
optional test.Inner
typeEnum test.TypeEnum
}
}
`

sch, err := schema.ParseString("test", schemaText)
assert.NoError(t, err)
actual, err := transformToAliasedFields(&schema.Ref{Module: "test", Name: "Test"}, sch, map[string]any{
Expand All @@ -104,6 +126,10 @@ func TestTransformToAliasedFields(t *testing.T) {
"optional": map[string]any{
"waz": "value",
},
"typeEnum": map[string]any{
"name": "A",
"value": map[string]any{"waz": "value"},
},
})
expected := map[string]any{
"bar": "value",
Expand All @@ -123,6 +149,10 @@ func TestTransformToAliasedFields(t *testing.T) {
"optional": map[string]any{
"foo": "value",
},
"typeEnum": map[string]any{
"name": "A",
"value": map[string]any{"foo": "value"},
},
}
assert.NoError(t, err)
assert.Equal(t, expected, actual)
Expand Down
31 changes: 29 additions & 2 deletions backend/controller/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ func validateValue(fieldType schema.Type, path path, value any, sch *schema.Sche
typeMatches = true
}
case *schema.Enum:
var inputName any
inputName = value
for _, v := range d.Variants {
switch t := v.Value.(type) {
case *schema.StringValue:
Expand All @@ -211,11 +213,36 @@ func validateValue(fieldType schema.Type, path path, value any, sch *schema.Sche
}
}
case *schema.TypeValue:
//TODO: Implement
if reqVariant, ok := value.(map[string]any); ok {
vName, ok := reqVariant["name"]
if !ok {
return fmt.Errorf(`missing name field in enum type %q: expected structure is `+
"{\"name\": \"<variant name>\", \"value\": <variant value>}", value)
}
vNameStr, ok := vName.(string)
if !ok {
return fmt.Errorf(`invalid type for enum %q; name field must be a string, was %T`,
fieldType, vName)
}
inputName = fmt.Sprintf("%q", vNameStr)

vValue, ok := reqVariant["value"]
if !ok {
return fmt.Errorf(`missing value field in enum type %q: expected structure is `+
"{\"name\": \"<variant name>\", \"value\": <variant value>}", value)
}

if v.Name == vNameStr {
return validateValue(t.Value, path, vValue, sch)
}
} else {
return fmt.Errorf(`malformed enum type %s: expected structure is `+
"{\"name\": \"<variant name>\", \"value\": <variant value>}", path)
}
}
}
if !typeMatches {
return fmt.Errorf("%s is not a valid variant of enum %s", value, fieldType)
return fmt.Errorf("%s is not a valid variant of enum %s", inputName, fieldType)
}

case *schema.Config, *schema.Database, *schema.Secret, *schema.Verb, *schema.FSM:
Expand Down
27 changes: 27 additions & 0 deletions backend/controller/ingress/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,8 @@ func TestValueForData(t *testing.T) {
{&schema.Array{Element: &schema.String{}}, []byte(`["test1", "test2"]`), []any{"test1", "test2"}},
{&schema.Map{Key: &schema.String{}, Value: &schema.String{}}, []byte(`{"key1": "value1", "key2": "value2"}`), obj{"key1": "value1", "key2": "value2"}},
{&schema.Ref{Module: "test", Name: "Test"}, []byte(`{"intValue": 10.0}`), obj{"intValue": 10.0}},
// type enum
{&schema.Ref{Module: "test", Name: "TypeEnum"}, []byte(`{"name": "A", "value": "hello"}`), obj{"name": "A", "value": "hello"}},
}

for _, test := range tests {
Expand Down Expand Up @@ -319,6 +321,13 @@ func TestEnumValidation(t *testing.T) {
{Name: "GreenInt", Value: &schema.IntValue{Value: 2}},
},
},
&schema.Enum{
Name: "TypeEnum",
Variants: []*schema.EnumVariant{
{Name: "String", Value: &schema.TypeValue{Value: &schema.String{}}},
{Name: "List", Value: &schema.TypeValue{Value: &schema.Array{Element: &schema.String{}}}},
},
},
&schema.Data{
Name: "StringEnumRequest",
Fields: []*schema.Field{
Expand All @@ -339,6 +348,14 @@ func TestEnumValidation(t *testing.T) {
}},
},
},
&schema.Data{
Name: "TypeEnumRequest",
Fields: []*schema.Field{
{Name: "message", Type: &schema.Optional{
Type: &schema.Ref{Name: "TypeEnum", Module: "test"},
}},
},
},
}},
},
}
Expand All @@ -354,6 +371,16 @@ func TestEnumValidation(t *testing.T) {
{&schema.Ref{Name: "OptionalEnumRequest", Module: "test"}, obj{"message": "Red"}, ""},
{&schema.Ref{Name: "StringEnumRequest", Module: "test"}, obj{"message": "akxznc"},
"akxznc is not a valid variant of enum test.Color"},
{&schema.Ref{Name: "TypeEnumRequest", Module: "test"}, obj{"message": obj{"name": "String", "value": "Hello"}}, ""},
{&schema.Ref{Name: "TypeEnumRequest", Module: "test"}, obj{"message": obj{"name": "String", "value": `["test1", "test2"]`}}, ""},
{&schema.Ref{Name: "TypeEnumRequest", Module: "test"}, obj{"message": obj{"name": "String", "value": 0}},
"test.TypeEnumRequest.message has wrong type, expected String found int"},
{&schema.Ref{Name: "TypeEnumRequest", Module: "test"}, obj{"message": obj{"name": "Invalid", "value": 0}},
"\"Invalid\" is not a valid variant of enum test.TypeEnum"},
{&schema.Ref{Name: "TypeEnumRequest", Module: "test"}, obj{"message": obj{"name": 0, "value": 0}},
`invalid type for enum "test.TypeEnum"; name field must be a string, was int`},
{&schema.Ref{Name: "TypeEnumRequest", Module: "test"}, obj{"message": "Hello"},
`malformed enum type test.TypeEnumRequest.message: expected structure is {"name": "<variant name>", "value": <variant value>}`},
}

for _, test := range tests {
Expand Down
4 changes: 3 additions & 1 deletion backend/controller/ingress/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ func extractHTTPRequestBody(route *dal.IngressRoute, r *http.Request, ref *schem
}

if ref, ok := bodyField.Type.(*schema.Ref); ok {
return buildRequestMap(route, r, ref, sch)
if _, ok := sch.ResolveRef(ref).(*schema.Data); ok {
return buildRequestMap(route, r, ref, sch)
}
}

bodyData, err := readRequestBody(r)
Expand Down
9 changes: 0 additions & 9 deletions backend/protos/xyz/block/ftl/v1/schema/schema.proto
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,6 @@ message StringValue {
string value = 2;
}

message SumTypeVariants {
repeated string value = 1;
}

message Time {
optional Position pos = 1;
}
Expand All @@ -261,11 +257,6 @@ message TypeParameter {
string name = 2;
}

message TypeRegistry {
map<string, Type> schemaTypes = 1;
map<string, SumTypeVariants> sumTypes = 2;
}

message TypeValue {
optional Position pos = 1;
Type value = 2;
Expand Down
26 changes: 22 additions & 4 deletions backend/schema/jsonschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,29 @@ func nodeToJSSchema(node Node, refs map[RefKey]*Ref) *jsonschema.Schema {
schema := &jsonschema.Schema{
Description: jsComments(node.Comments),
}
values := make([]any, len(node.Variants))
for i, v := range node.Variants {
values[i] = v.Value.GetValue()
if node.IsValueEnum() {
values := make([]any, len(node.Variants))
for i, v := range node.Variants {
values[i] = v.Value.GetValue()
}
return schema.WithEnum(values...)
}

variants := make([]jsonschema.SchemaOrBool, 0, len(node.Variants))
for _, v := range node.Variants {
obj := jsonschema.Object
str := jsonschema.String
variantSch := &jsonschema.Schema{
Description: jsComments(v.Comments),
Type: &jsonschema.Type{SimpleTypes: &obj},
Properties: map[string]jsonschema.SchemaOrBool{},
AdditionalProperties: jsBool(false),
}
variantSch.Properties["name"] = jsonschema.SchemaOrBool{TypeObject: &jsonschema.Schema{Type: &jsonschema.Type{SimpleTypes: &str}}}
variantSch.Properties["value"] = jsonschema.SchemaOrBool{TypeObject: nodeToJSSchema(v.Value.(*TypeValue).schemaValueType(), refs)} //nolint:forcetypeassert
variants = append(variants, jsonschema.SchemaOrBool{TypeObject: variantSch})
}
return schema.WithEnum(values...)
return schema.WithOneOf(variants...)

case *Int:
st := jsonschema.Integer
Expand Down
Loading

0 comments on commit fce8afd

Please sign in to comment.