Skip to content

Commit

Permalink
feat: fix enum refs and validate enums in calls (#1039)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
worstell and github-actions[bot] authored Mar 7, 2024
1 parent 2b8f713 commit c94e185
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 76 deletions.
28 changes: 28 additions & 0 deletions backend/controller/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,34 @@ func validateValue(fieldType schema.Type, path path, value any, sch *schema.Sche
typeMatches = true
}

case *schema.EnumRef:
enum := sch.ResolveEnumRef(fieldType)
if enum == nil {
return fmt.Errorf("unknown enum %v", fieldType)
}

for _, v := range enum.Variants {
switch t := v.Value.(type) {
case *schema.StringValue:
if valueStr, ok := value.(string); ok {
if t.Value == valueStr {
typeMatches = true
break
}
}
case *schema.IntValue:
if valueInt, ok := value.(int); ok {
if t.Value == valueInt {
typeMatches = true
break
}
}
}
}
if !typeMatches {
return fmt.Errorf("%s is not a valid variant of enum %s", value, fieldType)
}

case *schema.Bytes:
_, typeMatches = value.([]byte)
if bodyStr, ok := value.(string); ok {
Expand Down
69 changes: 69 additions & 0 deletions backend/controller/ingress/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,72 @@ func TestValueForData(t *testing.T) {
assert.Equal(t, test.result, result)
}
}

func TestEnumValidation(t *testing.T) {
sch := &schema.Schema{
Modules: []*schema.Module{
{Name: "test", Decls: []schema.Decl{
&schema.Enum{
Name: "Color",
Type: &schema.String{},
Variants: []*schema.EnumVariant{
{Name: "Red", Value: &schema.StringValue{Value: "Red"}},
{Name: "Blue", Value: &schema.StringValue{Value: "Blue"}},
{Name: "Green", Value: &schema.StringValue{Value: "Green"}},
},
},
&schema.Enum{
Name: "ColorInt",
Type: &schema.Int{},
Variants: []*schema.EnumVariant{
{Name: "RedInt", Value: &schema.IntValue{Value: 0}},
{Name: "BlueInt", Value: &schema.IntValue{Value: 1}},
{Name: "GreenInt", Value: &schema.IntValue{Value: 2}},
},
},
&schema.Data{
Name: "StringEnumRequest",
Fields: []*schema.Field{
{Name: "message", Type: &schema.EnumRef{Name: "Color", Module: "test"}},
},
},
&schema.Data{
Name: "IntEnumRequest",
Fields: []*schema.Field{
{Name: "message", Type: &schema.EnumRef{Name: "ColorInt", Module: "test"}},
},
},
&schema.Data{
Name: "OptionalEnumRequest",
Fields: []*schema.Field{
{Name: "message", Type: &schema.Optional{
Type: &schema.EnumRef{Name: "Color", Module: "test"},
}},
},
},
}},
},
}

tests := []struct {
validateRoot *schema.DataRef
req obj
err string
}{
{&schema.DataRef{Name: "StringEnumRequest", Module: "test"}, obj{"message": "Red"}, ""},
{&schema.DataRef{Name: "IntEnumRequest", Module: "test"}, obj{"message": 0}, ""},
{&schema.DataRef{Name: "OptionalEnumRequest", Module: "test"}, obj{}, ""},
{&schema.DataRef{Name: "OptionalEnumRequest", Module: "test"}, obj{"message": "Red"}, ""},
{&schema.DataRef{Name: "StringEnumRequest", Module: "test"}, obj{"message": "akxznc"},
"akxznc is not a valid variant of enum test.Color"},
}

for _, test := range tests {
err := validateValue(test.validateRoot, []string{test.validateRoot.String()}, test.req, sch)
if test.err == "" {
assert.NoError(t, err)
} else {
assert.Contains(t, err.Error(), test.err)
}
}
}
156 changes: 88 additions & 68 deletions backend/protos/xyz/block/ftl/v1/schema/schema.pb.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion backend/protos/xyz/block/ftl/v1/schema/schema.proto
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ message Type {
Any any = 9;
Unit unit = 10;
DataRef dataRef = 11;
Optional optional = 12;
EnumRef enumRef = 12;
Optional optional = 13;
}
}

