Skip to content

Commit

Permalink
feat: deterministic ordering for verb metadata
Browse files Browse the repository at this point in the history
inject in the correct position, but display metadata in a deterministic order in the schema. if param orderings change, schema will remain consistent
  • Loading branch information
worstell committed Nov 19, 2024
1 parent 8dd6664 commit 5c86346
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 166 deletions.
122 changes: 65 additions & 57 deletions go-runtime/compile/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strings"
"unicode"

"github.com/TBD54566975/ftl/go-runtime/schema/common"
"github.com/alecthomas/types/optional"
"github.com/alecthomas/types/result"
"github.com/block/scaffolder"
Expand Down Expand Up @@ -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,
Expand All @@ -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)
}
Expand Down Expand Up @@ -799,64 +802,69 @@ 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)
}
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 {
Expand Down
24 changes: 24 additions & 0 deletions go-runtime/schema/common/fact.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, <request>, <resource1>, <resource2>, ...).
//
// 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)
Expand Down Expand Up @@ -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.
//
Expand Down
43 changes: 25 additions & 18 deletions go-runtime/schema/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -142,32 +144,35 @@ 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]
}

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]),
}
}

Expand All @@ -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 {
Expand All @@ -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,
}
}

Expand Down
24 changes: 18 additions & 6 deletions go-runtime/schema/finalize/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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:
Expand All @@ -67,6 +70,14 @@ 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")
}
v.SortMetadata()
verbParamOrder[v] = paramOrder.Resources
}
case *common.FailedExtraction:
failed[schema.RefKey{Module: moduleName, Name: strcase.ToUpperCamel(obj.Name())}] = obj
}
Expand All @@ -82,12 +93,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
}

Expand Down
1 change: 1 addition & 0 deletions go-runtime/schema/testdata/namedext/types.ftl.go

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

Loading

0 comments on commit 5c86346

Please sign in to comment.