Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support source and sink func declarations #1070

Merged
merged 7 commits into from
Mar 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions buildengine/build_go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,23 @@ func TestGenerateGoModule(t *testing.T) {
Request: &schema.Ref{Name: "EchoRequest"},
Response: &schema.Ref{Name: "EchoResponse"},
},
&schema.Data{Name: "SinkReq"},
&schema.Verb{
Name: "sink",
Request: &schema.Ref{Name: "SinkReq"},
Response: &schema.Unit{},
},
&schema.Data{Name: "SourceResp"},
&schema.Verb{
Name: "source",
Request: &schema.Unit{},
Response: &schema.Ref{Name: "SourceResp"},
},
&schema.Verb{
Name: "nothing",
Request: &schema.Unit{},
Response: &schema.Unit{},
},
}},
{Name: "test"},
},
Expand Down Expand Up @@ -77,6 +94,27 @@ type EchoResponse struct {
func Echo(context.Context, EchoRequest) (EchoResponse, error) {
panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/ftl.Call()")
}

type SinkReq struct {
}

//ftl:verb
func Sink(context.Context, SinkReq) error {
panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/ftl.CallSink()")
}

type SourceResp struct {
}

//ftl:verb
func Source(context.Context) (SourceResp, error) {
panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/ftl.CallSource()")
}

//ftl:verb
func Nothing(context.Context) error {
panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/ftl.CallEmpty()")
}
`
bctx := buildContext{
moduleDir: "testdata/modules/another",
Expand Down
78 changes: 78 additions & 0 deletions buildengine/build_kotlin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,3 +305,81 @@ fun emptyVerb(context: Context, req: ftl.builtin.Empty): ftl.builtin.Empty = thr
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.Ignore
import xyz.block.ftl.Verb

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 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/modules/echokotlin",
buildDir: "target",
sch: sch,
}
testBuild(t, bctx, []assertion{
assertGeneratedModule("generated-sources/ftl/test/Test.kt", expected),
})
}
2 changes: 1 addition & 1 deletion examples/go/echo/echo.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ type EchoResponse struct {
//
//ftl:verb
func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) {
fmt.Println("Echo received a request!")
tresp, err := ftl.Call(ctx, time.Time, time.TimeRequest{})
if err != nil {
return EchoResponse{}, err
}

return EchoResponse{Message: fmt.Sprintf("Hello, %s!!! It is %s!", req.Name.Default(defaultName.Get(ctx)), tresp.Time)}, nil
}
18 changes: 13 additions & 5 deletions go-runtime/compile/build-template/_ftl.tmpl/go/main/main.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 13 additions & 3 deletions go-runtime/compile/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ type externalModuleContext struct {
}

type goVerb struct {
Name string
Name string
HasRequest bool
HasResponse bool
}

type mainModuleContext struct {
Expand Down Expand Up @@ -122,7 +124,15 @@ func Build(ctx context.Context, moduleDir string, sch *schema.Schema) error {
if !ok {
return fmt.Errorf("missing native name for verb %s", verb.Name)
}
goVerbs = append(goVerbs, goVerb{Name: nativeName})

goverb := goVerb{Name: nativeName}
if _, ok := verb.Request.(*schema.Unit); !ok {
goverb.HasRequest = true
}
if _, ok := verb.Response.(*schema.Unit); !ok {
goverb.HasResponse = true
}
goVerbs = append(goVerbs, goverb)
}
}
if err := internal.ScaffoldZip(buildTemplateFiles(), moduleDir, mainModuleContext{
Expand Down Expand Up @@ -183,7 +193,7 @@ var scaffoldFuncs = scaffolder.FuncMap{
case *schema.Time:
imports["time"] = "stdtime"

case *schema.Optional, *schema.Unit:
case *schema.Optional:
imports["github.com/TBD54566975/ftl/go-runtime/ftl"] = ""

default:
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions go-runtime/compile/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,12 @@ var (
errorIFaceType = once(func() *types.Interface {
return mustLoadRef("builtin", "error").Type().Underlying().(*types.Interface) //nolint:forcetypeassert
})

ftlCallFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Call"
ftlConfigFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Config"
ftlSecretFuncPath = "github.com/TBD54566975/ftl/go-runtime/ftl.Secret" //nolint:gosec

aliasFieldTag = "json"
ftlUnitTypePath = "github.com/TBD54566975/ftl/go-runtime/ftl.Unit"
aliasFieldTag = "json"
)

// NativeNames is a map of top-level declarations to their native Go names.
Expand Down Expand Up @@ -269,6 +270,10 @@ func checkSignature(sig *types.Signature) (req, resp *types.Var, err error) {
if !isType[*types.Struct](params.At(1).Type()) {
return nil, nil, fmt.Errorf("second parameter must be a struct but is %s", params.At(1).Type())
}
if params.At(1).Type().String() == ftlUnitTypePath {
return nil, nil, fmt.Errorf("second parameter must not be ftl.Unit")
}

req = params.At(1)
}

Expand All @@ -285,11 +290,11 @@ func checkSignature(sig *types.Signature) (req, resp *types.Var, err error) {
if !isType[*types.Struct](results.At(0).Type()) {
return nil, nil, fmt.Errorf("first result must be a struct but is %s", results.At(0).Type())
}
if results.At(1).Type().String() == ftlUnitTypePath {
return nil, nil, fmt.Errorf("first result must not be ftl.Unit")
}
resp = results.At(0)
}
if params.Len() == 1 && results.Len() == 1 {
return nil, nil, fmt.Errorf("must either accept an input or return a result, but does neither")
}
return req, resp, nil
}

Expand Down
12 changes: 12 additions & 0 deletions go-runtime/compile/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ func TestExtractModuleSchema(t *testing.T) {
data Resp {
}

data SinkReq {
}

data SourceResp {
}

enum Color(String) {
Red("Red")
Blue("Blue")
Expand Down Expand Up @@ -75,6 +81,12 @@ func TestExtractModuleSchema(t *testing.T) {

secret secretValue String

verb nothing(Unit) Unit

verb sink(one.SinkReq) Unit

verb source(Unit) one.SourceResp

verb verb(one.Req) one.Resp
}
`
Expand Down
19 changes: 19 additions & 0 deletions go-runtime/compile/testdata/one/one.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,22 @@ func Verb(ctx context.Context, req Req) (Resp, error) {
const Yellow Color = "Yellow"

const YellowInt ColorInt = 3

type SinkReq struct{}

//ftl:verb
func Sink(ctx context.Context, req SinkReq) error {
return nil
}

type SourceResp struct{}

//ftl:verb
func Source(ctx context.Context) (SourceResp, error) {
return SourceResp{}, nil
}

//ftl:verb
func Nothing(ctx context.Context) error {
return nil
}
44 changes: 31 additions & 13 deletions go-runtime/ftl/call.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,19 @@ import (

ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1"
"github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/ftlv1connect"
schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema"
"github.com/TBD54566975/ftl/backend/schema/strcase"
"github.com/TBD54566975/ftl/go-runtime/encoding"
"github.com/TBD54566975/ftl/internal/rpc"
)

// Call a Verb through the FTL Controller.
func Call[Req, Resp any](ctx context.Context, verb Verb[Req, Resp], req Req) (resp Resp, err error) {
callee := VerbToRef(verb)
func call[Req, Resp any](ctx context.Context, callee *schemapb.Ref, req Req) (resp Resp, err error) {
client := rpc.ClientFromContext[ftlv1connect.VerbServiceClient](ctx)
reqData, err := encoding.Marshal(req)
if err != nil {
return resp, fmt.Errorf("%s: failed to marshal request: %w", callee, err)
}
cresp, err := client.Call(ctx, connect.NewRequest(&ftlv1.CallRequest{Verb: callee.ToProto(), Body: reqData}))
cresp, err := client.Call(ctx, connect.NewRequest(&ftlv1.CallRequest{Verb: callee, Body: reqData}))
if err != nil {
return resp, fmt.Errorf("%s: failed to call Verb: %w", callee, err)
}
Expand All @@ -44,21 +43,40 @@ func Call[Req, Resp any](ctx context.Context, verb Verb[Req, Resp], req Req) (re
}
}

// Call a Sink through the FTL controller.
// func CallSink[Req any](ctx context.Context, sink Sink[Req]) error {
// }
// Call a Verb through the FTL Controller.
func Call[Req, Resp any](ctx context.Context, verb Verb[Req, Resp], req Req) (Resp, error) {
return call[Req, Resp](ctx, CallToSchemaRef(verb), req)
}

// VerbToRef returns the FTL reference for a Verb.
func VerbToRef[Req, Resp any](verb Verb[Req, Resp]) Ref {
ref := runtime.FuncForPC(reflect.ValueOf(verb).Pointer()).Name()
return goRefToFTLRef(ref)
// CallSink calls a Sink through the FTL controller.
func CallSink[Req any](ctx context.Context, sink Sink[Req], req Req) error {
_, err := call[Req, Unit](ctx, CallToSchemaRef(sink), req)
return err
}

// CallSource calls a Source through the FTL controller.
func CallSource[Resp any](ctx context.Context, source Source[Resp]) (Resp, error) {
return call[Unit, Resp](ctx, CallToSchemaRef(source), Unit{})
}

// CallEmpty calls a Verb with no request or response through the FTL controller.
func CallEmpty(ctx context.Context, empty Empty) error {
_, err := call[Unit, Unit](ctx, CallToSchemaRef(empty), Unit{})
return err
}

func SinkToRef[Req any](sink Sink[Req]) Ref {
ref := runtime.FuncForPC(reflect.ValueOf(sink).Pointer()).Name()
// CallToRef returns the Ref for a Verb, Sink, Source, or Empty.
func CallToRef(call any) Ref {
ref := runtime.FuncForPC(reflect.ValueOf(call).Pointer()).Name()
return goRefToFTLRef(ref)
}

// CallToSchemaRef returns the Ref for a Verb, Sink, Source, or Empty as a Schema Ref.
func CallToSchemaRef(call any) *schemapb.Ref {
ref := CallToRef(call)
return ref.ToProto()
}

func goRefToFTLRef(ref string) Ref {
parts := strings.Split(ref[strings.LastIndex(ref, "/")+1:], ".")
return Ref{parts[len(parts)-2], strcase.ToLowerCamel(parts[len(parts)-1])}
Expand Down
Loading
Loading