diff --git a/go-runtime/compile/build.go b/go-runtime/compile/build.go index e662330be..eabbf4b52 100644 --- a/go-runtime/compile/build.go +++ b/go-runtime/compile/build.go @@ -25,6 +25,7 @@ import ( "github.com/TBD54566975/ftl" extract "github.com/TBD54566975/ftl/go-runtime/schema" + "github.com/TBD54566975/ftl/go-runtime/schema/common" "github.com/TBD54566975/ftl/internal" "github.com/TBD54566975/ftl/internal/builderrors" "github.com/TBD54566975/ftl/internal/exec" @@ -549,10 +550,11 @@ func scaffoldBuildTemplateAndTidy(ctx context.Context, config moduleconfig.AbsMo } type mainModuleContextBuilder struct { - sch *schema.Schema - mainModule *schema.Module - nativeNames extract.NativeNames - imports map[string]string + sch *schema.Schema + mainModule *schema.Module + nativeNames extract.NativeNames + verbResourceParamOrders map[*schema.Verb][]common.VerbResourceParam + imports map[string]string } func buildMainModuleContext(sch *schema.Schema, result extract.Result, goModVersion, projectName string, @@ -565,10 +567,11 @@ func buildMainModuleContext(sch *schema.Schema, result extract.Result, goModVers Modules: append(sch.Modules, result.Module), } builder := &mainModuleContextBuilder{ - sch: combinedSch, - mainModule: result.Module, - nativeNames: result.NativeNames, - imports: imports(result.Module, false), + sch: combinedSch, + mainModule: result.Module, + nativeNames: result.NativeNames, + verbResourceParamOrders: result.VerbResourceParamOrder, + imports: imports(result.Module, false), } return builder.build(goModVersion, ftlVersion, projectName, sharedModulesPaths, replacements) } @@ -799,57 +802,17 @@ func (b *mainModuleContextBuilder) processExternalTypeAlias(alias *schema.TypeAl func (b *mainModuleContextBuilder) processVerb(verb *schema.Verb) (goVerb, error) { var resources []verbResource - for _, m := range verb.Metadata { - switch md := m.(type) { - case *schema.MetadataCalls: - for _, call := range md.Calls { - resolved, ok := b.sch.Resolve(call).Get() - if !ok { - return goVerb{}, fmt.Errorf("failed to resolve %s client, used by %s.%s", call, - b.mainModule.Name, verb.Name) - } - callee, ok := resolved.(*schema.Verb) - if !ok { - return goVerb{}, fmt.Errorf("%s.%s uses %s client, but %s is not a verb", - b.mainModule.Name, verb.Name, call, call) - } - calleeNativeName, ok := b.nativeNames[call] - if !ok { - return goVerb{}, fmt.Errorf("missing native name for verb client %s", call) - } - calleeverb, err := b.getGoVerb(calleeNativeName, callee) - if err != nil { - return goVerb{}, err - } - resources = append(resources, verbClient{ - calleeverb, - }) - } - case *schema.MetadataDatabases: - for _, call := range md.Calls { - resolved, ok := b.sch.Resolve(call).Get() - if !ok { - return goVerb{}, fmt.Errorf("failed to resolve %s database, used by %s.%s", call, - b.mainModule.Name, verb.Name) - } - db, ok := resolved.(*schema.Database) - if !ok { - return goVerb{}, fmt.Errorf("%s.%s uses %s database handle, but %s is not a database", - b.mainModule.Name, verb.Name, call, call) - } - - dbHandle, err := b.processDatabase(call.Module, db) - if err != nil { - return goVerb{}, err - } - resources = append(resources, dbHandle) - } - - default: - // TODO: implement other resources + verbResourceParams, ok := b.verbResourceParamOrders[verb] + if !ok { + return goVerb{}, fmt.Errorf("missing verb resource param order for %s", verb.Name) + } + for _, m := range verbResourceParams { + resource, err := b.getVerbResource(verb, m) + if err != nil { + return goVerb{}, err } + resources = append(resources, resource) } - nativeName, ok := b.nativeNames[verb] if !ok { return goVerb{}, fmt.Errorf("missing native name for verb %s", verb.Name) @@ -857,6 +820,51 @@ func (b *mainModuleContextBuilder) processVerb(verb *schema.Verb) (goVerb, error return b.getGoVerb(nativeName, verb, resources...) } +func (b *mainModuleContextBuilder) getVerbResource(verb *schema.Verb, param common.VerbResourceParam) (verbResource, error) { + ref := param.Ref + switch param.Type.(type) { + case *schema.MetadataCalls: + resolved, ok := b.sch.Resolve(ref).Get() + if !ok { + return verbClient{}, fmt.Errorf("failed to resolve %s client, used by %s.%s", ref, + b.mainModule.Name, verb.Name) + } + callee, ok := resolved.(*schema.Verb) + if !ok { + return verbClient{}, fmt.Errorf("%s.%s uses %s client, but %s is not a verb", + b.mainModule.Name, verb.Name, ref, ref) + } + calleeNativeName, ok := b.nativeNames[ref] + if !ok { + return verbClient{}, fmt.Errorf("missing native name for verb client %s", ref) + } + calleeverb, err := b.getGoVerb(calleeNativeName, callee) + if err != nil { + return verbClient{}, err + } + return verbClient{ + calleeverb, + }, nil + case *schema.MetadataDatabases: + resolved, ok := b.sch.Resolve(ref).Get() + if !ok { + return goDBHandle{}, fmt.Errorf("failed to resolve %s database, used by %s.%s", ref, + b.mainModule.Name, verb.Name) + } + db, ok := resolved.(*schema.Database) + if !ok { + return goDBHandle{}, fmt.Errorf("%s.%s uses %s database handle, but %s is not a database", + b.mainModule.Name, verb.Name, ref, ref) + } + + return b.processDatabase(ref.Module, db) + + default: + // TODO: implement other resources + return nil, fmt.Errorf("unsupported resource type for verb %q", verb.Name) + } +} + func (b *mainModuleContextBuilder) processDatabase(moduleName string, db *schema.Database) (goDBHandle, error) { nn, ok := b.nativeNames[db] if !ok { diff --git a/go-runtime/schema/common/fact.go b/go-runtime/schema/common/fact.go index 45a936f8b..569c2166d 100644 --- a/go-runtime/schema/common/fact.go +++ b/go-runtime/schema/common/fact.go @@ -149,6 +149,23 @@ type DatabaseConfig struct { func (*DatabaseConfig) schemaFactValue() {} +// VerbResourceParamOrder is a fact for marking the order of resource parameters used by a verb, where the signature +// is (context.Context, , , , ...). +// +// This is used in the generated code that registers verb resources. The order of parameters is important because it +// will determine the order in which resources are passed to the verb when the call is constructed via reflection at +// runtime. +type VerbResourceParamOrder struct { + Resources []VerbResourceParam +} + +type VerbResourceParam struct { + Ref *schema.Ref + Type schema.Metadata +} + +func (*VerbResourceParamOrder) schemaFactValue() {} + // MarkSchemaDecl marks the given object as having been extracted to the given schema decl. func MarkSchemaDecl(pass *analysis.Pass, obj types.Object, decl schema.Decl) { fact := newFact(pass, obj) @@ -220,6 +237,13 @@ func MarkDatabaseConfig(pass *analysis.Pass, obj types.Object, dbType DatabaseTy pass.ExportObjectFact(obj, fact) } +// MarkVerbResourceParamOrder marks the given verb object with the order of its parameters. +func MarkVerbResourceParamOrder(pass *analysis.Pass, obj types.Object, resources []VerbResourceParam) { + fact := newFact(pass, obj) + fact.Add(&VerbResourceParamOrder{Resources: resources}) + pass.ExportObjectFact(obj, fact) +} + // GetAllFactsExtractionStatus merges schema facts inclusive of all available results and the present pass facts. // For a given object, it provides the current extraction status. // diff --git a/go-runtime/schema/extract.go b/go-runtime/schema/extract.go index cac3b76e1..77929a05d 100644 --- a/go-runtime/schema/extract.go +++ b/go-runtime/schema/extract.go @@ -89,6 +89,8 @@ type Result struct { Module *schema.Module // NativeNames maps schema nodes to their native Go names. NativeNames NativeNames + // VerbResourceParamOrder contains the order of resource parameters for each verb. + VerbResourceParamOrder map[*schema.Verb][]common.VerbResourceParam // Errors is a list of errors encountered during schema extraction. Errors []builderrors.Error } @@ -142,16 +144,18 @@ type refResult struct { fqName optional.Option[string] } +// used to combine result data across passes (each pass analyzes one package within the module) type combinedData struct { module *schema.Module errs []builderrors.Error - nativeNames NativeNames - functionCalls map[schema.Position]finalize.FunctionCall - verbs map[types.Object]*schema.Verb - refResults map[schema.RefKey]refResult - extractedDecls map[schema.Decl]types.Object - externalTypeAliases sets.Set[*schema.TypeAlias] + nativeNames NativeNames + functionCalls map[schema.Position]finalize.FunctionCall + verbs map[types.Object]*schema.Verb + verbResourceParamOrder map[*schema.Verb][]common.VerbResourceParam + refResults map[schema.RefKey]refResult + extractedDecls map[schema.Decl]types.Object + externalTypeAliases sets.Set[*schema.TypeAlias] // for detecting duplicates typeUniqueness map[string]tuple.Pair[types.Object, schema.Position] globalUniqueness map[string]tuple.Pair[types.Object, schema.Position] @@ -159,15 +163,16 @@ type combinedData struct { func newCombinedData(diagnostics []analysis.SimpleDiagnostic) *combinedData { return &combinedData{ - errs: diagnosticsToSchemaErrors(diagnostics), - nativeNames: make(NativeNames), - functionCalls: make(map[schema.Position]finalize.FunctionCall), - verbs: make(map[types.Object]*schema.Verb), - refResults: make(map[schema.RefKey]refResult), - extractedDecls: make(map[schema.Decl]types.Object), - externalTypeAliases: sets.NewSet[*schema.TypeAlias](), - typeUniqueness: make(map[string]tuple.Pair[types.Object, schema.Position]), - globalUniqueness: make(map[string]tuple.Pair[types.Object, schema.Position]), + errs: diagnosticsToSchemaErrors(diagnostics), + nativeNames: make(NativeNames), + functionCalls: make(map[schema.Position]finalize.FunctionCall), + verbs: make(map[types.Object]*schema.Verb), + verbResourceParamOrder: make(map[*schema.Verb][]common.VerbResourceParam), + refResults: make(map[schema.RefKey]refResult), + extractedDecls: make(map[schema.Decl]types.Object), + externalTypeAliases: sets.NewSet[*schema.TypeAlias](), + typeUniqueness: make(map[string]tuple.Pair[types.Object, schema.Position]), + globalUniqueness: make(map[string]tuple.Pair[types.Object, schema.Position]), } } @@ -183,6 +188,7 @@ func (cd *combinedData) update(fr finalize.Result) { copyFailedRefs(cd.refResults, fr.Failed) maps.Copy(cd.nativeNames, fr.NativeNames) maps.Copy(cd.functionCalls, fr.FunctionCalls) + maps.Copy(cd.verbResourceParamOrder, fr.VerbResourceParamOrder) } func (cd *combinedData) toResult() Result { @@ -192,9 +198,10 @@ func (cd *combinedData) toResult() Result { cd.errorDirectVerbInvocations() builderrors.SortErrorsByPosition(cd.errs) return Result{ - Module: cd.module, - NativeNames: cd.nativeNames, - Errors: cd.errs, + Module: cd.module, + NativeNames: cd.nativeNames, + VerbResourceParamOrder: cd.verbResourceParamOrder, + Errors: cd.errs, } } diff --git a/go-runtime/schema/finalize/analyzer.go b/go-runtime/schema/finalize/analyzer.go index c97ab40f9..47fb1c03e 100644 --- a/go-runtime/schema/finalize/analyzer.go +++ b/go-runtime/schema/finalize/analyzer.go @@ -37,6 +37,8 @@ type Result struct { NativeNames map[schema.Node]string // FunctionCalls contains all function calls; key is the parent function, value is the called functions. FunctionCalls map[schema.Position]FunctionCall + // VerbResourceParamOrder contains the order of resource parameters for each verb. + VerbResourceParamOrder map[*schema.Verb][]common.VerbResourceParam } type FunctionCall struct { @@ -54,6 +56,7 @@ func Run(pass *analysis.Pass) (interface{}, error) { // for identifying duplicates declKeys := make(map[string]types.Object) nativeNames := make(map[schema.Node]string) + verbParamOrder := make(map[*schema.Verb][]common.VerbResourceParam) for obj, fact := range common.GetAllFactsExtractionStatus(pass) { switch f := fact.(type) { case *common.ExtractedDecl: @@ -67,6 +70,13 @@ func Run(pass *analysis.Pass) (interface{}, error) { declKeys[f.Decl.String()] = obj nativeNames[f.Decl] = common.GetNativeName(obj) } + if v, ok := f.Decl.(*schema.Verb); ok { + paramOrder, ok := common.GetFactForObject[*common.VerbResourceParamOrder](pass, obj).Get() + if !ok { + common.NoEndColumnErrorf(pass, obj.Pos(), "failed to extract verb schema") + } + verbParamOrder[v] = paramOrder.Resources + } case *common.FailedExtraction: failed[schema.RefKey{Module: moduleName, Name: strcase.ToUpperCamel(obj.Name())}] = obj } @@ -82,12 +92,13 @@ func Run(pass *analysis.Pass) (interface{}, error) { } } return Result{ - ModuleName: moduleName, - ModuleComments: extractModuleComments(pass), - Extracted: extracted, - Failed: failed, - NativeNames: nativeNames, - FunctionCalls: getFunctionCalls(pass), + ModuleName: moduleName, + ModuleComments: extractModuleComments(pass), + Extracted: extracted, + Failed: failed, + NativeNames: nativeNames, + FunctionCalls: getFunctionCalls(pass), + VerbResourceParamOrder: verbParamOrder, }, nil } diff --git a/go-runtime/schema/testdata/namedext/types.ftl.go b/go-runtime/schema/testdata/namedext/types.ftl.go index 7c5d283d6..d08d78d1d 100644 --- a/go-runtime/schema/testdata/namedext/types.ftl.go +++ b/go-runtime/schema/testdata/namedext/types.ftl.go @@ -1,2 +1,3 @@ // Code generated by FTL. DO NOT EDIT. package namedext + diff --git a/go-runtime/schema/verb/analyzer.go b/go-runtime/schema/verb/analyzer.go index 573cb96c4..48ad828e7 100644 --- a/go-runtime/schema/verb/analyzer.go +++ b/go-runtime/schema/verb/analyzer.go @@ -1,6 +1,7 @@ package verb import ( + "fmt" "go/ast" "go/types" "strings" @@ -23,6 +24,22 @@ const ( databaseHandle ) +type resource struct { + ref *schema.Ref + typ resourceType +} + +func (r resource) toMetadataType() (schema.Metadata, error) { + switch r.typ { + case verbClient: + return &schema.MetadataCalls{}, nil + case databaseHandle: + return &schema.MetadataDatabases{}, nil + default: + return nil, fmt.Errorf("unsupported resource type") + } +} + // Extractor extracts verbs to the module schema. var Extractor = common.NewDeclExtractor[*schema.Verb, *ast.FuncDecl]("verb", Extract) @@ -35,14 +52,21 @@ func Extract(pass *analysis.Pass, node *ast.FuncDecl, obj types.Object) optional loaded := pass.ResultOf[initialize.Analyzer].(initialize.Result) //nolint:forcetypeassert hasRequest := false + var orderedResourceParams []common.VerbResourceParam if !common.ApplyMetadata[*schema.Verb](pass, obj, func(md *common.ExtractedMetadata) { verb.Comments = md.Comments verb.Export = md.IsExported verb.Metadata = md.Metadata for idx, param := range node.Type.Params.List { - paramObj, hasObj := common.GetObjectForNode(pass.TypesInfo, param.Type).Get() - switch getParamResourceType(pass, paramObj) { - case none: + r, err := resolveResource(pass, param.Type) + if err != nil { + common.Wrapf(pass, param, err, "") + continue + } + + // if this parameter can't be resolved to a resource, it must either be the context or request parameter: + // Verb(context.Context, , , , ...) + if r == nil { if idx > 1 { common.Errorf(pass, param, "unsupported verb parameter type; verbs must have the "+ "signature func(Context, Request?, Resources...)") @@ -51,40 +75,28 @@ func Extract(pass *analysis.Pass, node *ast.FuncDecl, obj types.Object) optional if idx == 1 { hasRequest = true } + continue + } + + switch r.typ { case verbClient: - if !hasObj { - common.Errorf(pass, param, "unsupported verb parameter type") - continue - } - calleeRef := getResourceRef(paramObj, pass, param) - calleeRef.Name = strings.TrimSuffix(calleeRef.Name, "Client") - verb.AddCall(calleeRef) - common.MarkIncludeNativeName(pass, paramObj, calleeRef) + paramObj := common.GetObjectForNode(pass.TypesInfo, param.Type).MustGet() + common.MarkIncludeNativeName(pass, paramObj, r.ref) + verb.AddCall(r.ref) case databaseHandle: - idxExpr, ok := param.Type.(*ast.IndexExpr) - if !ok { - common.Errorf(pass, param, "unsupported verb parameter type") - continue - } - idxObj, ok := common.GetObjectForNode(pass.TypesInfo, idxExpr.Index).Get() - if !ok { - common.Errorf(pass, param, "unsupported verb parameter type") - continue - } - decl, ok := common.GetFactForObject[*common.ExtractedDecl](pass, idxObj).Get() - if !ok { - common.Errorf(pass, param, "unsupported verb parameter type") - continue - } - db, ok := decl.Decl.(*schema.Database) - if !ok { - common.Errorf(pass, param, "no database found for config provided to database handle") - continue - } - ref := getResourceRef(idxObj, pass, param) - ref.Name = db.Name - verb.AddDatabase(ref) + verb.AddDatabase(r.ref) + case none: + common.Errorf(pass, param, "unsupported verb parameter type; verbs must have the "+ + "signature func(Context, Request?, Resources...)") + } + rt, err := r.toMetadataType() + if err != nil { + common.Wrapf(pass, param, err, "") } + orderedResourceParams = append(orderedResourceParams, common.VerbResourceParam{ + Ref: r.ref, + Type: rt, + }) } }) { return optional.None[*schema.Verb]() @@ -121,69 +133,82 @@ func Extract(pass *analysis.Pass, node *ast.FuncDecl, obj types.Object) optional verb.Request = reqV verb.Response = resV + common.MarkVerbResourceParamOrder(pass, obj, orderedResourceParams) return optional.Some(verb) } -func checkSignature( - pass *analysis.Pass, - loaded initialize.Result, - node *ast.FuncDecl, - sig *types.Signature, - hasRequest bool, -) (req, resp optional.Option[*types.Var]) { - if node.Name.Name == "" { - common.Errorf(pass, node, "verb function must be named") - return optional.None[*types.Var](), optional.None[*types.Var]() - } - if !unicode.IsUpper(rune(node.Name.Name[0])) { - common.Errorf(pass, node, "verb name must be exported") - return optional.None[*types.Var](), optional.None[*types.Var]() - } - - params := sig.Params() - results := sig.Results() - if params.Len() == 0 { - common.Errorf(pass, node, "first parameter must be context.Context") - } else if !loaded.IsContextType(params.At(0).Type()) { - common.TokenErrorf(pass, params.At(0).Pos(), params.At(0).Name(), "first parameter must be of type context.Context but is %s", params.At(0).Type()) +func resolveResource(pass *analysis.Pass, typ ast.Expr) (*resource, error) { + obj := common.GetObjectForNode(pass.TypesInfo, typ) + if o, ok := obj.Get(); ok { + if _, ok := o.Type().(*types.Alias); ok { + ident, ok := typ.(*ast.Ident) + if !ok || ident.Obj == nil || ident.Obj.Decl == nil { + return nil, fmt.Errorf("unsupported verb parameter type") + } + ts, ok := ident.Obj.Decl.(*ast.TypeSpec) + if !ok { + return nil, fmt.Errorf("unsupported verb parameter type") + } + return resolveResource(pass, ts.Type) + } } - if params.Len() >= 2 { - if params.At(1).Type().String() == common.FtlUnitTypePath { - common.TokenErrorf(pass, params.At(1).Pos(), params.At(1).Name(), "second parameter must not be ftl.Unit") + var ref *schema.Ref + rType := getParamResourceType(pass, obj) + switch rType { + case none: + return nil, nil + case verbClient: + o, ok := obj.Get() + if !ok { + return nil, fmt.Errorf("unsupported verb parameter type") } - - if hasRequest { - req = optional.Some(params.At(1)) + calleeRef, err := getResourceRef(o) + if err != nil { + return nil, err } - } - - if results.Len() > 2 { - common.Errorf(pass, node, "must have at most two results (, error)") - } - if results.Len() == 0 { - common.Errorf(pass, node, "must at least return an error") - } else if !loaded.IsStdlibErrorType(results.At(results.Len() - 1).Type()) { - common.TokenErrorf(pass, results.At(results.Len()-1).Pos(), results.At(results.Len()-1).Name(), "must return an error but is %q", results.At(0).Type()) - } - if results.Len() == 2 { - if results.At(1).Type().String() == common.FtlUnitTypePath { - common.TokenErrorf(pass, results.At(1).Pos(), results.At(1).Name(), "second result must not be ftl.Unit") + calleeRef.Name = strings.TrimSuffix(calleeRef.Name, "Client") + ref = calleeRef + case databaseHandle: + idxExpr, ok := typ.(*ast.IndexExpr) + if !ok { + return nil, fmt.Errorf("unsupported verb parameter type; expected ftl.DatabaseHandle[Config]") } - resp = optional.Some(results.At(0)) + idxObj, ok := common.GetObjectForNode(pass.TypesInfo, idxExpr.Index).Get() + if !ok { + return nil, fmt.Errorf("unsupported database verb parameter type") + } + decl, ok := common.GetFactForObject[*common.ExtractedDecl](pass, idxObj).Get() + if !ok { + return nil, fmt.Errorf("no database found for config provided to database handle") + } + db, ok := decl.Decl.(*schema.Database) + if !ok { + return nil, fmt.Errorf("no database found for config provided to database handle") + } + r, err := getResourceRef(idxObj) + if err != nil { + return nil, err + } + r.Name = db.Name + ref = r } - return req, resp + if ref == nil { + return nil, fmt.Errorf("unsupported verb parameter type") + } + return &resource{ref: ref, typ: rType}, nil } -func getParamResourceType(pass *analysis.Pass, paramObj types.Object) resourceType { - if paramObj == nil { +func getParamResourceType(pass *analysis.Pass, maybeObj optional.Option[types.Object]) resourceType { + obj, ok := maybeObj.Get() + if !ok { return none } - if paramObj.Pkg() == nil { + if obj.Pkg() == nil { return none } - switch t := paramObj.Type().(type) { + switch t := obj.Type().(type) { case *types.Named: if isDatabaseHandleType(pass, t) { return databaseHandle @@ -199,23 +224,27 @@ func getParamResourceType(pass *analysis.Pass, paramObj types.Object) resourceTy if !ok { return none } - return getParamResourceType(pass, named.Obj()) + namedObj := optional.Some[types.Object](named.Obj()) + if named.Obj() == nil { + namedObj = optional.None[types.Object]() + } + return getParamResourceType(pass, namedObj) default: return none } } -func getResourceRef(paramObj types.Object, pass *analysis.Pass, param *ast.Field) *schema.Ref { +func getResourceRef(paramObj types.Object) (*schema.Ref, error) { paramModule, err := common.FtlModuleFromGoPackage(paramObj.Pkg().Path()) if err != nil { - common.Errorf(pass, param, "failed to resolve module for type: %v", err) + return nil, fmt.Errorf("failed to resolve module for type: %w", err) } dbRef := &schema.Ref{ Module: paramModule, Name: strcase.ToLowerCamel(paramObj.Name()), } - return dbRef + return dbRef, nil } func isDatabaseHandleType(pass *analysis.Pass, named *types.Named) bool { @@ -231,3 +260,54 @@ func isDatabaseHandleType(pass *analysis.Pass, named *types.Named) bool { // type argument implements `DatabaseConfig`, e.g. DatabaseHandle[MyConfig] where MyConfig implements DatabaseConfig return common.IsDatabaseConfigType(pass, typeArg) } + +func checkSignature( + pass *analysis.Pass, + loaded initialize.Result, + node *ast.FuncDecl, + sig *types.Signature, + hasRequest bool, +) (req, resp optional.Option[*types.Var]) { + if node.Name.Name == "" { + common.Errorf(pass, node, "verb function must be named") + return optional.None[*types.Var](), optional.None[*types.Var]() + } + if !unicode.IsUpper(rune(node.Name.Name[0])) { + common.Errorf(pass, node, "verb name must be exported") + return optional.None[*types.Var](), optional.None[*types.Var]() + } + + params := sig.Params() + results := sig.Results() + if params.Len() == 0 { + common.Errorf(pass, node, "first parameter must be context.Context") + } else if !loaded.IsContextType(params.At(0).Type()) { + common.TokenErrorf(pass, params.At(0).Pos(), params.At(0).Name(), "first parameter must be of type context.Context but is %s", params.At(0).Type()) + } + + if params.Len() >= 2 { + if params.At(1).Type().String() == common.FtlUnitTypePath { + common.TokenErrorf(pass, params.At(1).Pos(), params.At(1).Name(), "second parameter must not be ftl.Unit") + } + + if hasRequest { + req = optional.Some(params.At(1)) + } + } + + if results.Len() > 2 { + common.Errorf(pass, node, "must have at most two results (, error)") + } + if results.Len() == 0 { + common.Errorf(pass, node, "must at least return an error") + } else if !loaded.IsStdlibErrorType(results.At(results.Len() - 1).Type()) { + common.TokenErrorf(pass, results.At(results.Len()-1).Pos(), results.At(results.Len()-1).Name(), "must return an error but is %q", results.At(0).Type()) + } + if results.Len() == 2 { + if results.At(1).Type().String() == common.FtlUnitTypePath { + common.TokenErrorf(pass, results.At(1).Pos(), results.At(1).Name(), "second result must not be ftl.Unit") + } + resp = optional.Some(results.At(0)) + } + return req, resp +} diff --git a/internal/schema/validate.go b/internal/schema/validate.go index 77177d337..2075ceabe 100644 --- a/internal/schema/validate.go +++ b/internal/schema/validate.go @@ -303,6 +303,7 @@ func ValidateModule(module *Module) error { } case *Verb: + n.SortMetadata() merr = append(merr, validateVerbMetadata(scopes, module, n)...) case *Data: @@ -405,6 +406,87 @@ func getDeclSortingPriority(decl Decl) int { return priority } +func sortMetadata(md []Metadata) { + sort.SliceStable(md, func(i, j int) bool { + iMd := md[i] + jMd := md[j] + sortMetadataType(iMd) + sortMetadataType(jMd) + iPriority := getMetadataSortingPriority(iMd) + jPriority := getMetadataSortingPriority(jMd) + return iPriority < jPriority + }) +} + +func sortMetadataType(md Metadata) { + sortRefs := func(refs []*Ref) { + sort.SliceStable(refs, func(i, j int) bool { + if refs[i].Module == refs[j].Module { + return refs[i].Name < refs[j].Name + } + return refs[i].Module < refs[j].Module + }) + } + + switch m := md.(type) { + case *MetadataAlias: + return + case *MetadataCalls: + sortRefs(m.Calls) + case *MetadataConfig: + sortRefs(m.Config) + case *MetadataCronJob: + return + case *MetadataDatabases: + sortRefs(m.Calls) + case *MetadataEncoding: + return + case *MetadataIngress: + return + case *MetadataRetry: + return + case *MetadataSecrets: + sortRefs(m.Secrets) + case *MetadataSubscriber: + return + case *MetadataTypeMap: + return + case *MetadataPublisher: + sortRefs(m.Topics) + } +} + +func getMetadataSortingPriority(metadata Metadata) int { + priority := 0 + switch metadata.(type) { + case *MetadataIngress: + priority = 1 + case *MetadataAlias: + priority = 2 + case *MetadataEncoding: + priority = 3 + case *MetadataCalls: + priority = 4 + case *MetadataDatabases: + priority = 5 + case *MetadataSecrets: + priority = 6 + case *MetadataConfig: + priority = 7 + case *MetadataCronJob: + priority = 8 + case *MetadataPublisher: + priority = 9 + case *MetadataSubscriber: + priority = 10 + case *MetadataRetry: + priority = 11 + case *MetadataTypeMap: + priority = 12 + } + return priority +} + // Sort and de-duplicate errors. func cleanErrors(merr []error) []error { if len(merr) == 0 { diff --git a/internal/schema/verb.go b/internal/schema/verb.go index a9d6c9113..640c87a14 100644 --- a/internal/schema/verb.go +++ b/internal/schema/verb.go @@ -135,6 +135,10 @@ func (v *Verb) AddDatabase(db *Ref) { v.Metadata = append(v.Metadata, &MetadataDatabases{Calls: []*Ref{db}}) } +func (v *Verb) SortMetadata() { + sortMetadata(v.Metadata) +} + func (v *Verb) GetMetadataIngress() optional.Option[*MetadataIngress] { if m, ok := slices.FindVariant[*MetadataIngress](v.Metadata); ok { return optional.Some(m)