From 0ca3b2608bb5b0cc882fc33fe98fd3edacdd5201 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Fri, 26 Jul 2024 15:54:28 +1000 Subject: [PATCH] feat: add local schema diff If no remote URI is specified for schema diff it will attempt to diff the local project closes #2174 --- cmd/ftl/cmd_schema_diff.go | 72 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/cmd/ftl/cmd_schema_diff.go b/cmd/ftl/cmd_schema_diff.go index b7561d8407..5a0e4ee8cf 100644 --- a/cmd/ftl/cmd_schema_diff.go +++ b/cmd/ftl/cmd_schema_diff.go @@ -2,9 +2,13 @@ package main import ( "context" + "errors" "fmt" + "github.com/TBD54566975/ftl/common/projectconfig" + "google.golang.org/protobuf/proto" "net/url" "os" + "path/filepath" "connectrpc.com/connect" "github.com/hexops/gotextdiff" @@ -22,12 +26,19 @@ import ( ) type schemaDiffCmd struct { - OtherEndpoint url.URL `arg:"" help:"Other endpoint URL to compare against."` + OtherEndpoint url.URL `arg:"" help:"Other endpoint URL to compare against. If this is not specified then ftl will perform a diff against the local schema." optional:""` Color bool `help:"Enable colored output regardless of TTY."` } -func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL) error { - other, err := schemaForURL(ctx, d.OtherEndpoint) +func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL, projConfig projectconfig.Config) error { + var other *schema.Schema + var err error + if d.OtherEndpoint.String() == "" { + other, err = localSchema(ctx, projConfig) + } else { + other, err = schemaForURL(ctx, d.OtherEndpoint) + } + if err != nil { return fmt.Errorf("failed to get other schema: %w", err) } @@ -57,6 +68,61 @@ func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL) error { return nil } +func localSchema(ctx context.Context, moduleDirs projectconfig.Config) (*schema.Schema, error) { + pb := &schemapb.Schema{} + found := false + tried := "" + moduleFound := false + moduleTried := "" + for _, modulePaths := range moduleDirs.AbsModuleDirs() { + entries, err := os.ReadDir(modulePaths) + if err != nil { + return nil, fmt.Errorf("failed to list directory %w", err) + } + for _, module := range entries { + if !module.IsDir() { + continue + } + path := filepath.Join(modulePaths, module.Name(), ".ftl", "schema.pb") + mod, err := loadProtoSchema(path) + if err != nil { + moduleTried += fmt.Sprintf("failed to read schema file %s; did you run ftl build?", path) + } else { + println(path) + moduleFound = true + found = true + pb.Modules = append(pb.Modules, mod) + } + } + } + if !moduleFound { + tried += moduleTried + } + + if !found { + return nil, errors.New(tried) + } + + s, err := schema.FromProto(pb) + if err != nil { + return nil, fmt.Errorf("failed to parse local schema: %w", err) + } + return s, nil +} + +func loadProtoSchema(file string) (*schemapb.Module, error) { + content, err := os.ReadFile(file) + if err != nil { + return nil, err + } + module := &schemapb.Module{} + err = proto.Unmarshal(content, module) + if err != nil { + return nil, err + } + return module, nil +} + func schemaForURL(ctx context.Context, url url.URL) (*schema.Schema, error) { client := rpc.Dial(ftlv1connect.NewControllerServiceClient, url.String(), log.Error) resp, err := client.PullSchema(ctx, connect.NewRequest(&ftlv1.PullSchemaRequest{}))