diff --git a/Dockerfile.box b/Dockerfile.box index c794549d4f..edb14672f7 100644 --- a/Dockerfile.box +++ b/Dockerfile.box @@ -12,7 +12,7 @@ RUN hermit install openjre-18.0.2.1_1 RUN hermit uninstall openjre RUN hermit install jbr RUN go version -RUN mvn -f kotlin-runtime/ftl-runtime -B --version +RUN mvn -f jvm-runtime/ftl-runtime -B --version WORKDIR /src diff --git a/Dockerfile.controller b/Dockerfile.controller index 8867aa0571..5db1a90da8 100644 --- a/Dockerfile.controller +++ b/Dockerfile.controller @@ -10,7 +10,7 @@ WORKDIR /src # Seed some of the most common tools - this will be cached RUN go version -RUN mvn -f kotlin-runtime/ftl-runtime -B --version +RUN mvn -f jvm-runtime/ftl-runtime -B --version RUN node --version # Download Go dependencies separately so Docker will cache them diff --git a/Dockerfile.runner b/Dockerfile.runner index 83317006e9..013842cc17 100644 --- a/Dockerfile.runner +++ b/Dockerfile.runner @@ -12,7 +12,7 @@ RUN hermit install openjre-18.0.2.1_1 RUN hermit uninstall openjre RUN hermit install jbr RUN go version -RUN mvn -f kotlin-runtime/ftl-runtime -B --version +RUN mvn -f jvm-runtime/ftl-runtime -B --version WORKDIR /src diff --git a/Justfile b/Justfile index d80123a68f..42d6c0f8c4 100644 --- a/Justfile +++ b/Justfile @@ -4,12 +4,10 @@ set shell := ["bash", "-c"] WATCHEXEC_ARGS := "-d 1s -e proto -e go -e sql -f sqlc.yaml" RELEASE := "build/release" VERSION := `git describe --tags --always | sed -e 's/^v//'` -KT_RUNTIME_OUT := "kotlin-runtime/ftl-runtime/target/ftl-runtime-1.0-SNAPSHOT.jar" -KT_RUNTIME_RUNNER_TEMPLATE_OUT := "build/template/ftl/jars/ftl-runtime.jar" RUNNER_TEMPLATE_ZIP := "backend/controller/scaling/localscaling/template.zip" TIMESTAMP := `date +%s` SCHEMA_OUT := "backend/protos/xyz/block/ftl/v1/schema/schema.proto" -ZIP_DIRS := "go-runtime/compile/build-template go-runtime/compile/external-module-template go-runtime/compile/main-work-template common-runtime/scaffolding go-runtime/scaffolding kotlin-runtime/scaffolding kotlin-runtime/external-module-template" +ZIP_DIRS := "go-runtime/compile/build-template go-runtime/compile/external-module-template go-runtime/compile/main-work-template common-runtime/scaffolding go-runtime/scaffolding" FRONTEND_OUT := "frontend/dist/index.html" EXTENSION_OUT := "extensions/vscode/dist/extension.js" PROTOS_IN := "backend/protos/xyz/block/ftl/v1/schema/schema.proto backend/protos/xyz/block/ftl/v1/console/console.proto backend/protos/xyz/block/ftl/v1/ftl.proto backend/protos/xyz/block/ftl/v1/schema/runtime.proto" @@ -30,7 +28,6 @@ clean: rm -rf build rm -rf frontend/node_modules find . -name '*.zip' -exec rm {} \; - mvn -f kotlin-runtime/ftl-runtime clean mvn -f jvm-runtime/ftl-runtime clean # Live rebuild the ftl binary whenever source changes. diff --git a/buildengine/build.go b/buildengine/build.go index aff120ed65..23a725539d 100644 --- a/buildengine/build.go +++ b/buildengine/build.go @@ -47,10 +47,8 @@ func buildModule(ctx context.Context, projectRootDir string, sch *schema.Schema, switch module.Config.Language { case "go": err = buildGoModule(ctx, projectRootDir, sch, module, filesTransaction) - case "java": + case "java", "kotlin": err = buildJavaModule(ctx, module) - case "kotlin": - err = buildKotlinModule(ctx, sch, module) case "rust": err = buildRustModule(ctx, sch, module) default: diff --git a/buildengine/build_java.go b/buildengine/build_java.go index 57b3d79c44..b136ed5ac0 100644 --- a/buildengine/build_java.go +++ b/buildengine/build_java.go @@ -3,7 +3,11 @@ package buildengine import ( "context" "fmt" + "path/filepath" + "github.com/beevik/etree" + + "github.com/TBD54566975/ftl" "github.com/TBD54566975/ftl/internal/exec" "github.com/TBD54566975/ftl/internal/log" ) @@ -21,3 +25,38 @@ func buildJavaModule(ctx context.Context, module Module) error { return nil } + +// SetPOMProperties updates the ftl.version properties in the +// pom.xml file in the given base directory. +func SetPOMProperties(ctx context.Context, baseDir string) error { + logger := log.FromContext(ctx) + ftlVersion := ftl.Version + if ftlVersion == "dev" { + ftlVersion = "1.0-SNAPSHOT" + } + + pomFile := filepath.Clean(filepath.Join(baseDir, "pom.xml")) + + logger.Debugf("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) + + err := tree.WriteToFile(pomFile) + if err != nil { + return fmt.Errorf("unable to write %s: %w", pomFile, err) + } + return nil +} diff --git a/buildengine/build_kotlin.go b/buildengine/build_kotlin.go deleted file mode 100644 index 5e81c1664c..0000000000 --- a/buildengine/build_kotlin.go +++ /dev/null @@ -1,237 +0,0 @@ -package buildengine - -import ( - "context" - "fmt" - "os" - "path/filepath" - "reflect" - "sort" - "strings" - - "github.com/TBD54566975/scaffolder" - "github.com/beevik/etree" - sets "github.com/deckarep/golang-set/v2" - "golang.org/x/exp/maps" - - "github.com/TBD54566975/ftl" - "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/internal" - "github.com/TBD54566975/ftl/internal/exec" - "github.com/TBD54566975/ftl/internal/log" - kotlinruntime "github.com/TBD54566975/ftl/kotlin-runtime" -) - -type externalModuleContext struct { - module Module - *schema.Schema -} - -func (e externalModuleContext) ExternalModules() []*schema.Module { - modules := make([]*schema.Module, 0, len(e.Modules)) - for _, module := range e.Modules { - if module.Name == e.module.Config.Module { - continue - } - modules = append(modules, module) - } - return modules -} - -func buildKotlinModule(ctx context.Context, sch *schema.Schema, module Module) error { - logger := log.FromContext(ctx) - if err := SetPOMProperties(ctx, module.Config.Dir); err != nil { - return fmt.Errorf("unable to update ftl.version in %s: %w", module.Config.Dir, err) - } - if err := generateExternalModules(ctx, module, sch); err != nil { - return fmt.Errorf("unable to generate external modules for %s: %w", module.Config.Module, err) - } - if err := prepareFTLRoot(module); err != nil { - return fmt.Errorf("unable to prepare FTL root for %s: %w", module.Config.Module, err) - } - - logger.Infof("Using build command '%s'", module.Config.Build) - err := exec.Command(ctx, log.Debug, module.Config.Dir, "bash", "-c", module.Config.Build).RunBuffered(ctx) - if err != nil { - return fmt.Errorf("failed to build module %q: %w", module.Config.Module, err) - } - - return nil -} - -// SetPOMProperties updates the ftl.version properties in the -// pom.xml file in the given base directory. -func SetPOMProperties(ctx context.Context, baseDir string) error { - logger := log.FromContext(ctx) - ftlVersion := ftl.Version - if ftlVersion == "dev" { - ftlVersion = "1.0-SNAPSHOT" - } - - pomFile := filepath.Clean(filepath.Join(baseDir, "pom.xml")) - - logger.Debugf("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) - - return tree.WriteToFile(pomFile) -} - -func prepareFTLRoot(module Module) error { - buildDir := module.Config.Abs().DeployDir - if err := os.MkdirAll(buildDir, 0700); err != nil { - return err - } - - fileContent := fmt.Sprintf(` -SchemaExtractorRuleSet: - ExtractSchemaRule: - active: true - output: %s -`, buildDir) - - detektYmlPath := filepath.Join(buildDir, "detekt.yml") - if err := os.WriteFile(detektYmlPath, []byte(fileContent), 0600); err != nil { - return fmt.Errorf("unable to configure detekt for %s: %w", module.Config.Module, err) - } - - mainFilePath := filepath.Join(buildDir, "main") - - mainFile := `#!/bin/bash -exec java -cp "classes:$(cat classpath.txt)" xyz.block.ftl.main.MainKt -` - if err := os.WriteFile(mainFilePath, []byte(mainFile), 0700); err != nil { //nolint:gosec - return fmt.Errorf("unable to configure main executable for %s: %w", module.Config.Module, err) - } - return nil -} - -func generateExternalModules(ctx context.Context, module Module, sch *schema.Schema) error { - logger := log.FromContext(ctx) - funcs := maps.Clone(scaffoldFuncs) - - // Wipe the modules directory to ensure we don't have any stale modules. - _ = os.RemoveAll(filepath.Join(module.Config.Dir, "target", "generated-sources", "ftl")) - - logger.Debugf("Generating external modules") - return internal.ScaffoldZip(kotlinruntime.ExternalModuleTemplates(), module.Config.Dir, externalModuleContext{ - module: module, - Schema: sch, - }, scaffolder.Exclude("^go.mod$"), scaffolder.Functions(funcs)) -} - -var scaffoldFuncs = scaffolder.FuncMap{ - "comment": func(s []string) string { - if len(s) == 0 { - return "" - } - var sb strings.Builder - sb.WriteString("/**\n") - for _, line := range s { - sb.WriteString(" * ") - sb.WriteString(line) - sb.WriteString("\n") - } - sb.WriteString(" */\n") - return sb.String() - }, - "type": genType, - "is": func(kind string, t schema.Node) bool { - return reflect.Indirect(reflect.ValueOf(t)).Type().Name() == kind - }, - "imports": func(m *schema.Module) []string { - imports := sets.NewSet[string]() - _ = schema.VisitExcludingMetadataChildren(m, func(n schema.Node, next func() error) error { //nolint:errcheck - switch n.(type) { - case *schema.Data: - imports.Add("xyz.block.ftl.Data") - - case *schema.Enum: - imports.Add("xyz.block.ftl.Enum") - - case *schema.Verb: - imports.Append("xyz.block.ftl.Context", "xyz.block.ftl.Ignore", "xyz.block.ftl.Verb") - - case *schema.Time: - imports.Add("java.time.OffsetDateTime") - - default: - } - return next() - }) - importsList := imports.ToSlice() - sort.Strings(importsList) - return importsList - }, -} - -func genType(module *schema.Module, t schema.Type) string { - switch t := t.(type) { - case *schema.Ref: - decl := module.Resolve(schema.Ref{ - Module: t.Module, - Name: t.Name, - }) - if decl != nil { - if data, ok := decl.Symbol.(*schema.Data); ok { - if len(data.Fields) == 0 { - return "ftl.builtin.Empty" - } - } - } - - desc := t.Name - if t.Module != "" { - desc = "ftl." + t.Module + "." + desc - } - if len(t.TypeParameters) > 0 { - desc += "<" - for i, tp := range t.TypeParameters { - if i != 0 { - desc += ", " - } - desc += genType(module, tp) - } - desc += ">" - } - return desc - - case *schema.Time: - return "OffsetDateTime" - - case *schema.Array: - return "List<" + genType(module, t.Element) + ">" - - case *schema.Map: - return "Map<" + genType(module, t.Key) + ", " + genType(module, t.Value) + ">" - - case *schema.Optional: - return genType(module, t.Type) + "? = null" - - case *schema.Bytes: - return "ByteArray" - - case *schema.Bool: - return "Boolean" - - case *schema.Int: - return "Long" - - case *schema.Float, *schema.String, *schema.Any, *schema.Unit: - return t.String() - } - panic(fmt.Sprintf("unsupported type %T", t)) -} diff --git a/buildengine/build_kotlin_test.go b/buildengine/build_kotlin_test.go deleted file mode 100644 index 3db650b2f5..0000000000 --- a/buildengine/build_kotlin_test.go +++ /dev/null @@ -1,432 +0,0 @@ -//go:build ignore - -package buildengine - -import ( - "testing" - - "github.com/TBD54566975/ftl/backend/schema" -) - -func TestGenerateBasicModule(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - sch := &schema.Schema{ - Modules: []*schema.Module{ - schema.Builtins(), - {Name: "test"}, - }, - } - expected := `// Code generated by FTL. DO NOT EDIT. -package ftl.test - -` - bctx := buildContext{ - moduleDir: "testdata/echokotlin", - buildDir: "target", - sch: sch, - } - testBuild(t, bctx, "", []assertion{ - assertGeneratedModule("generated-sources/ftl/test/Test.kt", expected), - }) -} - -func TestKotlinBuildClearsBuildDir(t *testing.T) { - sch := &schema.Schema{ - Modules: []*schema.Module{ - schema.Builtins(), - {Name: "test"}, - }, - } - bctx := buildContext{ - moduleDir: "testdata/echokotlin", - buildDir: "target", - sch: sch, - } - testBuildClearsBuildDir(t, bctx) -} - -func TestGenerateAllTypes(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - sch := &schema.Schema{ - Modules: []*schema.Module{ - schema.Builtins(), - { - Name: "other", - Decls: []schema.Decl{ - &schema.Data{Name: "TestRequest", Fields: []*schema.Field{{Name: "field", Type: &schema.Int{}}}}, - }, - }, - { - Name: "test", - Comments: []string{"Module comments"}, - Decls: []schema.Decl{ - &schema.Data{ - Name: "ParamTestData", - TypeParameters: []*schema.TypeParameter{{Name: "T"}}, - Fields: []*schema.Field{ - {Name: "t", Type: &schema.Ref{Name: "T"}}, - }, - }, - &schema.Data{Name: "TestRequest", Fields: []*schema.Field{{Name: "field", Type: &schema.Int{}}}}, - &schema.Data{ - Name: "TestResponse", - Comments: []string{"Response comments"}, - Fields: []*schema.Field{ - {Name: "int", Type: &schema.Int{}}, - {Name: "float", Type: &schema.Float{}}, - {Name: "string", Type: &schema.String{}}, - {Name: "bytes", Type: &schema.Bytes{}}, - {Name: "bool", Type: &schema.Bool{}}, - {Name: "time", Type: &schema.Time{}}, - {Name: "optional", Type: &schema.Optional{ - Type: &schema.String{}, - }}, - {Name: "array", Type: &schema.Array{ - Element: &schema.String{}, - }}, - {Name: "nestedArray", Type: &schema.Array{ - Element: &schema.Array{Element: &schema.String{}}}, - }, - {Name: "RefArray", Type: &schema.Array{ - Element: &schema.Ref{Name: "TestRequest", Module: "test"}, - }}, - {Name: "map", Type: &schema.Map{ - Key: &schema.String{}, - Value: &schema.Int{}, - }}, - {Name: "nestedMap", Type: &schema.Map{ - Key: &schema.String{}, - Value: &schema.Map{Key: &schema.String{}, Value: &schema.Int{}}, - }}, - {Name: "Ref", Type: &schema.Ref{Name: "TestRequest"}}, - {Name: "externalRef", Type: &schema.Ref{Name: "TestRequest", Module: "other"}}, - {Name: "any", Type: &schema.Any{}}, - {Name: "parameterizedRef", Type: &schema.Ref{ - Name: "ParamTestData", - TypeParameters: []schema.Type{&schema.String{}}, - }, - }, - {Name: "withAlias", Type: &schema.String{}, Metadata: []schema.Metadata{&schema.MetadataAlias{Alias: "a"}}}, - {Name: "unit", Type: &schema.Unit{}}, - }, - }, - }, - }, - }, - } - - expected := `// Code generated by FTL. DO NOT EDIT. -/** - * Module comments - */ -package ftl.test - -import java.time.OffsetDateTime -import xyz.block.ftl.Data - -@Data -data class ParamTestData( - val t: T, -) - -@Data -data class TestRequest( - val field: Long, -) - -/** - * Response comments - */ -@Data -data class TestResponse( - val int: Long, - val float: Float, - val string: String, - val bytes: ByteArray, - val bool: Boolean, - val time: OffsetDateTime, - val optional: String? = null, - val array: List, - val nestedArray: List>, - val RefArray: List, - val map: Map, - val nestedMap: Map>, - val Ref: TestRequest, - val externalRef: ftl.other.TestRequest, - val any: Any, - val parameterizedRef: ParamTestData, - val withAlias: String, - val unit: Unit, -) - -` - bctx := buildContext{ - moduleDir: "testdata/echokotlin", - buildDir: "target", - sch: sch, - } - testBuild(t, bctx, "", []assertion{ - assertGeneratedModule("generated-sources/ftl/test/Test.kt", expected), - }) -} - -func TestGenerateAllVerbs(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - sch := &schema.Schema{ - Modules: []*schema.Module{ - schema.Builtins(), - { - Name: "test", - Comments: []string{"Module comments"}, - Decls: []schema.Decl{ - &schema.Data{ - Name: "Request", - Fields: []*schema.Field{ - {Name: "data", Type: &schema.Int{}}, - }, - }, - &schema.Verb{ - Name: "TestVerb", - Comments: []string{"TestVerb comments"}, - Request: &schema.Ref{Name: "Request"}, - Response: &schema.Ref{Name: "Empty", Module: "builtin"}, - }, - }, - }, - }, - } - - expected := `// Code generated by FTL. DO NOT EDIT. -/** - * Module comments - */ -package ftl.test - -import xyz.block.ftl.Context -import xyz.block.ftl.Data -import xyz.block.ftl.Ignore -import xyz.block.ftl.Verb - -@Data -data class Request( - val data: Long, -) - -/** - * TestVerb comments - */ -@Verb -@Ignore -fun testVerb(context: Context, req: Request): ftl.builtin.Empty = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.call(::testVerb, ...)") -` - bctx := buildContext{ - moduleDir: "testdata/echokotlin", - buildDir: "target", - sch: sch, - } - testBuild(t, bctx, "", []assertion{ - assertGeneratedModule("generated-sources/ftl/test/Test.kt", expected), - }) -} - -func TestGenerateBuiltins(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - sch := &schema.Schema{ - Modules: []*schema.Module{ - schema.Builtins(), - }, - } - expected := `// Code generated by FTL. DO NOT EDIT. -/** - * Built-in types for FTL. - */ -package ftl.builtin - -import xyz.block.ftl.Data - -/** - * HTTP request structure used for HTTP ingress verbs. - */ -@Data -data class HttpRequest( - val method: String, - val path: String, - val pathParameters: Map, - val query: Map>, - val headers: Map>, - val body: Body, -) - -/** - * HTTP response structure used for HTTP ingress verbs. - */ -@Data -data class HttpResponse( - val status: Long, - val headers: Map>, - val body: Body? = null, - val error: Error? = null, -) - -@Data -class Empty -` - bctx := buildContext{ - moduleDir: "testdata/echokotlin", - buildDir: "target", - sch: sch, - } - testBuild(t, bctx, "", []assertion{ - assertGeneratedModule("generated-sources/ftl/builtin/Builtin.kt", expected), - }) -} - -func TestGenerateEmptyRefs(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - sch := &schema.Schema{ - Modules: []*schema.Module{ - schema.Builtins(), - { - Name: "test", - Decls: []schema.Decl{ - &schema.Data{Name: "EmptyRequest"}, - &schema.Data{Name: "EmptyResponse"}, - &schema.Verb{ - Name: "EmptyVerb", - Request: &schema.Ref{Name: "EmptyRequest"}, - Response: &schema.Ref{Name: "EmptyResponse"}, - }, - }, - }, - }, - } - - expected := `// Code generated by FTL. DO NOT EDIT. -package ftl.test - -import xyz.block.ftl.Context -import xyz.block.ftl.Data -import xyz.block.ftl.Ignore -import xyz.block.ftl.Verb - -@Verb -@Ignore -fun emptyVerb(context: Context, req: ftl.builtin.Empty): ftl.builtin.Empty = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.call(::emptyVerb, ...)") -` - bctx := buildContext{ - moduleDir: "testdata/echokotlin", - buildDir: "target", - sch: sch, - } - testBuild(t, bctx, "", []assertion{ - assertGeneratedModule("generated-sources/ftl/test/Test.kt", expected), - }) -} - -func TestGenerateSourcesAndSinks(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - sch := &schema.Schema{ - Modules: []*schema.Module{ - schema.Builtins(), - { - Name: "test", - Decls: []schema.Decl{ - &schema.Data{ - Name: "SinkReq", - Fields: []*schema.Field{ - {Name: "data", Type: &schema.Int{}}, - }}, - &schema.Verb{ - Name: "sink", - Request: &schema.Ref{Name: "SinkReq"}, - Response: &schema.Unit{}, - }, - &schema.Data{ - Name: "SourceResp", - Fields: []*schema.Field{ - {Name: "data", Type: &schema.Int{}}, - }}, - &schema.Verb{ - Name: "source", - Request: &schema.Unit{}, - Response: &schema.Ref{Name: "SourceResp"}, - }, - &schema.Verb{ - Name: "nothing", - Request: &schema.Unit{}, - Response: &schema.Unit{}, - }, - }, - }, - }, - } - - expected := `// Code generated by FTL. DO NOT EDIT. -package ftl.test - -import xyz.block.ftl.Context -import xyz.block.ftl.Data -import xyz.block.ftl.Ignore -import xyz.block.ftl.Verb - -@Data -data class SinkReq( - val data: Long, -) - -@Verb -@Ignore -fun sink(context: Context, req: SinkReq): Unit = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.callSink(::sink, ...)") -@Data -data class SourceResp( - val data: Long, -) - -@Verb -@Ignore -fun source(context: Context): SourceResp = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.callSource(::source, ...)") -@Verb -@Ignore -fun nothing(context: Context): Unit = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.callEmpty(::nothing, ...)") -` - bctx := buildContext{ - moduleDir: "testdata/echokotlin", - buildDir: "target", - sch: sch, - } - testBuild(t, bctx, "", []assertion{ - assertGeneratedModule("generated-sources/ftl/test/Test.kt", expected), - }) -} - -func TestKotlinExternalType(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - bctx := buildContext{ - moduleDir: "testdata/externalkotlin", - buildDir: "target", - sch: &schema.Schema{}, - } - expectedErrMsg := "expected module name to be in the form ftl., but was com.google.type.DayOfWeek" - testBuild(t, bctx, expectedErrMsg, []assertion{ - assertBuildProtoErrors(expectedErrMsg), - }) -} diff --git a/buildengine/deps.go b/buildengine/deps.go index 2f4bf4a5a1..e6458acf6e 100644 --- a/buildengine/deps.go +++ b/buildengine/deps.go @@ -48,11 +48,8 @@ func extractDependencies(module Module) ([]string, error) { case "go": return extractGoFTLImports(module.Config.Module, module.Config.Dir) - case "kotlin": - return extractKotlinFTLImports(module.Config.Module, module.Config.Dir) - - case "java": - return extractJavaFTLImports(module.Config.Module, module.Config.Dir) + case "java", "kotlin": + return extractJVMFTLImports(module.Config.Module, module.Config.Dir) case "rust": return extractRustFTLImports(module.Config.Module, module.Config.Dir) @@ -143,7 +140,7 @@ func extractKotlinFTLImports(self, dir string) ([]string, error) { return modules, nil } -func extractJavaFTLImports(self, dir string) ([]string, error) { +func extractJVMFTLImports(self, dir string) ([]string, error) { dependencies := map[string]bool{} // We also attempt to look at kotlin files // As the Java module supports both diff --git a/buildengine/discover_test.go b/buildengine/discover_test.go index 26dbb8db02..4421bfd8a6 100644 --- a/buildengine/discover_test.go +++ b/buildengine/discover_test.go @@ -76,13 +76,12 @@ func TestDiscoverModules(t *testing.T) { Build: "mvn -B package", Deploy: []string{ "main", - "classes", - "dependency", - "classpath.txt", + "quarkus-app", }, - DeployDir: "target", - Schema: "schema.pb", - Errors: "errors.pb", + DeployDir: "target", + GeneratedSchemaDir: "src/main/ftl-module-schema", + Schema: "schema.pb", + Errors: "errors.pb", Watch: []string{ "pom.xml", "src/**", @@ -119,13 +118,12 @@ func TestDiscoverModules(t *testing.T) { Build: "mvn -B package", Deploy: []string{ "main", - "classes", - "dependency", - "classpath.txt", + "quarkus-app", }, - DeployDir: "target", - Schema: "schema.pb", - Errors: "errors.pb", + DeployDir: "target", + GeneratedSchemaDir: "src/main/ftl-module-schema", + Schema: "schema.pb", + Errors: "errors.pb", Watch: []string{ "pom.xml", "src/**", diff --git a/cmd/ftl/cmd_new.go b/cmd/ftl/cmd_new.go index 7a93c15df6..e5eddc5a92 100644 --- a/cmd/ftl/cmd_new.go +++ b/cmd/ftl/cmd_new.go @@ -16,13 +16,11 @@ import ( "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/backend/schema/strcase" - "github.com/TBD54566975/ftl/buildengine" "github.com/TBD54566975/ftl/common/projectconfig" goruntime "github.com/TBD54566975/ftl/go-runtime" "github.com/TBD54566975/ftl/internal" "github.com/TBD54566975/ftl/internal/exec" "github.com/TBD54566975/ftl/internal/log" - kotlinruntime "github.com/TBD54566975/ftl/kotlin-runtime" ) type newCmd struct { @@ -87,38 +85,7 @@ func (i newGoCmd) Run(ctx context.Context) error { } func (i newKotlinCmd) Run(ctx context.Context) error { - name, path, err := validateModule(i.Dir, i.Name) - if err != nil { - return err - } - - config, err := projectconfig.Load(ctx, "") - if err != nil { - return fmt.Errorf("failed to load project config: %w", err) - } - - logger := log.FromContext(ctx) - logger.Debugf("Creating FTL Kotlin module %q in %s", name, path) - if err := scaffold(ctx, config.Hermit, kotlinruntime.Files(), i.Dir, i); err != nil { - return err - } - - if err := buildengine.SetPOMProperties(ctx, path); err != nil { - return err - } - - logger.Debugf("Adding files to git") - if !config.NoGit { - if config.Hermit { - if err := maybeGitAdd(ctx, i.Dir, "bin/*"); err != nil { - return err - } - } - if err := maybeGitAdd(ctx, i.Dir, filepath.Join(path, "*")); err != nil { - return err - } - } - return nil + return fmt.Errorf("kotlin scaffolinging temporarily removed") } func validateModule(dir string, name string) (string, string, error) { diff --git a/common/moduleconfig/moduleconfig.go b/common/moduleconfig/moduleconfig.go index 53ff6b2246..9b137ca8a4 100644 --- a/common/moduleconfig/moduleconfig.go +++ b/common/moduleconfig/moduleconfig.go @@ -129,20 +129,7 @@ func setConfigDefaults(moduleDir string, config *ModuleConfig) error { config.Errors = "errors.pb" } switch config.Language { - case "kotlin": - if config.Build == "" { - config.Build = "mvn -B package" - } - if config.DeployDir == "" { - config.DeployDir = "target" - } - if len(config.Deploy) == 0 { - config.Deploy = []string{"main", "classes", "dependency", "classpath.txt"} - } - if len(config.Watch) == 0 { - config.Watch = []string{"pom.xml", "src/**", "target/generated-sources"} - } - case "java": + case "kotlin", "java": if config.Build == "" { config.Build = "mvn -B package" } diff --git a/kotlin-runtime/.gitignore b/kotlin-runtime/.gitignore deleted file mode 100644 index 08b1802abf..0000000000 --- a/kotlin-runtime/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -# Ignore Gradle project-specific cache directory -.gradle - -# Ignore Gradle build output directory -build -generated \ No newline at end of file diff --git a/kotlin-runtime/README.md b/kotlin-runtime/README.md deleted file mode 100644 index d1696df19e..0000000000 --- a/kotlin-runtime/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# FTL Kotlin Runtime - -This contains the code for the FTL Kotlin runtime environment. - -## Tips - -### Debugging Maven commands with IntelliJ - -The Kotlin runtime is built and packaged using Maven. If you would like to debug Maven commands using Intellij: - -1. Click `Run->Edit Configurations...` to bring up the run configurations window. - -2. Hit `+` to add a new configuration and select `Remove JVM Debug`. Provide the following configurations and save: -- `Debugger Mode`: `Attach to remote JVM` -- `Host`: `localhost` -- `Port`: `8000` -- `Command line arguments for remote JVM`: `-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000` - -3. Run any `mvn` command, substituting `mvnDebug` in place of `mvn` (e.g. `mvnDebug compile`). This command should hang in the terminal, awaiting your remote debugger invocation. -4. Select the newly created debugger from the `Run / Debug Configurations` drop-down in the top right of your IntelliJ -window. With the debugger selected, hit the debug icon to run it. From here the `mvn` command will -execute, stopping at any breakpoints specified. - -#### Debugging Detekt -FTL uses [Detekt](https://github.com/Ozsie/detekt-maven-plugin) to perform static analysis, extracting FTL schemas -from modules written in Kotlin. Detekt is run as part of the `compile` phase of the Maven lifecycle and thus -can be invoked by running `mvn compile` from the command line when inside an FTL module. Use the above -instructions to configure a Maven debugger and debug Detekt with `mvnDebug compile`. \ No newline at end of file diff --git a/kotlin-runtime/devel.go b/kotlin-runtime/devel.go deleted file mode 100644 index 2692073383..0000000000 --- a/kotlin-runtime/devel.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !release - -package kotlinruntime - -import ( - "archive/zip" - - "github.com/TBD54566975/ftl/internal" -) - -// Files is the FTL Kotlin runtime scaffolding files. -func Files() *zip.Reader { return internal.ZipRelativeToCaller("scaffolding") } - -// ExternalModuleTemplates are templates for scaffolding external modules in the FTL Kotlin runtime. -func ExternalModuleTemplates() *zip.Reader { - return internal.ZipRelativeToCaller("external-module-template") -} diff --git a/kotlin-runtime/external-module-template/go.mod b/kotlin-runtime/external-module-template/go.mod deleted file mode 100644 index 3c7276b96d..0000000000 --- a/kotlin-runtime/external-module-template/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module ignore - -go 1.22.2 diff --git a/kotlin-runtime/external-module-template/target.tmpl/generated-sources/ftl/{{ range .ExternalModules }}{{ push .Name . }}{{ end }}/{{ .Name | camel }}.kt b/kotlin-runtime/external-module-template/target.tmpl/generated-sources/ftl/{{ range .ExternalModules }}{{ push .Name . }}{{ end }}/{{ .Name | camel }}.kt deleted file mode 100644 index d88442c52c..0000000000 --- a/kotlin-runtime/external-module-template/target.tmpl/generated-sources/ftl/{{ range .ExternalModules }}{{ push .Name . }}{{ end }}/{{ .Name | camel }}.kt +++ /dev/null @@ -1,55 +0,0 @@ -{{- $moduleName := .Name -}} -// Code generated by FTL. DO NOT EDIT. -{{.Comments|comment -}} -package ftl.{{.Name}} - -{{- $imports := (.|imports)}} -{{- if $imports}} -{{range $import := $imports}} -import {{$import}} -{{- end}} -{{else}} -{{end -}} - -{{range .Decls}} -{{- if .IsExported}} -{{- if is "Data" . }} -{{- if and (eq $moduleName "builtin") (eq .Name "Empty")}} -{{.Comments|comment -}} -@Data -class Empty -{{- else if .Fields}} -{{.Comments|comment -}} -@Data -data class {{.Name|title}} -{{- if .TypeParameters}}< -{{- range $i, $tp := .TypeParameters}} -{{- if $i}}, {{end}}{{$tp}} -{{- end -}} ->{{- end}}( - {{- range .Fields}} - val {{.Name}}: {{type $ .Type}}, - {{- end}} -) -{{end}} - -{{- else if is "Verb" . }} -{{.Comments|comment -}}@Verb -@Ignore -{{- if and (eq (type $ .Request) "Unit") (eq (type $ .Response) "Unit")}} -fun {{.Name|lowerCamel}}(context: Context): Unit = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.callEmpty(::{{.Name|lowerCamel}}, ...)") -{{- else if eq (type $ .Request) "Unit"}} -fun {{.Name|lowerCamel}}(context: Context): {{type $ .Response}} = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.callSource(::{{.Name|lowerCamel}}, ...)") -{{- else if eq (type $ .Response) "Unit"}} -fun {{.Name|lowerCamel}}(context: Context, req: {{type $ .Request}}): Unit = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.callSink(::{{.Name|lowerCamel}}, ...)") -{{- else}} -fun {{.Name|lowerCamel}}(context: Context, req: {{type $ .Request}}): {{type $ .Response}} = throw - NotImplementedError("Verb stubs should not be called directly, instead use context.call(::{{.Name|lowerCamel}}, ...)") -{{- end}} -{{- end}} - -{{- end}} -{{- end}} diff --git a/kotlin-runtime/ftl-runtime/pom.xml b/kotlin-runtime/ftl-runtime/pom.xml deleted file mode 100644 index 4a2e9c72d1..0000000000 --- a/kotlin-runtime/ftl-runtime/pom.xml +++ /dev/null @@ -1,506 +0,0 @@ - - - 4.0.0 - - xyz.block - ftl-runtime - jar - 1.0-SNAPSHOT - FTL - Towards a 𝝺-calculus for large-scale systems - https://github.com/TBD54566975/ftl - - - - The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - - Alec Thomas - TBD - https://github.com/TBD54566975 - - - Wes Billman - TBD - https://github.com/TBD54566975 - - - Elizabeth Worstell - TBD - https://github.com/TBD54566975 - - - Matt Toohey - TBD - https://github.com/TBD54566975 - - - - - scm:git:git://github.com/TBD54566975/ftl.git - scm:git:ssh://github.com/TBD54566975/ftl.git - https://github.com/TBD54566975/ftl/tree/main - - - - ${basedir}/../.. - false - 1.23.6 - 17 - 2.0.10 - false - 4.9.9 - 1.66.0 - 1.5.6 - 5.10.3 - 8.0 - - - - - - org.junit - junit-bom - ${junit.version} - pom - import - - - - - - org.jetbrains.kotlin - kotlin-reflect - 2.0.10 - - - - io.gitlab.arturbosch.detekt - detekt-api - ${detekt.version} - - - - - io.github.classgraph - classgraph - 4.8.174 - - - - - com.google.code.gson - gson - 2.11.0 - - - - com.squareup.wire - wire-runtime-jvm - ${wire.version} - - - com.squareup.wire - wire-grpc-server - ${wire.version} - - - com.squareup.wire - wire-grpc-client-jvm - ${wire.version} - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - - net.logstash.logback - logstash-logback-encoder - ${logstash.version} - - - ch.qos.logback - logback-classic - ${logback.version} - - - ch.qos.logback - logback-core - ${logback.version} - - - - io.grpc - grpc-netty - ${grpc.version} - - - io.grpc - grpc-protobuf - ${grpc.version} - - - io.grpc - grpc-stub - ${grpc.version} - - - org.hotswapagent - hotswap-agent-core - 1.4.1 - - - - - io.gitlab.arturbosch.detekt - detekt-test - ${detekt.version} - test - - - org.jetbrains.kotlin - kotlin-test-junit5 - ${kotlin.version} - test - - - org.junit.jupiter - junit-jupiter - - - org.junit.jupiter - junit-jupiter-engine - test - - - org.junit.jupiter - junit-jupiter-params - test - - - org.junit-pioneer - junit-pioneer - 2.2.0 - compile - - - org.assertj - assertj-core - 3.26.3 - test - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.0 - - - generate-sources - - add-source - - - - ${project.basedir}/target/generated-sources/wire - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.3.1 - - - Test* - *Test - - - --add-opens java.base/java.util=ALL-UNNAMED - --add-opens java.base/java.lang=ALL-UNNAMED - - - - - kotlin-maven-plugin - org.jetbrains.kotlin - ${kotlin.version} - - - compile - - compile - - - - ${project.basedir}/src/main/kotlin - - - - - test-compile - - test-compile - - - - ${project.basedir}/src/test/kotlin - - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 3.7.1 - - - initialize - - copy - - - - - com.squareup.wire - wire-compiler - ${wire.version} - jar-with-dependencies - wire-compiler.jar - - - - - - - - - org.codehaus.mojo - exec-maven-plugin - 3.4.0 - - - wire-client - initialize - - exec - - - java - - -jar - target/dependency/wire-compiler.jar - --proto_path=${rootDir}/backend/protos - - --kotlin_out=${project.build.directory}/generated-sources/wire - - --kotlin_rpc_role=client - - - - - wire-server - initialize - - exec - - - java - - -jar - target/dependency/wire-compiler.jar - --proto_path=${rootDir}/backend/protos - --kotlin_out=target/generated-sources/wire - --kotlin_rpc_role=server - --kotlin_rpc_call_style=blocking - --kotlin_grpc_server_compatible - --includes=xyz.block.ftl.v1.VerbService - - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.6.0 - - - org.codehaus.mojo - flatten-maven-plugin - 1.6.0 - - - flatten - process-resources - - flatten - - - - flatten.clean - clean - - clean - - - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - org.apache.maven.plugins - maven-surefire-plugin - - - kotlin-maven-plugin - org.jetbrains.kotlin - - - - org.apache.maven.plugins - maven-dependency-plugin - - - - org.codehaus.mojo - exec-maven-plugin - - - org.apache.maven.plugins - maven-compiler-plugin - 3.13.0 - - ${java.version} - ${java.version} - - - - default-compile - none - - - - - - - - - - release - - - - org.codehaus.mojo - flatten-maven-plugin - - ossrh - true - - - - org.apache.maven.plugins - maven-source-plugin - 3.3.1 - - - attach-sources - package - - jar-no-fork - - - - - - org.jetbrains.dokka - dokka-maven-plugin - 1.9.20 - - - attach-javadoc - package - - javadocJar - - - - - - ${project.basedir}/src/main/kotlin - - ${project.build.directory} - - - - org.apache.maven.plugins - maven-gpg-plugin - 3.2.4 - - - sign-artifacts - verify - - sign - - - - --pinentry-mode - loopback - --keyserver - hkp://keys.openpgp.org/ - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.7.0 - true - - ${maven.deploy.skip} - ossrh - https://s01.oss.sonatype.org/ - true - 10 - - - - - - - - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - \ No newline at end of file diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Context.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Context.kt deleted file mode 100644 index b3a13a64a9..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Context.kt +++ /dev/null @@ -1,43 +0,0 @@ -package xyz.block.ftl - -import xyz.block.ftl.client.VerbServiceClient -import xyz.block.ftl.registry.Ref -import xyz.block.ftl.registry.ftlModuleFromJvmModule -import xyz.block.ftl.serializer.makeGson -import java.security.InvalidParameterException -import kotlin.jvm.internal.CallableReference -import kotlin.reflect.KFunction -import kotlin.reflect.full.hasAnnotation - -class Context( - val jvmModule: String, - val routingClient: VerbServiceClient, -) { - val gson = makeGson() - - /// Class method with Context. - inline fun call(verb: KFunction, request: Any): R { - if (!verb.hasAnnotation() && !verb.hasAnnotation() && !verb.hasAnnotation()) throw InvalidParameterException( - "verb must be annotated with @Verb, @HttpIngress, or @Cron" - ) - if (verb !is CallableReference) { - throw InvalidParameterException("could not determine module from verb name") - } - val ftlModule = ftlModuleFromJvmModule(jvmModule, verb) - val requestJson = gson.toJson(request) - val responseJson = routingClient.call(this, Ref(ftlModule, verb.name), requestJson) - return gson.fromJson(responseJson, R::class.java) - } - - inline fun callSink(verb: KFunction, request: Any) { - call(verb, request) - } - - inline fun callSource(verb: KFunction): R { - return call(verb, Unit) - } - - inline fun callEmpty(verb: KFunction) { - call(verb, Unit) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Cron.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Cron.kt deleted file mode 100644 index 554c247d4c..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Cron.kt +++ /dev/null @@ -1,9 +0,0 @@ -package xyz.block.ftl - -/** - * A Verb marked as Cron will be run on a schedule. - */ -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Cron(val pattern: String) diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Data.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Data.kt deleted file mode 100644 index 16426c2440..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Data.kt +++ /dev/null @@ -1,6 +0,0 @@ -package xyz.block.ftl - -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Data(val export: Boolean = false) diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Database.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Database.kt deleted file mode 100644 index 35e805539e..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Database.kt +++ /dev/null @@ -1,63 +0,0 @@ -package xyz.block.ftl - -import xyz.block.ftl.logging.Logging -import java.net.URI -import java.sql.Connection -import java.sql.DriverManager - -private const val FTL_DSN_VAR_PREFIX = "FTL_POSTGRES_DSN" - -/** - * `Database` is a simple wrapper around the JDBC driver manager that provides a connection to the database specified - * by the FTL_POSTGRES_DSN__ environment variable. - */ -class Database(private val name: String) { - private val logger = Logging.logger(Database::class) - private val moduleName: String = Thread.currentThread().stackTrace[2]?.let { - val components = it.className.split(".") - require(components.first() == "ftl") { - "Expected Database to be declared in package ftl., but was $it" - } - - return@let components[1] - } ?: throw IllegalStateException("Could not determine module name from Database declaration") - - fun conn(block: (c: Connection) -> R): R { - return try { - val envVar = listOf(FTL_DSN_VAR_PREFIX, moduleName.uppercase(), name.uppercase()).joinToString("_") - val dsn = System.getenv(envVar) - require(dsn != null) { "missing DSN environment variable $envVar" } - - DriverManager.getConnection(dsnToJdbcUrl(dsn)).use { - block(it) - } - } catch (e: Exception) { - logger.error("Could not connect to database", e) - throw e - } - } - - private fun dsnToJdbcUrl(dsn: String): String { - val uri = URI(dsn) - val scheme = uri.scheme ?: throw IllegalArgumentException("Missing scheme in DSN.") - val userInfo = uri.userInfo?.split(":") ?: throw IllegalArgumentException("Missing userInfo in DSN.") - val user = userInfo.firstOrNull() ?: throw IllegalArgumentException("Missing user in userInfo.") - val password = if (userInfo.size > 1) userInfo[1] else "" - val host = uri.host ?: throw IllegalArgumentException("Missing host in DSN.") - val port = if (uri.port != -1) uri.port.toString() else throw IllegalArgumentException("Missing port in DSN.") - val database = uri.path.trimStart('/') - val parameters = uri.query?.replace("&", "?") ?: "" - - val jdbcScheme = when (scheme) { - "postgres" -> "jdbc:postgresql" - else -> throw IllegalArgumentException("Unsupported scheme: $scheme") - } - - val jdbcUrl = "$jdbcScheme://$host:$port/$database?$parameters" - return if (user.isNotBlank() && password.isNotBlank()) { - "$jdbcUrl&user=$user&password=$password" - } else { - jdbcUrl - } - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Enum.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Enum.kt deleted file mode 100644 index 67bb069e04..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Enum.kt +++ /dev/null @@ -1,6 +0,0 @@ -package xyz.block.ftl - -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Enum(val export: Boolean = false) diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/HttpIngress.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/HttpIngress.kt deleted file mode 100644 index 1563fc58f3..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/HttpIngress.kt +++ /dev/null @@ -1,21 +0,0 @@ -package xyz.block.ftl - -enum class Method { - GET, POST, PUT, DELETE -} - -/** - * A Verb marked as Ingress accepts HTTP requests, where the request is decoded into an arbitrary FTL type. - */ -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class HttpIngress(val method: Method, val path: String) - -/** - * A field marked with Json will be renamed to the specified name on ingress from external inputs. - */ -@Target(AnnotationTarget.FIELD) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Json(val name: String) diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Ignore.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Ignore.kt deleted file mode 100644 index f5f2fe23e4..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Ignore.kt +++ /dev/null @@ -1,9 +0,0 @@ -package xyz.block.ftl - -/** - * Ignore a method when registering verbs. - */ -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Ignore diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Verb.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Verb.kt deleted file mode 100644 index 5c4e037839..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Verb.kt +++ /dev/null @@ -1,6 +0,0 @@ -package xyz.block.ftl - -@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Verb(val export: Boolean = false) diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/client/VerbServiceClient.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/client/VerbServiceClient.kt deleted file mode 100644 index 6655b08c6d..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/client/VerbServiceClient.kt +++ /dev/null @@ -1,49 +0,0 @@ -package xyz.block.ftl.client - -import okio.ByteString.Companion.encodeUtf8 -import xyz.block.ftl.Context -import xyz.block.ftl.registry.Registry -import xyz.block.ftl.registry.Ref -import xyz.block.ftl.v1.CallRequest -import xyz.block.ftl.v1.VerbServiceWireGrpc.VerbServiceBlockingStub - -/** - * Client for calling verbs. Concrete implementations of this interface may call via gRPC or directly. - */ -interface VerbServiceClient { - /** - * Call a verb. - * - * @param ref The verb to call. - * @param req The request encoded as JSON. - * @return The response encoded as JSON. - */ - fun call(context: Context, ref: Ref, req: String): String -} - -class GrpcVerbServiceClient(val client: VerbServiceBlockingStub) : VerbServiceClient { - override fun call(context: Context, ref: Ref, req: String): String { - val request = CallRequest( - verb = xyz.block.ftl.v1.schema.Ref( - module = ref.module, - name = ref.name - ), - body = req.encodeUtf8(), - ) - val response = client.Call(request) - return when { - response.error != null -> throw RuntimeException(response.error.message) - response.body != null -> response.body.utf8() - else -> error("unreachable") - } - } -} - -/** - * A client that calls verbs directly via the associated registry. - */ -class LoopbackVerbServiceClient(private val registry: Registry) : VerbServiceClient { - override fun call(context: Context, ref: Ref, req: String): String { - return registry.invoke(context, ref, req) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/client/grpc.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/client/grpc.kt deleted file mode 100644 index 89851c72a9..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/client/grpc.kt +++ /dev/null @@ -1,46 +0,0 @@ -package xyz.block.ftl.client - -import io.grpc.* -import io.grpc.netty.NettyChannelBuilder -import io.grpc.netty.NettyServerBuilder -import xyz.block.ftl.logging.Logging -import xyz.block.ftl.server.ServerInterceptor -import java.net.InetSocketAddress -import java.net.URL -import java.util.concurrent.TimeUnit.SECONDS - -internal fun makeGrpcClient(endpoint: String): ManagedChannel { - val url = URL(endpoint) - val port = if (url.port == -1) when (url.protocol) { - "http" -> 80 - "https" -> 443 - else -> throw IllegalArgumentException("Unsupported protocol: ${url.protocol}") - } else url.port - var builder = NettyChannelBuilder - .forAddress(InetSocketAddress(url.host, port)) - .keepAliveTime(5, SECONDS) - .intercept(VerbServiceClientInterceptor()) - if (url.protocol == "http") - builder = builder.usePlaintext() - return builder.build() -} - -private class VerbServiceClientInterceptor : ClientInterceptor { - override fun interceptCall( - method: MethodDescriptor?, - callOptions: CallOptions?, - next: Channel? - ): ClientCall { - val call = next?.newCall(method, callOptions) - return object : ForwardingClientCall.SimpleForwardingClientCall(call) { - override fun start(responseListener: Listener?, headers: Metadata?) { - ServerInterceptor.callers.get().forEach { caller -> - headers?.put(ServerInterceptor.callersMetadata, caller) - } - headers?.put(ServerInterceptor.requestIdMetadata, ServerInterceptor.requestId.get()) - super.start(responseListener, headers) - } - } - } - -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/Config.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/Config.kt deleted file mode 100644 index 8a1b645c3e..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/Config.kt +++ /dev/null @@ -1,32 +0,0 @@ -package xyz.block.ftl.config - -import xyz.block.ftl.serializer.makeGson - -class Config(private val cls: Class, val name: String) { - private val module: String - private val gson = makeGson() - - companion object { - /** - * A convenience method for creating a new Secret. - * - *
-     *   val secret = Config.new("test")
-     * 
- * - */ - inline fun new(name: String): Config = Config(T::class.java, name) } - - init { - val caller = Thread.currentThread().stackTrace[2].className - require(caller.startsWith("ftl.") || caller.startsWith("xyz.block.ftl.config.")) { "Config must be defined in an FTL module not $caller" } - val parts = caller.split(".") - module = parts[parts.size - 2] - } - - fun get(): T { - val key = "FTL_CONFIG_${module.uppercase()}_${name.uppercase()}" - val value = System.getenv(key) ?: throw Exception("Config key ${module}.${name} not found") - return gson.fromJson(value, cls) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/ConfigTest.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/ConfigTest.kt deleted file mode 100644 index 627196b206..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/config/ConfigTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package xyz.block.ftl.config - -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junitpioneer.jupiter.SetEnvironmentVariable - -class ConfigTest { - @Test - @SetEnvironmentVariable(key = "FTL_CONFIG_CONFIG_TEST", value = "testingtesting") - fun testSecret() { - val config = Config.new("test") - assertEquals("testingtesting", config.get()) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/hotswap/FtlHotswapAgentPlugin.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/hotswap/FtlHotswapAgentPlugin.kt deleted file mode 100644 index bfe9d51a66..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/hotswap/FtlHotswapAgentPlugin.kt +++ /dev/null @@ -1,18 +0,0 @@ -package xyz.block.ftl.hotswap - -import org.hotswap.agent.annotation.LoadEvent -import org.hotswap.agent.annotation.OnClassLoadEvent -import org.hotswap.agent.annotation.Plugin -import org.hotswap.agent.javassist.CtClass -import xyz.block.ftl.logging.Logging - -@Plugin(name = "FtlHotswapAgentPlugin", testedVersions = []) -object FtlHotswapAgentPlugin { - private val logger = Logging.logger(FtlHotswapAgentPlugin::class) - - @JvmStatic - @OnClassLoadEvent(classNameRegexp = ".*", events = [LoadEvent.REDEFINE]) - fun loaded(ctClass: CtClass) { - logger.debug("Reloaded " + ctClass.name) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/logging/Logging.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/logging/Logging.kt deleted file mode 100644 index 2294f463d5..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/logging/Logging.kt +++ /dev/null @@ -1,75 +0,0 @@ -package xyz.block.ftl.logging - -import ch.qos.logback.classic.Level -import ch.qos.logback.classic.Logger -import ch.qos.logback.classic.LoggerContext -import ch.qos.logback.classic.spi.ILoggingEvent -import ch.qos.logback.core.ConsoleAppender -import ch.qos.logback.core.joran.spi.ConsoleTarget -import com.fasterxml.jackson.core.JsonGenerator -import net.logstash.logback.composite.JsonProviders -import net.logstash.logback.composite.JsonWritingUtils -import net.logstash.logback.composite.loggingevent.LogLevelJsonProvider -import net.logstash.logback.composite.loggingevent.MessageJsonProvider -import net.logstash.logback.composite.loggingevent.ThrowableMessageJsonProvider -import net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder -import org.slf4j.LoggerFactory -import kotlin.reflect.KClass - -class Logging { - private val lc = LoggerFactory.getILoggerFactory() as LoggerContext - private val appender = ConsoleAppender() - - companion object { - private val logging = Logging() - private const val DEFAULT_LOG_LEVEL = "info" - - fun logger(name: String): Logger { - val logger = logging.lc.getLogger(name) as Logger - logger.addAppender(logging.appender) - logger.level = Level.DEBUG - logger.isAdditive = false /* set to true if root should log too */ - - return logger - } - - fun logger(kClass: KClass<*>): Logger { - return logger(kClass.qualifiedName!!) - } - - init { - val je = LoggingEventCompositeJsonEncoder() - je.context = logging.lc - - val providers: JsonProviders = je.providers - providers.setContext(je.context) - // Custom LogLevelJsonProvider converts level value to lowercase - providers.addProvider(object : LogLevelJsonProvider() { - override fun writeTo(generator: JsonGenerator, event: ILoggingEvent) { - JsonWritingUtils.writeStringField(generator, fieldName, event.level.toString().lowercase()) - } - }) - providers.addProvider(MessageJsonProvider()) - // Custom ThrowableMessageJsonProvider uses "error" as fieldname for throwable - providers.addProvider(object : ThrowableMessageJsonProvider() { - init { - this.fieldName = "error" - } - }) - je.providers = providers - je.start() - - logging.appender.target = ConsoleTarget.SystemErr.toString() - logging.appender.context = logging.lc - logging.appender.encoder = je - logging.appender.start() - - val rootLogger = logger(Logger.ROOT_LOGGER_NAME) - val rootLevelCfg = Level.valueOf(System.getenv("LOG_LEVEL") ?: DEFAULT_LOG_LEVEL) - rootLogger.level = rootLevelCfg - - // Explicitly set log level for grpc-netty - logger("io.grpc.netty").level = Level.WARN - } - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/main/main.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/main/main.kt deleted file mode 100644 index 95c114ec74..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/main/main.kt +++ /dev/null @@ -1,34 +0,0 @@ -package xyz.block.ftl.main - -import io.grpc.ServerInterceptors -import io.grpc.netty.NettyServerBuilder -import xyz.block.ftl.client.GrpcVerbServiceClient -import xyz.block.ftl.client.makeGrpcClient -import xyz.block.ftl.registry.Registry -import xyz.block.ftl.server.Server -import xyz.block.ftl.server.ServerInterceptor -import xyz.block.ftl.v1.VerbServiceWireGrpc.VerbServiceBlockingStub -import java.net.InetSocketAddress -import java.net.URL - -val defaultBindAddress = "http://127.0.0.1:8896" -val defaultFtlEndpoint = "http://127.0.0.1:8892" - -fun main() { - val bind = URL(System.getenv("FTL_BIND") ?: defaultBindAddress) - val addr = InetSocketAddress(bind.host, bind.port) - val registry = Registry() - registry.registerAll() - for (verb in registry.refs) { - println("Registered verb: ${verb.module}.${verb.name}") - } - val ftlEndpoint = System.getenv("FTL_ENDPOINT") ?: defaultFtlEndpoint - val grpcClient = VerbServiceBlockingStub(makeGrpcClient(ftlEndpoint)) - val verbRoutingClient = GrpcVerbServiceClient(grpcClient) - val server = Server(registry, verbRoutingClient) - val grpcServer = NettyServerBuilder.forAddress(addr) - .addService(ServerInterceptors.intercept(server, ServerInterceptor())) - .build() - grpcServer.start() - grpcServer.awaitTermination() -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/registry/Registry.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/registry/Registry.kt deleted file mode 100644 index 2109e95ba2..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/registry/Registry.kt +++ /dev/null @@ -1,103 +0,0 @@ -package xyz.block.ftl.registry - -import io.github.classgraph.ClassGraph -import xyz.block.ftl.* -import xyz.block.ftl.logging.Logging -import java.lang.reflect.Method -import java.util.concurrent.ConcurrentHashMap -import kotlin.reflect.KClass -import kotlin.reflect.KFunction -import kotlin.reflect.jvm.javaMethod -import kotlin.reflect.jvm.kotlinFunction - -const val defaultJvmModuleName = "ftl" - -fun test() {} -data class Ref(val module: String, val name: String) { - override fun toString() = "$module.$name" -} - -internal fun xyz.block.ftl.v1.schema.Ref.toModel() = Ref(module, name) - -/** - * FTL module registry. - * - * This will contain all the Verbs that are registered in the module and will be used to dispatch requests to the - * appropriate Verb. - */ -class Registry(val jvmModuleName: String = defaultJvmModuleName) { - private val logger = Logging.logger(Registry::class) - private val verbs = ConcurrentHashMap>() - private var ftlModuleName: String? = null - - /** Return the FTL module name. This can only be called after one of the register* methods are called. */ - val moduleName: String - get() { - if (ftlModuleName == null) throw IllegalStateException("FTL module name not set, call one of the register* methods first") - return ftlModuleName!! - } - - /** Register all Verbs in the JVM package by walking the class graph. */ - fun registerAll() { - logger.debug("Scanning for Verbs in ${jvmModuleName}...") - ClassGraph() - .enableAllInfo() // Scan classes, methods, fields, annotations - .acceptPackages(jvmModuleName) - .scan().use { scanResult -> - scanResult.allClasses.flatMap { - it.loadClass().kotlin.java.declaredMethods.asSequence() - }.filter { - it.hasVerbAnnotation() && !it.isAnnotationPresent(Ignore::class.java) - }.forEach { - val verb = it.kotlinFunction!! - maybeRegisterVerb(verb) - } - } - } - - val refs get() = verbs.keys.toList() - - private fun Method.hasVerbAnnotation(): Boolean { - return arrayOf( - Verb::class, - HttpIngress::class, - Cron::class - ).any { this.isAnnotationPresent(it.java) } - } - - private fun maybeRegisterVerb(function: KFunction<*>) { - if (ftlModuleName == null) { - ftlModuleName = ftlModuleFromJvmModule(jvmModuleName, function) - } - - logger.debug(" @Verb ${function.name}()") - val verbRef = Ref(module = ftlModuleName!!, name = function.name) - val verbHandle = VerbHandle(function) - if (verbs.containsKey(verbRef)) throw IllegalArgumentException("Duplicate Verb $verbRef") - verbs[verbRef] = verbHandle - } - - fun list(): Set = verbs.keys - - /** Invoke a Verb with JSON-encoded payload and return its JSON-encoded response. */ - fun invoke(context: Context, verbRef: Ref, request: String): String { - val verb = verbs[verbRef] ?: throw IllegalArgumentException("Unknown verb: $verbRef") - return verb.invokeVerbInternal(context, request) - } -} - -/** - * Return the FTL module name from a JVM module name and a top-level KFunction. - * - * For example, if the JVM module name is `ftl` and the qualified function name is - * `ftl.core.foo`, then the FTL module name is `core`. - */ -fun ftlModuleFromJvmModule(jvmModuleName: String, verb: KFunction<*>): String { - val packageName = verb.javaMethod?.declaringClass?.`package`?.name - ?: throw IllegalArgumentException("No package for $verb") - val qualifiedName = "$packageName.${verb.name}" - require(qualifiedName.startsWith("$jvmModuleName.")) { - "Function $qualifiedName must be in the form $jvmModuleName.." - } - return qualifiedName.split(".")[1] -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/registry/VerbHandle.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/registry/VerbHandle.kt deleted file mode 100644 index ce3aefbc58..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/registry/VerbHandle.kt +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.block.ftl.registry - -import xyz.block.ftl.Context -import xyz.block.ftl.serializer.makeGson -import kotlin.reflect.KFunction -import kotlin.reflect.jvm.javaType - -internal class VerbHandle( - private val verbFunction: KFunction, -) { - private val gson = makeGson() - - fun invokeVerbInternal(context: Context, argument: String): String { - val arguments = verbFunction.parameters.associateWith { parameter -> - when (parameter.type.classifier) { - Context::class -> context - else -> { - val deserialized: Any? = gson.fromJson(argument, parameter.type.javaType) - return@associateWith deserialized - } - } - } - - val result = verbFunction.callBy(arguments) - return gson.toJson(result) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt deleted file mode 100644 index 22c50eb799..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt +++ /dev/null @@ -1,822 +0,0 @@ -package xyz.block.ftl.schemaextractor - -import io.gitlab.arturbosch.detekt.api.Debt -import io.gitlab.arturbosch.detekt.api.Issue -import io.gitlab.arturbosch.detekt.api.Rule -import io.gitlab.arturbosch.detekt.api.Severity -import io.gitlab.arturbosch.detekt.api.config -import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution -import io.gitlab.arturbosch.detekt.rules.fqNameOrNull -import org.jetbrains.kotlin.backend.jvm.ir.psiElement -import org.jetbrains.kotlin.cfg.getDeclarationDescriptorIncludingConstructors -import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange -import org.jetbrains.kotlin.com.intellij.psi.PsiComment -import org.jetbrains.kotlin.com.intellij.psi.PsiElement -import org.jetbrains.kotlin.descriptors.ClassDescriptor -import org.jetbrains.kotlin.descriptors.ClassKind -import org.jetbrains.kotlin.descriptors.impl.referencedProperty -import org.jetbrains.kotlin.diagnostics.DiagnosticUtils.getLineAndColumnInPsiFile -import org.jetbrains.kotlin.diagnostics.PsiDiagnosticUtils.LineAndColumn -import org.jetbrains.kotlin.name.FqName -import org.jetbrains.kotlin.psi.KtAnnotationEntry -import org.jetbrains.kotlin.psi.KtCallExpression -import org.jetbrains.kotlin.psi.KtClass -import org.jetbrains.kotlin.psi.KtDeclaration -import org.jetbrains.kotlin.psi.KtDotQualifiedExpression -import org.jetbrains.kotlin.psi.KtElement -import org.jetbrains.kotlin.psi.KtEnumEntry -import org.jetbrains.kotlin.psi.KtExpression -import org.jetbrains.kotlin.psi.KtFile -import org.jetbrains.kotlin.psi.KtFunction -import org.jetbrains.kotlin.psi.KtNamedFunction -import org.jetbrains.kotlin.psi.KtProperty -import org.jetbrains.kotlin.psi.KtSuperTypeCallEntry -import org.jetbrains.kotlin.psi.KtTypeParameterList -import org.jetbrains.kotlin.psi.KtTypeReference -import org.jetbrains.kotlin.psi.KtValueArgument -import org.jetbrains.kotlin.psi.ValueArgument -import org.jetbrains.kotlin.psi.psiUtil.children -import org.jetbrains.kotlin.psi.psiUtil.getValueParameters -import org.jetbrains.kotlin.psi.psiUtil.startOffset -import org.jetbrains.kotlin.resolve.BindingContext -import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall -import org.jetbrains.kotlin.resolve.calls.util.getType -import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe -import org.jetbrains.kotlin.resolve.source.getPsi -import org.jetbrains.kotlin.resolve.typeBinding.createTypeBindingForReturnType -import org.jetbrains.kotlin.types.KotlinType -import org.jetbrains.kotlin.types.checker.SimpleClassicTypeSystemContext.getClassFqNameUnsafe -import org.jetbrains.kotlin.types.checker.SimpleClassicTypeSystemContext.isTypeParameterTypeConstructor -import org.jetbrains.kotlin.types.checker.anySuperTypeConstructor -import org.jetbrains.kotlin.types.isNullable -import org.jetbrains.kotlin.types.typeUtil.builtIns -import org.jetbrains.kotlin.types.typeUtil.isAny -import org.jetbrains.kotlin.types.typeUtil.isAnyOrNullableAny -import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf -import org.jetbrains.kotlin.util.containingNonLocalDeclaration -import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty -import xyz.block.ftl.Context -import xyz.block.ftl.Database -import xyz.block.ftl.HttpIngress -import xyz.block.ftl.Json -import xyz.block.ftl.Method -import xyz.block.ftl.Cron -import xyz.block.ftl.schemaextractor.SchemaExtractor.Companion.extractModuleName -import xyz.block.ftl.v1.schema.Array -import xyz.block.ftl.v1.schema.Config -import xyz.block.ftl.v1.schema.Data -import xyz.block.ftl.v1.schema.Decl -import xyz.block.ftl.v1.schema.Enum -import xyz.block.ftl.v1.schema.EnumVariant -import xyz.block.ftl.v1.schema.Error -import xyz.block.ftl.v1.schema.ErrorList -import xyz.block.ftl.v1.schema.Field -import xyz.block.ftl.v1.schema.IngressPathComponent -import xyz.block.ftl.v1.schema.IngressPathLiteral -import xyz.block.ftl.v1.schema.IngressPathParameter -import xyz.block.ftl.v1.schema.IntValue -import xyz.block.ftl.v1.schema.Metadata -import xyz.block.ftl.v1.schema.MetadataAlias -import xyz.block.ftl.v1.schema.MetadataCalls -import xyz.block.ftl.v1.schema.MetadataIngress -import xyz.block.ftl.v1.schema.MetadataCronJob -import xyz.block.ftl.v1.schema.Module -import xyz.block.ftl.v1.schema.Optional -import xyz.block.ftl.v1.schema.Position -import xyz.block.ftl.v1.schema.Ref -import xyz.block.ftl.v1.schema.Secret -import xyz.block.ftl.v1.schema.StringValue -import xyz.block.ftl.v1.schema.Type -import xyz.block.ftl.v1.schema.TypeParameter -import xyz.block.ftl.v1.schema.Unit -import xyz.block.ftl.v1.schema.Value -import xyz.block.ftl.v1.schema.Verb -import java.io.File -import java.io.FileOutputStream -import java.nio.file.Path -import java.time.OffsetDateTime -import kotlin.io.path.createDirectories -import io.gitlab.arturbosch.detekt.api.Config as DetektConfig - -data class ModuleData(var comments: List = emptyList(), val decls: MutableSet = mutableSetOf()) - -// Helpers -private fun Ref.compare(module: String, name: String): Boolean = this.name == name && this.module == module - -private fun isVerbAnnotation(annotationEntry: KtAnnotationEntry, bindingContext: BindingContext): Boolean { - val fqName = bindingContext.get(BindingContext.ANNOTATION, annotationEntry)?.fqName?.asString() - return fqName in setOf( - xyz.block.ftl.Verb::class.qualifiedName, - xyz.block.ftl.HttpIngress::class.qualifiedName, - xyz.block.ftl.Cron::class.qualifiedName, - ) -} - -@RequiresTypeResolution -class ExtractSchemaRule(config: DetektConfig) : Rule(config) { - private val output: String by config(defaultValue = ".") - private val modules: MutableMap = mutableMapOf() - private var extractor = SchemaExtractor(modules) - - override val issue = Issue( - javaClass.simpleName, - Severity.Performance, - "Verifies and extracts FTL Schema", - Debt.FIVE_MINS, - ) - - override fun preVisit(root: KtFile) { - extractor.setBindingContext(bindingContext) - extractor.addModuleComments(root) - } - - override fun visitAnnotationEntry(annotationEntry: KtAnnotationEntry) { - if (!isVerbAnnotation(annotationEntry, bindingContext)) { - return - } - - // Skip if annotated with @Ignore - if ( - annotationEntry.containingNonLocalDeclaration()!!.annotationEntries.any { - bindingContext.get( - BindingContext.ANNOTATION, - it - )?.fqName?.asString() == xyz.block.ftl.Ignore::class.qualifiedName - } - ) { - return - } - - when (val element = annotationEntry.parent.parent) { - is KtNamedFunction -> extractor.addVerbToSchema(element) - is KtClass -> { - when { - element.isData() -> extractor.addDataToSchema(element) - element.isEnum() -> extractor.addEnumToSchema(element) - } - } - } - } - - override fun visitProperty(property: KtProperty) { - when (property.getDeclarationDescriptorIncludingConstructors(bindingContext)?.referencedProperty?.returnType - ?.fqNameOrNull()?.asString()) { - Database::class.qualifiedName -> extractor.addDatabaseToSchema(property) - xyz.block.ftl.secrets.Secret::class.qualifiedName -> extractor.addSecretToSchema(property) - xyz.block.ftl.config.Config::class.qualifiedName -> extractor.addConfigToSchema(property) - } - } - - override fun postVisit(root: KtFile) { - val errors = extractor.getErrors() - if (errors.errors.isNotEmpty()) { - writeFile(errors.encode(), ERRORS_OUT) - throw IllegalStateException("could not extract schema") - } - - val moduleName = root.extractModuleName() - modules[moduleName]?.let { - writeFile(it.toModule(moduleName).encode(), SCHEMA_OUT) - } - } - - private fun writeFile(content: ByteArray, filename: String) { - val outputDirectory = File(output).also { f -> Path.of(f.absolutePath).createDirectories() } - val file = File(outputDirectory.absolutePath, filename) - file.createNewFile() - val os = FileOutputStream(file) - os.write(content) - os.close() - } - - private fun ModuleData.toModule(moduleName: String): Module = Module( - name = moduleName, - decls = this.decls.sortedBy { it.data_ == null }, - comments = this.comments - ) - - companion object { - const val SCHEMA_OUT = "schema.pb" - const val ERRORS_OUT = "errors.pb" - } -} - -class SchemaExtractor( - private val modules: MutableMap, -) { - private var bindingContext = BindingContext.EMPTY - private val errors = mutableListOf() - - fun setBindingContext(bindingContext: BindingContext) { - this.bindingContext = bindingContext - } - - fun getErrors(): ErrorList = ErrorList(errors = errors) - - fun addModuleComments(file: KtFile) { - val module = file.extractModuleName() - val comments = file.children - .filterIsInstance() - .flatMap { it.text.normalizeFromDocComment() } - modules[module]?.let { it.comments = comments } ?: run { - modules[module] = ModuleData(comments = comments) - } - } - - fun addVerbToSchema(verb: KtNamedFunction) { - validateVerb(verb) - addDecl(verb.extractModuleName(), Decl(verb = extractVerb(verb))) - } - - fun addDataToSchema(data: KtClass) { - addDecl(data.extractModuleName(), Decl(data_ = data.toSchemaData())) - } - - fun addEnumToSchema(enum: KtClass) { - addDecl(enum.extractModuleName(), Decl(enum_ = enum.toSchemaEnum())) - } - - fun addConfigToSchema(config: KtProperty) { - extractSecretOrConfig(config).let { - val decl = Decl( - config = Config( - pos = it.position, - name = it.name, - type = it.type - ) - ) - - addDecl(config.extractModuleName(), decl) - } - } - - fun addSecretToSchema(secret: KtProperty) { - extractSecretOrConfig(secret).let { - val decl = Decl( - secret = Secret( - pos = it.position, - name = it.name, - type = it.type - ) - ) - - addDecl(secret.extractModuleName(), decl) - } - } - - fun addDatabaseToSchema(database: KtProperty) { - val decl = database.children.single().let { - val dbName = (it as? KtCallExpression).getResolvedCall(bindingContext)?.valueArguments?.entries?.single { e -> - e.key.name.asString() == "name" - } - ?.value?.toString() - ?.trim('"') - require(dbName != null) { it.errorAtPosition("could not extract database name") } - - Decl( - database = xyz.block.ftl.v1.schema.Database( - pos = it.getPosition(), - name = dbName ?: "" - ) - ) - } - addDecl(database.extractModuleName(), decl) - } - - private fun addDecl(module: String, decl: Decl) { - modules[module]?.decls?.add(decl) ?: run { - modules[module] = ModuleData(decls = mutableSetOf(decl)) - } - } - - private fun validateVerb(verb: KtNamedFunction) { - require(verb.fqName?.asString() != null) { verb.errorAtPosition("verbs must be defined in a package") } - verb.fqName?.asString()!!.let { fqName -> - require(fqName.split(".").let { it.size >= 2 && it.first() == "ftl" }) { - verb.errorAtPosition("expected exported function to be in package ftl., but was $fqName") - } - - // Validate parameters - require(verb.valueParameters.size >= 1) { verb.errorAtPosition("verbs must have at least one argument") } - require(verb.valueParameters.size <= 2) { verb.errorAtPosition("verbs must have at most two arguments") } - val ctxParam = verb.valueParameters.first() - require(ctxParam.typeReference?.resolveType()?.fqNameOrNull()?.asString() == Context::class.qualifiedName) { - ctxParam.errorAtPosition("first argument of verb must be Context") - } - - if (verb.valueParameters.size == 2) { - val reqParam = verb.valueParameters.last() - require(reqParam.typeReference?.resolveType() - ?.let { it.toClassDescriptor().isData || it.isEmptyBuiltin() } == true - ) { - verb.valueParameters.last().errorAtPosition("request type must be a data class or builtin.Empty") - } - } - - // Validate return type - verb.createTypeBindingForReturnType(bindingContext)?.type?.let { - require(it.toClassDescriptor().isData || it.isEmptyBuiltin() || it.isUnit()) { - verb.errorAtPosition("if present, return type must be a data class or builtin.Empty") - } - } - } - } - - private fun extractVerb(verb: KtNamedFunction): Verb { - val requestRef = verb.valueParameters.takeIf { it.size > 1 }?.last()?.let { - return@let it.typeReference?.resolveType()?.toSchemaType(it.getPosition(), it.textLength) - } ?: Type(unit = Unit()) - - val returnRef = verb.createTypeBindingForReturnType(bindingContext)?.let { - return@let it.type.toSchemaType(it.psiElement.getPosition(), it.psiElement.textLength) - } ?: Type(unit = Unit()) - - val metadata = mutableListOf() - extractIngress(verb, requestRef, returnRef)?.apply { metadata.add(Metadata(ingress = this)) } - extractCron(verb, requestRef, returnRef)?.apply { metadata.add(Metadata(cronJob = this)) } - extractCalls(verb)?.apply { metadata.add(Metadata(calls = this)) } - require(verb.name != null) { - verb.errorAtPosition("verbs must be named") - } - - return Verb( - name = verb.name ?: "", - request = requestRef, - response = returnRef, - metadata = metadata, - comments = verb.comments(), - ) - } - - data class SecretConfigData(val name: String, val type: Type, val position: Position) - - private fun extractSecretOrConfig(property: KtProperty): SecretConfigData { - return property.children.single().let { - var type: KotlinType? = null - var name = "" - when (it) { - is KtCallExpression -> { - it.getResolvedCall(bindingContext)?.valueArguments?.entries?.forEach { arg -> - if (arg.key.name.asString() == "name") { - name = arg.value.toString().trim('"') - } else if (arg.key.name.asString() == "cls") { - type = (arg.key.varargElementType ?: arg.key.type).arguments.single().type - } - } - } - - is KtDotQualifiedExpression -> { - it.getResolvedCall(bindingContext)?.let { call -> - name = call.valueArguments.entries.single().value.toString().trim('"') - type = call.typeArguments.values.single() - } - } - - else -> { - errors.add(it.errorAtPosition("could not extract secret or config")) - } - } - - val position = it.getPosition() - SecretConfigData(name, type!!.toSchemaType(position, it.textLength), position) - } - } - - private fun extractIngress(verb: KtNamedFunction, requestType: Type, responseType: Type): MetadataIngress? { - return verb.annotationEntries.firstOrNull { - bindingContext.get(BindingContext.ANNOTATION, it)?.fqName?.asString() == HttpIngress::class.qualifiedName - }?.let { annotationEntry -> - require(requestType.ref != null) { - annotationEntry.errorAtPosition("ingress ${verb.name} request must be a data class") - } - require(responseType.ref != null) { - annotationEntry.errorAtPosition("ingress ${verb.name} response must be a data class") - } - require(requestType.ref?.compare("builtin", "HttpRequest") == true) { - annotationEntry.errorAtPosition("@HttpIngress-annotated ${verb.name} request must be ftl.builtin.HttpRequest") - } - require(responseType.ref?.compare("builtin", "HttpResponse") == true) { - annotationEntry.errorAtPosition("@HttpIngress-annotated ${verb.name} response must be ftl.builtin.HttpResponse") - } - require(annotationEntry.valueArguments.size >= 2) { - annotationEntry.errorAtPosition("@HttpIngress annotation requires at least 2 arguments") - } - - val args = annotationEntry.valueArguments.partition { arg -> - // Method arg is named "method" or is of type xyz.block.ftl.Method (in the case where args are - // positional rather than named). - arg.getArgumentName()?.asName?.asString() == "method" - || arg.getArgumentExpression()?.getType(bindingContext)?.fqNameOrNull() - ?.asString() == Method::class.qualifiedName - } - - val methodArg = args.first.single().getArgumentExpression()?.text?.substringAfter(".") - require(methodArg != null) { - annotationEntry.errorAtPosition("could not extract method from ${verb.name} @HttpIngress annotation") - } - - val pathArg = args.second.single().getArgumentExpression()?.text?.let { - extractPathComponents(it.trim('\"')) - } - require(pathArg != null) { - annotationEntry.errorAtPosition("could not extract path from ${verb.name} @HttpIngress annotation") - } - - MetadataIngress( - type = "http", - path = pathArg ?: emptyList(), - method = methodArg ?: "", - pos = annotationEntry.getPosition(), - ) - } - } - - private fun extractCron(verb: KtNamedFunction, requestType: Type, responseType: Type): MetadataCronJob? { - return verb.annotationEntries.firstOrNull { - bindingContext.get(BindingContext.ANNOTATION, it)?.fqName?.asString() == Cron::class.qualifiedName - }?.let { annotationEntry -> - require(annotationEntry.valueArguments.size == 1) { - annotationEntry.errorAtPosition("@Cron annotation requires 1 argument") - } - val patternArg = annotationEntry.valueArguments.firstOrNull() - require(patternArg != null) { - annotationEntry.errorAtPosition("could not find cron pattern for @Cron annotation") - } - val patternVal = patternArg?.getArgumentExpression()?.text - MetadataCronJob( - cron = patternVal ?: "", - pos = annotationEntry.getPosition(), - ) - } - } - - private fun extractPathComponents(path: String): List { - return path.split("/").filter { it.isNotEmpty() }.map { part -> - if (part.startsWith("{") && part.endsWith("}")) { - IngressPathComponent(ingressPathParameter = IngressPathParameter(name = part.substring(1, part.length - 1))) - } else { - IngressPathComponent(ingressPathLiteral = IngressPathLiteral(text = part)) - } - } - } - - private fun extractCalls(verb: KtNamedFunction): MetadataCalls? { - val verbs = mutableSetOf() - extractCalls(verb, verbs) - return verbs.ifNotEmpty { MetadataCalls(calls = verbs.toList()) } - } - - private fun extractCalls(element: KtElement, calls: MutableSet) { - // Step into function calls inside this expression body to look for transitive calls. - if (element is KtCallExpression) { - val resolvedCall = element.getResolvedCall(bindingContext)?.candidateDescriptor?.source?.getPsi() as? KtFunction - if (resolvedCall != null) { - extractCalls(resolvedCall, calls) - } - } - - val func = element as? KtNamedFunction - if (func != null) { - val body = func.bodyExpression - require(body != null) { - func.errorAtPosition("function body cannot be empty") - } - - val commentRanges = body?.node?.children() - ?.filterIsInstance() - ?.map { it.textRange.shiftLeft(it.startOffset).shiftRight(it.startOffsetInParent) } - - val imports = func.containingKtFile.importList?.imports ?: emptyList() - - // Look for all params of type Context and extract a matcher for each based on its variable name. - // e.g. fun foo(ctx: Context) { ctx.call(...) } => "ctx.call(...)" - val callMatchers = func.valueParameters.filter { - it.typeReference?.resolveType()?.fqNameOrNull()?.asString() == Context::class.qualifiedName - }.map { ctxParam -> getCallMatcher(ctxParam.text.split(":")[0].trim()) } - - val refs = callMatchers.flatMap { matcher -> - matcher.findAll(body?.text ?: "").mapNotNull { match -> - // ignore commented out matches - if (commentRanges?.any { it.contains(TextRange(match.range.first, match.range.last)) } ?: false) { - return@mapNotNull null - } - - val verbCall = match.groups["fn"]?.value?.substringAfter("::")?.trim() - require(verbCall != null) { - func.errorAtPosition("could not extract outgoing verb call") - } - imports.firstOrNull { import -> - // if aliased import, match the alias - (import.text.split(" ").takeIf { it.size > 2 }?.last() - // otherwise match the last part of the import - ?: import.importedFqName?.asString()?.split(".")?.last()) == verbCall - }?.let { import -> - val moduleRefName = import.importedFqName?.asString()?.extractModuleName() - .takeIf { refModule -> refModule != element.extractModuleName() } - Ref( - name = import.importedFqName!!.asString().split(".").last(), - module = moduleRefName ?: "", - ) - } ?: let { - // if no matching import, validate that the referenced verb is in the same module - element.containingFile.children.singleOrNull { - (it is KtNamedFunction) && it.name == verbCall && it.annotationEntries.any { annotationEntry -> - isVerbAnnotation(annotationEntry, bindingContext) - } - } ?: errors.add( - func.errorAtPosition("could not resolve outgoing verb call") - ) - - Ref( - name = verbCall ?: "", - module = element.extractModuleName(), - ) - } - } - } - calls.addAll(refs) - } - - element.children - .filter { it is KtFunction || it is KtExpression } - .mapNotNull { it as? KtElement } - .forEach { - extractCalls(it, calls) - } - } - - private fun KtClass.toSchemaData(): Data { - return Data( - name = this.name!!, - fields = this.getValueParameters().map { param -> - // Metadata containing JSON alias if present. - val metadata = param.annotationEntries.firstOrNull { - bindingContext.get(BindingContext.ANNOTATION, it)?.fqName?.asString() == Json::class.qualifiedName - }?.valueArguments?.single()?.let { - listOf(Metadata(alias = MetadataAlias(alias = (it as KtValueArgument).text.trim('"', ' ')))) - } ?: listOf() - Field( - name = param.name!!, - type = param.typeReference?.let { - return@let it.resolveType()?.toSchemaType(it.getPosition(), it.textLength) - }, - metadata = metadata, - ) - }.toList(), - comments = this.comments(), - typeParameters = this.children.flatMap { (it as? KtTypeParameterList)?.parameters ?: emptyList() }.map { - TypeParameter( - name = it.name!!, - pos = getLineAndColumnInPsiFile(it.containingFile, it.textRange).toPosition(this.containingFile.name), - ) - }.toList(), - pos = getLineAndColumnInPsiFile(this.containingFile, this.textRange).toPosition(this.containingFile.name), - ) - } - - private fun KtClass.toSchemaEnum(): Enum { - val variants: List - require(this.getValueParameters().isEmpty() || this.getValueParameters().size == 1) { - this.errorAtPosition("enums can have at most one value parameter, of type string or number") - } - - if (this.getValueParameters().isEmpty()) { - var ordinal = 0L - variants = this.declarations.filterIsInstance().map { - val variant = EnumVariant( - name = it.name!!, - value_ = Value(intValue = IntValue(value_ = ordinal)), - comments = it.comments(), - ) - ordinal = ordinal.inc() - return@map variant - } - } else { - variants = this.declarations.filterIsInstance().map { entry -> - val name: String = entry.name!! - val arg: ValueArgument = entry.initializerList?.initializers?.single().let { - (it as KtSuperTypeCallEntry).valueArguments.single() - } - - var value: Value? = null - try { - arg.getArgumentExpression()?.let { expr -> - if (expr.text.startsWith('"')) { - value = Value(stringValue = StringValue(value_ = expr.text.trim('"'))) - } else { - value = Value(intValue = IntValue(value_ = expr.text.toLong())) - } - } - if (value == null) { - entry.errorAtPosition("could not extract enum variant value") - } - } catch (e: NumberFormatException) { - errors.add(entry.errorAtPosition("enum variant value must be a string or number")) - } - - EnumVariant( - name = name, - value_ = value, - pos = entry.getPosition(), - comments = entry.comments(), - ) - } - } - - return Enum( - name = this.name!!, - variants = variants, - comments = this.comments(), - type = variants.map { it.value_?.schemaValueType() }.distinct().singleOrNull(), - pos = getLineAndColumnInPsiFile(this.containingFile, this.textRange).toPosition(this.containingFile.name), - ) - } - - private fun KotlinType.toSchemaType(position: Position, tokenLength: Int): Type { - if (this.unwrap().constructor.isTypeParameterTypeConstructor()) { - return Type( - ref = Ref( - name = this.constructor.declarationDescriptor?.name?.asString() ?: "T", - pos = position, - ) - ) - } - val builtIns = this.builtIns - val type = this.constructor.declarationDescriptor!!.defaultType - val schemaType = when { - type.isSubtypeOf(builtIns.stringType) -> Type(string = xyz.block.ftl.v1.schema.String()) - type.isSubtypeOf(builtIns.intType) -> Type(int = xyz.block.ftl.v1.schema.Int()) - type.isSubtypeOf(builtIns.longType) -> Type(int = xyz.block.ftl.v1.schema.Int()) - type.isSubtypeOf(builtIns.floatType) -> Type(float = xyz.block.ftl.v1.schema.Float()) - type.isSubtypeOf(builtIns.doubleType) -> Type(float = xyz.block.ftl.v1.schema.Float()) - type.isSubtypeOf(builtIns.booleanType) -> Type(bool = xyz.block.ftl.v1.schema.Bool()) - type.isSubtypeOf(builtIns.unitType) -> Type(unit = Unit()) - type.anySuperTypeConstructor { - it.getClassFqNameUnsafe().asString() == ByteArray::class.qualifiedName - } -> Type(bytes = xyz.block.ftl.v1.schema.Bytes()) - - type.anySuperTypeConstructor { - it.getClassFqNameUnsafe().asString() == builtIns.list.fqNameSafe.asString() - } -> Type( - array = Array( - element = this.arguments.first().type.toSchemaType(position, tokenLength) - ) - ) - - type.anySuperTypeConstructor { - it.getClassFqNameUnsafe().asString() == builtIns.map.fqNameSafe.asString() - } -> Type( - map = xyz.block.ftl.v1.schema.Map( - key = this.arguments.first().type.toSchemaType(position, tokenLength), - value_ = this.arguments.last().type.toSchemaType(position, tokenLength), - ) - ) - - this.isAnyOrNullableAny() -> Type(any = xyz.block.ftl.v1.schema.Any()) - this.fqNameOrNull() - ?.asString() == OffsetDateTime::class.qualifiedName -> Type(time = xyz.block.ftl.v1.schema.Time()) - - else -> { - val descriptor = this.toClassDescriptor() - if (!(descriptor.isData || descriptor.kind == ClassKind.ENUM_CLASS || this.isEmptyBuiltin())) { - errors.add( - Error( - msg = "expected type to be a data class or builtin.Empty, but was ${this.fqNameOrNull()?.asString()}", - pos = position, - endColumn = position.column + tokenLength - ) - ) - return Type() - } - - val refName = descriptor.name.asString() - val fqName = this.fqNameOrNull()!!.asString() - require(fqName.startsWith("ftl.")) { - Error( - msg = "expected module name to be in the form ftl., but was $fqName", - pos = position, - endColumn = position.column + tokenLength - ) - } - - // add all referenced types to the schema - // TODO: remove once we require explicit exporting of types - (descriptor.psiElement as? KtClass)?.let { - when { - it.isData() -> addDecl(it.extractModuleName(), Decl(data_ = it.toSchemaData())) - it.isEnum() -> addDecl(it.extractModuleName(), Decl(enum_ = it.toSchemaEnum())) - } - } - - Type( - ref = Ref( - name = refName ?: "", - module = fqName.extractModuleName(), - pos = position, - typeParameters = this.arguments.map { it.type.toSchemaType(position, tokenLength) }.toList(), - ) - ) - } - } - if (this.isNullable()) { - return Type(optional = Optional(type = schemaType)) - } - if (this.isAny()) { - return Type(any = xyz.block.ftl.v1.schema.Any()) - } - return schemaType - } - - private fun KtTypeReference.resolveType(): KotlinType? { - val type = bindingContext.get(BindingContext.TYPE, this) - require(type != null) { this.errorAtPosition("could not resolve type ${this.text}") } - return type - } - - private fun KotlinType.toClassDescriptor(): ClassDescriptor = - requireNotNull(this.unwrap().constructor.declarationDescriptor as? ClassDescriptor) - - private fun require(condition: Boolean, error: () -> Error) { - try { - require(condition) - } catch (e: IllegalArgumentException) { - errors.add(error()) - } - } - - companion object { - private fun KtElement.getPosition() = this.getLineAndColumn().toPosition(this.containingFile.name) - - private fun PsiElement.getPosition() = this.getLineAndColumn().toPosition(this.containingFile.name) - - private fun PsiElement.errorAtPosition(message: String): Error { - val pos = this.getPosition() - return Error( - msg = message, - pos = pos, - endColumn = pos.column + this.textLength - ) - } - - private fun PsiElement.getLineAndColumn(): LineAndColumn = - getLineAndColumnInPsiFile(this.containingFile, this.textRange) - - private fun LineAndColumn.toPosition(filename: String) = - Position( - filename = filename, - line = this.line.toLong(), - column = this.column.toLong(), - ) - - private fun getCallMatcher(ctxVarName: String): Regex { - return """${ctxVarName}\.call\((?[^,]+),\s*(?[^,]+?)\s*[()]""".toRegex(RegexOption.IGNORE_CASE) - } - - fun KtElement.extractModuleName(): String { - return this.containingKtFile.packageFqName.extractModuleName() - } - - fun FqName.extractModuleName(): String { - return this.asString().extractModuleName() - } - - private fun String.extractModuleName(): String { - return this.split(".")[1] - } - - private fun KtDeclaration.comments(): List { - return this.docComment?.text?.trim()?.normalizeFromDocComment() ?: emptyList() - } - - private fun String.normalizeFromDocComment(): List { - // get comments without comment markers - return this.lines() - .mapNotNull { line -> - line.removePrefix("/**") - .removePrefix("/*") - .removeSuffix("*/") - .takeIf { it.isNotBlank() } - } - .map { it.trim('*', '/', ' ') } - .toList() - } - - private fun KotlinType.isEmptyBuiltin(): Boolean { - return this.fqNameOrNull()?.asString() == "ftl.builtin.Empty" - } - - private fun KotlinType.isUnit(): Boolean { - return this.fqNameOrNull()?.asString() == "kotlin.Unit" - } - - private fun Value.schemaValueType(): Type? { - return when { - this.stringValue != null -> return Type(string = xyz.block.ftl.v1.schema.String()) - this.intValue != null -> return Type(int = xyz.block.ftl.v1.schema.Int()) - this.typeValue != null -> return this.typeValue.value_ - else -> null - } - } - } -} - diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/SchemaExtractorRuleSetProvider.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/SchemaExtractorRuleSetProvider.kt deleted file mode 100644 index 33a6f2d14f..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/SchemaExtractorRuleSetProvider.kt +++ /dev/null @@ -1,18 +0,0 @@ -package xyz.block.ftl.schemaextractor - -import io.gitlab.arturbosch.detekt.api.Config -import io.gitlab.arturbosch.detekt.api.RuleSet -import io.gitlab.arturbosch.detekt.api.RuleSetProvider - -class SchemaExtractorRuleSetProvider : RuleSetProvider { - override val ruleSetId: String = "SchemaExtractorRuleSet" - - override fun instance(config: Config): RuleSet { - return RuleSet( - ruleSetId, - listOf( - ExtractSchemaRule(config), - ), - ) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/secrets/Secret.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/secrets/Secret.kt deleted file mode 100644 index f0061589ad..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/secrets/Secret.kt +++ /dev/null @@ -1,32 +0,0 @@ -package xyz.block.ftl.secrets - -import xyz.block.ftl.serializer.makeGson - -class Secret(private val cls: Class, private val name: String) { - private val module: String - private val gson = makeGson() - - companion object { - /** - * A convenience method for creating a new Secret. - * - *
-     *   val secret = Secret.new("test")
-     * 
- * - */ - inline fun new(name: String): Secret = Secret(T::class.java, name) } - - init { - val caller = Thread.currentThread().getStackTrace()[2].className - require(caller.startsWith("ftl.") || caller.startsWith("xyz.block.ftl.secrets.")) { "Secrets must be defined in an FTL module not ${caller}" } - val parts = caller.split(".") - module = parts[parts.size - 2] - } - - fun get(): T { - val key = "FTL_SECRET_${module.uppercase()}_${name.uppercase()}" - val value = System.getenv(key) ?: throw Exception("Secret ${module}.${name} not found") - return gson.fromJson(value, cls) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/serializer/serializer.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/serializer/serializer.kt deleted file mode 100644 index a4d0403105..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/serializer/serializer.kt +++ /dev/null @@ -1,27 +0,0 @@ -package xyz.block.ftl.serializer - -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonDeserializer -import com.google.gson.JsonPrimitive -import com.google.gson.JsonSerializer -import java.time.OffsetDateTime -import java.time.format.DateTimeFormatter -import java.util.* - -private val FORMATTER: DateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME!! - -fun makeGson(): Gson = GsonBuilder() - .registerTypeAdapter(OffsetDateTime::class.java, JsonSerializer { src, _, _ -> - JsonPrimitive(FORMATTER.format(src)) - }) - .registerTypeAdapter(OffsetDateTime::class.java, JsonDeserializer { json, _, _ -> - OffsetDateTime.parse(json.asString, DateTimeFormatter.ISO_OFFSET_DATE_TIME) - }) - .registerTypeAdapter(ByteArray::class.java, JsonSerializer { src, _, _ -> - JsonPrimitive(Base64.getEncoder().encodeToString(src)) - }) - .registerTypeAdapter(ByteArray::class.java, JsonDeserializer { json, _, _ -> - Base64.getDecoder().decode(json.asString) - }) - .create() diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/server/Server.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/server/Server.kt deleted file mode 100644 index a3b49b18f9..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/server/Server.kt +++ /dev/null @@ -1,57 +0,0 @@ -package xyz.block.ftl.server - -import io.grpc.stub.StreamObserver -import okio.ByteString.Companion.encodeUtf8 -import xyz.block.ftl.Context -import xyz.block.ftl.client.VerbServiceClient -import xyz.block.ftl.logging.Logging -import xyz.block.ftl.registry.Registry -import xyz.block.ftl.registry.defaultJvmModuleName -import xyz.block.ftl.registry.toModel -import xyz.block.ftl.v1.* - -/** - * FTL verb server. - */ -class Server( - val registry: Registry, - val routingClient: VerbServiceClient, - val jvmModule: String = defaultJvmModuleName, -) : VerbServiceWireGrpc.VerbServiceImplBase() { - - private val logger = Logging.logger(Server::class) - - override fun Ping(request: PingRequest, response: StreamObserver) { - response.onNext(PingResponse()) - response.onCompleted() - } - - override fun Call(request: CallRequest, response: StreamObserver) { - val verbRef = request.verb - if (verbRef == null) { - response.onError(IllegalArgumentException("verb is required")) - return - } - - try { - val out = registry.invoke( - Context(jvmModule, routingClient), - verbRef.toModel(), - request.body.utf8() - ) - response.onNext(CallResponse(body = out.encodeUtf8())) - } catch (t: Throwable) { - val stackTrace = t.stackTraceToString() - logger.error("error calling verb $verbRef: $stackTrace") - response.onNext( - CallResponse( - error = CallResponse.Error( - message = (t.message ?: stackTrace.lineSequence().firstOrNull() ?: "internal error"), - stack = stackTrace - ) - ), - ) - } - response.onCompleted() - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/server/ServerInterceptor.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/server/ServerInterceptor.kt deleted file mode 100644 index 25b3ef09ba..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/server/ServerInterceptor.kt +++ /dev/null @@ -1,37 +0,0 @@ -package xyz.block.ftl.server - -import io.grpc.* -import io.grpc.ServerInterceptor - -const val ftlVerbHeader = "FTL-Verb" -const val ftlRequestIdHeader = "FTL-Request-ID" - -internal class ServerInterceptor : ServerInterceptor { - - companion object { - internal var callersMetadata = Metadata.Key.of(ftlVerbHeader, Metadata.ASCII_STRING_MARSHALLER) - internal var requestIdMetadata = Metadata.Key.of(ftlRequestIdHeader, Metadata.ASCII_STRING_MARSHALLER) - - internal var callers = Context.key>(ftlVerbHeader) - internal var requestId = Context.key(ftlRequestIdHeader) - } - - override fun interceptCall( - call: ServerCall?, - headers: Metadata?, - next: ServerCallHandler? - ): ServerCall.Listener { - call?.setCompression("gzip") - - var context = Context.current() - - headers?.getAll(callersMetadata)?.apply { - context = context.withValue(callers, this.toList()) - } - headers?.get(requestIdMetadata)?.apply { - context = context.withValue(requestId, this) - } - - return Contexts.interceptCall(context, call, headers, next) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider b/kotlin-runtime/ftl-runtime/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider deleted file mode 100644 index d21d2ffbec..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/resources/META-INF/services/io.gitlab.arturbosch.detekt.api.RuleSetProvider +++ /dev/null @@ -1 +0,0 @@ -xyz.block.ftl.schemaextractor.SchemaExtractorRuleSetProvider \ No newline at end of file diff --git a/kotlin-runtime/ftl-runtime/src/main/resources/hotswap-agent.properties b/kotlin-runtime/ftl-runtime/src/main/resources/hotswap-agent.properties deleted file mode 100644 index ed5d0424f5..0000000000 --- a/kotlin-runtime/ftl-runtime/src/main/resources/hotswap-agent.properties +++ /dev/null @@ -1,87 +0,0 @@ -# Default agent properties -# You can override them in your application by creating hotswap-agent.properties file in class root -# and specifying new property values. - -# Add a directory prior to application classpath (load classes and resources). -# -# This may be useful for example in multimodule maven project to load class changes from upstream project -# classes. Set extraClasspath to upstream project compiler output and .class file will have precedence to -# classes from built JAR file. -extraClasspath=target/classes - -pluginPackages=xyz.block.ftl.hotswap - -# Watch for changes in a directory (resources only). If not set, changes of resources won't be observed. -# -# Similar to extraClasspath this property adds classpath when searching for resources (not classes). -# While extra classpath just modifies the classloader, this setting does nothing until the resource -# is really changed. -# -# Sometimes it is not possible to point extraClasspath to your i.e. src/main/resources, because there are multiple -# replacements of resources in a building step (maven filtering resource option). -# This setting will leave i.e. src/target/classes as default source for resources, but after the resource is modified -# in src/main/resources, the new changed resource is served instead. -watchResources= - -# Load static web resources from different directory. -# -# This setting is dependent on application server plugin(Jetty, Tomcat, ...). -# Jboss and Glassfish are not yet supported. -# Use this setting to set to serve resources from source directory directly (e.g. src/main/webapp). -webappDir= - - -# Comma separated list of disabled plugins -# Use plugin name - e.g. Hibernate, Spring, ZK, Hotswapper, AnonymousClassPatch, Tomcat, Logback .... -disabledPlugins=JdkPlugin, Hibernate, Hibernate3JPA, Hibernate3, Spring, Jersey1, Jersey2, Jetty, Tomcat, ZK, Logback, Log4j2, MyFaces, Mojarra, Omnifaces, ELResolver, WildFlyELResolver, OsgiEquinox, Owb, Proxy, WebObjects, Weld, JBossModules, ResteasyRegistry, Deltaspike, GlassFish, Vaadin, Wicket, CxfJAXRS, FreeMarker, Undertow, MyBatis - -# Watch for changed class files on watchResources path and reload class definition in the running application. -# -# Usually you will launch debugging session from your IDE and use standard hotswap feature. -# This property is useful if you do not want to use debugging session for some reason or -# if you want to enable hotswap at runtime environment. -# -# Internally this uses java Instrumentation API to reload class bytecode. If you need to use JPDA API instead, -# specify autoHotswap.port with JPDA port. -autoHotswap=true - -# The base package prefix of your spring application (e.g. org.hotswap.). -# Needed when component scan is turned off, so we can still know which classes is your beans -# Can also be set to filter beans we handle to improve performance (So that we won't create proxy for thirty party lib's beans). -# Comma separated. -#spring.basePackagePrefix= - -# Create Java Platform Debugger Architecture (JPDA) connection on autoHotswap.port, watch for changed class files -# and do the hotswap (reload) in background. -# -# You need to specify JPDA port at startup -#
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8000
-# autoHotswap.port=8000 - -# Enables debugging in OsgiEquinox -# osgiEquinox.debugMode=true - -# Setup reloading strategy of bean INSTANCE(s) in Weld CONTEXT(s). While bean class is redefined by DCEVM, reloading of bean instances -# can be customized by this parameter. Available values: -# - CLASS_CHANGE - reload bean instance on any class modification, plus reaload on changes specified in -# METHOD_FIELD_SIGNATURE_CHANGE and FIELD_SIGNATURE_CHANGE strategies -# - METHOD_FIELD_SIGNATURE_CHANGE - reload bean instance on any method/field change. Includes changes specified in -# strategy FIELD_SIGNATURE_CHANGE -# - FIELD_SIGNATURE_CHANGE - reload bean instance on any field signature change. Includes also field annotation changes -# - NEVER - never reload bean (default) -# weld.beanReloadStrategy=NEVER - -# Logger setup - use entries in the format of -# format: LOGGER.my.package=LEVEL -# e.g. LOGGER.org.hotswap.agent.plugin.myPlugin=trace -# root level -LOGGER=error -# DateTime format using format of SimpleDateFormat, default value HH:mm:ss.SSS -# LOGGER_DATETIME_FORMAT=HH:mm:ss.SSS - -# Print output into logfile (with choice to append - false by default) -# LOGFILE=hotswap-agent.log -# LOGFILE.append=true - -# Comma separated list of class loaders to exclude from initialization, in the form of RegEx patterns. -excludedClassLoaderPatterns=jdk.nashorn.* diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/ContextTest.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/ContextTest.kt deleted file mode 100644 index 984be21f88..0000000000 --- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/ContextTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package xyz.block.ftl - -import ftl.builtin.Empty -import ftl.test.* -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource -import xyz.block.ftl.client.LoopbackVerbServiceClient -import xyz.block.ftl.registry.Registry - -data class TestCase(val expected: Any, val invoke: (ctx: Context) -> Any) - -class ContextTest { - companion object { - @JvmStatic - fun endToEnd(): List { - return listOf( - TestCase( - invoke = { ctx -> ctx.call(::echo, EchoRequest("Alice")) }, - expected = EchoResponse("Hello Alice, the time is $staticTime!"), - ), - TestCase( - invoke = { ctx -> ctx.call(::time, Empty()) }, - expected = TimeResponse(staticTime), - ), - TestCase( - invoke = { ctx -> ctx.callSink(::time, EchoRequest("Alice")) }, - expected = Unit, - ), - TestCase( - invoke = { ctx -> ctx.callSource(::time) }, - expected = TimeResponse(staticTime), - ), - TestCase( - invoke = { ctx -> ctx.callEmpty(::time) }, - expected = Unit, - ), - ) - } - } - - @ParameterizedTest - @MethodSource - fun endToEnd(testCase: TestCase) { - val registry = Registry("ftl.test") - registry.registerAll() - val routingClient = LoopbackVerbServiceClient(registry) - val context = Context("ftl.test", routingClient) - val result = testCase.invoke(context) - assertEquals(testCase.expected, result) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/registry/RegistryTest.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/registry/RegistryTest.kt deleted file mode 100644 index 284fb9a575..0000000000 --- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/registry/RegistryTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package xyz.block.ftl.registry - -import ftl.test.VerbRequest -import ftl.test.VerbResponse -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test -import xyz.block.ftl.Context -import xyz.block.ftl.client.LoopbackVerbServiceClient -import xyz.block.ftl.serializer.makeGson -import kotlin.test.assertContentEquals - -class RegistryTest { - private val gson = makeGson() - private val verbRef = Ref(module = "test", name = "verb") - - @Test - fun moduleName() { - val registry = Registry("ftl.test") - registry.registerAll() - assertEquals("test", registry.moduleName) - } - - @Test - fun registerAll() { - val registry = Registry("ftl.test") - registry.registerAll() - assertContentEquals(listOf( - Ref(module = "test", name = "echo"), - Ref(module = "test", name = "time"), - Ref(module = "test", name = "verb"), - ), registry.refs.sortedBy { it.toString() }) - } - - @Test - fun invoke() { - val registry = Registry("ftl.test") - registry.registerAll() - val context = Context("ftl.test", LoopbackVerbServiceClient(registry)) - val result = registry.invoke(context, verbRef, gson.toJson(VerbRequest("test"))) - assertEquals(result, gson.toJson(VerbResponse("test"))) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt deleted file mode 100644 index 5f91c7c5c1..0000000000 --- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt +++ /dev/null @@ -1,814 +0,0 @@ -package xyz.block.ftl.schemaextractor - -import io.gitlab.arturbosch.detekt.api.Config -import io.gitlab.arturbosch.detekt.rules.KotlinCoreEnvironmentTest -import io.gitlab.arturbosch.detekt.test.compileAndLintWithContext -import org.assertj.core.api.Assertions.assertThat -import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment -import org.jetbrains.kotlin.cli.jvm.config.addJvmClasspathRoots -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import xyz.block.ftl.schemaextractor.ExtractSchemaRule.Companion.ERRORS_OUT -import xyz.block.ftl.schemaextractor.ExtractSchemaRule.Companion.SCHEMA_OUT -import xyz.block.ftl.v1.schema.Array -import xyz.block.ftl.v1.schema.Data -import xyz.block.ftl.v1.schema.Decl -import xyz.block.ftl.v1.schema.Enum -import xyz.block.ftl.v1.schema.EnumVariant -import xyz.block.ftl.v1.schema.Error -import xyz.block.ftl.v1.schema.ErrorList -import xyz.block.ftl.v1.schema.Field -import xyz.block.ftl.v1.schema.IngressPathComponent -import xyz.block.ftl.v1.schema.IngressPathLiteral -import xyz.block.ftl.v1.schema.IntValue -import xyz.block.ftl.v1.schema.Map -import xyz.block.ftl.v1.schema.Metadata -import xyz.block.ftl.v1.schema.MetadataAlias -import xyz.block.ftl.v1.schema.MetadataCalls -import xyz.block.ftl.v1.schema.MetadataIngress -import xyz.block.ftl.v1.schema.Module -import xyz.block.ftl.v1.schema.Optional -import xyz.block.ftl.v1.schema.Position -import xyz.block.ftl.v1.schema.Ref -import xyz.block.ftl.v1.schema.String -import xyz.block.ftl.v1.schema.StringValue -import xyz.block.ftl.v1.schema.Type -import xyz.block.ftl.v1.schema.TypeParameter -import xyz.block.ftl.v1.schema.Unit -import xyz.block.ftl.v1.schema.Value -import xyz.block.ftl.v1.schema.Verb -import java.io.File -import kotlin.test.AfterTest -import kotlin.test.assertContains - -@KotlinCoreEnvironmentTest -internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { - @BeforeEach - fun setup() { - val dependenciesDir = File("src/test/kotlin/xyz/block/ftl/schemaextractor/testdata/dependencies") - val dependencies = dependenciesDir.listFiles { file -> file.extension == "kt" }?.toList() ?: emptyList() - env.configuration.addJvmClasspathRoots(dependencies) - } - - @AfterTest - fun cleanup() { - File(SCHEMA_OUT).delete() - File(ERRORS_OUT).delete() - } - - @Test - fun `extracts schema`() { - val code = """ - // Echo module. - package ftl.echo - - import ftl.builtin.Empty - import ftl.builtin.HttpRequest - import ftl.builtin.HttpResponse - import ftl.time.time as verb - import ftl.time.other - import ftl.time.TimeRequest - import ftl.time.TimeResponse - import xyz.block.ftl.Json - import xyz.block.ftl.Context - import xyz.block.ftl.HttpIngress - import xyz.block.ftl.Method - import xyz.block.ftl.Module - import xyz.block.ftl.Verb - - class InvalidInput(val field: String) : Exception() - - data class MapValue(val value: String) - data class EchoMessage(val message: String, val metadata: Map? = null) - - /** - * Request to echo a message. - * - * More comments. - */ - data class EchoRequest( - val t: T, - val name: String, - @Json("stf") val stuff: Any, - ) - data class EchoResponse(val messages: List) - - /** - * Echoes the given message. - */ - @Throws(InvalidInput::class) - @HttpIngress(Method.GET, "/echo") - fun echo(context: Context, req: HttpRequest>): HttpResponse { - callTime(context) - - return HttpResponse( - status = 200, - headers = mapOf("Get" to arrayListOf("Header from FTL")), - body = EchoResponse(messages = listOf(EchoMessage(message = "Hello!"))) - ) - } - - @Verb - fun empty(context: Context, req: Empty): Empty { - return builtin.Empty() - } - - fun callTime(context: Context): TimeResponse { - context.call(::empty, builtin.Empty()) - context.call(::other, builtin.Empty()) - // commented out call is ignored: - //context.call(::foo, builtin.Empty()) - return context.call(::verb, builtin.Empty()) - } - - @Verb - fun sink(context: Context, req: Empty) {} - - @Verb - fun source(context: Context): Empty {} - - @Verb - fun emptyVerb(context: Context) {} - """ - ExtractSchemaRule(Config.empty).compileAndLintWithContext(env, code) - val file = File(SCHEMA_OUT) - val module = Module.ADAPTER.decode(file.inputStream()) - - val expected = Module( - name = "echo", - comments = listOf("Echo module."), - decls = listOf( - Decl( - data_ = Data( - name = "EchoRequest", - fields = listOf( - Field( - name = "t", - type = Type(ref = Ref(name = "T")) - ), - Field( - name = "name", - type = Type(string = xyz.block.ftl.v1.schema.String()) - ), - Field( - name = "stuff", - type = Type(any = xyz.block.ftl.v1.schema.Any()), - metadata = listOf(Metadata(alias = MetadataAlias(alias = "stf"))), - ) - ), - comments = listOf( - "Request to echo a message.", "", "More comments." - ), - typeParameters = listOf( - TypeParameter(name = "T") - ) - ), - ), - Decl( - data_ = Data( - name = "MapValue", - fields = listOf( - Field( - name = "value", - type = Type(string = xyz.block.ftl.v1.schema.String()) - ) - ), - ), - ), - Decl( - data_ = Data( - name = "EchoMessage", - fields = listOf( - Field( - name = "message", - type = Type(string = xyz.block.ftl.v1.schema.String()) - ), - Field( - name = "metadata", - type = Type( - optional = Optional( - type = Type( - map = Map( - key = Type(string = xyz.block.ftl.v1.schema.String()), - value_ = Type( - ref = Ref( - name = "MapValue", - module = "echo" - ) - ) - ) - ) - ), - ) - ), - ), - ), - ), - Decl( - data_ = Data( - name = "EchoResponse", - fields = listOf( - Field( - name = "messages", - type = Type( - array = Array( - element = Type( - ref = Ref( - name = "EchoMessage", - module = "echo" - ) - ) - ) - ) - ) - ), - ), - ), - Decl( - verb = Verb( - name = "echo", - comments = listOf( - """Echoes the given message.""" - ), - request = Type( - ref = Ref( - name = "HttpRequest", - typeParameters = listOf( - Type( - ref = Ref( - name = "EchoRequest", - typeParameters = listOf( - Type(string = xyz.block.ftl.v1.schema.String()) - ), - module = "echo" - ) - ) - ), - module = "builtin" - ) - ), - response = Type( - ref = Ref( - name = "HttpResponse", - module = "builtin", - typeParameters = listOf( - Type( - ref = Ref( - name = "EchoResponse", - module = "echo" - ) - ), - Type( - string = xyz.block.ftl.v1.schema.String() - ), - ), - ), - ), - metadata = listOf( - Metadata( - ingress = MetadataIngress( - type = "http", - method = "GET", - path = listOf( - IngressPathComponent( - ingressPathLiteral = IngressPathLiteral(text = "echo") - ) - ) - ) - ), - Metadata( - calls = MetadataCalls( - calls = listOf( - Ref( - name = "empty", - module = "echo" - ), - Ref( - name = "other", - module = "time" - ), - Ref( - name = "time", - module = "time" - ) - ) - ) - ) - ) - ), - ), - Decl( - verb = Verb( - name = "empty", - request = Type( - ref = Ref( - name = "Empty", - module = "builtin" - ) - ), - response = Type( - ref = Ref( - name = "Empty", - module = "builtin" - ) - ), - ), - ), - Decl( - verb = Verb( - name = "sink", - request = Type( - ref = Ref( - name = "Empty", - module = "builtin" - ) - ), - response = Type( - unit = Unit() - ), - ), - ), - Decl( - verb = Verb( - name = "source", - request = Type( - unit = Unit() - ), - response = Type( - ref = Ref( - name = "Empty", - module = "builtin" - ) - ), - ), - ), - Decl( - verb = Verb( - name = "emptyVerb", - request = Type( - unit = Unit() - ), - response = Type( - unit = Unit() - ), - ), - ) - ) - ) - - assertThat(module) - .usingRecursiveComparison() - .withEqualsForType({ _, _ -> true }, Position::class.java) - .ignoringFieldsMatchingRegexes(".*hashCode\$") - .isEqualTo(expected) - } - - @Test - fun `fails if invalid schema type is included`() { - val code = """/** - * Echo module. - */ -package ftl.echo - -import ftl.builtin.Empty -import ftl.time.time -import ftl.time.TimeRequest -import ftl.time.TimeResponse -import xyz.block.ftl.Context -import xyz.block.ftl.Method -import xyz.block.ftl.Verb - -class InvalidInput(val field: String) : Exception() - -data class EchoMessage(val message: String, val metadata: Map? = null) - -/** - * Request to echo a message. - */ -data class EchoRequest(val name: Char) -data class EchoResponse(val messages: List) - -/** - * Echoes the given message. - */ -@Throws(InvalidInput::class) -@Verb -fun echo(context: Context, req: EchoRequest): EchoResponse { - callTime(context) - return EchoResponse(messages = listOf(EchoMessage(message = "Hello!"))) -} - -fun callTime(context: Context): TimeResponse { - return context.call(::time, Empty()) -} -""" - val message = assertThrows { - ExtractSchemaRule(Config.empty).compileAndLintWithContext(env, code) - }.message!! - assertContains(message, "could not extract schema") - - assertErrorsFileContainsExactly( - Error( - msg = "expected type to be a data class or builtin.Empty, but was kotlin.Char", - ) - ) - } - - @Test - fun `fails if http ingress without http request-response types`() { - val code = """ - /** - * Echo module. - */ -package ftl.echo - -import xyz.block.ftl.Context -import xyz.block.ftl.HttpIngress -import xyz.block.ftl.Method - -/** - * Request to echo a message. - */ -data class EchoRequest(val name: String) -data class EchoResponse(val message: String) - -/** - * Echoes the given message. - */ -@Throws(InvalidInput::class) -@HttpIngress(Method.GET, "/echo") -fun echo(context: Context, req: EchoRequest): EchoResponse { - return EchoResponse(messages = listOf(EchoMessage(message = "Hello!"))) -} - """ - val message = assertThrows { - ExtractSchemaRule(Config.empty).compileAndLintWithContext(env, code) - }.message!! - assertContains(message, "could not extract schema") - - assertErrorsFileContainsExactly( - Error( - msg = "@HttpIngress-annotated echo request must be ftl.builtin.HttpRequest", - ), - Error( - msg = "@HttpIngress-annotated echo response must be ftl.builtin.HttpResponse", - ) - ) - } - - @Test - fun `source and sink types`() { - val code = """ - /** - * Echo module. - */ -package ftl.echo - -import xyz.block.ftl.Context -import xyz.block.ftl.HttpIngress -import xyz.block.ftl.Method -import xyz.block.ftl.Verb - -/** - * Request to echo a message. - */ -data class EchoRequest(val name: String) -data class EchoResponse(val message: String) - -/** - * Echoes the given message. - */ -@Throws(InvalidInput::class) -@HttpIngress(Method.GET, "/echo") -fun echo(context: Context, req: EchoRequest): EchoResponse { - return EchoResponse(messages = listOf(EchoMessage(message = "Hello!"))) -} - """ - val message = assertThrows { - ExtractSchemaRule(Config.empty).compileAndLintWithContext(env, code) - }.message!! - assertContains(message, "could not extract schema") - - assertErrorsFileContainsExactly( - Error( - msg = "@HttpIngress-annotated echo request must be ftl.builtin.HttpRequest", - ), - Error( - msg = "@HttpIngress-annotated echo response must be ftl.builtin.HttpResponse", - ) - ) - } - - @Test - fun `extracts enums`() { - val code = """ - package ftl.things - - import ftl.time.Color - import xyz.block.ftl.Json - import xyz.block.ftl.Context - import xyz.block.ftl.Method - import xyz.block.ftl.Verb - import xyz.block.ftl.Enum - - class InvalidInput(val field: String) : Exception() - - @Enum - enum class Thing { - /** - * A comment. - */ - A, - B, - C, - } - - /** - * Comments. - */ - @Enum - enum class StringThing(val value: String) { - /** - * A comment. - */ - A("A"), - /** - * B comment. - */ - B("B"), - C("C"), - } - - @Enum - enum class IntThing(val value: Int) { - A(1), - B(2), - /** - * C comment. - */ - C(3), - } - - data class Request( - val color: Color, - val thing: Thing, - val stringThing: StringThing, - val intThing: IntThing - ) - - data class Response(val message: String) - - @Verb - fun something(context: Context, req: Request): Response { - return Response(message = "response") - } - """ - ExtractSchemaRule(Config.empty).compileAndLintWithContext(env, code) - val file = File(SCHEMA_OUT) - val module = Module.ADAPTER.decode(file.inputStream()) - - val expected = Module( - name = "things", - decls = listOf( - Decl( - data_ = Data( - name = "Request", - fields = listOf( - Field( - name = "color", - type = Type(ref = Ref(name = "Color", module = "time")) - ), - Field( - name = "thing", - type = Type(ref = Ref(name = "Thing", module = "things")) - ), - Field( - name = "stringThing", - type = Type(ref = Ref(name = "StringThing", module = "things")) - ), - Field( - name = "intThing", - type = Type(ref = Ref(name = "IntThing", module = "things")) - ), - ), - ), - ), - Decl( - data_ = Data( - name = "Response", - fields = listOf( - Field( - name = "message", - type = Type(string = xyz.block.ftl.v1.schema.String()) - ) - ), - ), - ), - Decl( - enum_ = Enum( - name = "Thing", - type = Type(int = xyz.block.ftl.v1.schema.Int()), - variants = listOf( - EnumVariant( - name = "A", value_ = Value(intValue = IntValue(value_ = 0)), - comments = listOf("A comment.") - ), - EnumVariant( - name = "B", - value_ = Value(intValue = IntValue(value_ = 1)), - ), - EnumVariant( - name = "C", - value_ = Value(intValue = IntValue(value_ = 2)), - ), - ), - ), - ), - Decl( - enum_ = Enum( - name = "StringThing", - comments = listOf("Comments."), - type = Type(string = String()), - variants = listOf( - EnumVariant( - name = "A", - value_ = Value(stringValue = StringValue(value_ = "A")), - comments = listOf("A comment.") - ), - EnumVariant( - name = "B", - value_ = Value(stringValue = StringValue(value_ = "B")), - comments = listOf("B comment.") - ), - EnumVariant( - name = "C", - value_ = Value(stringValue = StringValue(value_ = "C")), - ), - ), - ), - ), - Decl( - enum_ = Enum( - name = "IntThing", - type = Type(int = xyz.block.ftl.v1.schema.Int()), - variants = listOf( - EnumVariant( - name = "A", - value_ = Value(intValue = IntValue(value_ = 1)), - ), - EnumVariant( - name = "B", - value_ = Value(intValue = IntValue(value_ = 2)), - ), - EnumVariant( - name = "C", - value_ = Value(intValue = IntValue(value_ = 3)), - comments = listOf("C comment.") - ), - ), - ), - ), - Decl( - verb = Verb( - name = "something", - request = Type(ref = Ref(name = "Request", module = "things")), - response = Type(ref = Ref(name = "Response", module = "things")), - ), - ), - ) - ) - - assertThat(module) - .usingRecursiveComparison() - .withEqualsForType({ _, _ -> true }, Position::class.java) - .ignoringFieldsMatchingRegexes(".*hashCode\$") - .isEqualTo(expected) - } - - - @Test - fun `extracts secrets and configs`() { - val code = """ - package ftl.test - - import ftl.time.Color - import xyz.block.ftl.Json - import xyz.block.ftl.Context - import xyz.block.ftl.Method - import xyz.block.ftl.Verb - import xyz.block.ftl.config.Config - import xyz.block.ftl.secrets.Secret - - val secret = Secret.new("secret") - val anotherSecret = Secret(String::class.java, "anotherSecret") - - val config = Config.new("config") - val anotherConfig = Config(ConfigData::class.java, "anotherConfig") - - data class ConfigData(val field: String) - - data class Request(val message: String) - - data class Response(val message: String) - - @Verb - fun something(context: Context, req: Request): Response { - return Response(message = "response") - } - """ - ExtractSchemaRule(Config.empty).compileAndLintWithContext(env, code) - val file = File(SCHEMA_OUT) - val module = Module.ADAPTER.decode(file.inputStream()) - - val expected = Module( - name = "test", - decls = listOf( - Decl( - data_ = Data( - name = "ConfigData", - fields = listOf( - Field( - name = "field", - type = Type(string = String()) - ) - ), - ), - ), - Decl( - data_ = Data( - name = "Request", - fields = listOf( - Field( - name = "message", - type = Type(string = String()) - ), - ), - ), - ), - Decl( - data_ = Data( - name = "Response", - fields = listOf( - Field( - name = "message", - type = Type(string = String()) - ) - ), - ), - ), - Decl( - secret = xyz.block.ftl.v1.schema.Secret( - name = "secret", - type = Type(string = String()) - ), - ), - - Decl( - secret = xyz.block.ftl.v1.schema.Secret( - name = "anotherSecret", - type = Type(string = String()) - ), - ), - Decl( - config = xyz.block.ftl.v1.schema.Config( - name = "config", - type = Type(ref = Ref(name = "ConfigData", module = "test")), - ), - ), - Decl( - config = xyz.block.ftl.v1.schema.Config( - name = "anotherConfig", - type = Type(ref = Ref(name = "ConfigData", module = "test")), - ), - ), - Decl( - verb = Verb( - name = "something", - request = Type(ref = Ref(name = "Request", module = "test")), - response = Type(ref = Ref(name = "Response", module = "test")), - ), - ), - ) - ) - - assertThat(module) - .usingRecursiveComparison() - .withEqualsForType({ _, _ -> true }, Position::class.java) - .ignoringFieldsMatchingRegexes(".*hashCode\$") - .isEqualTo(expected) - } - - private fun assertErrorsFileContainsExactly(vararg expected: Error) { - val file = File(ERRORS_OUT) - val actual = ErrorList.ADAPTER.decode(file.inputStream()) - - assertThat(actual.errors) - .usingRecursiveComparison() - .withEqualsForType({ _, _ -> true }, Position::class.java) - .ignoringFieldsMatchingRegexes(".*hashCode\$") - .ignoringFields("endColumn") - .ignoringCollectionOrder() - .isEqualTo(expected.toList()) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/testdata/dependencies/BuiltinModuleClient.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/testdata/dependencies/BuiltinModuleClient.kt deleted file mode 100644 index 05b4afb95a..0000000000 --- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/testdata/dependencies/BuiltinModuleClient.kt +++ /dev/null @@ -1,28 +0,0 @@ -// Built-in types for FTL. -// -// This is copied from the FTL runtime and is not meant to be edited. -package ftl.builtin - -/** - * HTTP request structure used for HTTP ingress verbs. - */ -public data class HttpRequest( - public val method: String, - public val path: String, - public val pathParameters: Map, - public val query: Map>, - public val headers: Map>, - public val body: Body, -) - -/** - * HTTP response structure used for HTTP ingress verbs. - */ -public data class HttpResponse( - public val status: Long, - public val headers: Map>, - public val body: Body? = null, - public val error: Error? = null, -) - -public class Empty diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/testdata/dependencies/TimeModuleClient.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/testdata/dependencies/TimeModuleClient.kt deleted file mode 100644 index 71eaa63965..0000000000 --- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/testdata/dependencies/TimeModuleClient.kt +++ /dev/null @@ -1,32 +0,0 @@ -package ftl.time - -import ftl.builtin.Empty -import xyz.block.ftl.Context -import xyz.block.ftl.HttpIngress -import xyz.block.ftl.Method.GET -import xyz.block.ftl.Verb -import java.time.OffsetDateTime - -data class TimeResponse( - val time: OffsetDateTime, -) - -enum class Color { - RED, - GREEN, - BLUE, -} - -/** - * Time returns the current time. - */ -@HttpIngress( - GET, - "/time", -) -fun time(context: Context, req: Empty): TimeResponse = - throw NotImplementedError("Verb stubs should not be called directly, instead use context.call(TimeModuleClient::time, ...)") - -@Verb -fun other(context: Context, req: Empty): TimeResponse = - throw NotImplementedError("Verb stubs should not be called directly, instead use context.call(TimeModuleClient::time, ...)") diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/secrets/SecretTest.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/secrets/SecretTest.kt deleted file mode 100644 index 7dcaa3961d..0000000000 --- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/secrets/SecretTest.kt +++ /dev/null @@ -1,14 +0,0 @@ -package xyz.block.ftl.secrets - -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test -import org.junitpioneer.jupiter.SetEnvironmentVariable - -class SecretTest { - @Test - @SetEnvironmentVariable(key = "FTL_SECRET_SECRETS_TEST", value = "testingtesting") - fun testSecret() { - val secret = Secret.new("test") - assertEquals("testingtesting", secret.get()) - } -} diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/testdata/TestModule.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/testdata/TestModule.kt deleted file mode 100644 index 70b1cd2611..0000000000 --- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/testdata/TestModule.kt +++ /dev/null @@ -1,39 +0,0 @@ -package ftl.test - -import ftl.builtin.Empty -import xyz.block.ftl.* -import java.time.OffsetDateTime - - -data class EchoRequest(val user: String) -data class EchoResponse(val text: String) - -@Verb -fun echo(context: Context, req: EchoRequest): EchoResponse { - val time = context.call(::time, Empty()) - return EchoResponse("Hello ${req.user}, the time is ${time.time}!") -} - -data class TimeResponse(val time: OffsetDateTime) - -val staticTime = OffsetDateTime.now() - -@Verb -fun time(context: Context, req: Empty): TimeResponse { - return TimeResponse(staticTime) -} - -data class VerbRequest(val text: String = "") -data class VerbResponse(val text: String = "") - -@HttpIngress(Method.GET, "/test") -fun verb(context: Context, req: VerbRequest): VerbResponse { - return VerbResponse("test") -} - - -@Verb -@Ignore -fun anotherVerb(context: Context, req: VerbRequest): VerbResponse { - return VerbResponse("ignored") -} diff --git a/kotlin-runtime/release.go b/kotlin-runtime/release.go deleted file mode 100644 index 1f79325792..0000000000 --- a/kotlin-runtime/release.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build release - -package goruntime - -import ( - "archive/zip" - "bytes" - _ "embed" -) - -//go:embed scaffolding.zip -var archive []byte - -//go:embed external-module-template.zip -var externalModuleTemplate []byte - -// Files is the FTL Kotlin runtime scaffolding files. -func Files() *zip.Reader { - zr, err := zip.NewReader(bytes.NewReader(archive), int64(len(archive))) - if err != nil { - panic(err) - } - return zr -} - -// ExternalModuleTemplates are templates for scaffolding external modules in the FTL Kotlin runtime. -func ExternalModuleTemplates() *zip.Reader { - zr, err := zip.NewReader(bytes.NewReader(externalModuleTemplate), int64(len(externalModuleTemplate))) - if err != nil { - panic(err) - } - return zr -} diff --git a/kotlin-runtime/scaffolding/bin/.ftl@latest.pkg b/kotlin-runtime/scaffolding/bin/.ftl@latest.pkg deleted file mode 120000 index 383f4511d4..0000000000 --- a/kotlin-runtime/scaffolding/bin/.ftl@latest.pkg +++ /dev/null @@ -1 +0,0 @@ -hermit \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/.maven-3.9.8.pkg b/kotlin-runtime/scaffolding/bin/.maven-3.9.8.pkg deleted file mode 120000 index 383f4511d4..0000000000 --- a/kotlin-runtime/scaffolding/bin/.maven-3.9.8.pkg +++ /dev/null @@ -1 +0,0 @@ -hermit \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/.openjdk-17.0.8_7.pkg b/kotlin-runtime/scaffolding/bin/.openjdk-17.0.8_7.pkg deleted file mode 120000 index 383f4511d4..0000000000 --- a/kotlin-runtime/scaffolding/bin/.openjdk-17.0.8_7.pkg +++ /dev/null @@ -1 +0,0 @@ -hermit \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/README.hermit.md b/kotlin-runtime/scaffolding/bin/README.hermit.md deleted file mode 100644 index e889550ba4..0000000000 --- a/kotlin-runtime/scaffolding/bin/README.hermit.md +++ /dev/null @@ -1,7 +0,0 @@ -# Hermit environment - -This is a [Hermit](https://github.com/cashapp/hermit) bin directory. - -The symlinks in this directory are managed by Hermit and will automatically -download and install Hermit itself as well as packages. These packages are -local to this environment. diff --git a/kotlin-runtime/scaffolding/bin/activate-hermit b/kotlin-runtime/scaffolding/bin/activate-hermit deleted file mode 100755 index fe28214d33..0000000000 --- a/kotlin-runtime/scaffolding/bin/activate-hermit +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# This file must be used with "source bin/activate-hermit" from bash or zsh. -# You cannot run it directly -# -# THIS FILE IS GENERATED; DO NOT MODIFY - -if [ "${BASH_SOURCE-}" = "$0" ]; then - echo "You must source this script: \$ source $0" >&2 - exit 33 -fi - -BIN_DIR="$(dirname "${BASH_SOURCE[0]:-${(%):-%x}}")" -if "${BIN_DIR}/hermit" noop > /dev/null; then - eval "$("${BIN_DIR}/hermit" activate "${BIN_DIR}/..")" - - if [ -n "${BASH-}" ] || [ -n "${ZSH_VERSION-}" ]; then - hash -r 2>/dev/null - fi - - echo "Hermit environment $("${HERMIT_ENV}"/bin/hermit env HERMIT_ENV) activated" -fi diff --git a/kotlin-runtime/scaffolding/bin/ftl b/kotlin-runtime/scaffolding/bin/ftl deleted file mode 120000 index 47611de9b6..0000000000 --- a/kotlin-runtime/scaffolding/bin/ftl +++ /dev/null @@ -1 +0,0 @@ -.ftl@latest.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/ftl-controller b/kotlin-runtime/scaffolding/bin/ftl-controller deleted file mode 120000 index 47611de9b6..0000000000 --- a/kotlin-runtime/scaffolding/bin/ftl-controller +++ /dev/null @@ -1 +0,0 @@ -.ftl@latest.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/ftl-initdb b/kotlin-runtime/scaffolding/bin/ftl-initdb deleted file mode 120000 index 47611de9b6..0000000000 --- a/kotlin-runtime/scaffolding/bin/ftl-initdb +++ /dev/null @@ -1 +0,0 @@ -.ftl@latest.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/ftl-runner b/kotlin-runtime/scaffolding/bin/ftl-runner deleted file mode 120000 index 47611de9b6..0000000000 --- a/kotlin-runtime/scaffolding/bin/ftl-runner +++ /dev/null @@ -1 +0,0 @@ -.ftl@latest.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/hermit b/kotlin-runtime/scaffolding/bin/hermit deleted file mode 100755 index 7fef769248..0000000000 --- a/kotlin-runtime/scaffolding/bin/hermit +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -# -# THIS FILE IS GENERATED; DO NOT MODIFY - -set -eo pipefail - -export HERMIT_USER_HOME=~ - -if [ -z "${HERMIT_STATE_DIR}" ]; then - case "$(uname -s)" in - Darwin) - export HERMIT_STATE_DIR="${HERMIT_USER_HOME}/Library/Caches/hermit" - ;; - Linux) - export HERMIT_STATE_DIR="${XDG_CACHE_HOME:-${HERMIT_USER_HOME}/.cache}/hermit" - ;; - esac -fi - -export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" -HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" -export HERMIT_CHANNEL -export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} - -if [ ! -x "${HERMIT_EXE}" ]; then - echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 - INSTALL_SCRIPT="$(mktemp)" - # This value must match that of the install script - INSTALL_SCRIPT_SHA256="180e997dd837f839a3072a5e2f558619b6d12555cd5452d3ab19d87720704e38" - if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then - curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}" - else - # Install script is versioned by its sha256sum value - curl -fsSL "${HERMIT_DIST_URL}/install-${INSTALL_SCRIPT_SHA256}.sh" -o "${INSTALL_SCRIPT}" - # Verify install script's sha256sum - openssl dgst -sha256 "${INSTALL_SCRIPT}" | \ - awk -v EXPECTED="$INSTALL_SCRIPT_SHA256" \ - '$2!=EXPECTED {print "Install script sha256 " $2 " does not match " EXPECTED; exit 1}' - fi - /bin/bash "${INSTALL_SCRIPT}" 1>&2 -fi - -exec "${HERMIT_EXE}" --level=fatal exec "$0" -- "$@" diff --git a/kotlin-runtime/scaffolding/bin/hermit.hcl b/kotlin-runtime/scaffolding/bin/hermit.hcl deleted file mode 100644 index d68191fd18..0000000000 --- a/kotlin-runtime/scaffolding/bin/hermit.hcl +++ /dev/null @@ -1 +0,0 @@ -sources = ["https://github.com/TBD54566975/hermit-ftl.git", "https://github.com/cashapp/hermit-packages.git"] \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jar b/kotlin-runtime/scaffolding/bin/jar deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jar +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jarsigner b/kotlin-runtime/scaffolding/bin/jarsigner deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jarsigner +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/java b/kotlin-runtime/scaffolding/bin/java deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/java +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/javac b/kotlin-runtime/scaffolding/bin/javac deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/javac +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/javadoc b/kotlin-runtime/scaffolding/bin/javadoc deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/javadoc +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/javap b/kotlin-runtime/scaffolding/bin/javap deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/javap +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jcmd b/kotlin-runtime/scaffolding/bin/jcmd deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jcmd +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jconsole b/kotlin-runtime/scaffolding/bin/jconsole deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jconsole +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jdb b/kotlin-runtime/scaffolding/bin/jdb deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jdb +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jdeprscan b/kotlin-runtime/scaffolding/bin/jdeprscan deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jdeprscan +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jdeps b/kotlin-runtime/scaffolding/bin/jdeps deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jdeps +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jfr b/kotlin-runtime/scaffolding/bin/jfr deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jfr +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jhsdb b/kotlin-runtime/scaffolding/bin/jhsdb deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jhsdb +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jimage b/kotlin-runtime/scaffolding/bin/jimage deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jimage +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jinfo b/kotlin-runtime/scaffolding/bin/jinfo deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jinfo +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jlink b/kotlin-runtime/scaffolding/bin/jlink deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jlink +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jmap b/kotlin-runtime/scaffolding/bin/jmap deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jmap +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jmod b/kotlin-runtime/scaffolding/bin/jmod deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jmod +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jpackage b/kotlin-runtime/scaffolding/bin/jpackage deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jpackage +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jps b/kotlin-runtime/scaffolding/bin/jps deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jps +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jrunscript b/kotlin-runtime/scaffolding/bin/jrunscript deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jrunscript +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jshell b/kotlin-runtime/scaffolding/bin/jshell deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jshell +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jstack b/kotlin-runtime/scaffolding/bin/jstack deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jstack +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jstat b/kotlin-runtime/scaffolding/bin/jstat deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jstat +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/jstatd b/kotlin-runtime/scaffolding/bin/jstatd deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/jstatd +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/keytool b/kotlin-runtime/scaffolding/bin/keytool deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/keytool +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/mvn b/kotlin-runtime/scaffolding/bin/mvn deleted file mode 120000 index 9158f83b62..0000000000 --- a/kotlin-runtime/scaffolding/bin/mvn +++ /dev/null @@ -1 +0,0 @@ -.maven-3.9.8.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/mvnDebug b/kotlin-runtime/scaffolding/bin/mvnDebug deleted file mode 120000 index 9158f83b62..0000000000 --- a/kotlin-runtime/scaffolding/bin/mvnDebug +++ /dev/null @@ -1 +0,0 @@ -.maven-3.9.8.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/rmiregistry b/kotlin-runtime/scaffolding/bin/rmiregistry deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/rmiregistry +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/bin/serialver b/kotlin-runtime/scaffolding/bin/serialver deleted file mode 120000 index 7d19c5ad1d..0000000000 --- a/kotlin-runtime/scaffolding/bin/serialver +++ /dev/null @@ -1 +0,0 @@ -.openjdk-17.0.8_7.pkg \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/go.mod b/kotlin-runtime/scaffolding/go.mod deleted file mode 100644 index 2464bc5512..0000000000 --- a/kotlin-runtime/scaffolding/go.mod +++ /dev/null @@ -1,4 +0,0 @@ -// This needs to exist so that the Go toolchain doesn't include this directory. Annoying. -module exclude - -go 1.22.2 diff --git a/kotlin-runtime/scaffolding/{{ .Name | lower }}/ftl.toml b/kotlin-runtime/scaffolding/{{ .Name | lower }}/ftl.toml deleted file mode 100644 index c21e1aaa9d..0000000000 --- a/kotlin-runtime/scaffolding/{{ .Name | lower }}/ftl.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "{{ .Name | lower }}" -language = "kotlin" diff --git a/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml b/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml deleted file mode 100644 index 789035c48d..0000000000 --- a/kotlin-runtime/scaffolding/{{ .Name | lower }}/pom.xml +++ /dev/null @@ -1,187 +0,0 @@ - - - 4.0.0 - - {{ .GroupID }} - {{ .Name | camel | lower }} - 1.0-SNAPSHOT - - - 1.0-SNAPSHOT - 1.8 - 2.0.10 - true - ${java.version} - ${java.version} - - - - - org.jetbrains.kotlin - kotlin-stdlib - ${kotlin.version} - - - xyz.block - ftl-runtime - ${ftl.version} - - - org.postgresql - postgresql - 42.7.3 - - - - - - - - kotlin-maven-plugin - org.jetbrains.kotlin - ${kotlin.version} - - - compile - - compile - - - - ${project.basedir}/src/main/kotlin - - - - - test-compile - - test-compile - - - - ${project.basedir}/src/test/kotlin - - - - - - - org.apache.maven.plugins - maven-dependency-plugin - 3.7.1 - - - - copy-dependencies - compile - - copy-dependencies - - - ${project.build.directory}/dependency - runtime - - - - - build-classpath - compile - - build-classpath - - - ${project.build.directory}/classpath.txt - dependency - - - - build-classpath-property - compile - - build-classpath - - - generated.classpath - ${project.build.directory}/dependency - - - - - - - org.codehaus.mojo - build-helper-maven-plugin - 3.6.0 - - - generate-sources - - add-source - - - - ${project.build.directory}/generated-sources/ftl - - - - - - - com.github.ozsie - detekt-maven-plugin - 1.23.5 - - true - ${generated.classpath} - ${java.version} - ${java.home} - ${project.build.directory}/detekt.yml - - - ${project.build.directory}/dependency/ftl-runtime-${ftl.version}.jar - - - ${project.basedir}/src/main/kotlin,${project.build.directory}/generated-sources - - - - compile - - check-with-type-resolution - - - - - - xyz.block - ftl-runtime - ${ftl.version} - - - - - - - - kotlin-maven-plugin - org.jetbrains.kotlin - - - org.apache.maven.plugins - maven-dependency-plugin - - - - org.codehaus.mojo - build-helper-maven-plugin - - - - com.github.ozsie - detekt-maven-plugin - - - - \ No newline at end of file diff --git a/kotlin-runtime/scaffolding/{{ .Name | lower }}/src/main/kotlin/ftl/{{ .Name | camel | lower }}/{{ .Name | camel }}.kt b/kotlin-runtime/scaffolding/{{ .Name | lower }}/src/main/kotlin/ftl/{{ .Name | camel | lower }}/{{ .Name | camel }}.kt deleted file mode 100644 index e2a9edd71f..0000000000 --- a/kotlin-runtime/scaffolding/{{ .Name | lower }}/src/main/kotlin/ftl/{{ .Name | camel | lower }}/{{ .Name | camel }}.kt +++ /dev/null @@ -1,13 +0,0 @@ -package ftl.{{ .Name | camel | lower }} - -import xyz.block.ftl.Context -import xyz.block.ftl.Method -import xyz.block.ftl.Verb - -data class EchoRequest(val name: String? = "anonymous") -data class EchoResponse(val message: String) - -@Verb -fun echo(context: Context, req: EchoRequest): EchoResponse { - return EchoResponse(message = "Hello, ${req.name}!") -}