Skip to content

Commit

Permalink
feat: verb request and response types can be any FTL type (#1912)
Browse files Browse the repository at this point in the history
closes #1854
  • Loading branch information
matt2e authored Jul 15, 2024
1 parent d012354 commit 1f073ab
Show file tree
Hide file tree
Showing 17 changed files with 64 additions and 36 deletions.
24 changes: 12 additions & 12 deletions backend/controller/dal/fsm_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,34 @@ func TestFSM(t *testing.T) {
in.CopyModule("fsm"),
in.Deploy("fsm"),

in.Call("fsm", "sendOne", in.Obj{"instance": "1"}, nil),
in.Call("fsm", "sendOne", in.Obj{"instance": "2"}, nil),
in.Call[in.Obj, in.Obj]("fsm", "sendOne", in.Obj{"instance": "1"}, nil),
in.Call[in.Obj, in.Obj]("fsm", "sendOne", in.Obj{"instance": "2"}, nil),
in.FileContains(logFilePath, "start 1"),
in.FileContains(logFilePath, "start 2"),
fsmInState("1", "running", "fsm.start"),
fsmInState("2", "running", "fsm.start"),

in.Call("fsm", "sendOne", in.Obj{"instance": "1"}, nil),
in.Call[in.Obj, in.Obj]("fsm", "sendOne", in.Obj{"instance": "1"}, nil),
in.FileContains(logFilePath, "middle 1"),
fsmInState("1", "running", "fsm.middle"),

in.Call("fsm", "sendOne", in.Obj{"instance": "1"}, nil),
in.Call[in.Obj, in.Obj]("fsm", "sendOne", in.Obj{"instance": "1"}, nil),
in.FileContains(logFilePath, "end 1"),
fsmInState("1", "completed", "fsm.end"),

in.Fail(in.Call("fsm", "sendOne", in.Obj{"instance": "1"}, nil),
in.Fail(in.Call[in.Obj, in.Obj]("fsm", "sendOne", in.Obj{"instance": "1"}, nil),
"FSM instance 1 is already in state fsm.end"),

// Invalid state transition
in.Fail(in.Call("fsm", "sendTwo", in.Obj{"instance": "2"}, nil),
in.Fail(in.Call[in.Obj, in.Obj]("fsm", "sendTwo", in.Obj{"instance": "2"}, nil),
"invalid state transition"),

in.Call("fsm", "sendOne", in.Obj{"instance": "2"}, nil),
in.Call[in.Obj, in.Obj]("fsm", "sendOne", in.Obj{"instance": "2"}, nil),
in.FileContains(logFilePath, "middle 2"),
fsmInState("2", "running", "fsm.middle"),

// Invalid state transition
in.Fail(in.Call("fsm", "sendTwo", in.Obj{"instance": "2"}, nil),
in.Fail(in.Call[in.Obj, in.Obj]("fsm", "sendTwo", in.Obj{"instance": "2"}, nil),
"invalid state transition"),
)
}
Expand Down Expand Up @@ -86,14 +86,14 @@ func TestFSMRetry(t *testing.T) {
in.Build("fsmretry"),
in.Deploy("fsmretry"),
// start 2 FSM instances
in.Call("fsmretry", "start", in.Obj{"id": "1"}, func(t testing.TB, response in.Obj) {}),
in.Call("fsmretry", "start", in.Obj{"id": "2"}, func(t testing.TB, response in.Obj) {}),
in.Call("fsmretry", "start", in.Obj{"id": "1"}, func(t testing.TB, response any) {}),
in.Call("fsmretry", "start", in.Obj{"id": "2"}, func(t testing.TB, response any) {}),

in.Sleep(2*time.Second),

// transition the FSM, should fail each time.
in.Call("fsmretry", "startTransitionToTwo", in.Obj{"id": "1"}, func(t testing.TB, response in.Obj) {}),
in.Call("fsmretry", "startTransitionToThree", in.Obj{"id": "2"}, func(t testing.TB, response in.Obj) {}),
in.Call("fsmretry", "startTransitionToTwo", in.Obj{"id": "1"}, func(t testing.TB, response any) {}),
in.Call("fsmretry", "startTransitionToThree", in.Obj{"id": "2"}, func(t testing.TB, response any) {}),

in.Sleep(8*time.Second), //5s is longest run of retries

Expand Down
9 changes: 4 additions & 5 deletions backend/controller/ingress/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,12 @@ func matchSegments(pattern, urlPath string, onMatch func(segment, value string))
}

func ValidateCallBody(body []byte, verb *schema.Verb, sch *schema.Schema) error {
var requestMap map[string]any
err := json.Unmarshal(body, &requestMap)
var root any
err := json.Unmarshal(body, &root)
if err != nil {
return fmt.Errorf("HTTP request body is not valid JSON: %w", err)
return fmt.Errorf("request body is not valid JSON: %w", err)
}

err = schema.ValidateJSONValue(verb.Request, []string{verb.Request.String()}, requestMap, sch)
err = schema.ValidateJSONValue(verb.Request, []string{verb.Request.String()}, root, sch)
if err != nil {
return fmt.Errorf("could not validate HTTP request body: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion backend/controller/sql/database_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func TestDatabase(t *testing.T) {
in.CopyModule("database"),
in.CreateDBAction("database", "testdb", false),
in.Deploy("database"),
in.Call("database", "insert", in.Obj{"data": "hello"}, nil),
in.Call[in.Obj, in.Obj]("database", "insert", in.Obj{"data": "hello"}, nil),
in.QueryRow("testdb", "SELECT data FROM requests", "hello"),

// run tests which should only affect "testdb_test"
Expand Down
2 changes: 1 addition & 1 deletion cmd/ftl/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestBox(t *testing.T) {
CopyModule("echo"),
Exec("ftl", "box", "echo", "--compose=echo-compose.yml"),
Exec("docker", "compose", "-f", "echo-compose.yml", "up", "--wait"),
Call("echo", "echo", Obj{"name": "Alice"}, nil),
Call[Obj, Obj]("echo", "echo", Obj{"name": "Alice"}, nil),
Exec("docker", "compose", "-f", "echo-compose.yml", "down", "--rmi", "local"),
)
}
Expand Down
13 changes: 13 additions & 0 deletions go-runtime/compile/compile_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package compile_test

import (
"testing"
"time"

"github.com/alecthomas/assert/v2"

Expand Down Expand Up @@ -47,3 +48,15 @@ func TestNonFTLTypes(t *testing.T) {
}),
)
}

func TestNonStructRequestResponse(t *testing.T) {
in.Run(t, "",
in.CopyModule("two"),
in.Deploy("two"),
in.CopyModule("one"),
in.Deploy("one"),
in.Call("one", "stringToTime", "1985-04-12T23:20:50.52Z", func(t testing.TB, response time.Time) {
assert.Equal(t, time.Date(1985, 04, 12, 23, 20, 50, 520_000_000, time.UTC), response)
}),
)
}
14 changes: 8 additions & 6 deletions go-runtime/compile/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ func TestExtractModuleSchema(t *testing.T) {
if testing.Short() {
t.SkipNow()
}
assert.NoError(t, prebuildTestModule(t, "testdata/one", "testdata/two"))
assert.NoError(t, prebuildTestModule(t, "testdata/go/one", "testdata/go/two"))

r, err := ExtractModuleSchema("testdata/one", &schema.Schema{})
r, err := ExtractModuleSchema("testdata/go/one", &schema.Schema{})
assert.NoError(t, err)
actual := schema.Normalise(r.Module)
expected := `module one {
Expand Down Expand Up @@ -159,6 +159,8 @@ func TestExtractModuleSchema(t *testing.T) {
data WithoutDirectiveStruct {
}
verb batchStringToTime([String]) [Time]
export verb http(builtin.HttpRequest<one.Req>) builtin.HttpResponse<one.Resp, Unit>
+ingress http GET /get
Expand All @@ -168,6 +170,8 @@ func TestExtractModuleSchema(t *testing.T) {
verb source(Unit) one.SourceResp
verb stringToTime(String) Time
verb verb(one.Req) one.Resp
}
`
Expand All @@ -179,9 +183,9 @@ func TestExtractModuleSchemaTwo(t *testing.T) {
t.SkipNow()
}

assert.NoError(t, prebuildTestModule(t, "testdata/two"))
assert.NoError(t, prebuildTestModule(t, "testdata/go/two"))

r, err := ExtractModuleSchema("testdata/two", &schema.Schema{})
r, err := ExtractModuleSchema("testdata/go/two", &schema.Schema{})
assert.NoError(t, err)
for _, e := range r.Errors {
// only warns
Expand Down Expand Up @@ -541,7 +545,6 @@ func TestErrorReporting(t *testing.T) {
`45:1-2: must have at most two parameters (context.Context, struct)`,
`45:69-69: unsupported response type "ftl/failing.Response"`,
`50:22-27: first parameter must be of type context.Context but is ftl/failing.Request`,
`50:37-43: second parameter must be a struct but is string`,
`50:53-53: unsupported response type "ftl/failing.Response"`,
`55:43-47: second parameter must not be ftl.Unit`,
`55:59-59: unsupported response type "ftl/failing.Response"`,
Expand All @@ -554,7 +557,6 @@ func TestErrorReporting(t *testing.T) {
`74:35-35: unsupported request type "ftl/failing.Request"`,
`74:48-48: must return an error but is ftl/failing.Response`,
`79:41-41: unsupported request type "ftl/failing.Request"`,
`79:55-55: first result must be a struct but is string`,
`79:63-63: must return an error but is string`,
`79:63-63: second result must not be ftl.Unit`,
// `86:1-2: duplicate declaration of "WrongResponse" at 79:6`, TODO: fix this
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module ftl/one

go 1.22.2

replace github.com/TBD54566975/ftl => ../../../..
replace github.com/TBD54566975/ftl => ../../../../..

require github.com/TBD54566975/ftl v0.150.3

Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,21 @@ type NonFTLStruct struct {
}

func (NonFTLStruct) NonFTLInterface() {}

//ftl:verb
func StringToTime(ctx context.Context, input string) (time.Time, error) {
return time.Parse(time.RFC3339, input)
}

//ftl:verb
func BatchStringToTime(ctx context.Context, input []string) ([]time.Time, error) {
var output = []time.Time{}
for _, s := range input {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
return nil, err
}
output = append(output, t)
}
return output, nil
}
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ module ftl/two

go 1.22.2

replace github.com/TBD54566975/ftl => ../../../..
replace github.com/TBD54566975/ftl => ../../../../..

require github.com/TBD54566975/ftl v0.150.3

Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion go-runtime/internal/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func TestRealMap(t *testing.T) {
Call("mapper", "get", Obj{}, func(t testing.TB, response Obj) {
assert.Equal(t, Obj{"underlyingCounter": 2.0, "mapCounter": 1.0, "mapped": "0"}, response)
}),
Call("mapper", "inc", Obj{}, nil),
Call[Obj, Obj]("mapper", "inc", Obj{}, nil),
Call("mapper", "get", Obj{}, func(t testing.TB, response Obj) {
assert.Equal(t, Obj{"underlyingCounter": 3.0, "mapCounter": 2.0, "mapped": "1"}, response)
}),
Expand Down
6 changes: 0 additions & 6 deletions go-runtime/schema/verb/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,6 @@ func checkSignature(pass *analysis.Pass, node *ast.FuncDecl, sig *types.Signatur
}

if params.Len() == 2 {
if !common.IsType[*types.Struct](params.At(1).Type()) {
common.TokenErrorf(pass, params.At(1).Pos(), params.At(1).Name(), "second parameter must be a struct but is %s", params.At(1).Type())
}
if params.At(1).Type().String() == common.FtlUnitTypePath {
common.TokenErrorf(pass, params.At(1).Pos(), params.At(1).Name(), "second parameter must not be ftl.Unit")
}
Expand All @@ -106,9 +103,6 @@ func checkSignature(pass *analysis.Pass, node *ast.FuncDecl, sig *types.Signatur
common.TokenErrorf(pass, results.At(results.Len()-1).Pos(), results.At(results.Len()-1).Name(), "must return an error but is %s", results.At(0).Type())
}
if results.Len() == 2 {
if !common.IsType[*types.Struct](results.At(0).Type()) {
common.TokenErrorf(pass, results.At(0).Pos(), results.At(0).Name(), "first result must be a struct but is %s", results.At(0).Type())
}
if results.At(1).Type().String() == common.FtlUnitTypePath {
common.TokenErrorf(pass, results.At(1).Pos(), results.At(1).Name(), "second result must not be ftl.Unit")
}
Expand Down
6 changes: 4 additions & 2 deletions integration/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"strings"
"testing"
"time"
"unicode"

"connectrpc.com/connect"
"github.com/alecthomas/assert/v2"
Expand Down Expand Up @@ -284,17 +285,18 @@ type Obj map[string]any
// Call a verb.
//
// "check" may be nil
func Call(module, verb string, request Obj, check func(t testing.TB, response Obj)) Action {
func Call[Req any, Resp any](module, verb string, request Req, check func(t testing.TB, response Resp)) Action {
return func(t testing.TB, ic TestContext) {
Infof("Calling %s.%s", module, verb)
assert.False(t, unicode.IsUpper([]rune(verb)[0]), "verb %q must start with an lowercase letter", verb)
data, err := json.Marshal(request)
assert.NoError(t, err)
resp, err := ic.Verbs.Call(ic, connect.NewRequest(&ftlv1.CallRequest{
Verb: &schemapb.Ref{Module: module, Name: verb},
Body: data,
}))
assert.NoError(t, err)
var response Obj
var response Resp
assert.Zero(t, resp.Msg.GetError(), "verb failed: %s", resp.Msg.GetError().GetMessage())
err = json.Unmarshal(resp.Msg.GetBody(), &response)
assert.NoError(t, err)
Expand Down

0 comments on commit 1f073ab

Please sign in to comment.