diff --git a/backend/schema/encoding.go b/backend/schema/encoding.go index f7c1124661..ed6914c005 100644 --- a/backend/schema/encoding.go +++ b/backend/schema/encoding.go @@ -21,10 +21,10 @@ func encodeMetadata(metadata []Metadata) string { } w := &strings.Builder{} fmt.Fprintln(w) - for _, c := range metadata { - fmt.Fprint(w, indent(c.String())) + for _, m := range metadata { + fmt.Fprintln(w, m.String()) } - return w.String() + return strings.TrimSuffix(w.String(), "\n") } func encodeComments(comments []string) string { diff --git a/backend/schema/fsm.go b/backend/schema/fsm.go index 2793ca1d12..dfb495be87 100644 --- a/backend/schema/fsm.go +++ b/backend/schema/fsm.go @@ -78,7 +78,7 @@ type FSMTransition struct { Pos Position `parser:"" protobuf:"1,optional"` Comments []string `parser:"@Comment*" protobuf:"2"` - From *Ref `parser:"@@" protobuf:"3"` + From *Ref `parser:"@@" protobuf:"3,optional"` To *Ref `parser:"'to' @@" protobuf:"4"` } diff --git a/backend/schema/schema_test.go b/backend/schema/schema_test.go index ea6e282bd2..05228c1aec 100644 --- a/backend/schema/schema_test.go +++ b/backend/schema/schema_test.go @@ -43,7 +43,8 @@ module todo { } export verb create(todo.CreateRequest) todo.CreateResponse - +calls todo.destroy +database calls todo.testdb + +calls todo.destroy + +database calls todo.testdb export verb destroy(builtin.HttpRequest) builtin.HttpResponse +ingress http GET /todo/destroy/{name} @@ -102,13 +103,13 @@ module payments { data OnlinePaymentPaid { } - verb completed(payments.OnlinePaymentCompleted) builtin.Empty + verb completed(payments.OnlinePaymentCompleted) Unit - verb created(payments.OnlinePaymentCreated) builtin.Empty + verb created(payments.OnlinePaymentCreated) Unit - verb failed(payments.OnlinePaymentFailed) builtin.Empty + verb failed(payments.OnlinePaymentFailed) Unit - verb paid(payments.OnlinePaymentPaid) builtin.Empty + verb paid(payments.OnlinePaymentPaid) Unit } ` assert.Equal(t, normaliseString(expected), normaliseString(testSchema.String())) @@ -624,19 +625,19 @@ var testSchema = MustValidate(&Schema{ &Data{Name: "OnlinePaymentCompleted"}, &Verb{Name: "created", Request: &Ref{Module: "payments", Name: "OnlinePaymentCreated"}, - Response: &Ref{Module: "builtin", Name: "Empty"}, + Response: &Unit{}, }, &Verb{Name: "paid", Request: &Ref{Module: "payments", Name: "OnlinePaymentPaid"}, - Response: &Ref{Module: "builtin", Name: "Empty"}, + Response: &Unit{}, }, &Verb{Name: "failed", Request: &Ref{Module: "payments", Name: "OnlinePaymentFailed"}, - Response: &Ref{Module: "builtin", Name: "Empty"}, + Response: &Unit{}, }, &Verb{Name: "completed", Request: &Ref{Module: "payments", Name: "OnlinePaymentCompleted"}, - Response: &Ref{Module: "builtin", Name: "Empty"}, + Response: &Unit{}, }, &FSM{ Name: "payment", diff --git a/backend/schema/validate.go b/backend/schema/validate.go index f571e801a8..dca2d9d36a 100644 --- a/backend/schema/validate.go +++ b/backend/schema/validate.go @@ -184,20 +184,24 @@ func ValidateModuleInSchema(schema *Schema, m optional.Option[*Module]) (*Schema if sym, decl := ResolveAs[*Verb](scopes, *start); decl == nil { merr = append(merr, errorf(start, "unknown start verb %q", start)) } else if sym == nil { - merr = append(merr, errorf(start, "start state %q must be a verb", start)) + merr = append(merr, errorf(start, "start state %q must be a sink", start)) + } else if sym.Kind() != VerbKindSink { + merr = append(merr, errorf(start, "start state %q must be a sink but is %s", start, sym.Kind())) } } case *FSMTransition: if sym, decl := ResolveAs[*Verb](scopes, *n.From); decl == nil { - merr = append(merr, errorf(n, "unknown source verb %q", n.From)) + merr = append(merr, errorf(n.From, "unknown source verb %q", n.From)) } else if sym == nil { - merr = append(merr, errorf(n, "source state %q is not a verb", n.From)) + merr = append(merr, errorf(n.From, "source state %q is not a verb", n.From)) } if sym, decl := ResolveAs[*Verb](scopes, *n.To); decl == nil { - merr = append(merr, errorf(n, "unknown destination verb %q", n.To)) + merr = append(merr, errorf(n.To, "unknown destination verb %q", n.To)) } else if sym == nil { - merr = append(merr, errorf(n, "destination state %q is not a verb", n.To)) + merr = append(merr, errorf(n.To, "destination state %q is not a sink", n.To)) + } else if sym.Kind() != VerbKindSink { + merr = append(merr, errorf(n.To, "destination state %q must be a sink but is %s", n.To, sym.Kind())) } case *Array, *Bool, *Bytes, *Data, *Database, Decl, *Field, *Float, diff --git a/backend/schema/validate_test.go b/backend/schema/validate_test.go index 46840d1882..194403545a 100644 --- a/backend/schema/validate_test.go +++ b/backend/schema/validate_test.go @@ -257,7 +257,7 @@ func TestValidate(t *testing.T) { {name: "InvalidFSM", schema: ` module one { - verb A(Empty) Empty + verb A(Empty) Unit verb B(one.C) Empty fsm FSM { @@ -269,14 +269,15 @@ func TestValidate(t *testing.T) { `4:13-13: unknown reference "one.C"`, `6:6-6: "FSM" has no start states`, `7:18-18: unknown source verb "one.C"`, + `7:27-27: destination state "one.B" must be a sink but is verb`, }, }, {name: "DuplicateFSM", schema: ` module one { - verb A(Empty) Empty - verb B(Empty) Empty - verb C(Empty) Empty + verb A(Empty) Unit + verb B(Empty) Unit + verb C(Empty) Unit fsm FSM { start one.A diff --git a/backend/schema/verb.go b/backend/schema/verb.go index 6f29e7d4fd..5d28639361 100644 --- a/backend/schema/verb.go +++ b/backend/schema/verb.go @@ -24,6 +24,39 @@ type Verb struct { var _ Decl = (*Verb)(nil) var _ Symbol = (*Verb)(nil) +// VerbKind is the kind of Verb: verb, sink, source or empty. +type VerbKind string + +const ( + // VerbKindVerb is a normal verb taking an input and an output of any non-unit type. + VerbKindVerb VerbKind = "verb" + // VerbKindSink is a verb that takes an input and returns unit. + VerbKindSink VerbKind = "sink" + // VerbKindSource is a verb that returns an output and takes unit. + VerbKindSource VerbKind = "source" + // VerbKindEmpty is a verb that takes unit and returns unit. + VerbKindEmpty VerbKind = "empty" +) + +// Kind returns the kind of Verb this is. +func (v *Verb) Kind() VerbKind { + _, inIsUnit := v.Request.(*Unit) + _, outIsUnit := v.Response.(*Unit) + switch { + case inIsUnit && outIsUnit: + return VerbKindEmpty + + case inIsUnit: + return VerbKindSource + + case outIsUnit: + return VerbKindSink + + default: + return VerbKindVerb + } +} + func (v *Verb) Position() Position { return v.Pos } func (v *Verb) schemaDecl() {} func (v *Verb) schemaSymbol() {} diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index 54f1190551..e620097c19 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -34,6 +34,9 @@ var ( }) ftlCallFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Call" + ftlFSMFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.FSM" + ftlTransitionFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Transition" + ftlStartFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Start" ftlConfigFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Config" ftlSecretFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Secret" //nolint:gosec ftlPostgresDBFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.PostgresDatabase" @@ -201,56 +204,143 @@ func visitCallExpr(pctx *parseContext, node *ast.CallExpr) { case ftlConfigFuncPath, ftlSecretFuncPath: // Secret/config declaration: ftl.Config[]() parseConfigDecl(pctx, node, fn) + + case ftlFSMFuncPath: + parseFSMDecl(pctx, node) + case ftlPostgresDBFuncPath: parseDatabaseDecl(pctx, node, schema.PostgresDatabaseType) } } func parseCall(pctx *parseContext, node *ast.CallExpr) { + if pctx.activeVerb == nil { + return + } if len(node.Args) != 3 { pctx.errors.add(errorf(node, "call must have exactly three arguments")) return } - _, verbFn := deref[*types.Func](pctx.pkg, node.Args[1]) - if verbFn == nil { + ref := parseVerbRef(pctx, node.Args[1]) + if ref == nil { if sel, ok := node.Args[1].(*ast.SelectorExpr); ok { pctx.errors.add(errorf(node.Args[1], "call first argument must be a function but is an unresolved reference to %s.%s", sel.X, sel.Sel)) } - pctx.errors.add(errorf(node.Args[1], "call first argument must be a function")) + pctx.errors.add(errorf(node.Args[1], "call first argument must be a function in an ftl module")) return } - if pctx.activeVerb == nil { - return + pctx.activeVerb.AddCall(ref) +} + +func parseVerbRef(pctx *parseContext, node ast.Expr) *schema.Ref { + _, verbFn := deref[*types.Func](pctx.pkg, node) + if verbFn == nil { + return nil } moduleName, ok := ftlModuleFromGoModule(verbFn.Pkg().Path()).Get() if !ok { - pctx.errors.add(errorf(node.Args[1], "call first argument must be a function in an ftl module")) - return + return nil } - ref := &schema.Ref{ + return &schema.Ref{ Pos: goPosToSchemaPos(node.Pos()), Module: moduleName, Name: strcase.ToLowerCamel(verbFn.Name()), } - pctx.activeVerb.AddCall(ref) } -func parseConfigDecl(pctx *parseContext, node *ast.CallExpr, fn *types.Func) { - var name string - if len(node.Args) == 1 { - if literal, ok := node.Args[0].(*ast.BasicLit); ok && literal.Kind == token.STRING { - var err error - name, err = strconv.Unquote(literal.Value) - if err != nil { - pctx.errors.add(wrapf(node, err, "")) - return - } +func parseFSMDecl(pctx *parseContext, node *ast.CallExpr) { + var literal *ast.BasicLit + if len(node.Args) > 0 { + literal, _ = node.Args[0].(*ast.BasicLit) + } + if literal == nil || literal.Kind != token.STRING { + pctx.errors.add(errorf(node, "first argument to an FSM declaration must be the name as a string literal")) + return + } + + name, err := strconv.Unquote(literal.Value) + if err != nil { + panic(err) // Should never happen + } + + if !schema.ValidateName(name) { + pctx.errors.add(errorf(node, "FSM names must be valid identifiers")) + } + + fsm := &schema.FSM{ + Pos: goPosToSchemaPos(node.Pos()), + Name: name, + } + pctx.module.Decls = append(pctx.module.Decls, fsm) + + for _, arg := range node.Args[1:] { + call, ok := arg.(*ast.CallExpr) + if !ok { + pctx.errors.add(errorf(arg, "expected call to Start or Transition")) + continue } + _, fn := deref[*types.Func](pctx.pkg, call.Fun) + if fn == nil { + pctx.errors.add(errorf(call, "expected call to Start or Transition")) + continue + } + parseFSMTransition(pctx, call, fn, fsm) } - if name == "" { - pctx.errors.add(errorf(node, "config and secret declarations must have a single string literal argument")) +} + +// Parse a Start or Transition call in an FSM declaration and add it to the FSM. +func parseFSMTransition(pctx *parseContext, node *ast.CallExpr, fn *types.Func, fsm *schema.FSM) { + refs := make([]*schema.Ref, len(node.Args)) + for i, arg := range node.Args { + ref := parseVerbRef(pctx, arg) + if ref == nil { + pctx.errors.add(errorf(arg, "expected a reference to a sink")) + return + } + refs[i] = ref + } + switch fn.FullName() { + case ftlStartFuncPath: + if len(refs) != 1 { + pctx.errors.add(errorf(node, "expected one reference to a sink")) + return + } + fsm.Start = append(fsm.Start, refs...) + + case ftlTransitionFuncPath: + if len(refs) != 2 { + pctx.errors.add(errorf(node, "expected two references to sinks")) + return + } + fsm.Transitions = append(fsm.Transitions, &schema.FSMTransition{ + Pos: goPosToSchemaPos(node.Pos()), + From: refs[0], + To: refs[1], + }) + + default: + pctx.errors.add(errorf(node, "expected call to Start or Transition")) + } +} + +func parseConfigDecl(pctx *parseContext, node *ast.CallExpr, fn *types.Func) { + var literal *ast.BasicLit + if len(node.Args) > 0 { + literal, _ = node.Args[0].(*ast.BasicLit) + } + if literal == nil || literal.Kind != token.STRING { + pctx.errors.add(errorf(node, "first argument to config and secret declarations must be the name as a string literal")) return } + name, err := strconv.Unquote(literal.Value) + if err != nil { + panic(err) // Should never happen + } + + if !schema.ValidateName(name) { + pctx.errors.add(errorf(node, "config and secret names must be valid identifiers")) + } + index := node.Fun.(*ast.IndexExpr) //nolint:forcetypeassert // Type parameter @@ -530,7 +620,7 @@ func maybeVisitTypeEnumVariant(pctx *parseContext, node *ast.GenDecl, directives for _, dir := range directives { if exportableDir, ok := dir.(exportable); ok { if pctx.enums[enumName].Export && !exportableDir.IsExported() { - pctx.errors.add(errorf(node, "parent enum %q is exported, but directive %q on %q is not. All variants of exported enums that have a directive must be explicitly exported as well", enumName, exportableDir, t.Name.Name)) + pctx.errors.add(errorf(node, "parent enum %q is exported, but directive %q on %q is not: all variants of exported enums that have a directive must be explicitly exported as well", enumName, exportableDir, t.Name.Name)) } isExported = exportableDir.IsExported() } diff --git a/go-runtime/compile/schema_test.go b/go-runtime/compile/schema_test.go index 5832cc4abb..41042fbadc 100644 --- a/go-runtime/compile/schema_test.go +++ b/go-runtime/compile/schema_test.go @@ -13,7 +13,6 @@ import ( "github.com/alecthomas/participle/v2/lexer" "github.com/TBD54566975/ftl/backend/schema" - "github.com/TBD54566975/ftl/internal/errors" "github.com/TBD54566975/ftl/internal/exec" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/slices" @@ -139,8 +138,8 @@ func TestExtractModuleSchema(t *testing.T) { data WithoutDirectiveStruct { } - export verb http(builtin.HttpRequest) builtin.HttpResponse - +ingress http GET /get + export verb http(builtin.HttpRequest) builtin.HttpResponse + +ingress http GET /get export verb nothing(Unit) Unit @@ -151,7 +150,7 @@ func TestExtractModuleSchema(t *testing.T) { verb verb(one.Req) one.Resp } ` - assert.Equal(t, expected, actual.String()) + assert.Equal(t, normaliseString(expected), normaliseString(actual.String())) } func TestExtractModuleSchemaTwo(t *testing.T) { @@ -204,6 +203,46 @@ func TestExtractModuleSchemaTwo(t *testing.T) { assert.Equal(t, normaliseString(expected), normaliseString(actual.String())) } +func TestExtractModuleSchemaFSM(t *testing.T) { + if testing.Short() { + t.SkipNow() + } + r, err := ExtractModuleSchema("testdata/fsm") + assert.NoError(t, err) + actual := schema.Normalise(r.MustGet().Module) + expected := `module fsm { + fsm payment { + start fsm.created + start fsm.paid + transition fsm.created to fsm.paid + transition fsm.created to fsm.failed + transition fsm.paid to fsm.completed + } + + data OnlinePaymentCompleted { + } + + data OnlinePaymentCreated { + } + + data OnlinePaymentFailed { + } + + data OnlinePaymentPaid { + } + + verb completed(fsm.OnlinePaymentCompleted) Unit + + verb created(fsm.OnlinePaymentCreated) Unit + + verb failed(fsm.OnlinePaymentFailed) Unit + + verb paid(fsm.OnlinePaymentPaid) Unit + } +` + assert.Equal(t, normaliseString(expected), normaliseString(actual.String())) +} + func TestParseDirectives(t *testing.T) { tests := []struct { name string @@ -281,9 +320,9 @@ func normaliseString(s string) string { } func TestErrorReporting(t *testing.T) { - if testing.Short() { - t.SkipNow() - } + // if testing.Short() { + // t.SkipNow() + // } ctx := log.ContextWithNewDefaultLogger(context.Background()) pwd, _ := os.Getwd() err := exec.Command(ctx, log.Debug, "testdata/failing", "go", "mod", "tidy").RunBuffered(ctx) @@ -292,50 +331,46 @@ func TestErrorReporting(t *testing.T) { assert.NoError(t, err) filename := filepath.Join(pwd, `testdata/failing/failing.go`) - assert.EqualError(t, errors.Join(genericizeErrors(r.MustGet().Errors)...), - filename+":10:13-35: config and secret declarations must have a single string literal argument\n"+ - filename+":13:18-52: duplicate config declaration at 12:18-52\n"+ - filename+":16:18-52: duplicate secret declaration at 15:18-52\n"+ - filename+":19:14-44: duplicate database declaration at 18:14-44\n"+ - filename+":22:2-10: unsupported type \"error\" for field \"BadParam\"\n"+ - filename+":25:2-17: unsupported type \"uint64\" for field \"AnotherBadParam\"\n"+ - filename+":28:3-3: unexpected token \"export\" (expected Directive)\n"+ - filename+":34:36-39: unsupported request type \"ftl/failing.Request\"\n"+ - filename+":34:50-50: unsupported response type \"ftl/failing.Response\"\n"+ - filename+":35:16-29: call first argument must be a function in an ftl module\n"+ - filename+":36:2-46: call must have exactly three arguments\n"+ - filename+":37:16-25: call first argument must be a function\n"+ - filename+":42:1-2: must have at most two parameters (context.Context, struct)\n"+ - filename+":42:69-69: unsupported response type \"ftl/failing.Response\"\n"+ - filename+":47:22-27: first parameter must be of type context.Context but is ftl/failing.Request\n"+ - filename+":47:37-43: second parameter must be a struct but is string\n"+ - filename+":47:53-53: unsupported response type \"ftl/failing.Response\"\n"+ - filename+":52:43-47: second parameter must not be ftl.Unit\n"+ - filename+":52:59-59: unsupported response type \"ftl/failing.Response\"\n"+ - filename+":57:1-2: first parameter must be context.Context\n"+ - filename+":57:18-18: unsupported response type \"ftl/failing.Response\"\n"+ - filename+":62:1-2: must have at most two results (struct, error)\n"+ - filename+":62:41-44: unsupported request type \"ftl/failing.Request\"\n"+ - filename+":67:1-2: must at least return an error\n"+ - filename+":67:36-39: unsupported request type \"ftl/failing.Request\"\n"+ - filename+":71:35-38: unsupported request type \"ftl/failing.Request\"\n"+ - filename+":71:48-48: must return an error but is ftl/failing.Response\n"+ - filename+":76:41-44: unsupported request type \"ftl/failing.Request\"\n"+ - filename+":76:55-55: first result must be a struct but is string\n"+ - filename+":76:63-63: must return an error but is string\n"+ - filename+":76:63-63: second result must not be ftl.Unit\n"+ - filename+":83:1-1: duplicate verb name \"WrongResponse\"\n"+ - filename+":89:2-12: struct field unexported must be exported by starting with an uppercase letter\n"+ - filename+":100:2-23: cannot attach enum value to BadValueEnum because it is a variant of type enum TypeEnum, not a value enum\n"+ - filename+":106:2-40: cannot attach enum value to BadValueEnumOrderDoesntMatter because it is a variant of type enum TypeEnum, not a value enum\n"+ - filename+":115:1-26: parent enum \"ExportedTypeEnum\" is exported, but directive \"ftl:data\" on \"PrivateData\" is not. All variants of exported enums that have a directive must be explicitly exported as well", - ) -} - -func genericizeErrors(schemaErrs []*schema.Error) []error { - errs := make([]error, len(schemaErrs)) - for i, schemaErr := range schemaErrs { - errs[i] = schemaErr + actual := slices.Map(r.MustGet().Errors, func(e *schema.Error) string { return strings.TrimPrefix(e.Error(), filename+":") }) + expected := []string{ + `10:13-34: first argument to config and secret declarations must be the name as a string literal`, + `13:18-52: duplicate config declaration at 12:18-52`, + `16:18-52: duplicate secret declaration at 15:18-52`, + `19:14-44: duplicate database declaration at 18:14-44`, + `22:2-10: unsupported type "error" for field "BadParam"`, + `25:2-17: unsupported type "uint64" for field "AnotherBadParam"`, + `28:3-3: unexpected token "export" (expected Directive)`, + `34:36-39: unsupported request type "ftl/failing.Request"`, + `34:50-50: unsupported response type "ftl/failing.Response"`, + `35:16-29: call first argument must be a function but is an unresolved reference to lib.OtherFunc`, + `35:16-29: call first argument must be a function in an ftl module`, + `36:2-46: call must have exactly three arguments`, + `37:16-25: call first argument must be a function in an ftl module`, + `42:1-2: must have at most two parameters (context.Context, struct)`, + `42:69-69: unsupported response type "ftl/failing.Response"`, + `47:22-27: first parameter must be of type context.Context but is ftl/failing.Request`, + `47:37-43: second parameter must be a struct but is string`, + `47:53-53: unsupported response type "ftl/failing.Response"`, + `52:43-47: second parameter must not be ftl.Unit`, + `52:59-59: unsupported response type "ftl/failing.Response"`, + `57:1-2: first parameter must be context.Context`, + `57:18-18: unsupported response type "ftl/failing.Response"`, + `62:1-2: must have at most two results (struct, error)`, + `62:41-44: unsupported request type "ftl/failing.Request"`, + `67:1-2: must at least return an error`, + `67:36-39: unsupported request type "ftl/failing.Request"`, + `71:35-38: unsupported request type "ftl/failing.Request"`, + `71:48-48: must return an error but is ftl/failing.Response`, + `76:41-44: unsupported request type "ftl/failing.Request"`, + `76:55-55: first result must be a struct but is string`, + `76:63-63: must return an error but is string`, + `76:63-63: second result must not be ftl.Unit`, + `83:1-1: duplicate verb name "WrongResponse"`, + `89:2-12: struct field unexported must be exported by starting with an uppercase letter`, + `100:2-23: cannot attach enum value to BadValueEnum because it is a variant of type enum TypeEnum, not a value enum`, + `106:2-40: cannot attach enum value to BadValueEnumOrderDoesntMatter because it is a variant of type enum TypeEnum, not a value enum`, + `115:1-26: parent enum "ExportedTypeEnum" is exported, but directive "ftl:data" on "PrivateData" is not: all variants of exported enums that have a directive must be explicitly exported as well`, + `119:21-60: config and secret names must be valid identifiers`, } - return errs + assert.Equal(t, expected, actual) } diff --git a/go-runtime/compile/testdata/failing/failing.go b/go-runtime/compile/testdata/failing/failing.go index f58487d53b..8730ea5c14 100644 --- a/go-runtime/compile/testdata/failing/failing.go +++ b/go-runtime/compile/testdata/failing/failing.go @@ -7,7 +7,7 @@ import ( "github.com/TBD54566975/ftl/go-runtime/ftl" ) -var empty = ftl.Config[string]("") +var empty = ftl.Config[string](1) var goodConfig = ftl.Config[string]("FTL_ENDPOINT") var duplConfig = ftl.Config[string]("FTL_ENDPOINT") @@ -115,3 +115,5 @@ type ExportedTypeEnum interface{ exportedTypeEnum() } type PrivateData struct{} func (PrivateData) exportedTypeEnum() {} + +var invalidConfig = ftl.Config[string]("not an identifier") diff --git a/go-runtime/compile/testdata/fsm/fsm.go b/go-runtime/compile/testdata/fsm/fsm.go new file mode 100644 index 0000000000..14343bb60a --- /dev/null +++ b/go-runtime/compile/testdata/fsm/fsm.go @@ -0,0 +1,40 @@ +package fsm + +import ( + "context" + + "github.com/TBD54566975/ftl/go-runtime/ftl" +) + +var paymentFSM = ftl.FSM("payment", + ftl.Start(Created), + ftl.Start(Paid), + ftl.Transition(Created, Paid), + ftl.Transition(Created, Failed), + ftl.Transition(Paid, Completed), +) + +type OnlinePaymentCompleted struct{} +type OnlinePaymentFailed struct{} +type OnlinePaymentPaid struct{} +type OnlinePaymentCreated struct{} + +//ftl:verb +func Completed(ctx context.Context, in OnlinePaymentCompleted) error { + return nil +} + +//ftl:verb +func Created(ctx context.Context, in OnlinePaymentCreated) error { + return nil +} + +//ftl:verb +func Failed(ctx context.Context, in OnlinePaymentFailed) error { + return nil +} + +//ftl:verb +func Paid(ctx context.Context, in OnlinePaymentPaid) error { + return nil +} diff --git a/go-runtime/compile/testdata/fsm/ftl.toml b/go-runtime/compile/testdata/fsm/ftl.toml new file mode 100644 index 0000000000..50ba9e6cf4 --- /dev/null +++ b/go-runtime/compile/testdata/fsm/ftl.toml @@ -0,0 +1,2 @@ +module = "fsm" +language = "go" diff --git a/go-runtime/compile/testdata/fsm/go.mod b/go-runtime/compile/testdata/fsm/go.mod new file mode 100644 index 0000000000..481b468480 --- /dev/null +++ b/go-runtime/compile/testdata/fsm/go.mod @@ -0,0 +1,46 @@ +module ftl/fsm + +go 1.22.2 + +require github.com/TBD54566975/ftl v0.200.0 + +require ( + connectrpc.com/connect v1.16.1 // indirect + connectrpc.com/grpcreflect v1.2.0 // indirect + connectrpc.com/otelconnect v0.7.0 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/TBD54566975/scaffolder v0.8.0 // indirect + github.com/alecthomas/concurrency v0.0.2 // indirect + github.com/alecthomas/kong v0.9.0 // indirect + github.com/alecthomas/participle/v2 v2.1.1 // indirect + github.com/alecthomas/types v0.14.0 // indirect + github.com/alessio/shellescape v1.4.2 // indirect + github.com/danieljoos/wincred v1.2.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/pgx/v5 v5.5.5 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/multiformats/go-base36 v0.2.0 // indirect + github.com/swaggest/jsonschema-go v0.3.70 // indirect + github.com/swaggest/refl v1.3.0 // indirect + github.com/zalando/go-keyring v0.2.4 // indirect + go.opentelemetry.io/otel v1.26.0 // indirect + go.opentelemetry.io/otel/metric v1.26.0 // indirect + go.opentelemetry.io/otel/trace v1.26.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.34.0 // indirect +) + +replace github.com/TBD54566975/ftl => ../../../../ diff --git a/go-runtime/compile/testdata/fsm/go.sum b/go-runtime/compile/testdata/fsm/go.sum new file mode 100644 index 0000000000..0131370846 --- /dev/null +++ b/go-runtime/compile/testdata/fsm/go.sum @@ -0,0 +1,142 @@ +connectrpc.com/connect v1.16.1 h1:rOdrK/RTI/7TVnn3JsVxt3n028MlTRwmK5Q4heSpjis= +connectrpc.com/connect v1.16.1/go.mod h1:XpZAduBQUySsb4/KO5JffORVkDI4B6/EYPi7N8xpNZw= +connectrpc.com/grpcreflect v1.2.0 h1:Q6og1S7HinmtbEuBvARLNwYmTbhEGRpHDhqrPNlmK+U= +connectrpc.com/grpcreflect v1.2.0/go.mod h1:nwSOKmE8nU5u/CidgHtPYk1PFI3U9ignz7iDMxOYkSY= +connectrpc.com/otelconnect v0.7.0 h1:ZH55ZZtcJOTKWWLy3qmL4Pam4RzRWBJFOqTPyAqCXkY= +connectrpc.com/otelconnect v0.7.0/go.mod h1:Bt2ivBymHZHqxvo4HkJ0EwHuUzQN6k2l0oH+mp/8nwc= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/TBD54566975/scaffolder v0.8.0 h1:DWl1K3dWcLsOPAYGQGPQXtffrml6XCB0tF05JdpMqZU= +github.com/TBD54566975/scaffolder v0.8.0/go.mod h1:Ab/jbQ4q8EloYL0nbkdh2DVvkGc4nxr1OcIbdMpTxxg= +github.com/alecthomas/assert/v2 v2.9.0 h1:ZcLG8ccMEtlMLkLW4gwGpBWBb0N8MUCmsy1lYBVd1xQ= +github.com/alecthomas/assert/v2 v2.9.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/concurrency v0.0.2 h1:Q3kGPtLbleMbH9lHX5OBFvJygfyFw29bXZKBg+IEVuo= +github.com/alecthomas/concurrency v0.0.2/go.mod h1:GmuQb/iHX7mbNtPlC/WDzEFxDMB0HYFer2Qda9QTs7w= +github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= +github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= +github.com/alecthomas/participle/v2 v2.1.1 h1:hrjKESvSqGHzRb4yW1ciisFJ4p3MGYih6icjJvbsmV8= +github.com/alecthomas/participle/v2 v2.1.1/go.mod h1:Y1+hAs8DHPmc3YUFzqllV+eSQ9ljPTk0ZkPMtEdAx2c= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/alecthomas/types v0.14.0 h1:4pCdEWVctLZQP9dE48fCyXWYkcoQtkf1EAxx9xGfCRY= +github.com/alecthomas/types v0.14.0/go.mod h1:fIOGnLeeUJXe1AAVofQmMaEMWLxY9bK4QxTLGIo30PA= +github.com/alessio/shellescape v1.4.2 h1:MHPfaU+ddJ0/bYWpgIeUnQUqKrlJ1S7BfEYPM4uEoM0= +github.com/alessio/shellescape v1.4.2/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/bool64/dev v0.2.34 h1:P9n315P8LdpxusnYQ0X7MP1CZXwBK5ae5RZrd+GdSZE= +github.com/bool64/dev v0.2.34/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= +github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= +github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw= +github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= +github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= +github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/jsonschema-go v0.3.70 h1:8Vx5nm5t/6DBFw2+WC0/Vp1ZVe9/4mpuA0tuAe0wwCI= +github.com/swaggest/jsonschema-go v0.3.70/go.mod h1:7N43/CwdaWgPUDfYV70K7Qm79tRqe/al7gLSt9YeGIE= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= +github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= +go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= +go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= +go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= +go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= +go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= +go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= +go.opentelemetry.io/otel/sdk/metric v1.26.0 h1:cWSks5tfriHPdWFnl+qpX3P681aAYqlZHcAyHw5aU9Y= +go.opentelemetry.io/otel/sdk/metric v1.26.0/go.mod h1:ClMFFknnThJCksebJwz7KIyEDHO+nTB6gK8obLy8RyE= +go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= +go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= +golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= +modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= +modernc.org/libc v1.49.3 h1:j2MRCRdwJI2ls/sGbeSk0t2bypOG/uvPZUsGQFDulqg= +modernc.org/libc v1.49.3/go.mod h1:yMZuGkn7pXbKfoT/M35gFJOAEdSKdxL0q64sF7KqCDo= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/sqlite v1.29.9 h1:9RhNMklxJs+1596GNuAX+O/6040bvOwacTxuFcRuQow= +modernc.org/sqlite v1.29.9/go.mod h1:ItX2a1OVGgNsFh6Dv60JQvGfJfTPHPVpV6DF59akYOA= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= diff --git a/go-runtime/ftl/fsm.go b/go-runtime/ftl/fsm.go new file mode 100644 index 0000000000..08e90b32c7 --- /dev/null +++ b/go-runtime/ftl/fsm.go @@ -0,0 +1,27 @@ +package ftl + +type FSMHandle struct { + transitions []FSMTransition +} + +type FSMTransition struct { + from Ref + to Ref +} + +// Start specifies a start state in an FSM. +func Start[In any](state Sink[In]) FSMTransition { + return FSMTransition{to: FuncRef(state)} +} + +// Transition specifies a transition in an FSM. +// +// The "event" triggering the transition is the input to the "from" state. +func Transition[FromIn, ToIn any](from Sink[FromIn], to Sink[ToIn]) FSMTransition { + return FSMTransition{from: FuncRef(from), to: FuncRef(to)} +} + +// FSM creates a new finite-state machine. +func FSM(name string, transitions ...FSMTransition) *FSMHandle { + return &FSMHandle{transitions: transitions} +}