Expand Down
2 changes: 1 addition & 1 deletion backend/schema/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var (
&Map{}, &Any{}, &Unit{},
// Note: any types resolved by identifier (eg. "Any", "Unit", etc.) must
// be prior to DataRef.
&DataRef{},
&DataRef{}, &EnumRef{},
}
typeUnion = append(nonOptionalTypeUnion, &Optional{})
metadataUnion = []Metadata{&MetadataCalls{}, &MetadataIngress{}, &MetadataDatabases{}, &MetadataAlias{}}
Expand Down
2 changes: 2 additions & 0 deletions backend/schema/protobuf_dec.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ func typeToSchema(s *schemapb.Type) Type {
// return verbRefToSchema(s.VerbRef)
case *schemapb.Type_DataRef:
return DataRefFromProto(s.DataRef)
case *schemapb.Type_EnumRef:
return EnumRefFromProto(s.EnumRef)
case *schemapb.Type_Int:
return &Int{Pos: posFromProto(s.Int.Pos)}
case *schemapb.Type_Float:
Expand Down
5 changes: 4 additions & 1 deletion backend/schema/protobuf_enc.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,12 @@ func typeToProto(t Type) *schemapb.Type {
case *Unit:
return &schemapb.Type{Value: &schemapb.Type_Unit{Unit: t.ToProto().(*schemapb.Unit)}}

case *VerbRef, *SourceRef, *SinkRef, *EnumRef:
case *VerbRef, *SourceRef, *SinkRef:
panic("unreachable")

case *EnumRef:
return &schemapb.Type{Value: &schemapb.Type_EnumRef{EnumRef: t.ToProto().(*schemapb.EnumRef)}}

case *DataRef:
return &schemapb.Type{Value: &schemapb.Type_DataRef{DataRef: t.ToProto().(*schemapb.DataRef)}}

Expand Down
13 changes: 13 additions & 0 deletions backend/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,19 @@ func (s *Schema) ResolveVerbRef(ref *VerbRef) *Verb {
return nil
}

func (s *Schema) ResolveEnumRef(ref *EnumRef) *Enum {
for _, module := range s.Modules {
if module.Name == ref.Module {
for _, decl := range module.Decls {
if enum, ok := decl.(*Enum); ok && enum.Name == ref.Name {
return enum
}
}
}
}
return nil
}

// Module returns the named module if it exists.
func (s *Schema) Module(name string) optional.Option[*Module] {
for _, module := range s.Modules {
Expand Down
11 changes: 9 additions & 2 deletions frontend/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions go-runtime/compile/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,17 @@ func genType(module *schema.Module, t schema.Type) string {
}
return desc

case *schema.EnumRef:
desc := ""
if module != nil && t.Module == module.Name {
desc = t.Name
} else if t.Module == "" {
desc = t.Name
} else {
desc = "ftl" + t.Module + "." + t.Name
}
return desc

case *schema.VerbRef:
if module != nil && t.Module == module.Name {
return t.Name
Expand Down
11 changes: 8 additions & 3 deletions go-runtime/compile/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,11 +591,16 @@ func visitType(pctx *parseContext, node ast.Node, tnode types.Type) (schema.Type
}
switch underlying := tnode.Underlying().(type) {
case *types.Basic:
// If this type is named and declared in another module, it's a reference. The only basic-typed references
// supported are enums.
if named, ok := tnode.(*types.Named); ok {
nodePath := named.Obj().Pkg().Path()
if !strings.HasPrefix(nodePath, pctx.pkg.PkgPath) {
if pctx.enums[named.Obj().Name()] != nil {
return &schema.EnumRef{
Pos: goPosToSchemaPos(node.Pos()),
Name: named.Obj().Name(),
}, nil
} else if !strings.HasPrefix(nodePath, pctx.pkg.PkgPath) {
// If this type is named and declared in another module, it's a reference.
// The only basic-typed references supported are enums.
base := path.Dir(pctx.pkg.PkgPath)
destModule := path.Base(strings.TrimPrefix(nodePath, base+"/"))
enumRef := &schema.EnumRef{
Expand Down

0 comments on commit c94e185

Please sign in to comment.