diff --git a/cmd/ftl/cmd_init.go b/cmd/ftl/cmd_init.go index e7f03634c4..fa75b7ea9b 100644 --- a/cmd/ftl/cmd_init.go +++ b/cmd/ftl/cmd_init.go @@ -30,7 +30,11 @@ func (i initGoCmd) Run(ctx context.Context, parent *initCmd) error { if i.Name == "" { i.Name = filepath.Base(i.Dir) } - if err := internal.Scaffold(goruntime.Files, i.Dir, i); err != nil { + err := internal.UnzipDir(goruntime.Files, i.Dir) + if err != nil { + return errors.WithStack(err) + } + if err := internal.Scaffold(i.Dir, i); err != nil { return errors.WithStack(err) } if !parent.Hermit { @@ -53,7 +57,11 @@ func (i *initKotlinCmd) Run(parent *initCmd) error { if i.Name == "" { i.Name = filepath.Base(i.Dir) } - if err := internal.Scaffold(kotlinruntime.Files, i.Dir, i); err != nil { + err := internal.UnzipDir(kotlinruntime.Files, i.Dir) + if err != nil { + return errors.WithStack(err) + } + if err := internal.Scaffold(i.Dir, i); err != nil { return errors.WithStack(err) } if !parent.Hermit { diff --git a/cmd/ftl/cmd_schema.go b/cmd/ftl/cmd_schema.go index d88669d95b..6ebf2bb35f 100644 --- a/cmd/ftl/cmd_schema.go +++ b/cmd/ftl/cmd_schema.go @@ -8,8 +8,11 @@ import ( "connectrpc.com/connect" "github.com/alecthomas/errors" "github.com/golang/protobuf/proto" + "github.com/otiai10/copy" + "github.com/TBD54566975/ftl/backend/common/log" "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/internal" ftlv1 "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1" "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/ftlv1connect" schemapb "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/schema" @@ -18,6 +21,7 @@ import ( type schemaCmd struct { Get getSchemaCmd `default:"" cmd:"" help:"Retrieve the cluster FTL schema."` Protobuf schemaProtobufCmd `cmd:"" help:"Generate protobuf schema mirroring the FTL schema structure."` + Generate schemaGenerateCmd `cmd:"" help:"Stream the schema from the cluster and generate files from the template."` } type schemaProtobufCmd struct{} @@ -72,3 +76,51 @@ func (g *getSchemaCmd) generateProto(resp *connect.ServerStreamForClient[ftlv1.P _, err = os.Stdout.Write(pb) return errors.WithStack(err) } + +type schemaGenerateCmd struct { + Template string `arg:"" help:"Template directory to use." type:"existingdir"` + Dest string `arg:"" help:"Destination directory to write files to (will be erased)."` +} + +func (s *schemaGenerateCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceClient) error { + stream, err := client.PullSchema(ctx, connect.NewRequest(&ftlv1.PullSchemaRequest{})) + if err != nil { + return errors.WithStack(err) + } + logger := log.FromContext(ctx) + modules := map[string]*schema.Module{} + regenerate := false + for stream.Receive() { + msg := stream.Msg() + switch msg.ChangeType { + case ftlv1.DeploymentChangeType_DEPLOYMENT_ADDED, ftlv1.DeploymentChangeType_DEPLOYMENT_CHANGED: + module, err := schema.ModuleFromProto(msg.Schema) + if err != nil { + return errors.Wrap(err, "invalid module schema") + } + modules[module.Name] = module + + case ftlv1.DeploymentChangeType_DEPLOYMENT_REMOVED: + delete(modules, msg.ModuleName) + } + if !msg.More { + regenerate = true + } + if !regenerate { + continue + } + if err := os.RemoveAll(s.Dest); err != nil { + return errors.WithStack(err) + } + for _, module := range modules { + if err := copy.Copy(s.Template, s.Dest); err != nil { + return errors.WithStack(err) + } + if err := internal.Scaffold(s.Dest, module); err != nil { + return errors.WithStack(err) + } + } + logger.Infof("Generated %d modules in %s", len(modules), s.Dest) + } + return nil +} diff --git a/internal/scaffolder.go b/internal/scaffolder.go index 486d5d60f7..25e8ab8024 100644 --- a/internal/scaffolder.go +++ b/internal/scaffolder.go @@ -1,10 +1,10 @@ package internal import ( - "archive/zip" "io/fs" "os" "path/filepath" + "reflect" "strings" "text/template" @@ -12,22 +12,15 @@ import ( "github.com/iancoleman/strcase" ) -// Scaffold copies the scaffolding files from the given source to the given -// destination, evaluating any templates against ctx in the process. +// Scaffold evaluates the scaffolding files at the given destination against +// ctx. // // Both paths and file contents are evaluated. // // The functions "snake", "camel", "lowerCamel", "kebab", "upper", and "lower" // are available. -func Scaffold(source *zip.Reader, destination string, ctx any) error { - err := UnzipDir(source, destination) - if err != nil { - return errors.WithStack(err) - } +func Scaffold(destination string, ctx any) error { return errors.WithStack(walkDir(destination, func(path string, d fs.DirEntry) error { - if err != nil { - return errors.WithStack(err) - } info, err := d.Info() if err != nil { return errors.WithStack(err) @@ -100,6 +93,9 @@ func evaluate(tmpl string, ctx any) (string, error) { "kebab": strcase.ToKebab, "upper": strings.ToUpper, "lower": strings.ToLower, + "typename": func(v any) string { + return reflect.Indirect(reflect.ValueOf(v)).Type().Name() + }, }, ).Parse(tmpl) if err != nil {