From 0b56f8b4814c1c54c44ca707ba0950edafa4f7ec 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 | 86 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/cmd/ftl/cmd_schema_diff.go b/cmd/ftl/cmd_schema_diff.go index 0798609098..7155e278e4 100644 --- a/cmd/ftl/cmd_schema_diff.go +++ b/cmd/ftl/cmd_schema_diff.go @@ -2,9 +2,11 @@ package main import ( "context" + "errors" "fmt" "net/url" "os" + "path/filepath" "connectrpc.com/connect" "github.com/alecthomas/chroma/v2/quick" @@ -12,22 +14,33 @@ import ( "github.com/hexops/gotextdiff/myers" "github.com/hexops/gotextdiff/span" "github.com/mattn/go-isatty" + "google.golang.org/protobuf/proto" ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect" schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema" "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/common/projectconfig" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/rpc" ) 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 + sameModulesOnly := false + if d.OtherEndpoint.String() == "" { + sameModulesOnly = true + other, err = localSchema(projConfig) + } else { + other, err = schemaForURL(ctx, d.OtherEndpoint) + } + if err != nil { return fmt.Errorf("failed to get other schema: %w", err) } @@ -35,6 +48,19 @@ func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL) error { if err != nil { return fmt.Errorf("failed to get current schema: %w", err) } + if sameModulesOnly { + tempModules := current.Modules + current.Modules = []*schema.Module{} + moduleMap := map[string]*schema.Module{} + for _, i := range tempModules { + moduleMap[i.Name] = i + } + for _, i := range other.Modules { + if mod, ok := moduleMap[i.Name]; ok { + current.Modules = append(current.Modules, mod) + } + } + } edits := myers.ComputeEdits(span.URIFromPath(""), other.String(), current.String()) diff := fmt.Sprint(gotextdiff.ToUnified(d.OtherEndpoint.String(), currentURL.String(), other.String(), edits)) @@ -57,6 +83,60 @@ func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL) error { return nil } +func localSchema(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 { + 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, fmt.Errorf("failed to read schema file %w", err) + } + module := &schemapb.Module{} + err = proto.Unmarshal(content, module) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal schema %w", 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{}))