From 0aa5428b0bfddbaa2d8e3b8efd4fc3707ee10492 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Sun, 14 Jan 2024 19:39:06 +1100 Subject: [PATCH] feat: make `ftl build` et al work with Go (#775) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `ftl-go` has been deleted. - There must be a 1:1 mapping between FTL modules and Go modules. - Updated `ftl init go` to generate a valid module. - Updated all of the Go examples, including online-boutique, which can now be trivially started with `ftl dev ./examples/online-boutique` - The tooling is quite restrictive about code layout at the moment, but we can come back to that later. eg. Create, deploy and call a new module. ``` 🐚 ~/dev/ftl $ ftl init go examples echo2 info: Initializing FTL Go module echo2 in examples info: Running go mod tidy 🐚 ~/dev/ftl $ ftl deploy examples/echo2 info: Creating deployment for module echo2 info: Building Go module 'echo2' info: Generating external modules info: Extracting schema info: Generating main module info: Compiling info: Uploading 1/1 files info: Uploaded examples/echo2/_ftl/main as ba6f7f20ee5dac4e97197633348b5bb41e68e63c7e2d9b1da614d0a011b83872:main info: Created deployment echo2-0a1cebbfcc 🐚 ~/dev/ftl $ ftl call echo2.echo {"message":"Hello, anonymous!"} ``` --------- Co-authored-by: github-actions[bot] --- .gitignore | 3 +- Bitfile | 8 + Justfile | 4 +- backend/common/moduleconfig/config.go | 54 ++- backend/schema/builtin.go | 18 +- backend/schema/schema_test.go | 6 +- backend/schema/validate.go | 158 ++++--- bin/{.dbmate-2.10.0.pkg => .scc-3.1.0.pkg} | 0 bin/dbmate | 1 - bin/scc | 1 + cmd/ftl-go/main.go | 444 ------------------ cmd/ftl/cmd_build.go | 72 +-- cmd/ftl/cmd_build_go.go | 32 ++ cmd/ftl/cmd_build_kotlin.go | 73 +++ cmd/ftl/cmd_deploy.go | 4 +- cmd/ftl/cmd_dev.go | 35 +- cmd/ftl/cmd_init.go | 29 +- examples/echo/echo.go | 9 +- examples/echo/go.mod | 39 ++ examples/echo/go.sum | 131 ++++++ examples/kotlin/ftl-module-ad/ftl.toml | 2 + .../ftl-module-ad}/pom.xml | 0 .../src/main/kotlin/ftl/ad/Ad.kt | 57 +++ .../online-boutique/common/money/money.go | 2 +- examples/online-boutique/go.mod | 39 -- examples/online-boutique/go.sum | 131 ------ .../ad-kotlin/src/main/kotlin/ftl/ad/Ad.kt | 78 --- examples/online-boutique/services/ad/ftl.toml | 2 + examples/online-boutique/services/ad/go.mod | 12 + examples/online-boutique/services/ad/go.sum | 2 + .../online-boutique/services/cart/ftl.toml | 2 + examples/online-boutique/services/cart/go.mod | 9 + examples/online-boutique/services/cart/go.sum | 2 + .../services/checkout/checkout.go | 17 +- .../services/checkout/ftl.toml | 2 + .../online-boutique/services/checkout/go.mod | 46 ++ .../services/checkout}/go.sum | 0 .../services/currency/ftl.toml | 2 + .../online-boutique/services/currency/go.mod | 12 + .../online-boutique/services/currency/go.sum | 2 + .../services/echo/generated_ftl_module.go | 2 - .../online-boutique/services/payment/ftl.toml | 2 + .../online-boutique/services/payment/go.mod | 9 + .../online-boutique/services/payment/go.sum | 2 + .../services/payment/payment.go | 2 +- .../services/productcatalog/ftl.toml | 2 + .../services/productcatalog/go.mod | 9 + .../services/productcatalog/productcatalog.go | 3 +- .../services/recommendation/ftl.toml | 2 + .../services/recommendation}/go.mod | 6 +- .../services/recommendation/go.sum | 129 +++++ .../services/recommendation/recommendation.go | 7 +- .../services/shipping/ftl.toml | 2 + .../online-boutique/services/shipping/go.mod | 7 + .../services/shipping/shipping.go | 4 +- examples/time/ftl.toml | 3 + examples/time/go.mod | 3 + .../build-template/_ftl/go/main/go.mod.tmpl | 1 + .../_ftl/go/main/main.go} | 8 +- go-runtime/compile/build-template/go.mod | 3 + go-runtime/compile/build.go | 182 +++++++ go-runtime/compile/devel.go | 16 + .../_ftl/go/modules/go.mod.tmpl | 3 + .../external_module.go} | 8 +- .../compile/external-module-template/go.mod | 3 + .../compile/generate/external_module.go | 124 ----- .../compile/generate/external_module_test.go | 66 --- go-runtime/compile/generate/file.go | 21 - go-runtime/compile/generate/go.mod.tmpl | 8 - go-runtime/compile/generate/go.work.tmpl | 8 - go-runtime/compile/generate/gomod.go | 21 - go-runtime/compile/generate/gowork.go | 16 - go-runtime/compile/generate/main.go | 30 -- go-runtime/compile/generate/main_test.go | 44 -- go-runtime/compile/release.go | 31 ++ go-runtime/compile/schema.go | 3 + go-runtime/compile/schema_test.go | 6 +- go-runtime/devel.go | 4 +- go-runtime/release.go | 4 +- go-runtime/scaffolding/README.md | 17 - .../go.mod.tmpl | 2 +- .../{{ .Name | camel | lower }}.go.tmpl | 2 +- integration/integration_test.go | 2 +- internal/scaffold.go | 21 + kotlin-runtime/devel.go | 4 +- kotlin-runtime/release.go | 4 +- 86 files changed, 1128 insertions(+), 1268 deletions(-) rename bin/{.dbmate-2.10.0.pkg => .scc-3.1.0.pkg} (100%) delete mode 120000 bin/dbmate create mode 120000 bin/scc delete mode 100644 cmd/ftl-go/main.go create mode 100644 cmd/ftl/cmd_build_go.go create mode 100644 cmd/ftl/cmd_build_kotlin.go create mode 100644 examples/echo/go.mod create mode 100644 examples/echo/go.sum create mode 100644 examples/kotlin/ftl-module-ad/ftl.toml rename examples/{online-boutique/services/ad-kotlin => kotlin/ftl-module-ad}/pom.xml (100%) create mode 100644 examples/kotlin/ftl-module-ad/src/main/kotlin/ftl/ad/Ad.kt delete mode 100644 examples/online-boutique/services/ad-kotlin/src/main/kotlin/ftl/ad/Ad.kt create mode 100644 examples/online-boutique/services/ad/ftl.toml create mode 100644 examples/online-boutique/services/ad/go.mod create mode 100644 examples/online-boutique/services/ad/go.sum create mode 100644 examples/online-boutique/services/cart/ftl.toml create mode 100644 examples/online-boutique/services/cart/go.mod create mode 100644 examples/online-boutique/services/cart/go.sum create mode 100644 examples/online-boutique/services/checkout/ftl.toml create mode 100644 examples/online-boutique/services/checkout/go.mod rename examples/{ => online-boutique/services/checkout}/go.sum (100%) create mode 100644 examples/online-boutique/services/currency/ftl.toml create mode 100644 examples/online-boutique/services/currency/go.mod create mode 100644 examples/online-boutique/services/currency/go.sum delete mode 100644 examples/online-boutique/services/echo/generated_ftl_module.go create mode 100644 examples/online-boutique/services/payment/ftl.toml create mode 100644 examples/online-boutique/services/payment/go.mod create mode 100644 examples/online-boutique/services/payment/go.sum create mode 100644 examples/online-boutique/services/productcatalog/ftl.toml create mode 100644 examples/online-boutique/services/productcatalog/go.mod create mode 100644 examples/online-boutique/services/recommendation/ftl.toml rename examples/{ => online-boutique/services/recommendation}/go.mod (91%) create mode 100644 examples/online-boutique/services/recommendation/go.sum create mode 100644 examples/online-boutique/services/shipping/ftl.toml create mode 100644 examples/online-boutique/services/shipping/go.mod create mode 100644 examples/time/go.mod create mode 100644 go-runtime/compile/build-template/_ftl/go/main/go.mod.tmpl rename go-runtime/compile/{generate/main.go.tmpl => build-template/_ftl/go/main/main.go} (75%) create mode 100644 go-runtime/compile/build-template/go.mod create mode 100644 go-runtime/compile/build.go create mode 100644 go-runtime/compile/devel.go create mode 100644 go-runtime/compile/external-module-template/_ftl/go/modules/go.mod.tmpl rename go-runtime/compile/{generate/external_module.go.tmpl => external-module-template/_ftl/go/modules/{{ range .NonMainModules }}{{ push .Name . }}{{ end }}/external_module.go} (63%) create mode 100644 go-runtime/compile/external-module-template/go.mod delete mode 100644 go-runtime/compile/generate/external_module.go delete mode 100644 go-runtime/compile/generate/external_module_test.go delete mode 100644 go-runtime/compile/generate/file.go delete mode 100644 go-runtime/compile/generate/go.mod.tmpl delete mode 100644 go-runtime/compile/generate/go.work.tmpl delete mode 100644 go-runtime/compile/generate/gomod.go delete mode 100644 go-runtime/compile/generate/gowork.go delete mode 100644 go-runtime/compile/generate/main.go delete mode 100644 go-runtime/compile/generate/main_test.go create mode 100644 go-runtime/compile/release.go delete mode 100644 go-runtime/scaffolding/README.md rename go-runtime/scaffolding/{ => {{ .Name | camel | lower }}}/go.mod.tmpl (69%) create mode 100644 internal/scaffold.go diff --git a/.gitignore b/.gitignore index c296568dbd..1c767da164 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ target/ build/ deploy/ -.ftl*/ .DS_Store reflex.conf /logs/ @@ -14,3 +13,5 @@ reflex.conf generated_ftl_module.go *.zip dist/ +examples/**/_ftl/ +go.work* diff --git a/Bitfile b/Bitfile index b4d5ffc3dc..b02bd40347 100644 --- a/Bitfile +++ b/Bitfile @@ -90,6 +90,14 @@ go-runtime/scaffolding.zip: go-runtime/scaffolding/**/* cd go-runtime/scaffolding build: zip -q --symlinks -r ../scaffolding.zip . +go-runtime/compile/build-template.zip: go-runtime/compile/build-template/**/* + cd go-runtime/compile/build-template + build: zip -q --symlinks -r ../build-template.zip . + +go-runtime/compile/external-module-template.zip: go-runtime/compile/external-module-template/**/* + cd go-runtime/compile/external-module-template + build: zip -q --symlinks -r ../external-module-template.zip . + kotlin-runtime/scaffolding.zip: kotlin-runtime/scaffolding/**/* cd kotlin-runtime/scaffolding build: zip -q --symlinks -r ../scaffolding.zip . diff --git a/Justfile b/Justfile index 727b4cd862..9ffe7ac326 100644 --- a/Justfile +++ b/Justfile @@ -15,11 +15,11 @@ install-runtime-jar: # Deploy the Go time module deploy-time: - cd examples && ftl-go deploy time + ftl deploy examples/time # Deploy the Kotlin echo module deploy-echo-kotlin: - cd examples/echo-kotlin && mvn compile && ftl deploy target + ftl deploy examples/echo-kotlin regen-schema: bit protos/xyz/block/ftl/v1/schema/schema.proto diff --git a/backend/common/moduleconfig/config.go b/backend/common/moduleconfig/config.go index be707655bd..13a087c3f3 100644 --- a/backend/common/moduleconfig/config.go +++ b/backend/common/moduleconfig/config.go @@ -1,11 +1,21 @@ package moduleconfig import ( + "fmt" "path/filepath" + "strings" "github.com/BurntSushi/toml" ) +// GoConfig is language-specific configuration for Go modules. +type GoConfig struct { +} + +// KotlinConfig is language-specific configuration for Kotlin modules. +type KotlinConfig struct { +} + // ModuleConfig is the configuration for an FTL module. // // Module config files are currently TOML. @@ -17,6 +27,9 @@ type ModuleConfig struct { DeployDir string `toml:"deploy-dir"` Schema string `toml:"schema"` Watch []string `toml:"watch"` + + Go GoConfig `toml:"go,optional"` + Kotlin KotlinConfig `toml:"kotlin,optional"` } // LoadConfig from a directory. @@ -27,11 +40,16 @@ func LoadConfig(dir string) (ModuleConfig, error) { if err != nil { return ModuleConfig{}, err } - setConfigDefaults(&config) + if err := setConfigDefaults(dir, &config); err != nil { + return config, fmt.Errorf("%s: %w", path, err) + } return config, nil } -func setConfigDefaults(config *ModuleConfig) { +func setConfigDefaults(moduleDir string, config *ModuleConfig) error { + if config.Schema == "" { + config.Schema = "schema.pb" + } switch config.Language { case "kotlin": if config.Build == "" { @@ -43,18 +61,40 @@ func setConfigDefaults(config *ModuleConfig) { if len(config.Deploy) == 0 { config.Deploy = []string{"main", "classes", "dependency", "classpath.txt"} } - if config.Schema == "" { - config.Schema = "schema.pb" - } if len(config.Watch) == 0 { config.Watch = []string{"pom.xml", "src/**", "target/generated-sources"} } + case "go": if config.DeployDir == "" { - config.DeployDir = "build" + config.DeployDir = "_ftl" } if len(config.Deploy) == 0 { - config.Deploy = []string{"main", "schema.pb"} + config.Deploy = []string{"main"} + } + if len(config.Watch) == 0 { + config.Watch = []string{"**/*.go", "go.mod", "go.sum"} + } + } + + // Do some validation. + if !isBeneath(moduleDir, config.DeployDir) { + return fmt.Errorf("deploy-dir must be relative to the module directory") + } + for _, deploy := range config.Deploy { + if !isBeneath(moduleDir, deploy) { + return fmt.Errorf("deploy files must be relative to the module directory") } } + for _, watch := range config.Watch { + if !isBeneath(moduleDir, watch) { + return fmt.Errorf("watch files must be relative to the module directory") + } + } + return nil +} + +func isBeneath(moduleDir, path string) bool { + resolved := filepath.Clean(filepath.Join(moduleDir, path)) + return strings.HasPrefix(resolved, strings.TrimSuffix(moduleDir, "/")+"/") } diff --git a/backend/schema/builtin.go b/backend/schema/builtin.go index c97671ff1f..71b5acdebf 100644 --- a/backend/schema/builtin.go +++ b/backend/schema/builtin.go @@ -1,7 +1,11 @@ package schema +import ( + "golang.design/x/reflect" +) + // BuiltinsSource is the schema source code for built-in types. -var BuiltinsSource = ` +const BuiltinsSource = ` // Built-in types for FTL. builtin module builtin { // HTTP request structure used for HTTP ingress verbs. @@ -23,11 +27,15 @@ builtin module builtin { } ` -// Builtins returns a [Module] containing built-in types. -func Builtins() *Module { - module, err := ParseModuleString("builtins.ftl", BuiltinsSource) +var builtinsModuleParsed = func() *Module { + module, err := moduleParser.ParseString("", BuiltinsSource) if err != nil { - panic("failed to parse builtins: " + err.Error()) + panic(err) } return module +}() + +// Builtins returns a [Module] containing built-in types. +func Builtins() *Module { + return reflect.DeepCopy(builtinsModuleParsed) } diff --git a/backend/schema/schema_test.go b/backend/schema/schema_test.go index 23ffdb2607..36743f8324 100644 --- a/backend/schema/schema_test.go +++ b/backend/schema/schema_test.go @@ -218,8 +218,8 @@ func TestParsing(t *testing.T) { {name: "InvalidRequestRef", input: `module test { verb test(InvalidRequest) InvalidResponse}`, errors: []string{ - "1:25: reference to unknown data structure \"InvalidRequest\"", - "1:41: reference to unknown data structure \"InvalidResponse\""}}, + "1:25: unqualified reference to unknown data structure \"InvalidRequest\"", + "1:41: unqualified reference to unknown data structure \"InvalidResponse\""}}, {name: "InvalidDataRef", input: `module test { data Data { user user.User }}`, errors: []string{ @@ -234,7 +234,7 @@ func TestParsing(t *testing.T) { input: `module test { data Data {} calls verb }`, errors: []string{ "1:28: metadata \"calls verb\" is not valid on data structures", - "1:34: reference to unknown Verb \"verb\"", + "1:34: unqualified reference to unknown Verb \"verb\"", }}, {name: "KeywordAsName", input: `module int { data String { name String } verb verb(String) String }`, diff --git a/backend/schema/validate.go b/backend/schema/validate.go index 295c770bf5..582e2d9496 100644 --- a/backend/schema/validate.go +++ b/backend/schema/validate.go @@ -38,19 +38,16 @@ func Validate(schema *Schema) (*Schema, error) { modules := map[string]bool{} verbs := map[string]bool{} data := map[string]bool{} - verbRefs := []*VerbRef{} - dataRefs := []*DataRef{} merr := []error{} ingress := map[string]*Verb{} + verbRefs := []*VerbRef{} + dataRefs := []*DataRef{} // Inject builtins. builtins := Builtins() schema.Modules = slices.DeleteFunc(schema.Modules, func(m *Module) bool { return m.Name == builtins.Name }) schema.Modules = append([]*Module{builtins}, schema.Modules...) - // Map from reference to the module it is defined in. - localModule := map[*Ref]*Module{} - // Validate modules. for _, module := range schema.Modules { if _, seen := modules[module.Name]; seen { @@ -66,11 +63,9 @@ func Validate(schema *Schema) (*Schema, error) { err := Visit(module, func(n Node, next func() error) error { switch n := n.(type) { case *VerbRef: - localModule[(*Ref)(n)] = module verbRefs = append(verbRefs, n) case *DataRef: - localModule[(*Ref)(n)] = module dataRefs = append(dataRefs, n) case *Verb: @@ -105,91 +100,126 @@ func Validate(schema *Schema) (*Schema, error) { } for _, ref := range verbRefs { - if !resolveRef(localModule[(*Ref)(ref)], (*Ref)(ref), verbs) { + if ref.Module != "" && !verbs[ref.String()] { merr = append(merr, fmt.Errorf("%s: reference to unknown Verb %q", ref.Pos, ref)) } } for _, ref := range dataRefs { - if !resolveRef(localModule[(*Ref)(ref)], (*Ref)(ref), data) { + if ref.Module != "" && !data[ref.String()] { merr = append(merr, fmt.Errorf("%s: reference to unknown data structure %q", ref.Pos, ref)) } } return schema, errors.Join(merr...) } -// Try to resolve a relative reference (ie. one without a module). -// -// This first tries to resolve the reference against the local module, then against -// the builtins module. -func resolveRef(localModule *Module, ref *Ref, exist map[string]bool) bool { - if ref.Module != "" { - return exist[ref.String()] - } - for _, module := range []string{localModule.Name, "builtin"} { - clone := reflect.DeepCopy(ref) - clone.Module = module - if exist[clone.String()] { - ref.Module = module - return true - } - } - return false -} - var validNameRe = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`) // ValidateModule performs the subset of semantic validation possible on a single module. func ValidateModule(module *Module) error { verbs := map[string]bool{} data := map[string]bool{} + verbRefs := []*VerbRef{} + dataRefs := []*DataRef{} merr := []error{} + if !validNameRe.MatchString(module.Name) { merr = append(merr, fmt.Errorf("%s: module name %q is invalid", module.Pos, module.Name)) } if module.Builtin && module.Name != "builtin" { merr = append(merr, fmt.Errorf("%s: only the \"ftl\" module can be marked as builtin", module.Pos)) } - err := Visit(module, func(n Node, next func() error) error { - switch n := n.(type) { - case *Verb: - if !validNameRe.MatchString(n.Name) { - merr = append(merr, fmt.Errorf("%s: Verb name %q is invalid", n.Pos, n.Name)) - } - if _, ok := reservedIdentNames[n.Name]; ok { - merr = append(merr, fmt.Errorf("%s: Verb name %q is a reserved word", n.Pos, n.Name)) - } - if _, ok := verbs[n.Name]; ok { - merr = append(merr, fmt.Errorf("%s: duplicate Verb %q", n.Pos, n.Name)) - } - verbs[n.Name] = true + visit := func(module *Module) error { + return Visit(module, func(n Node, next func() error) error { + switch n := n.(type) { + case *VerbRef: + verbRefs = append(verbRefs, n) - case *Data: - if !validNameRe.MatchString(n.Name) { - merr = append(merr, fmt.Errorf("%s: data structure name %q is invalid", n.Pos, n.Name)) - } - if _, ok := reservedIdentNames[n.Name]; ok { - merr = append(merr, fmt.Errorf("%s: data structure name %q is a reserved word", n.Pos, n.Name)) - } - if _, ok := data[n.Name]; ok { - merr = append(merr, fmt.Errorf("%s: duplicate data structure %q", n.Pos, n.Name)) - } - for _, md := range n.Metadata { - if md, ok := md.(*MetadataCalls); ok { - merr = append(merr, fmt.Errorf("%s: metadata %q is not valid on data structures", md.Pos, strings.TrimSpace(md.String()))) + case *DataRef: + dataRefs = append(dataRefs, n) + + case *Verb: + if !validNameRe.MatchString(n.Name) { + merr = append(merr, fmt.Errorf("%s: Verb name %q is invalid", n.Pos, n.Name)) + } + if _, ok := reservedIdentNames[n.Name]; ok { + merr = append(merr, fmt.Errorf("%s: Verb name %q is a reserved word", n.Pos, n.Name)) + } + if _, ok := verbs[n.Name]; ok { + merr = append(merr, fmt.Errorf("%s: duplicate Verb %q", n.Pos, n.Name)) } + verbs[module.Name+"."+n.Name] = true + + case *Data: + if !validNameRe.MatchString(n.Name) { + merr = append(merr, fmt.Errorf("%s: data structure name %q is invalid", n.Pos, n.Name)) + } + if _, ok := reservedIdentNames[n.Name]; ok { + merr = append(merr, fmt.Errorf("%s: data structure name %q is a reserved word", n.Pos, n.Name)) + } + if _, ok := data[n.Name]; ok { + merr = append(merr, fmt.Errorf("%s: duplicate data structure %q", n.Pos, n.Name)) + } + for _, md := range n.Metadata { + if md, ok := md.(*MetadataCalls); ok { + merr = append(merr, fmt.Errorf("%s: metadata %q is not valid on data structures", md.Pos, strings.TrimSpace(md.String()))) + } + } + data[module.Name+"."+n.Name] = true + + case *Array, *Bool, *Database, *Field, *Float, *Int, + *Time, *Map, *Module, *Schema, *String, *Bytes, + *MetadataCalls, *MetadataDatabases, *MetadataIngress, IngressPathComponent, + *IngressPathLiteral, *IngressPathParameter, *Optional, + *SourceRef, *SinkRef, *Unit: + + case Type, Metadata, Decl: // Union types. } + return next() + }) + } + + // Collect all the builtin verbs and data structures so that unqualified refs in our module can be resolved. + builtins := Builtins() + if err := visit(builtins); err != nil { + return err + } + // Clear collected refs because we don't care about any that might be in the builtins. + verbRefs = nil + dataRefs = nil + + // Collect verbs and data structures from this module. + if err := visit(module); err != nil { + return err + } - case *Array, *Bool, *DataRef, *Database, *Field, *Float, *Int, - *Time, *Map, *Module, *Schema, *String, *Bytes, *VerbRef, - *MetadataCalls, *MetadataDatabases, *MetadataIngress, IngressPathComponent, - *IngressPathLiteral, *IngressPathParameter, *Optional, - *SourceRef, *SinkRef, *Unit: - case Type, Metadata, Decl: // Union sql. + for _, ref := range verbRefs { + if !resolveLocalRef(module, (*Ref)(ref), verbs) { + merr = append(merr, fmt.Errorf("%s: unqualified reference to unknown Verb %q", ref.Pos, ref)) + } + } + for _, ref := range dataRefs { + if !resolveLocalRef(module, (*Ref)(ref), data) { + merr = append(merr, fmt.Errorf("%s: unqualified reference to unknown data structure %q", ref.Pos, ref)) } - return next() - }) - if err != nil { - merr = append(merr, err) } return errors.Join(merr...) } + +// Try to resolve a relative reference (ie. one without a module). +// +// This first tries to resolve the reference against the local module, then against +// the builtins module. +func resolveLocalRef(localModule *Module, ref *Ref, exist map[string]bool) bool { + if ref.Module != "" { + return true + } + for _, module := range []string{localModule.Name, "builtin"} { + clone := reflect.DeepCopy(ref) + clone.Module = module + if exist[clone.String()] { + ref.Module = module + return true + } + } + return false +} diff --git a/bin/.dbmate-2.10.0.pkg b/bin/.scc-3.1.0.pkg similarity index 100% rename from bin/.dbmate-2.10.0.pkg rename to bin/.scc-3.1.0.pkg diff --git a/bin/dbmate b/bin/dbmate deleted file mode 120000 index 880db848ae..0000000000 --- a/bin/dbmate +++ /dev/null @@ -1 +0,0 @@ -.dbmate-2.10.0.pkg \ No newline at end of file diff --git a/bin/scc b/bin/scc new file mode 120000 index 0000000000..7b7e58c117 --- /dev/null +++ b/bin/scc @@ -0,0 +1 @@ +.scc-3.1.0.pkg \ No newline at end of file diff --git a/cmd/ftl-go/main.go b/cmd/ftl-go/main.go deleted file mode 100644 index 682f22d1c4..0000000000 --- a/cmd/ftl-go/main.go +++ /dev/null @@ -1,444 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "io" - "os" - "path" - "path/filepath" - "runtime" - "strings" - "time" - - "connectrpc.com/connect" - "github.com/alecthomas/kong" - "github.com/radovskyb/watcher" - "golang.org/x/mod/modfile" - "golang.org/x/sync/errgroup" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" - - "github.com/TBD54566975/ftl/backend/common/exec" - "github.com/TBD54566975/ftl/backend/common/log" - "github.com/TBD54566975/ftl/backend/common/model" - "github.com/TBD54566975/ftl/backend/common/rpc" - "github.com/TBD54566975/ftl/backend/common/sha256" - "github.com/TBD54566975/ftl/backend/common/slices" - "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/go-runtime/compile" - "github.com/TBD54566975/ftl/go-runtime/compile/generate" - 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" -) - -type watchCmd struct{} - -func (w *watchCmd) Run(ctx context.Context, c *cli, client ftlv1connect.ControllerServiceClient, bctx BuildContext) error { - err := buildRemoteModules(ctx, client, bctx) - if err != nil { - return err - } - - wg, ctx := errgroup.WithContext(ctx) - wg.Go(func() error { return pullModules(ctx, client, bctx) }) - wg.Go(func() error { return pushModules(ctx, client, c.WatchFrequency, bctx, c.FTL) }) - - return wg.Wait() -} - -type deployCmd struct { - Name string `arg:"" required:"" help:"Name of module to deploy."` -} - -func (d *deployCmd) Run(ctx context.Context, c *cli, client ftlv1connect.ControllerServiceClient, bctx BuildContext) error { - return pushModule(ctx, client, c.FTL, filepath.Join(c.Root, d.Name), bctx) -} - -type cli struct { - LogConfig log.Config `embed:""` - FTL string `env:"FTL_ENDPOINT" help:"FTL endpoint to connect to." default:"http://localhost:8892"` - WatchFrequency time.Duration `short:"w" default:"500ms" help:"Frequency to watch for changes to local FTL modules."` - Root string `short:"r" type:"existingdir" help:"Root directory to sync FTL modules into." default:"."` - OS string `short:"o" help:"OS to build for." env:"GOOS"` - Arch string `short:"a" help:"Architecture to build for." env:"GOARCH"` - - Watch watchCmd `cmd:"" default:"" help:"Watch for and rebuild local and remote FTL modules."` - Deploy deployCmd `cmd:"" help:"Deploy a local FTL module."` -} - -type BuildContext struct { - OS string - Arch string - Root string - ImportRoot -} - -func main() { - c := &cli{} - kctx := kong.Parse(c) - - client := rpc.Dial(ftlv1connect.NewControllerServiceClient, c.FTL, log.Warn) - logger := log.Configure(os.Stderr, c.LogConfig) - ctx := log.ContextWithLogger(context.Background(), logger) - - importRoot, err := findImportRoot(c.Root) - kctx.FatalIfErrorf(err) - - bctx := BuildContext{ - OS: c.OS, - Arch: c.Arch, - Root: c.Root, - ImportRoot: importRoot, - } - - kctx.Bind(bctx) - kctx.BindTo(ctx, (*context.Context)(nil)) - kctx.BindTo(client, (*ftlv1connect.ControllerServiceClient)(nil)) - err = kctx.Run() - kctx.FatalIfErrorf(err) - -} - -type ImportRoot struct { - Module *modfile.File - GoModuleDir string - FTLBasePkg string - FTLBaseDir string -} - -// "prefix" is the import prefix for FTL modules. -func findImportRoot(root string) (importRoot ImportRoot, err error) { - modDir := root - for { - if modDir == "/" { - return ImportRoot{}, fmt.Errorf("no go.mod file found") - } - if _, err := os.Stat(filepath.Join(modDir, "go.mod")); err == nil { - break - } - modDir = filepath.Dir(modDir) - } - modFile := filepath.Join(modDir, "go.mod") - data, err := os.ReadFile(modFile) - if err != nil { - return ImportRoot{}, fmt.Errorf("%s: %w", "failed to read go.mod", err) - } - module, err := modfile.Parse(modFile, data, nil) - if err != nil { - return ImportRoot{}, fmt.Errorf("%s: %w", "failed to parse go.mod", err) - } - return ImportRoot{ - Module: module, - GoModuleDir: modDir, - FTLBasePkg: path.Join(module.Module.Mod.Path, strings.TrimPrefix(strings.TrimPrefix(root, modDir), "/")), - FTLBaseDir: root, - }, nil -} - -func pushModules(ctx context.Context, client ftlv1connect.ControllerServiceClient, watchFrequency time.Duration, bctx BuildContext, endpoint string) error { - logger := log.FromContext(ctx) - entries, err := os.ReadDir(bctx.Root) - if err != nil { - return fmt.Errorf("%s: %w", "failed to read root directory", err) - } - for _, entry := range entries { - if !entry.IsDir() { - continue - } - dir := filepath.Join(bctx.Root, entry.Name()) - if _, err := os.Stat(filepath.Join(dir, "generated_ftl_module.go")); err == nil { - continue - } - - logger.Infof("Pushing local FTL module %q", entry.Name()) - err := pushModule(ctx, client, endpoint, dir, bctx) - if err != nil { - if connect.CodeOf(err) == connect.CodeAlreadyExists { - logger.Infof("Module %q already exists, skipping", entry.Name()) - continue - } - logger.Warnf("Failed to push module %q, continuing: %s", entry.Name(), err) - } - } - - logger.Infof("Watching %s for changes", bctx.Root) - wg, ctx := errgroup.WithContext(ctx) - watch := watcher.New() - defer watch.Close() - wg.Go(func() error { - for { - select { - case <-ctx.Done(): - return nil - - case event := <-watch.Event: - if event.IsDir() || - strings.Contains(event.Path, "/.") || - strings.Contains(event.Path, "/generated_ftl_module.go") || - !strings.HasPrefix(event.Path, bctx.Root) || - strings.Contains(event.Path, "/build/") { - continue - } - dir := strings.TrimPrefix(event.Path, bctx.Root+"/") - dir = filepath.Join(bctx.Root, strings.Split(dir, "/")[0]) - logger.Infof("Detected change to %s, pushing module", dir) - - err := pushModule(ctx, client, endpoint, dir, bctx) - if err != nil { - logger.Errorf(err, "failed to rebuild module") - } - - case err := <-watch.Error: - return fmt.Errorf("%s: %w", "watch error", err) - } - } - }) - err = watch.AddRecursive(bctx.Root) - if err != nil { - return fmt.Errorf("%s: %w", "failed to watch root directory", err) - } - wg.Go(func() error { return watch.Start(watchFrequency) }) - return wg.Wait() -} - -func pushModule(ctx context.Context, client ftlv1connect.ControllerServiceClient, endpoint string, dir string, bctx BuildContext) error { - logger := log.FromContext(ctx) - - sch, err := compile.ExtractModuleSchema(dir) - if err != nil { - return fmt.Errorf("failed to extract schema for module %q: %w", dir, err) - } - - if !hasVerbs(sch) { - logger.Warnf("No verbs found in module %q, ignored", dir) - return nil - } - - tmpDir, err := generateBuildDir(dir, sch, bctx) - if err != nil { - return fmt.Errorf("%s: %w", "failed to generate build directory", err) - } - - logger.Infof("Building module %s in %s", sch.Name, tmpDir) - cmd := exec.Command(ctx, log.Info, tmpDir, "go", "build", "-o", "main", "-trimpath", "-ldflags=-s -w -buildid=", ".") - cmd.Env = append(cmd.Environ(), "GOOS="+bctx.OS, "GOARCH="+bctx.Arch, "CGO_ENABLED=0") - if err := cmd.Run(); err != nil { - return fmt.Errorf("%s: %w", "failed to build module", err) - } - dest := filepath.Join(tmpDir, "main") - - logger.Infof("Preparing deployment") - digest, err := sha256.SumFile(dest) - if err != nil { - return err - } - r, err := os.Open(dest) - if err != nil { - return err - } - deployment := &model.Deployment{ - Language: "go", - Name: model.NewDeploymentName(sch.Name), - Schema: sch, - Module: sch.Name, - Artefacts: []*model.Artefact{ - {Path: "main", Executable: true, Digest: digest, Content: r}, - }, - } - defer deployment.Close() - - err = uploadArtefacts(ctx, client, deployment) - if err != nil { - return fmt.Errorf("%s: %w", "failed to upload artefacts", err) - } - - err = deploy(ctx, client, deployment, endpoint) - if err != nil { - return fmt.Errorf("%s: %w", "failed to deploy", err) - } - return nil -} - -func deploy(ctx context.Context, client ftlv1connect.ControllerServiceClient, deployment *model.Deployment, endpoint string) error { - logger := log.FromContext(ctx) - module := deployment.Schema.ToProto().(*schemapb.Module) //nolint:forcetypeassert - module.Runtime = &schemapb.ModuleRuntime{ - Language: deployment.Language, - CreateTime: timestamppb.Now(), - MinReplicas: 1, - } - labels, err := structpb.NewStruct(map[string]any{ - "os": runtime.GOOS, - "arch": runtime.GOARCH, - "languages": []any{"go"}, - }) - if err != nil { - return fmt.Errorf("%s: %w", "failed to create labels", err) - } - cdResp, err := client.CreateDeployment(ctx, connect.NewRequest(&ftlv1.CreateDeploymentRequest{ - Schema: module, - Labels: labels, - Artefacts: slices.Map(deployment.Artefacts, func(t *model.Artefact) *ftlv1.DeploymentArtefact { - return &ftlv1.DeploymentArtefact{ - Digest: t.Digest.String(), - Path: t.Path, - Executable: t.Executable, - } - }), - })) - if err != nil { - return fmt.Errorf("%s: %w", "failed to create deployment", err) - } - logger.Infof("Created deployment %s (%s/deployments/%s)", cdResp.Msg.DeploymentName, endpoint, cdResp.Msg.DeploymentName) - _, err = client.ReplaceDeploy(ctx, connect.NewRequest(&ftlv1.ReplaceDeployRequest{ - DeploymentName: cdResp.Msg.DeploymentName, - MinReplicas: 1, - })) - if err != nil { - return fmt.Errorf("failed to deploy %q: %w", cdResp.Msg.DeploymentName, err) - } - return nil -} - -func uploadArtefacts(ctx context.Context, client ftlv1connect.ControllerServiceClient, deployment *model.Deployment) error { - logger := log.FromContext(ctx) - digests := slices.Map(deployment.Artefacts, func(t *model.Artefact) string { return t.Digest.String() }) - gadResp, err := client.GetArtefactDiffs(ctx, connect.NewRequest(&ftlv1.GetArtefactDiffsRequest{ClientDigests: digests})) - if err != nil { - return err - } - artefactsToUpload := slices.Filter(deployment.Artefacts, func(t *model.Artefact) bool { - for _, missing := range gadResp.Msg.MissingDigests { - if t.Digest.String() == missing { - return true - } - } - return false - }) - for _, artefact := range artefactsToUpload { - content, err := io.ReadAll(artefact.Content) - if err != nil { - return fmt.Errorf("failed to read artefact %q: %w", artefact.Path, err) - } - _, err = client.UploadArtefact(ctx, connect.NewRequest(&ftlv1.UploadArtefactRequest{Content: content})) - if err != nil { - return fmt.Errorf("failed to upload artefact %q: %w", artefact.Path, err) - } - logger.Infof("Uploaded %s:%s", artefact.Digest, artefact.Path) - } - return nil -} - -func generateBuildDir(dir string, sch *schema.Module, bctx BuildContext) (string, error) { - cacheDir, err := os.UserCacheDir() - if err != nil { - return "", fmt.Errorf("%s: %w", "failed to get user cache directory", err) - } - dirHash := sha256.Sum([]byte(dir)) - tmpDir := filepath.Join(cacheDir, "ftl-go", "build", fmt.Sprintf("%s-%s", sch.Name, dirHash)) - if err := os.MkdirAll(tmpDir, 0750); err != nil { - return "", fmt.Errorf("%s: %w", "failed to create build directory", err) - } - mainFile := filepath.Join(tmpDir, "main.go") - if err := generate.File(mainFile, bctx.FTLBasePkg, generate.Main, sch); err != nil { - return "", fmt.Errorf("%s: %w", "failed to generate main.go", err) - } - goWorkFile := filepath.Join(tmpDir, "go.work") - if err := generate.File(goWorkFile, bctx.FTLBasePkg, generate.GenerateGoWork, []string{ - bctx.GoModuleDir, - }); err != nil { - return "", fmt.Errorf("%s: %w", "failed to generate go.work", err) - } - goModFile := filepath.Join(tmpDir, "go.mod") - replace := map[string]string{ - bctx.Module.Module.Mod.Path: bctx.GoModuleDir, - } - if err := generate.File(goModFile, bctx.FTLBasePkg, generate.GenerateGoMod, generate.GoModConfig{ - Replace: replace, - }); err != nil { - return "", fmt.Errorf("%s: %w", "failed to generate go.mod", err) - } - return tmpDir, nil -} - -func hasVerbs(sch *schema.Module) bool { - for _, decl := range sch.Decls { - if _, ok := decl.(*schema.Verb); ok { - return true - } - } - return false -} - -func pullModules(ctx context.Context, client ftlv1connect.ControllerServiceClient, bctx BuildContext) error { - resp, err := client.PullSchema(ctx, connect.NewRequest(&ftlv1.PullSchemaRequest{})) - if err != nil { - return fmt.Errorf("%s: %w", "failed to pull schema", err) - } - for resp.Receive() { - msg := resp.Msg() - err = generateModuleFromSchema(ctx, msg.Schema, bctx) - if err != nil { - return fmt.Errorf("%s: %w", "failed to sync module", err) - } - } - if err := resp.Err(); err != nil { - return fmt.Errorf("%s: %w", "failed to pull schema", err) - } - return nil -} - -func buildRemoteModules(ctx context.Context, client ftlv1connect.ControllerServiceClient, bctx BuildContext) error { - fullSchema, err := client.GetSchema(ctx, connect.NewRequest(&ftlv1.GetSchemaRequest{})) - if err != nil { - return fmt.Errorf("%s: %w", "failed to retrieve schema", err) - } - for _, module := range fullSchema.Msg.Schema.Modules { - err := generateModuleFromSchema(ctx, module, bctx) - if err != nil { - return fmt.Errorf("%s: %w", "failed to generate module", err) - } - } - return err -} - -func generateModuleFromSchema(ctx context.Context, msg *schemapb.Module, bctx BuildContext) error { - sch, err := schema.ModuleFromProto(msg) - if err != nil { - return fmt.Errorf("%s: %w", "failed to parse schema", err) - } - dir := filepath.Join(bctx.Root, sch.Name) - if _, err := os.Stat(dir); err == nil { - if _, err = os.Stat(filepath.Join(dir, "generated_ftl_module.go")); errors.Is(err, os.ErrNotExist) { - return nil - } - } - if err := generateModule(ctx, dir, sch, bctx); err != nil { - return fmt.Errorf("%s: %w", "failed to generate module", err) - } - return nil -} - -func generateModule(ctx context.Context, dir string, sch *schema.Module, bctx BuildContext) error { - logger := log.FromContext(ctx) - logger.Infof("Generating stubs for FTL module %s", sch.Name) - if err := os.MkdirAll(dir, 0750); err != nil { - return fmt.Errorf("%s: %w", "failed to create module directory", err) - } - w, err := os.Create(filepath.Join(dir, "generated_ftl_module.go~")) - if err != nil { - return fmt.Errorf("%s: %w", "failed to create stub file", err) - } - defer w.Close() //nolint:gosec - defer os.Remove(w.Name()) - if err := generate.ExternalModule(w, sch, bctx.FTLBasePkg); err != nil { - return fmt.Errorf("%s: %w", "failed to generate stubs", err) - } - if err := os.Rename(w.Name(), strings.TrimRight(w.Name(), "~")); err != nil { - return fmt.Errorf("%s: %w", "failed to rename stub file", err) - } - return nil -} diff --git a/cmd/ftl/cmd_build.go b/cmd/ftl/cmd_build.go index 3c25c6edcb..10c1b7d179 100644 --- a/cmd/ftl/cmd_build.go +++ b/cmd/ftl/cmd_build.go @@ -3,22 +3,16 @@ package main import ( "context" "fmt" - "os" - "path/filepath" - "github.com/beevik/etree" - - "github.com/TBD54566975/ftl" - "github.com/TBD54566975/ftl/backend/common/exec" - "github.com/TBD54566975/ftl/backend/common/log" "github.com/TBD54566975/ftl/backend/common/moduleconfig" + "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/ftlv1connect" ) type buildCmd struct { ModuleDir string `arg:"" help:"Directory containing ftl.toml" type:"existingdir" default:"."` } -func (b *buildCmd) Run(ctx context.Context) error { +func (b *buildCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceClient) error { // Load the TOML file. config, err := moduleconfig.LoadConfig(b.ModuleDir) if err != nil { @@ -28,65 +22,11 @@ func (b *buildCmd) Run(ctx context.Context) error { switch config.Language { case "kotlin": return b.buildKotlin(ctx, config) - default: - return fmt.Errorf("unable to build. unknown language %q", config.Language) - } -} -func (b *buildCmd) buildKotlin(ctx context.Context, config moduleconfig.ModuleConfig) error { - logger := log.FromContext(ctx) - - logger.Infof("Building kotlin module '%s'", config.Module) - - if err := setPomProperties(logger, filepath.Join(b.ModuleDir, "..")); err != nil { - return fmt.Errorf("unable to update ftl.version in %s: %w", b.ModuleDir, err) - } - - logger.Infof("Using build command '%s'", config.Build) - err := exec.Command(ctx, logger.GetLevel(), b.ModuleDir, "bash", "-c", config.Build).Run() - if err != nil { - return err - } - - return nil -} + case "go": + return b.buildGo(ctx, config, client) -func setPomProperties(logger *log.Logger, baseDir string) error { - ftlVersion := ftl.Version - if ftlVersion == "dev" { - ftlVersion = "1.0-SNAPSHOT" - } - - ftlEndpoint := os.Getenv("FTL_ENDPOINT") - if ftlEndpoint == "" { - ftlEndpoint = "http://127.0.0.1:8892" - } - - pomFile := filepath.Clean(filepath.Join(baseDir, "pom.xml")) - - logger.Infof("Setting ftl.version in %s to %s", pomFile, ftlVersion) - - tree := etree.NewDocument() - if err := tree.ReadFromFile(pomFile); err != nil { - return fmt.Errorf("unable to read %s: %w", pomFile, err) - } - root := tree.Root() - properties := root.SelectElement("properties") - if properties == nil { - return fmt.Errorf("unable to find in %s", pomFile) - } - version := properties.SelectElement("ftl.version") - if version == nil { - return fmt.Errorf("unable to find / in %s", pomFile) - } - version.SetText(ftlVersion) - - endpoint := properties.SelectElement("ftlEndpoint") - if endpoint == nil { - logger.Warnf("unable to find / in %s", pomFile) - } else { - endpoint.SetText(ftlEndpoint) + default: + return fmt.Errorf("unable to build, unknown language %q", config.Language) } - - return tree.WriteToFile(pomFile) } diff --git a/cmd/ftl/cmd_build_go.go b/cmd/ftl/cmd_build_go.go new file mode 100644 index 0000000000..f68e728127 --- /dev/null +++ b/cmd/ftl/cmd_build_go.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + "fmt" + + "connectrpc.com/connect" + + "github.com/TBD54566975/ftl/backend/common/log" + "github.com/TBD54566975/ftl/backend/common/moduleconfig" + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/go-runtime/compile" + ftlv1 "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1" + "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/ftlv1connect" +) + +func (b *buildCmd) buildGo(ctx context.Context, config moduleconfig.ModuleConfig, client ftlv1connect.ControllerServiceClient) error { + logger := log.FromContext(ctx) + logger.Infof("Building Go module '%s'", config.Module) + resp, err := client.GetSchema(ctx, connect.NewRequest(&ftlv1.GetSchemaRequest{})) + if err != nil { + return err + } + sch, err := schema.FromProto(resp.Msg.Schema) + if err != nil { + return err + } + if err := compile.Build(ctx, b.ModuleDir, sch); err != nil { + return fmt.Errorf("failed to build module: %w", err) + } + return nil +} diff --git a/cmd/ftl/cmd_build_kotlin.go b/cmd/ftl/cmd_build_kotlin.go new file mode 100644 index 0000000000..4ba700dfd4 --- /dev/null +++ b/cmd/ftl/cmd_build_kotlin.go @@ -0,0 +1,73 @@ +package main + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/beevik/etree" + + "github.com/TBD54566975/ftl" + "github.com/TBD54566975/ftl/backend/common/exec" + "github.com/TBD54566975/ftl/backend/common/log" + "github.com/TBD54566975/ftl/backend/common/moduleconfig" +) + +func (b *buildCmd) buildKotlin(ctx context.Context, config moduleconfig.ModuleConfig) error { + logger := log.FromContext(ctx) + + logger.Infof("Building kotlin module '%s'", config.Module) + + if err := setPomProperties(logger, filepath.Join(b.ModuleDir, "..")); err != nil { + return fmt.Errorf("unable to update ftl.version in %s: %w", b.ModuleDir, err) + } + + logger.Infof("Using build command '%s'", config.Build) + err := exec.Command(ctx, logger.GetLevel(), b.ModuleDir, "bash", "-c", config.Build).Run() + if err != nil { + return err + } + + return nil +} + +func setPomProperties(logger *log.Logger, baseDir string) error { + ftlVersion := ftl.Version + if ftlVersion == "dev" { + ftlVersion = "1.0-SNAPSHOT" + } + + ftlEndpoint := os.Getenv("FTL_ENDPOINT") + if ftlEndpoint == "" { + ftlEndpoint = "http://127.0.0.1:8892" + } + + pomFile := filepath.Clean(filepath.Join(baseDir, "pom.xml")) + + logger.Infof("Setting ftl.version in %s to %s", pomFile, ftlVersion) + + tree := etree.NewDocument() + if err := tree.ReadFromFile(pomFile); err != nil { + return fmt.Errorf("unable to read %s: %w", pomFile, err) + } + root := tree.Root() + properties := root.SelectElement("properties") + if properties == nil { + return fmt.Errorf("unable to find in %s", pomFile) + } + version := properties.SelectElement("ftl.version") + if version == nil { + return fmt.Errorf("unable to find / in %s", pomFile) + } + version.SetText(ftlVersion) + + endpoint := properties.SelectElement("ftlEndpoint") + if endpoint == nil { + logger.Warnf("unable to find / in %s", pomFile) + } else { + endpoint.SetText(ftlEndpoint) + } + + return tree.WriteToFile(pomFile) +} diff --git a/cmd/ftl/cmd_deploy.go b/cmd/ftl/cmd_deploy.go index abcf547320..1bf083fcf5 100644 --- a/cmd/ftl/cmd_deploy.go +++ b/cmd/ftl/cmd_deploy.go @@ -41,7 +41,7 @@ func (d *deployCmd) Run(ctx context.Context, client ftlv1connect.ControllerServi } build := buildCmd{ModuleDir: d.ModuleDir} - err = build.Run(ctx) + err = build.Run(ctx, client) if err != nil { return err } @@ -64,7 +64,7 @@ func (d *deployCmd) Run(ctx context.Context, client ftlv1connect.ControllerServi module, err := d.loadProtoSchema(deployDir, config) if err != nil { - return err + return fmt.Errorf("failed to load protobuf schema from %q: %w", config.Schema, err) } logger.Infof("Uploading %d/%d files", len(gadResp.Msg.MissingDigests), len(files)) diff --git a/cmd/ftl/cmd_dev.go b/cmd/ftl/cmd_dev.go index 181d5688f7..2d7c85a215 100644 --- a/cmd/ftl/cmd_dev.go +++ b/cmd/ftl/cmd_dev.go @@ -13,6 +13,7 @@ import ( "github.com/bmatcuk/doublestar/v4" + "github.com/TBD54566975/ftl/backend/common/exec" "github.com/TBD54566975/ftl/backend/common/log" "github.com/TBD54566975/ftl/backend/common/moduleconfig" "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/ftlv1connect" @@ -41,7 +42,7 @@ func (d *devCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceC for { iterationStartTime := time.Now() - tomls, err := d.getTomls() + tomls, err := d.getTomls(ctx) if err != nil { return err } @@ -50,7 +51,7 @@ func (d *devCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceC for dir := range d.modules { currentModule := d.modules[dir] - err := d.updateFileInfo(dir) + err := d.updateFileInfo(ctx, dir) if err != nil { return err } @@ -77,9 +78,9 @@ func (d *devCmd) Run(ctx context.Context, client ftlv1connect.ControllerServiceC } } -func (d *devCmd) getTomls() ([]string, error) { +func (d *devCmd) getTomls(ctx context.Context) ([]string, error) { baseDir := d.BaseDir - ignores := loadGitIgnore(os.DirFS(baseDir), ".") + ignores := initGitIgnore(ctx, baseDir) tomls := []string{} err := walkDir(baseDir, ignores, func(srcPath string, d fs.DirEntry) error { @@ -120,13 +121,13 @@ func (d *devCmd) addOrRemoveModules(tomls []string) { } } -func (d *devCmd) updateFileInfo(dir string) error { +func (d *devCmd) updateFileInfo(ctx context.Context, dir string) error { config, err := moduleconfig.LoadConfig(dir) if err != nil { return err } - ignores := loadGitIgnore(os.DirFS(dir), ".") + ignores := initGitIgnore(ctx, dir) d.modules[dir] = moduleFolderInfo{} err = walkDir(dir, ignores, func(srcPath string, entry fs.DirEntry) error { @@ -221,7 +222,7 @@ func walkDir(dir string, ignores []string, fn func(path string, d fs.DirEntry) e // Then, recurse into subdirectories for _, dirEntry := range dirs { dirPath := filepath.Join(dir, dirEntry.Name()) - ignores = append(ignores, loadGitIgnore(os.DirFS(dirPath), ".")...) + ignores = append(ignores, loadGitIgnore(dirPath)...) if err := walkDir(dirPath, ignores, fn); err != nil { if errors.Is(err, errSkip) { return errSkip // Propagate errSkip upwards to stop this branch of recursion @@ -232,15 +233,31 @@ func walkDir(dir string, ignores []string, fn func(path string, d fs.DirEntry) e return nil } -func loadGitIgnore(root fs.FS, dir string) []string { +func initGitIgnore(ctx context.Context, dir string) []string { ignore := []string{ "**/.*", "**/.*/**", } - r, err := root.Open(path.Join(dir, ".gitignore")) + home, err := os.UserHomeDir() + if err == nil { + ignore = append(ignore, loadGitIgnore(home)...) + } + gitRootBytes, err := exec.Capture(ctx, dir, "git", "rev-parse", "--show-toplevel") + if err == nil { + gitRoot := strings.TrimSpace(string(gitRootBytes)) + for current := dir; strings.HasPrefix(current, gitRoot); current = path.Dir(current) { + ignore = append(ignore, loadGitIgnore(current)...) + } + } + return ignore +} + +func loadGitIgnore(dir string) []string { + r, err := os.Open(path.Join(dir, ".gitignore")) if err != nil { return nil } + ignore := []string{} lr := bufio.NewScanner(r) for lr.Scan() { line := lr.Text() diff --git a/cmd/ftl/cmd_init.go b/cmd/ftl/cmd_init.go index 9a89e9850d..763ff2c762 100644 --- a/cmd/ftl/cmd_init.go +++ b/cmd/ftl/cmd_init.go @@ -14,6 +14,7 @@ import ( "github.com/beevik/etree" "github.com/iancoleman/strcase" + "github.com/TBD54566975/ftl/backend/common/exec" "github.com/TBD54566975/ftl/backend/common/log" goruntime "github.com/TBD54566975/ftl/go-runtime" "github.com/TBD54566975/ftl/internal" @@ -21,7 +22,7 @@ import ( ) type initCmd struct { - Hermit bool `default:"true" help:"Include Hermit language-specific toolchain binaries in the module." negatable:""` + Hermit bool `help:"Include Hermit language-specific toolchain binaries in the module." negatable:""` Go initGoCmd `cmd:"" help:"Initialize a new FTL Go module."` Kotlin initKotlinCmd `cmd:"" help:"Initialize a new FTL Kotlin module."` } @@ -31,17 +32,17 @@ type initGoCmd struct { Name string `arg:"" help:"Name of the FTL module to create underneath the base directory."` } -func (i initGoCmd) Run(parent *initCmd) error { +func (i initGoCmd) Run(ctx context.Context, parent *initCmd) error { if i.Name == "" { i.Name = filepath.Base(i.Dir) } - tmpDir, err := unzipToTmpDir(goruntime.Files) - if err != nil { - return fmt.Errorf("%s: %w", "failed to unzip kotlin runtime", err) + logger := log.FromContext(ctx) + logger.Infof("Initializing FTL Go module %s in %s", i.Name, i.Dir) + if err := scaffold(parent.Hermit, goruntime.Files(), i.Dir, i, scaffolder.Exclude("^go.mod$")); err != nil { + return err } - defer os.RemoveAll(tmpDir) - - return scaffold(parent.Hermit, tmpDir, i.Dir, i, scaffolder.Exclude("^go.mod$")) + logger.Infof("Running go mod tidy") + return exec.Command(ctx, log.Debug, filepath.Join(i.Dir, i.Name), "go", "mod", "tidy").Run() } type initKotlinCmd struct { @@ -71,13 +72,7 @@ func (i initKotlinCmd) Run(ctx context.Context, parent *initCmd) error { } } - tmpDir, err := unzipToTmpDir(kotlinruntime.Files) - if err != nil { - return fmt.Errorf("%s: %w", "failed to unzip kotlin runtime", err) - } - defer os.RemoveAll(tmpDir) - - if err := scaffold(parent.Hermit, tmpDir, i.Dir, i, options...); err != nil { + if err := scaffold(parent.Hermit, kotlinruntime.Files(), i.Dir, i, options...); err != nil { return err } @@ -122,13 +117,13 @@ func unzipToTmpDir(reader *zip.Reader) (string, error) { return tmpDir, nil } -func scaffold(hermit bool, source string, destination string, ctx any, options ...scaffolder.Option) error { +func scaffold(hermit bool, source *zip.Reader, destination string, ctx any, options ...scaffolder.Option) error { opts := []scaffolder.Option{scaffolder.Functions(scaffoldFuncs), scaffolder.Exclude("^go.mod$")} if !hermit { opts = append(opts, scaffolder.Exclude("^bin")) } opts = append(opts, options...) - if err := scaffolder.Scaffold(source, destination, ctx, opts...); err != nil { + if err := internal.ScaffoldZip(source, destination, ctx, opts...); err != nil { return fmt.Errorf("%s: %w", "failed to scaffold", err) } return nil diff --git a/examples/echo/echo.go b/examples/echo/echo.go index cd319adec1..caa6419287 100644 --- a/examples/echo/echo.go +++ b/examples/echo/echo.go @@ -7,13 +7,14 @@ import ( "context" "fmt" - timemodule "github.com/TBD54566975/ftl/examples/time" - ftl "github.com/TBD54566975/ftl/go-runtime/sdk" + "ftl/time" + + "github.com/TBD54566975/ftl/go-runtime/sdk" ) // An echo request. type EchoRequest struct { - Name ftl.Option[string] `json:"name"` + Name sdk.Option[string] `json:"name"` } type EchoResponse struct { @@ -26,7 +27,7 @@ type EchoResponse struct { //ftl:ingress GET /echo func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { fmt.Println("Echo received a request!") - tresp, err := ftl.Call(ctx, timemodule.Time, timemodule.TimeRequest{}) + tresp, err := sdk.Call(ctx, time.Time, time.TimeRequest{}) if err != nil { return EchoResponse{}, err } diff --git a/examples/echo/go.mod b/examples/echo/go.mod new file mode 100644 index 0000000000..01739b33f8 --- /dev/null +++ b/examples/echo/go.mod @@ -0,0 +1,39 @@ +module ftl/echo + +go 1.21.5 + +require github.com/TBD54566975/ftl v0.95.0 + +require ( + connectrpc.com/connect v1.14.0 // indirect + connectrpc.com/grpcreflect v1.2.0 // indirect + connectrpc.com/otelconnect v0.6.0 // indirect + github.com/alecthomas/concurrency v0.0.2 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/alecthomas/types v0.9.0 // indirect + github.com/alessio/shellescape v1.4.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/iancoleman/strcase v0.2.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect + github.com/swaggest/jsonschema-go v0.3.64 // indirect + github.com/swaggest/refl v1.3.0 // indirect + github.com/zalando/go-keyring v0.2.3 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect +) diff --git a/examples/echo/go.sum b/examples/echo/go.sum new file mode 100644 index 0000000000..d6040a3de8 --- /dev/null +++ b/examples/echo/go.sum @@ -0,0 +1,131 @@ +connectrpc.com/connect v1.14.0 h1:PDS+J7uoz5Oui2VEOMcfz6Qft7opQM9hPiKvtGC01pA= +connectrpc.com/connect v1.14.0/go.mod h1:uoAq5bmhhn43TwhaKdGKN/bZcGtzPW1v+ngDTn5u+8s= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +connectrpc.com/otelconnect v0.6.0 h1:VJAdQL9+sgdUw9+7+J+jq8pQo/h1S7tSFv2+vDcR7bU= +connectrpc.com/otelconnect v0.6.0/go.mod h1:jdcs0uiwXQVmSMgTJ2dAaWR5VbpNd7QKNkuoH7n86RA= +github.com/TBD54566975/ftl v0.95.0 h1:cAleh+j0owIlmPcCEmmhkaNj0gCWVyQ+pJINAispW/M= +github.com/TBD54566975/ftl v0.95.0/go.mod h1:8M4ZC4WrDAwxNSsj1CjrGiFHyfbyh/Xdobo2LjT4Kq4= +github.com/alecthomas/assert/v2 v2.4.1 h1:mwPZod/d35nlaCppr6sFP0rbCL05WH9fIo7lvsf47zo= +github.com/alecthomas/assert/v2 v2.4.1/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= +github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= +github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= +github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/types v0.9.0 h1:g/P/fNElr7Ot1tWxSEms/I2gnRemxAqGNeetW6HOlO4= +github.com/alecthomas/types v0.9.0/go.mod h1:t7PnU03TVweFpbPVKaeLtFykjJD8rqiBJ7gfkp6UvLQ= +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ= +github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.64 h1:HyB41fkA4XP0BZkqWfGap5i2JtRHQGXG/21dGDPbyLM= +github.com/swaggest/jsonschema-go v0.3.64/go.mod h1:DYuKqdpms/edvywsX6p1zHXCZkdwB28wRaBdFCe3Duw= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= +go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b h1:lkOPTy76R9NZ6FeDQWkDj3NsLtD8Csc9AAFYEl3kiME= +golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b/go.mod h1:QXG482h3unP32W/YwIPOc+09bvY447B7T+iLjC/JPcA= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs= +modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= +modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/examples/kotlin/ftl-module-ad/ftl.toml b/examples/kotlin/ftl-module-ad/ftl.toml new file mode 100644 index 0000000000..1438ed1b23 --- /dev/null +++ b/examples/kotlin/ftl-module-ad/ftl.toml @@ -0,0 +1,2 @@ +module = "ad" +language = "kotlin" diff --git a/examples/online-boutique/services/ad-kotlin/pom.xml b/examples/kotlin/ftl-module-ad/pom.xml similarity index 100% rename from examples/online-boutique/services/ad-kotlin/pom.xml rename to examples/kotlin/ftl-module-ad/pom.xml diff --git a/examples/kotlin/ftl-module-ad/src/main/kotlin/ftl/ad/Ad.kt b/examples/kotlin/ftl-module-ad/src/main/kotlin/ftl/ad/Ad.kt new file mode 100644 index 0000000000..3faf1406ce --- /dev/null +++ b/examples/kotlin/ftl-module-ad/src/main/kotlin/ftl/ad/Ad.kt @@ -0,0 +1,57 @@ +package ftl.ad + +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken +import xyz.block.ftl.Context +import xyz.block.ftl.Ingress +import xyz.block.ftl.Method +import xyz.block.ftl.Verb +import java.util.* + +data class Ad(val redirectUrl: String, val text: String) +data class AdRequest(val contextKeys: List? = null) +data class AdResponse(val ads: List) + +class AdModule { + private val database: Map = loadDatabase() + + @Verb + @Ingress(Method.GET, "/get") + fun get(context: Context, req: AdRequest): AdResponse { + return when { + req.contextKeys != null -> AdResponse(ads = contextualAds(req.contextKeys)) + else -> AdResponse(ads = randomAds()) + } + } + + private fun contextualAds(contextKeys: List): List { + return contextKeys.map { database[it] ?: throw Exception("no ad registered for this context key") } + } + + private fun randomAds(): List { + val ads = mutableListOf() + val random = Random() + repeat(MAX_ADS_TO_SERVE) { + ads.add(database.entries.elementAt(random.nextInt(database.size)).value) + } + return ads + } + + companion object { + private const val MAX_ADS_TO_SERVE = 2 + private val DATABASE = mapOf( + "hair" to Ad("/product/2ZYFJ3GM2N", "Hairdryer for sale. 50% off."), + "clothing" to Ad("/product/66VCHSJNUP", "Tank top for sale. 20% off."), + "accessories" to Ad("/product/1YMWWN1N4O", "Watch for sale. Buy one, get second kit for free"), + "footwear" to Ad("/product/L9ECAV7KIM", "Loafers for sale. Buy one, get second one for free"), + "decor" to Ad("/product/0PUK6V6EV0", "Candle holder for sale. 30% off."), + "kitchen" to Ad("/product/9SIQT8TOJO", "Bamboo glass jar for sale. 10% off.") + ) + + private fun loadDatabase(): Map { + return DATABASE + } + + inline fun Gson.fromJson(json: String) = fromJson(json, object : TypeToken() {}.type) + } +} diff --git a/examples/online-boutique/common/money/money.go b/examples/online-boutique/common/money/money.go index f93c234662..50592b8370 100644 --- a/examples/online-boutique/common/money/money.go +++ b/examples/online-boutique/common/money/money.go @@ -17,7 +17,7 @@ package money import ( "errors" - "github.com/TBD54566975/ftl/examples/online-boutique/services/currency" + "ftl/currency" ) const ( diff --git a/examples/online-boutique/go.mod b/examples/online-boutique/go.mod index f02a4af6ee..91771f71cc 100644 --- a/examples/online-boutique/go.mod +++ b/examples/online-boutique/go.mod @@ -3,42 +3,3 @@ module github.com/TBD54566975/ftl/examples/online-boutique go 1.21.5 replace github.com/TBD54566975/ftl => ../.. - -require ( - github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 - github.com/google/uuid v1.5.0 - github.com/hashicorp/golang-lru/v2 v2.0.5 - golang.org/x/exp v0.0.0-20231127185646-65229373498e -) - -require ( - connectrpc.com/connect v1.14.0 // indirect - connectrpc.com/grpcreflect v1.2.0 // indirect - connectrpc.com/otelconnect v0.6.0 // indirect - github.com/alecthomas/concurrency v0.0.2 // indirect - github.com/alecthomas/participle/v2 v2.1.1 // indirect - github.com/alecthomas/types v0.9.0 // indirect - github.com/alessio/shellescape v1.4.2 // indirect - github.com/danieljoos/wincred v1.2.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/iancoleman/strcase v0.2.0 // indirect - github.com/jpillora/backoff v1.0.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/oklog/ulid/v2 v2.1.0 // indirect - github.com/swaggest/jsonschema-go v0.3.64 // indirect - github.com/swaggest/refl v1.3.0 // indirect - github.com/zalando/go-keyring v0.2.3 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect - golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b // indirect - golang.org/x/mod v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sync v0.6.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect -) diff --git a/examples/online-boutique/go.sum b/examples/online-boutique/go.sum index 7dc1b3b728..e69de29bb2 100644 --- a/examples/online-boutique/go.sum +++ b/examples/online-boutique/go.sum @@ -1,131 +0,0 @@ -connectrpc.com/connect v1.14.0 h1:PDS+J7uoz5Oui2VEOMcfz6Qft7opQM9hPiKvtGC01pA= -connectrpc.com/connect v1.14.0/go.mod h1:uoAq5bmhhn43TwhaKdGKN/bZcGtzPW1v+ngDTn5u+8s= -connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= -connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= -connectrpc.com/otelconnect v0.6.0 h1:VJAdQL9+sgdUw9+7+J+jq8pQo/h1S7tSFv2+vDcR7bU= -connectrpc.com/otelconnect v0.6.0/go.mod h1:jdcs0uiwXQVmSMgTJ2dAaWR5VbpNd7QKNkuoH7n86RA= -github.com/alecthomas/assert/v2 v2.4.1 h1:mwPZod/d35nlaCppr6sFP0rbCL05WH9fIo7lvsf47zo= -github.com/alecthomas/assert/v2 v2.4.1/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= -github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= -github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= -github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= -github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= -github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= -github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/alecthomas/types v0.9.0 h1:g/P/fNElr7Ot1tWxSEms/I2gnRemxAqGNeetW6HOlO4= -github.com/alecthomas/types v0.9.0/go.mod h1:t7PnU03TVweFpbPVKaeLtFykjJD8rqiBJ7gfkp6UvLQ= -github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= -github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= -github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ= -github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= -github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= -github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= -github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= -github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= -github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= -github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= -github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= -github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= -github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= -github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= -github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= -github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= -github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= -github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= -github.com/swaggest/jsonschema-go v0.3.64 h1:HyB41fkA4XP0BZkqWfGap5i2JtRHQGXG/21dGDPbyLM= -github.com/swaggest/jsonschema-go v0.3.64/go.mod h1:DYuKqdpms/edvywsX6p1zHXCZkdwB28wRaBdFCe3Duw= -github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= -github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= -github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= -go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= -golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b h1:lkOPTy76R9NZ6FeDQWkDj3NsLtD8Csc9AAFYEl3kiME= -golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b/go.mod h1:QXG482h3unP32W/YwIPOc+09bvY447B7T+iLjC/JPcA= -golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= -golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= -golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= -golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= -golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= -modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= -modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= -modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= -modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs= -modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ= -modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= -modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= -modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= -modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= -modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/examples/online-boutique/services/ad-kotlin/src/main/kotlin/ftl/ad/Ad.kt b/examples/online-boutique/services/ad-kotlin/src/main/kotlin/ftl/ad/Ad.kt deleted file mode 100644 index 6b65692123..0000000000 --- a/examples/online-boutique/services/ad-kotlin/src/main/kotlin/ftl/ad/Ad.kt +++ /dev/null @@ -1,78 +0,0 @@ -package ftl.ad - -import com.google.gson.Gson -import com.google.gson.annotations.SerializedName -import com.google.gson.reflect.TypeToken -import xyz.block.ftl.Context -import xyz.block.ftl.Ingress -import xyz.block.ftl.Method -import xyz.block.ftl.Verb -import xyz.block.ftl.serializer.makeGson -import java.util.* - -data class Ad(@SerializedName("RedirectURL") val redirectUrl: String, @SerializedName("Text") val text: String) -data class AdRequest(val contextKeys: List) -data class AdResponse(val ads: List) - -class AdModule { - private val database: Map = loadDatabase() - - @Throws(Exception::class) - @Verb - @Ingress(Type.FTL, Method.GET, "/get") - fun get(context: Context, req: AdRequest): AdResponse { - return when { - req.contextKeys.isNotEmpty() -> AdResponse(ads = contextualAds(req.contextKeys)) - else -> AdResponse(ads = randomAds()) - } - } - - private fun contextualAds(contextKeys: List): List { - return contextKeys.map { database[it] ?: throw Exception("no ad registered for this context key") } - } - - private fun randomAds(): List { - val ads = mutableListOf() - val random = Random() - repeat(MAX_ADS_TO_SERVE) { - ads.add(database.entries.elementAt(random.nextInt(database.size)).value) - } - return ads - } - - companion object { - private const val MAX_ADS_TO_SERVE = 2 - private const val DATABASE_JSON = "{\n" + - " \"hair\": {\n" + - " \"RedirectURL\": \"/product/2ZYFJ3GM2N\",\n" + - " \"Text\": \"Hairdryer for sale. 50% off.\"\n" + - " },\n" + - " \"clothing\": {\n" + - " \"RedirectURL\": \"/product/66VCHSJNUP\",\n" + - " \"Text\": \"Tank top for sale. 20% off.\"\n" + - " },\n" + - " \"accessories\": {\n" + - " \"RedirectURL\": \"/product/1YMWWN1N4O\",\n" + - " \"Text\": \"Watch for sale. Buy one, get second kit for free\"\n" + - " },\n" + - " \"footwear\": {\n" + - " \"RedirectURL\": \"/product/L9ECAV7KIM\",\n" + - " \"Text\": \"Loafers for sale. Buy one, get second one for free\"\n" + - " },\n" + - " \"decor\": {\n" + - " \"RedirectURL\": \"/product/0PUK6V6EV0\",\n" + - " \"Text\": \"Candle holder for sale. 30% off.\"\n" + - " },\n" + - " \"kitchen\": {\n" + - " \"RedirectURL\": \"/product/9SIQT8TOJO\",\n" + - " \"Text\": \"Bamboo glass jar for sale. 10% off.\"\n" + - " }\n" + - "}" - - private fun loadDatabase(): Map { - return makeGson().fromJson>(DATABASE_JSON) - } - - inline fun Gson.fromJson(json: String) = fromJson(json, object : TypeToken() {}.type) - } -} diff --git a/examples/online-boutique/services/ad/ftl.toml b/examples/online-boutique/services/ad/ftl.toml new file mode 100644 index 0000000000..ad433ebe95 --- /dev/null +++ b/examples/online-boutique/services/ad/ftl.toml @@ -0,0 +1,2 @@ +module = "ad" +language = "go" diff --git a/examples/online-boutique/services/ad/go.mod b/examples/online-boutique/services/ad/go.mod new file mode 100644 index 0000000000..be66e4bdb9 --- /dev/null +++ b/examples/online-boutique/services/ad/go.mod @@ -0,0 +1,12 @@ +module ftl/ad + +go 1.21.5 + +replace github.com/TBD54566975/ftl => ../../../.. + +replace github.com/TBD54566975/ftl/examples/online-boutique => ../.. + +require ( + github.com/TBD54566975/ftl/examples/online-boutique v0.0.0-00010101000000-000000000000 + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 +) diff --git a/examples/online-boutique/services/ad/go.sum b/examples/online-boutique/services/ad/go.sum new file mode 100644 index 0000000000..3707af3cb0 --- /dev/null +++ b/examples/online-boutique/services/ad/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= diff --git a/examples/online-boutique/services/cart/ftl.toml b/examples/online-boutique/services/cart/ftl.toml new file mode 100644 index 0000000000..36d2a708c6 --- /dev/null +++ b/examples/online-boutique/services/cart/ftl.toml @@ -0,0 +1,2 @@ +module = "cart" +language = "go" diff --git a/examples/online-boutique/services/cart/go.mod b/examples/online-boutique/services/cart/go.mod new file mode 100644 index 0000000000..a893891542 --- /dev/null +++ b/examples/online-boutique/services/cart/go.mod @@ -0,0 +1,9 @@ +module ftl/cart + +go 1.21.5 + +replace github.com/TBD54566975/ftl => ../../../.. + +replace github.com/TBD54566975/ftl/examples/online-boutique => ../.. + +require github.com/hashicorp/golang-lru/v2 v2.0.7 diff --git a/examples/online-boutique/services/cart/go.sum b/examples/online-boutique/services/cart/go.sum new file mode 100644 index 0000000000..a33c54a15f --- /dev/null +++ b/examples/online-boutique/services/cart/go.sum @@ -0,0 +1,2 @@ +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= diff --git a/examples/online-boutique/services/checkout/checkout.go b/examples/online-boutique/services/checkout/checkout.go index 62127e1c19..fd02049533 100644 --- a/examples/online-boutique/services/checkout/checkout.go +++ b/examples/online-boutique/services/checkout/checkout.go @@ -7,13 +7,14 @@ import ( "github.com/google/uuid" + "ftl/cart" + "ftl/currency" + "ftl/payment" + "ftl/productcatalog" + "ftl/shipping" + "github.com/TBD54566975/ftl/backend/common/slices" "github.com/TBD54566975/ftl/examples/online-boutique/common/money" - "github.com/TBD54566975/ftl/examples/online-boutique/services/cart" - "github.com/TBD54566975/ftl/examples/online-boutique/services/currency" - "github.com/TBD54566975/ftl/examples/online-boutique/services/payment" - "github.com/TBD54566975/ftl/examples/online-boutique/services/productcatalog" - "github.com/TBD54566975/ftl/examples/online-boutique/services/shipping" ftl "github.com/TBD54566975/ftl/go-runtime/sdk" ) @@ -49,7 +50,7 @@ func PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) { orders := make([]OrderItem, len(cartItems.Items)) for i, item := range cartItems.Items { - product, err := ftl.Call(ctx, productcatalog.Get, productcatalog.GetRequest{ID: item.ProductID}) + product, err := ftl.Call(ctx, productcatalog.Get, productcatalog.GetRequest{Id: item.ProductID}) if err != nil { return Order{}, fmt.Errorf("failed to get product #%q: %w", item.ProductID, err) } @@ -103,14 +104,14 @@ func PlaceOrder(ctx context.Context, req PlaceOrderRequest) (Order, error) { if err != nil { return Order{}, fmt.Errorf("shipping error: %w", err) } - fmt.Printf("Shipped order, tracking ID %s", shippingTrackingID.ID) + fmt.Printf("Shipped order, tracking ID %s", shippingTrackingID.Id) // Empty the cart, but don't worry about errors. _, _ = ftl.Call(ctx, cart.EmptyCart, cart.EmptyCartRequest{UserID: req.UserID}) order := Order{ ID: uuid.New().String(), - ShippingTrackingID: shippingTrackingID.ID, + ShippingTrackingID: shippingTrackingID.Id, ShippingCost: shippingPrice, ShippingAddress: req.Address, Items: orders, diff --git a/examples/online-boutique/services/checkout/ftl.toml b/examples/online-boutique/services/checkout/ftl.toml new file mode 100644 index 0000000000..6096398f21 --- /dev/null +++ b/examples/online-boutique/services/checkout/ftl.toml @@ -0,0 +1,2 @@ +module = "checkout" +language = "go" diff --git a/examples/online-boutique/services/checkout/go.mod b/examples/online-boutique/services/checkout/go.mod new file mode 100644 index 0000000000..28f0e282dc --- /dev/null +++ b/examples/online-boutique/services/checkout/go.mod @@ -0,0 +1,46 @@ +module ftl/checkout + +go 1.21.5 + +replace github.com/TBD54566975/ftl => ../../../.. + +replace github.com/TBD54566975/ftl/examples/online-boutique => ../.. + +require ( + github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 + github.com/TBD54566975/ftl/examples/online-boutique v0.0.0-00010101000000-000000000000 + github.com/google/uuid v1.5.0 +) + +require ( + connectrpc.com/connect v1.14.0 // indirect + connectrpc.com/grpcreflect v1.2.0 // indirect + connectrpc.com/otelconnect v0.6.0 // indirect + github.com/alecthomas/concurrency v0.0.2 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/alecthomas/types v0.9.0 // indirect + github.com/alessio/shellescape v1.4.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/iancoleman/strcase v0.2.0 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect + github.com/swaggest/jsonschema-go v0.3.64 // indirect + github.com/swaggest/refl v1.3.0 // indirect + github.com/zalando/go-keyring v0.2.3 // indirect + go.opentelemetry.io/otel v1.21.0 // indirect + go.opentelemetry.io/otel/metric v1.21.0 // indirect + go.opentelemetry.io/otel/trace v1.21.0 // indirect + golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b // indirect + golang.org/x/exp v0.0.0-20231127185646-65229373498e // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.32.0 // indirect +) diff --git a/examples/go.sum b/examples/online-boutique/services/checkout/go.sum similarity index 100% rename from examples/go.sum rename to examples/online-boutique/services/checkout/go.sum diff --git a/examples/online-boutique/services/currency/ftl.toml b/examples/online-boutique/services/currency/ftl.toml new file mode 100644 index 0000000000..07218ce631 --- /dev/null +++ b/examples/online-boutique/services/currency/ftl.toml @@ -0,0 +1,2 @@ +module = "currency" +language = "go" diff --git a/examples/online-boutique/services/currency/go.mod b/examples/online-boutique/services/currency/go.mod new file mode 100644 index 0000000000..b6c71e6acc --- /dev/null +++ b/examples/online-boutique/services/currency/go.mod @@ -0,0 +1,12 @@ +module ftl/currency + +go 1.21.5 + +replace github.com/TBD54566975/ftl => ../../../.. + +replace github.com/TBD54566975/ftl/examples/online-boutique => ../.. + +require ( + github.com/TBD54566975/ftl/examples/online-boutique v0.0.0-00010101000000-000000000000 + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 +) diff --git a/examples/online-boutique/services/currency/go.sum b/examples/online-boutique/services/currency/go.sum new file mode 100644 index 0000000000..3707af3cb0 --- /dev/null +++ b/examples/online-boutique/services/currency/go.sum @@ -0,0 +1,2 @@ +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= diff --git a/examples/online-boutique/services/echo/generated_ftl_module.go b/examples/online-boutique/services/echo/generated_ftl_module.go deleted file mode 100644 index 792ab75730..0000000000 --- a/examples/online-boutique/services/echo/generated_ftl_module.go +++ /dev/null @@ -1,2 +0,0 @@ -//ftl:module echo -package echo diff --git a/examples/online-boutique/services/payment/ftl.toml b/examples/online-boutique/services/payment/ftl.toml new file mode 100644 index 0000000000..cd591d5bf5 --- /dev/null +++ b/examples/online-boutique/services/payment/ftl.toml @@ -0,0 +1,2 @@ +module = "payment" +language = "go" diff --git a/examples/online-boutique/services/payment/go.mod b/examples/online-boutique/services/payment/go.mod new file mode 100644 index 0000000000..c02aa600a6 --- /dev/null +++ b/examples/online-boutique/services/payment/go.mod @@ -0,0 +1,9 @@ +module ftl/payment + +go 1.21.5 + +replace github.com/TBD54566975/ftl => ../../../.. + +replace github.com/TBD54566975/ftl/examples/online-boutique => ../.. + +require github.com/google/uuid v1.5.0 diff --git a/examples/online-boutique/services/payment/go.sum b/examples/online-boutique/services/payment/go.sum new file mode 100644 index 0000000000..040e221350 --- /dev/null +++ b/examples/online-boutique/services/payment/go.sum @@ -0,0 +1,2 @@ +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/examples/online-boutique/services/payment/payment.go b/examples/online-boutique/services/payment/payment.go index 8a05efaf32..588293dbcc 100644 --- a/examples/online-boutique/services/payment/payment.go +++ b/examples/online-boutique/services/payment/payment.go @@ -9,7 +9,7 @@ import ( "github.com/google/uuid" - "github.com/TBD54566975/ftl/examples/online-boutique/services/currency" + "ftl/currency" ) type InvalidCreditCardErr struct{} diff --git a/examples/online-boutique/services/productcatalog/ftl.toml b/examples/online-boutique/services/productcatalog/ftl.toml new file mode 100644 index 0000000000..b66dc4976f --- /dev/null +++ b/examples/online-boutique/services/productcatalog/ftl.toml @@ -0,0 +1,2 @@ +module = "productcatalog" +language = "go" diff --git a/examples/online-boutique/services/productcatalog/go.mod b/examples/online-boutique/services/productcatalog/go.mod new file mode 100644 index 0000000000..8664f140ef --- /dev/null +++ b/examples/online-boutique/services/productcatalog/go.mod @@ -0,0 +1,9 @@ +module ftl/productcatalog + +go 1.21.5 + +replace github.com/TBD54566975/ftl => ../../../.. + +replace github.com/TBD54566975/ftl/examples/online-boutique => ../.. + +require github.com/TBD54566975/ftl/examples/online-boutique v0.0.0-00010101000000-000000000000 diff --git a/examples/online-boutique/services/productcatalog/productcatalog.go b/examples/online-boutique/services/productcatalog/productcatalog.go index 3be803072a..dfc9af0d28 100644 --- a/examples/online-boutique/services/productcatalog/productcatalog.go +++ b/examples/online-boutique/services/productcatalog/productcatalog.go @@ -7,8 +7,9 @@ import ( "fmt" "strings" + "ftl/currency" + "github.com/TBD54566975/ftl/examples/online-boutique/common" - "github.com/TBD54566975/ftl/examples/online-boutique/services/currency" ) var ( diff --git a/examples/online-boutique/services/recommendation/ftl.toml b/examples/online-boutique/services/recommendation/ftl.toml new file mode 100644 index 0000000000..b9adcbd93a --- /dev/null +++ b/examples/online-boutique/services/recommendation/ftl.toml @@ -0,0 +1,2 @@ +module = "recommendation" +language = "go" diff --git a/examples/go.mod b/examples/online-boutique/services/recommendation/go.mod similarity index 91% rename from examples/go.mod rename to examples/online-boutique/services/recommendation/go.mod index 7efe0b6e81..6eede2b07f 100644 --- a/examples/go.mod +++ b/examples/online-boutique/services/recommendation/go.mod @@ -1,8 +1,10 @@ -module github.com/TBD54566975/ftl/examples +module ftl/recommendation go 1.21.5 -replace github.com/TBD54566975/ftl => ../ +replace github.com/TBD54566975/ftl => ../../../.. + +replace github.com/TBD54566975/ftl/examples/online-boutique => ../.. require github.com/TBD54566975/ftl v0.0.0-00010101000000-000000000000 diff --git a/examples/online-boutique/services/recommendation/go.sum b/examples/online-boutique/services/recommendation/go.sum new file mode 100644 index 0000000000..436e0976cb --- /dev/null +++ b/examples/online-boutique/services/recommendation/go.sum @@ -0,0 +1,129 @@ +connectrpc.com/connect v1.14.0 h1:PDS+J7uoz5Oui2VEOMcfz6Qft7opQM9hPiKvtGC01pA= +connectrpc.com/connect v1.14.0/go.mod h1:uoAq5bmhhn43TwhaKdGKN/bZcGtzPW1v+ngDTn5u+8s= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +connectrpc.com/otelconnect v0.6.0 h1:VJAdQL9+sgdUw9+7+J+jq8pQo/h1S7tSFv2+vDcR7bU= +connectrpc.com/otelconnect v0.6.0/go.mod h1:jdcs0uiwXQVmSMgTJ2dAaWR5VbpNd7QKNkuoH7n86RA= +github.com/alecthomas/assert/v2 v2.4.1 h1:mwPZod/d35nlaCppr6sFP0rbCL05WH9fIo7lvsf47zo= +github.com/alecthomas/assert/v2 v2.4.1/go.mod h1:fw5suVxB+wfYJ3291t0hRTqtGzFYdSwstnRQdaQx2DM= +github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= +github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.3.0 h1:NeYzUPfjjlqHY4KtzgKJiWd6sVq2eNUPTi34PiFGjY8= +github.com/alecthomas/repr v0.3.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/types v0.9.0 h1:g/P/fNElr7Ot1tWxSEms/I2gnRemxAqGNeetW6HOlO4= +github.com/alecthomas/types v0.9.0/go.mod h1:t7PnU03TVweFpbPVKaeLtFykjJD8rqiBJ7gfkp6UvLQ= +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/bool64/dev v0.2.31 h1:OS57EqYaYe2M/2bw9uhDCIFiZZwywKFS/4qMLN6JUmQ= +github.com/bool64/dev v0.2.31/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.64 h1:HyB41fkA4XP0BZkqWfGap5i2JtRHQGXG/21dGDPbyLM= +github.com/swaggest/jsonschema-go v0.3.64/go.mod h1:DYuKqdpms/edvywsX6p1zHXCZkdwB28wRaBdFCe3Duw= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= +github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk/metric v1.21.0 h1:smhI5oD714d6jHE6Tie36fPx4WDFIg+Y6RfAY4ICcR0= +go.opentelemetry.io/otel/sdk/metric v1.21.0/go.mod h1:FJ8RAsoPGv/wYMgBdUJXOm+6pzFY3YdljnXtv1SBE8Q= +go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b h1:lkOPTy76R9NZ6FeDQWkDj3NsLtD8Csc9AAFYEl3kiME= +golang.design/x/reflect v0.0.0-20220504060917-02c43be63f3b/go.mod h1:QXG482h3unP32W/YwIPOc+09bvY447B7T+iLjC/JPcA= +golang.org/x/exp v0.0.0-20231127185646-65229373498e h1:Gvh4YaCaXNs6dKTlfgismwWZKyjVZXwOPfIyUaqU3No= +golang.org/x/exp v0.0.0-20231127185646-65229373498e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= +modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= +modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= +modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= +modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs= +modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= +modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= +modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= +modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/examples/online-boutique/services/recommendation/recommendation.go b/examples/online-boutique/services/recommendation/recommendation.go index 5bcf125fad..ab3b1a5db5 100644 --- a/examples/online-boutique/services/recommendation/recommendation.go +++ b/examples/online-boutique/services/recommendation/recommendation.go @@ -6,7 +6,8 @@ import ( "fmt" "math/rand" - "github.com/TBD54566975/ftl/examples/online-boutique/services/productcatalog" + "ftl/productcatalog" + ftl "github.com/TBD54566975/ftl/go-runtime/sdk" ) @@ -36,10 +37,10 @@ func List(ctx context.Context, req ListRequest) (ListResponse, error) { } filtered := make([]string, 0, len(catalog.Products)) for _, product := range catalog.Products { - if _, ok := userIDs[product.ID]; ok { + if _, ok := userIDs[product.Id]; ok { continue } - filtered = append(filtered, product.ID) + filtered = append(filtered, product.Id) } // Sample from filtered products and return them. diff --git a/examples/online-boutique/services/shipping/ftl.toml b/examples/online-boutique/services/shipping/ftl.toml new file mode 100644 index 0000000000..c7cbbc5253 --- /dev/null +++ b/examples/online-boutique/services/shipping/ftl.toml @@ -0,0 +1,2 @@ +module = "shipping" +language = "go" diff --git a/examples/online-boutique/services/shipping/go.mod b/examples/online-boutique/services/shipping/go.mod new file mode 100644 index 0000000000..45b9cb7416 --- /dev/null +++ b/examples/online-boutique/services/shipping/go.mod @@ -0,0 +1,7 @@ +module ftl/shipping + +go 1.21.5 + +replace github.com/TBD54566975/ftl => ../../../.. + +replace github.com/TBD54566975/ftl/examples/online-boutique => ../.. diff --git a/examples/online-boutique/services/shipping/shipping.go b/examples/online-boutique/services/shipping/shipping.go index 60852d47d9..5f7e178e64 100644 --- a/examples/online-boutique/services/shipping/shipping.go +++ b/examples/online-boutique/services/shipping/shipping.go @@ -6,8 +6,8 @@ import ( "fmt" "math" - "github.com/TBD54566975/ftl/examples/online-boutique/services/cart" - "github.com/TBD54566975/ftl/examples/online-boutique/services/currency" + "ftl/cart" + "ftl/currency" ) type Address struct { diff --git a/examples/time/ftl.toml b/examples/time/ftl.toml index ec58cb9d08..c7449d155e 100644 --- a/examples/time/ftl.toml +++ b/examples/time/ftl.toml @@ -1,2 +1,5 @@ module = "time" language = "go" + +[go.replace] +"github.com/TBD54566975/ftl" = "../.." diff --git a/examples/time/go.mod b/examples/time/go.mod new file mode 100644 index 0000000000..9c6e6ba94b --- /dev/null +++ b/examples/time/go.mod @@ -0,0 +1,3 @@ +module ftl/time + +go 1.21.5 diff --git a/go-runtime/compile/build-template/_ftl/go/main/go.mod.tmpl b/go-runtime/compile/build-template/_ftl/go/main/go.mod.tmpl new file mode 100644 index 0000000000..50960ff45f --- /dev/null +++ b/go-runtime/compile/build-template/_ftl/go/main/go.mod.tmpl @@ -0,0 +1 @@ +module main \ No newline at end of file diff --git a/go-runtime/compile/generate/main.go.tmpl b/go-runtime/compile/build-template/_ftl/go/main/main.go similarity index 75% rename from go-runtime/compile/generate/main.go.tmpl rename to go-runtime/compile/build-template/_ftl/go/main/main.go index 953965266e..84c2a3c288 100644 --- a/go-runtime/compile/generate/main.go.tmpl +++ b/go-runtime/compile/build-template/_ftl/go/main/main.go @@ -1,4 +1,3 @@ -{{- /*gotype: github.com/TBD54566975/ftl/backend/schema.Module*/ -}} // Code generated by FTL. DO NOT EDIT. package main @@ -6,16 +5,17 @@ import ( "context" "github.com/TBD54566975/ftl/backend/common/plugin" - "{{.ImportRoot}}/{{.Name}}" "github.com/TBD54566975/ftl/go-runtime/server" "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/ftlv1connect" + + "ftl/{{.Name}}" ) func main() { verbConstructor := server.NewUserVerbServer("{{.Name}}", {{- range .Verbs}} - server.Handle({{$.Name}}.{{.Name|ExportGoName}}), + server.Handle({{$.Name}}.{{.Name|camel}}), {{- end}} ) plugin.Start(context.Background(), "{{.Name}}", verbConstructor, ftlv1connect.VerbServiceName, ftlv1connect.NewVerbServiceHandler) -} +} \ No newline at end of file diff --git a/go-runtime/compile/build-template/go.mod b/go-runtime/compile/build-template/go.mod new file mode 100644 index 0000000000..08efce4a82 --- /dev/null +++ b/go-runtime/compile/build-template/go.mod @@ -0,0 +1,3 @@ +module exclude + +go 1.21.5 diff --git a/go-runtime/compile/build.go b/go-runtime/compile/build.go new file mode 100644 index 0000000000..e906f70a12 --- /dev/null +++ b/go-runtime/compile/build.go @@ -0,0 +1,182 @@ +package compile + +import ( + "context" + "fmt" + "maps" + "os" + "path" + "path/filepath" + "reflect" + "strings" + + "github.com/iancoleman/strcase" + "google.golang.org/protobuf/proto" + + "github.com/TBD54566975/scaffolder" + + "github.com/TBD54566975/ftl/backend/common/exec" + "github.com/TBD54566975/ftl/backend/common/log" + "github.com/TBD54566975/ftl/backend/common/moduleconfig" + "github.com/TBD54566975/ftl/backend/schema" + "github.com/TBD54566975/ftl/internal" +) + +type buildContext struct { + ModuleDir string + *schema.Schema + Main string +} + +func (b buildContext) NonMainModules() []*schema.Module { + modules := make([]*schema.Module, 0, len(b.Modules)-1) + for _, module := range b.Modules { + if module.Name == b.Main { + continue + } + modules = append(modules, module) + } + return modules +} + +const buildDirName = "_ftl" + +// Build the given module. +func Build(ctx context.Context, moduleDir string, sch *schema.Schema) error { + config, err := moduleconfig.LoadConfig(moduleDir) + if err != nil { + return fmt.Errorf("failed to load module config: %w", err) + } + logger := log.FromContext(ctx) + + funcs := maps.Clone(scaffoldFuncs) + + buildDir := filepath.Join(moduleDir, buildDirName) + + // Wipe the modules directory to ensure we don't have any stale modules. + _ = os.RemoveAll(filepath.Join(buildDir, "go", "modules")) + + logger.Infof("Generating external modules") + if err := internal.ScaffoldZip(externalModuleTemplateFiles(), moduleDir, buildContext{ + ModuleDir: moduleDir, + Schema: sch, + Main: config.Module, + }, scaffolder.Exclude("^go.mod$"), scaffolder.Functions(funcs)); err != nil { + return err + } + + logger.Infof("Extracting schema") + main, err := ExtractModuleSchema(moduleDir) + if err != nil { + return fmt.Errorf("failed to extract module schema: %w", err) + } + schemaBytes, err := proto.Marshal(main.ToProto()) + if err != nil { + return fmt.Errorf("failed to marshal schema: %w", err) + } + err = os.WriteFile(filepath.Join(buildDir, "schema.pb"), schemaBytes, 0600) + if err != nil { + return fmt.Errorf("failed to write schema: %w", err) + } + + logger.Infof("Generating main module") + if err := internal.ScaffoldZip(buildTemplateFiles(), moduleDir, main, scaffolder.Exclude("^go.mod$"), scaffolder.Functions(funcs)); err != nil { + return err + } + + logger.Infof("Compiling") + mainDir := filepath.Join(buildDir, "go", "main") + if err := exec.Command(ctx, log.Debug, mainDir, "go", "mod", "tidy").Run(); err != nil { + return fmt.Errorf("failed to tidy go.mod: %w", err) + } + return exec.Command(ctx, log.Info, mainDir, "go", "build", "-o", "../../main", ".").Run() +} + +var scaffoldFuncs = scaffolder.FuncMap{ + "snake": strcase.ToSnake, + "screamingSnake": strcase.ToScreamingSnake, + "camel": strcase.ToCamel, + "lowerCamel": strcase.ToLowerCamel, + "kebab": strcase.ToKebab, + "screamingKebab": strcase.ToScreamingKebab, + "upper": strings.ToUpper, + "lower": strings.ToLower, + "title": strings.Title, + "typename": func(v any) string { + return reflect.Indirect(reflect.ValueOf(v)).Type().Name() + }, + "comment": func(s []string) string { + if len(s) == 0 { + return "" + } + return "// " + strings.Join(s, "\n// ") + }, + // Overridden in ExternalModule(). + "type": genType, + "is": func(kind string, t schema.Node) bool { + return reflect.Indirect(reflect.ValueOf(t)).Type().Name() == kind + }, + "imports": func(m *schema.Module) map[string]string { + imports := map[string]string{} + _ = schema.Visit(m, func(n schema.Node, next func() error) error { + switch n := n.(type) { + case *schema.DataRef: + if n.Module == m.Name { + break + } + imports[path.Join("ftl", n.Module)] = "ftl" + n.Module + + case *schema.Time: + imports["time"] = "stdtime" + + case *schema.Optional, *schema.Unit: + imports["github.com/TBD54566975/ftl/go-runtime/sdk"] = "" + + default: + } + return next() + }) + return imports + }, +} + +func genType(module *schema.Module, t schema.Type) string { + switch t := t.(type) { + case *schema.DataRef: + if module != nil && t.Module == module.Name { + return t.Name + } + return "ftl" + t.String() + + case *schema.VerbRef: + if module != nil && t.Module == module.Name { + return t.Name + } + return "ftl" + t.String() + + case *schema.Float: + return "float64" + + case *schema.Time: + return "stdtime.Time" + + case *schema.Int, *schema.Bool, *schema.String: + return strings.ToLower(t.String()) + + case *schema.Array: + return "[]" + genType(module, t.Element) + + case *schema.Map: + return "map[" + genType(module, t.Key) + "]" + genType(module, t.Value) + + case *schema.Optional: + return "sdk.Option[" + genType(module, t.Type) + "]" + + case *schema.Unit: + return "sdk.Unit" + + case *schema.Bytes: + return "[]byte" + } + panic(fmt.Sprintf("unsupported type %T", t)) +} diff --git a/go-runtime/compile/devel.go b/go-runtime/compile/devel.go new file mode 100644 index 0000000000..c6f2f6270c --- /dev/null +++ b/go-runtime/compile/devel.go @@ -0,0 +1,16 @@ +//go:build !release + +package compile + +import ( + "archive/zip" + + "github.com/TBD54566975/ftl/internal" +) + +func externalModuleTemplateFiles() *zip.Reader { + return internal.ZipRelativeToCaller("external-module-template") +} +func buildTemplateFiles() *zip.Reader { + return internal.ZipRelativeToCaller("build-template") +} diff --git a/go-runtime/compile/external-module-template/_ftl/go/modules/go.mod.tmpl b/go-runtime/compile/external-module-template/_ftl/go/modules/go.mod.tmpl new file mode 100644 index 0000000000..c1606b9a2e --- /dev/null +++ b/go-runtime/compile/external-module-template/_ftl/go/modules/go.mod.tmpl @@ -0,0 +1,3 @@ +module ftl + +go 1.21.5 diff --git a/go-runtime/compile/generate/external_module.go.tmpl b/go-runtime/compile/external-module-template/_ftl/go/modules/{{ range .NonMainModules }}{{ push .Name . }}{{ end }}/external_module.go similarity index 63% rename from go-runtime/compile/generate/external_module.go.tmpl rename to go-runtime/compile/external-module-template/_ftl/go/modules/{{ range .NonMainModules }}{{ push .Name . }}{{ end }}/external_module.go index 56c0a6bfd7..58f221cf8f 100644 --- a/go-runtime/compile/generate/external_module.go.tmpl +++ b/go-runtime/compile/external-module-template/_ftl/go/modules/{{ range .NonMainModules }}{{ push .Name . }}{{ end }}/external_module.go @@ -3,15 +3,15 @@ package {{.Name}} import ( "context" -{{- range .|imports}} - "{{.}}" +{{- range $import, $alias := (.|imports)}} + {{if $alias}}{{$alias}} {{end}}"{{$import}}" {{- end}} ) {{- range .Decls }} {{if is "Data" . }} type {{.Name|title}} struct { {{- range .Fields}} - {{.Name|title}} {{.Type|type}} `json:"{{.Name}}"` + {{.Name|title}} {{type $ .Type}} `json:"{{.Name}}"` {{- end}} } {{- else if is "Verb" .}} @@ -19,7 +19,7 @@ type {{.Name|title}} struct { {{if .Comments}}// {{end -}} //ftl:verb -func {{.Name|title}}(context.Context, {{.Request|type}}) ({{.Response|type}}, error) { +func {{.Name|title}}(context.Context, {{type $ .Request}}) ({{type $ .Response}}, error) { panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/sdk.Call()") } {{- end}} diff --git a/go-runtime/compile/external-module-template/go.mod b/go-runtime/compile/external-module-template/go.mod new file mode 100644 index 0000000000..08efce4a82 --- /dev/null +++ b/go-runtime/compile/external-module-template/go.mod @@ -0,0 +1,3 @@ +module exclude + +go 1.21.5 diff --git a/go-runtime/compile/generate/external_module.go b/go-runtime/compile/generate/external_module.go deleted file mode 100644 index b06ab8cec2..0000000000 --- a/go-runtime/compile/generate/external_module.go +++ /dev/null @@ -1,124 +0,0 @@ -package generate - -import ( - _ "embed" // For embedding templates. - "fmt" - "io" - "path" - "reflect" - "strings" - "text/template" - - "github.com/iancoleman/strcase" - "golang.org/x/exp/maps" - - "github.com/TBD54566975/ftl/backend/schema" -) - -type externalModuleCtx struct { - ImportRoot string - *schema.Module -} - -//go:embed external_module.go.tmpl -var moduleTmplSource string -var moduleTmpl = template.Must(template.New("external_module.go.tmpl"). - Funcs(template.FuncMap{ - "title": strcase.ToCamel, - "comment": func(s []string) string { - if len(s) == 0 { - return "" - } - return "// " + strings.Join(s, "\n// ") - }, - // Overridden in ExternalModule(). - "type": func(schema.Type) string { panic("stub") }, - "is": func(kind string, t schema.Node) bool { - return reflect.Indirect(reflect.ValueOf(t)).Type().Name() == kind - }, - "imports": func(m *externalModuleCtx) []string { - pkgs := map[string]bool{} - _ = schema.Visit(m, func(n schema.Node, next func() error) error { - switch n := n.(type) { - case *schema.DataRef: - if n.Module != m.Name { - pkgs[path.Join(m.ImportRoot, n.Module)] = true - } - - case *schema.Time: - pkgs["time"] = true - - case *schema.Optional: - pkgs["github.com/TBD54566975/ftl/go-runtime/sdk"] = true - - default: - } - return next() - }) - return maps.Keys(pkgs) - }, - }). - Parse(moduleTmplSource)) - -// ExternalModule Go stubs for the given module. -func ExternalModule(w io.Writer, module *schema.Module, importRoot string) error { - tmpl := moduleTmpl.Funcs(template.FuncMap{"type": func(t schema.Type) string { - return genType(module, t) - }}) - return tmpl.Execute(w, &externalModuleCtx{ - ImportRoot: importRoot, - Module: module, - }) -} - -func genType(module *schema.Module, t schema.Type) string { - switch t := t.(type) { - case *schema.Unit: - return "sdk.Unit" - - case *schema.DataRef: - if t.Module == module.Name { - return t.Name - } - return t.String() - - case *schema.VerbRef: - if t.Module == module.Name { - return t.Name - } - return t.String() - - case *schema.SourceRef: - if t.Module == module.Name { - return t.Name - } - return t.String() - - case *schema.SinkRef: - if t.Module == module.Name { - return t.Name - } - - case *schema.Float: - return "float64" - - case *schema.Time: - return "time.Time" - - case *schema.Bytes: - return "[]byte" - - case *schema.Int, *schema.Bool, *schema.String: - return strings.ToLower(t.String()) - - case *schema.Array: - return "[]" + genType(module, t.Element) - - case *schema.Map: - return "map[" + genType(module, t.Key) + "]" + genType(module, t.Value) - - case *schema.Optional: - return "sdk.Option[" + genType(module, t.Type) + "]" - } - panic(fmt.Sprintf("unsupported type %T", t)) -} diff --git a/go-runtime/compile/generate/external_module_test.go b/go-runtime/compile/generate/external_module_test.go deleted file mode 100644 index 91470ffcc3..0000000000 --- a/go-runtime/compile/generate/external_module_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package generate - -import ( - "strings" - "testing" - - "github.com/alecthomas/assert/v2" - - "github.com/TBD54566975/ftl/backend/schema" -) - -func TestCodegen(t *testing.T) { - w := &strings.Builder{} - module := ` - module basket { - data ItemRequest { - basketID String - itemID String - } - data BasketSummary { - items Int - name String? - } - // Add an item to the basket. - verb Add(ItemRequest) BasketSummary - // Remove an item from the basket. - verb Remove(ItemRequest) BasketSummary - }` - s, err := schema.ParseString("", module) - assert.NoError(t, err) - err = ExternalModule(w, s.Modules[1], "github.com/TBD54566975/ftl/examples") - assert.NoError(t, err) - expected := `//ftl:module basket -package basket - -import ( - "context" - "github.com/TBD54566975/ftl/go-runtime/sdk" -) - -type ItemRequest struct { - BasketID string ` + "`json:\"basketID\"`" + ` - ItemID string ` + "`json:\"itemID\"`" + ` -} - -type BasketSummary struct { - Items int ` + "`json:\"items\"`" + ` - Name sdk.Option[string] ` + "`json:\"name\"`" + ` -} - -// Add an item to the basket. -// -//ftl:verb -func Add(context.Context, ItemRequest) (BasketSummary, error) { - panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/sdk.Call()") -} - -// Remove an item from the basket. -// -//ftl:verb -func Remove(context.Context, ItemRequest) (BasketSummary, error) { - panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/sdk.Call()") -} -` - assert.Equal(t, expected, w.String()) -} diff --git a/go-runtime/compile/generate/file.go b/go-runtime/compile/generate/file.go deleted file mode 100644 index 7f81140ccc..0000000000 --- a/go-runtime/compile/generate/file.go +++ /dev/null @@ -1,21 +0,0 @@ -package generate - -import ( - "io" - "os" - "path/filepath" -) - -// File is a helper function for the generator functions in this package, to create a file and its parent directories, then call a generator function. -func File[T any](path string, importRoot string, generator func(io.Writer, T, string) error, parameter T) error { - err := os.MkdirAll(filepath.Dir(path), 0o750) - if err != nil { - return err - } - w, err := os.Create(path) - if err != nil { - return err - } - defer w.Close() //nolint:gosec - return generator(w, parameter, importRoot) -} diff --git a/go-runtime/compile/generate/go.mod.tmpl b/go-runtime/compile/generate/go.mod.tmpl deleted file mode 100644 index 217cadaa34..0000000000 --- a/go-runtime/compile/generate/go.mod.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -{{- /*gotype: github.com/TBD54566975/ftl/go-runtime/compile/generate.GoModConfig*/ -}} -module main - -go 1.21 - -{{range $module, $dir := .Replace}} -replace {{$module}} => {{$dir}} -{{end}} \ No newline at end of file diff --git a/go-runtime/compile/generate/go.work.tmpl b/go-runtime/compile/generate/go.work.tmpl deleted file mode 100644 index e251ddf81a..0000000000 --- a/go-runtime/compile/generate/go.work.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -go 1.21.5 - -use ( - . -{{range . -}} - {{.}} -{{end}} -) diff --git a/go-runtime/compile/generate/gomod.go b/go-runtime/compile/generate/gomod.go deleted file mode 100644 index a9f99e6d3f..0000000000 --- a/go-runtime/compile/generate/gomod.go +++ /dev/null @@ -1,21 +0,0 @@ -package generate - -import ( - _ "embed" - "io" - "text/template" -) - -//go:embed go.mod.tmpl -var goModTmplSource string -var goModTmpl = template.Must(template.New("go.mod.tmpl").Parse(goModTmplSource)) - -type GoModConfig struct { - // Replace directives - Replace map[string]string -} - -// GenerateGoMod generates a go.mod file. -func GenerateGoMod(w io.Writer, config GoModConfig, importRoot string) error { - return goModTmpl.Execute(w, config) -} diff --git a/go-runtime/compile/generate/gowork.go b/go-runtime/compile/generate/gowork.go deleted file mode 100644 index 01e5f6fc71..0000000000 --- a/go-runtime/compile/generate/gowork.go +++ /dev/null @@ -1,16 +0,0 @@ -package generate - -import ( - _ "embed" - "io" - "text/template" -) - -//go:embed go.work.tmpl -var goWorkTmplSource string -var goWorkTmpl = template.Must(template.New("go.mod.tmpl").Parse(goWorkTmplSource)) - -// GenerateGoWork generates a go.work file. -func GenerateGoWork(w io.Writer, modules []string, importRoot string) error { - return goWorkTmpl.Execute(w, modules) -} diff --git a/go-runtime/compile/generate/main.go b/go-runtime/compile/generate/main.go deleted file mode 100644 index 1460466bba..0000000000 --- a/go-runtime/compile/generate/main.go +++ /dev/null @@ -1,30 +0,0 @@ -package generate - -import ( - _ "embed" - "io" - "strings" - "text/template" - - "github.com/TBD54566975/ftl/backend/schema" -) - -//go:embed main.go.tmpl -var mainTmplSource string -var mainTmpl = template.Must(template.New("main.go.tmpl"). - Funcs(template.FuncMap{ - "ExportGoName": func(s string) string { return strings.ToTitle(s[:1]) + s[1:] }, - }). - Parse(mainTmplSource)) - -type mainTmplCtx struct { - ImportRoot string - *schema.Module -} - -func Main(w io.Writer, module *schema.Module, importRoot string) error { - return mainTmpl.Execute(w, &mainTmplCtx{ - ImportRoot: importRoot, - Module: module, - }) -} diff --git a/go-runtime/compile/generate/main_test.go b/go-runtime/compile/generate/main_test.go deleted file mode 100644 index 065131dcfe..0000000000 --- a/go-runtime/compile/generate/main_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package generate - -import ( - "strings" - "testing" - - "github.com/TBD54566975/ftl/backend/schema" - "github.com/alecthomas/assert/v2" -) - -func TestGenerateMain(t *testing.T) { - w := &strings.Builder{} - err := Main(w, &schema.Module{ - Name: "echo", - Decls: []schema.Decl{ - &schema.Verb{Name: "echo", Metadata: []schema.Metadata{ - &schema.MetadataCalls{Calls: []*schema.VerbRef{{Module: "time", Name: "time"}}}, - }}, - &schema.Verb{Name: "anotherEcho"}, - }, - }, "github.com/TBD54566975/ftl/examples") - assert.NoError(t, err) - expected := `// Code generated by FTL. DO NOT EDIT. -package main - -import ( - "context" - - "github.com/TBD54566975/ftl/backend/common/plugin" - "github.com/TBD54566975/ftl/examples/echo" - "github.com/TBD54566975/ftl/go-runtime/server" - "github.com/TBD54566975/ftl/protos/xyz/block/ftl/v1/ftlv1connect" -) - -func main() { - verbConstructor := server.NewUserVerbServer("echo", - server.Handle(echo.Echo), - server.Handle(echo.AnotherEcho), - ) - plugin.Start(context.Background(), "echo", verbConstructor, ftlv1connect.VerbServiceName, ftlv1connect.NewVerbServiceHandler) -} -` - assert.Equal(t, expected, w.String()) -} diff --git a/go-runtime/compile/release.go b/go-runtime/compile/release.go new file mode 100644 index 0000000000..8b909688cd --- /dev/null +++ b/go-runtime/compile/release.go @@ -0,0 +1,31 @@ +//go:build release + +package compile + +import ( + "archive/zip" + "bytes" + _ "embed" +) + +//go:embed external-module-template.zip +var externalModuleTemplateBytes []byte + +//go:embed build-template.zip +var buildTemplateBytes []byte + +func externalModuleTemplateFiles() *zip.Reader { + zr, err := zip.NewReader(bytes.NewReader(externalModuleTemplateBytes), int64(len(externalModuleTemplateBytes))) + if err != nil { + panic(err) + } + return zr +} + +func buildTemplateFiles() *zip.Reader { + zr, err := zip.NewReader(bytes.NewReader(buildTemplateBytes), int64(len(buildTemplateBytes))) + if err != nil { + panic(err) + } + return zr +} diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index 09fe91f149..3c17d9de15 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -44,6 +44,9 @@ func ExtractModuleSchema(dir string) (*schema.Module, error) { } module := &schema.Module{} for _, pkg := range pkgs { + if len(pkg.Errors) > 0 { + return nil, fmt.Errorf("%s: %w", pkg.PkgPath, pkg.Errors[0]) + } pctx := &parseContext{pkg: pkg, pkgs: pkgs, module: module} for _, file := range pkg.Syntax { var verb *schema.Verb diff --git a/go-runtime/compile/schema_test.go b/go-runtime/compile/schema_test.go index f23b87b9fa..89824447b5 100644 --- a/go-runtime/compile/schema_test.go +++ b/go-runtime/compile/schema_test.go @@ -26,8 +26,8 @@ func TestExtractModuleSchema(t *testing.T) { string String slice [String] map {String: String} - nested Nested - optional Nested? + nested one.Nested + optional one.Nested? time Time user two.User bytes Bytes @@ -36,7 +36,7 @@ func TestExtractModuleSchema(t *testing.T) { data Resp { } - verb verb(Req) Resp + verb verb(one.Req) one.Resp } ` assert.Equal(t, expected, actual.String()) diff --git a/go-runtime/devel.go b/go-runtime/devel.go index 3e35421389..ebdb5eb65d 100644 --- a/go-runtime/devel.go +++ b/go-runtime/devel.go @@ -3,8 +3,10 @@ package goruntime import ( + "archive/zip" + "github.com/TBD54566975/ftl/internal" ) // Files is the FTL Go runtime scaffolding files. -var Files = internal.ZipRelativeToCaller("scaffolding") +func Files() *zip.Reader { return internal.ZipRelativeToCaller("scaffolding") } diff --git a/go-runtime/release.go b/go-runtime/release.go index d342bb4d84..c27d878ee1 100644 --- a/go-runtime/release.go +++ b/go-runtime/release.go @@ -15,10 +15,10 @@ var archive []byte // // scaffolding.zip can be generated by running `bit go-runtime/scaffolding.zip` // or indirectly via `bit build/release/ftl`. -var Files = func() *zip.Reader { +func Files() *zip.Reader { zr, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) if err != nil { panic(err) } return zr -}() +} diff --git a/go-runtime/scaffolding/README.md b/go-runtime/scaffolding/README.md deleted file mode 100644 index 26c2a52ac1..0000000000 --- a/go-runtime/scaffolding/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# FTL modules - -Each subdirectory represents an FTL module. Remote modules will be -code-generated into their own directories, one module per directory. Note that -this is a temporary solution. - -For example given an `echo` module written in Go that calls a `time` module -written in Kotlin, the filesystem might look like this once the FTL tooling is -started: - -``` -README.md -go.mod -echo/ftl.toml -echo/echo.go -time/generated_ftl_module.go -``` diff --git a/go-runtime/scaffolding/go.mod.tmpl b/go-runtime/scaffolding/{{ .Name | camel | lower }}/go.mod.tmpl similarity index 69% rename from go-runtime/scaffolding/go.mod.tmpl rename to go-runtime/scaffolding/{{ .Name | camel | lower }}/go.mod.tmpl index 138c7ce083..b10c1a1a12 100644 --- a/go-runtime/scaffolding/go.mod.tmpl +++ b/go-runtime/scaffolding/{{ .Name | camel | lower }}/go.mod.tmpl @@ -1,4 +1,4 @@ -module ftl +module ftl/{{ .Name }} go 1.21.5 diff --git a/go-runtime/scaffolding/{{ .Name | camel | lower }}/{{ .Name | camel | lower }}.go.tmpl b/go-runtime/scaffolding/{{ .Name | camel | lower }}/{{ .Name | camel | lower }}.go.tmpl index 8b84762f21..25e8356a29 100644 --- a/go-runtime/scaffolding/{{ .Name | camel | lower }}/{{ .Name | camel | lower }}.go.tmpl +++ b/go-runtime/scaffolding/{{ .Name | camel | lower }}/{{ .Name | camel | lower }}.go.tmpl @@ -18,5 +18,5 @@ type EchoResponse struct { //ftl:verb func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { - return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name)}, nil + return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil } \ No newline at end of file diff --git a/integration/integration_test.go b/integration/integration_test.go index 0d4959ab86..65f6ed2da2 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -48,7 +48,7 @@ func TestIntegration(t *testing.T) { assertions assertions }{ {name: "DeployTime", assertions: assertions{ - run("examples", "ftl-go", "deploy", "time"), + run("examples", "ftl", "deploy", "time"), deploymentExists("time"), }}, {name: "CallTime", assertions: assertions{ diff --git a/internal/scaffold.go b/internal/scaffold.go new file mode 100644 index 0000000000..0c85f4cbc3 --- /dev/null +++ b/internal/scaffold.go @@ -0,0 +1,21 @@ +package internal + +import ( + "archive/zip" + "os" + + "github.com/TBD54566975/scaffolder" +) + +// ScaffoldZip is a convenience function for scaffolding a zip archive with scaffolder. +func ScaffoldZip(source *zip.Reader, destination string, ctx any, options ...scaffolder.Option) error { + tmpDir, err := os.MkdirTemp("", "scaffold-") + if err != nil { + return err + } + defer os.RemoveAll(tmpDir) + if err := UnzipDir(source, tmpDir); err != nil { + return err + } + return scaffolder.Scaffold(tmpDir, destination, ctx, options...) +} diff --git a/kotlin-runtime/devel.go b/kotlin-runtime/devel.go index 730fab569e..59b817dabb 100644 --- a/kotlin-runtime/devel.go +++ b/kotlin-runtime/devel.go @@ -3,8 +3,10 @@ package kotlinruntime import ( + "archive/zip" + "github.com/TBD54566975/ftl/internal" ) // Files is the FTL Kotlin runtime scaffolding files. -var Files = internal.ZipRelativeToCaller("scaffolding") +func Files() *zip.Reader { return internal.ZipRelativeToCaller("scaffolding") } diff --git a/kotlin-runtime/release.go b/kotlin-runtime/release.go index ffdf9e3416..5c61dbefe1 100644 --- a/kotlin-runtime/release.go +++ b/kotlin-runtime/release.go @@ -15,10 +15,10 @@ var archive []byte // // scaffolding.zip can be generated by running `bit kotlin-runtime/scaffolding.zip` // or indirectly via `bit build/release/ftl`. -var Files = func() *zip.Reader { +func Files() *zip.Reader { zr, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) if err != nil { panic(err) } return zr -}() +}