Skip to content

Commit

Permalink
feat: add local schema diff
Browse files Browse the repository at this point in the history
If no remote URI is specified for schema diff it will attempt to diff the local project

closes #2174
  • Loading branch information
stuartwdouglas committed Jul 29, 2024
1 parent 48bc91d commit 4d0d51b
Show file tree
Hide file tree
Showing 3 changed files with 96 additions and 4 deletions.
55 changes: 52 additions & 3 deletions cmd/ftl/cmd_schema_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"context"
"errors"
"fmt"
"net/url"
"os"
Expand All @@ -17,24 +18,48 @@ import (
"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/buildengine"
"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(ctx, projConfig)
} else {
other, err = schemaForURL(ctx, d.OtherEndpoint)
}

if err != nil {
return fmt.Errorf("failed to get other schema: %w", err)
}
current, err := schemaForURL(ctx, *currentURL)
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))
Expand All @@ -57,6 +82,30 @@ func (d *schemaDiffCmd) Run(ctx context.Context, currentURL *url.URL) error {
return nil
}

func localSchema(ctx context.Context, projectConfig projectconfig.Config) (*schema.Schema, error) {
pb := &schema.Schema{}
found := false
tried := ""
modules, err := buildengine.DiscoverModules(ctx, projectConfig.AbsModuleDirs())
if err != nil {
return nil, fmt.Errorf("failed to discover modules %w", err)
}
for _, moduleSettings := range modules {
mod, err := schema.ModuleFromProtoFile(moduleSettings.Config.Abs().Schema)
if err != nil {
tried += fmt.Sprintf(" failed to read schema file %s; did you run ftl build?", moduleSettings.Config.Abs().Schema)
} else {
found = true
pb.Modules = append(pb.Modules, mod)
}

}
if !found {
return nil, errors.New(tried)
}
return pb, 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{}))
Expand Down
27 changes: 27 additions & 0 deletions cmd/ftl/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,30 @@ func testImportExport(t *testing.T, object string) {
}),
)
}

func TestLocalSchemaDiff(t *testing.T) {
newVerb := `
//ftl:verb
func NewFunction(ctx context.Context, req TimeRequest) (TimeResponse, error) {
return TimeResponse{Time: time.Now()}, nil
}
`
Run(t, "",
CopyModule("time"),
Deploy("time"),
ExecWithOutput("ftl", []string{"schema", "diff"}, func(output string) {
assert.Equal(t, "", output)
}),
EditFile("time", func(bytes []byte) []byte {
s := string(bytes)
s += newVerb
return []byte(s)
}, "time.go"),
Build("time"),
// We exit with code 1 when there is a difference
ExpectError(
ExecWithOutput("ftl", []string{"schema", "diff"}, func(output string) {
assert.Contains(t, output, "- verb newFunction(time.TimeRequest) time.TimeResponse")
}), "exit status 1"),
)
}
18 changes: 17 additions & 1 deletion integration/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ import (
"github.com/kballard/go-shellquote"
"github.com/otiai10/copy"

"github.com/TBD54566975/scaffolder"

ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1"
schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema"
ftlexec "github.com/TBD54566975/ftl/internal/exec"
"github.com/TBD54566975/ftl/internal/log"
"github.com/TBD54566975/scaffolder"
)

// Scaffold a directory relative to the testdata directory to a directory relative to the working directory.
Expand Down Expand Up @@ -280,6 +281,21 @@ func WriteFile(path string, content []byte) Action {
}
}

// EditFile edits a file in a module
func EditFile(module string, editFunc func([]byte) []byte, path ...string) Action {
return func(t testing.TB, ic TestContext) {
parts := []string{ic.workDir, module}
parts = append(parts, path...)
file := filepath.Join(parts...)
Infof("Editing %s", file)
contents, err := os.ReadFile(file)
assert.NoError(t, err)
contents = editFunc(contents)
err = os.WriteFile(file, contents, os.FileMode(0))
assert.NoError(t, err)
}
}

type Obj map[string]any

// Call a verb.
Expand Down

0 comments on commit 4d0d51b

Please sign in to comment.