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 9e1a2fc
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 4 deletions.
65 changes: 62 additions & 3 deletions cmd/ftl/cmd_schema_diff.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package main

import (
"context"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"

"connectrpc.com/connect"
"github.com/alecthomas/chroma/v2/quick"
Expand All @@ -17,24 +19,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 +83,39 @@ 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 := ""
moduleFound := false
moduleTried := ""
modules, err := buildengine.DiscoverModules(ctx, projectConfig.AbsModuleDirs())
for _, moduleSettings := range modules {
path := filepath.Join(moduleSettings.Config.Abs().Dir, ".ftl", "schema.pb")
mod, err := schema.ModuleFromProtoFile(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)
}

if err != nil {
return nil, fmt.Errorf("failed to parse local schema: %w", err)
}
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 9e1a2fc

Please sign in to comment.