diff --git a/backend/protos/xyz/block/ftl/v1/schema/schema.pb.go b/backend/protos/xyz/block/ftl/v1/schema/schema.pb.go index 7aec0956f0..dae73af128 100644 --- a/backend/protos/xyz/block/ftl/v1/schema/schema.pb.go +++ b/backend/protos/xyz/block/ftl/v1/schema/schema.pb.go @@ -2769,13 +2769,14 @@ type Verb struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Runtime *VerbRuntime `protobuf:"bytes,31634,opt,name=runtime,proto3,oneof" json:"runtime,omitempty"` - Pos *Position `protobuf:"bytes,1,opt,name=pos,proto3,oneof" json:"pos,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - Comments []string `protobuf:"bytes,3,rep,name=comments,proto3" json:"comments,omitempty"` - Request *Type `protobuf:"bytes,4,opt,name=request,proto3" json:"request,omitempty"` - Response *Type `protobuf:"bytes,5,opt,name=response,proto3" json:"response,omitempty"` - Metadata []*Metadata `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty"` + Runtime *VerbRuntime `protobuf:"bytes,31634,opt,name=runtime,proto3,oneof" json:"runtime,omitempty"` + Pos *Position `protobuf:"bytes,1,opt,name=pos,proto3,oneof" json:"pos,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Visibility string `protobuf:"bytes,3,opt,name=visibility,proto3" json:"visibility,omitempty"` + Comments []string `protobuf:"bytes,4,rep,name=comments,proto3" json:"comments,omitempty"` + Request *Type `protobuf:"bytes,5,opt,name=request,proto3" json:"request,omitempty"` + Response *Type `protobuf:"bytes,6,opt,name=response,proto3" json:"response,omitempty"` + Metadata []*Metadata `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty"` } func (x *Verb) Reset() { @@ -2831,6 +2832,13 @@ func (x *Verb) GetName() string { return "" } +func (x *Verb) GetVisibility() string { + if x != nil { + return x.Visibility + } + return "" +} + func (x *Verb) GetComments() []string { if x != nil { return x.Comments @@ -3280,7 +3288,7 @@ var file_xyz_block_ftl_v1_schema_schema_proto_rawDesc = []byte{ 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x49, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x48, 0x00, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x22, 0xfe, 0x02, 0x0a, 0x04, 0x56, 0x65, 0x72, 0x62, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x75, + 0x65, 0x22, 0x9e, 0x03, 0x0a, 0x04, 0x56, 0x65, 0x72, 0x62, 0x12, 0x45, 0x0a, 0x07, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x92, 0xf7, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x56, 0x65, 0x72, 0x62, 0x52, 0x75, 0x6e, 0x74, @@ -3290,16 +3298,18 @@ var file_xyz_block_ftl_v1_schema_schema_proto_rawDesc = []byte{ 0x31, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x01, 0x52, 0x03, 0x70, 0x6f, 0x73, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x1e, 0x0a, 0x0a, 0x76, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x76, 0x69, 0x73, 0x69, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, + 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x37, 0x0a, 0x07, 0x72, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x78, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x06, 0x20, 0x03, 0x28, + 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x78, 0x79, 0x7a, 0x2e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x2e, 0x66, 0x74, 0x6c, 0x2e, 0x76, 0x31, 0x2e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0a, diff --git a/backend/protos/xyz/block/ftl/v1/schema/schema.proto b/backend/protos/xyz/block/ftl/v1/schema/schema.proto index 4a61a58672..a2c596d052 100644 --- a/backend/protos/xyz/block/ftl/v1/schema/schema.proto +++ b/backend/protos/xyz/block/ftl/v1/schema/schema.proto @@ -262,8 +262,9 @@ message Verb { optional Position pos = 1; string name = 2; - repeated string comments = 3; - Type request = 4; - Type response = 5; - repeated Metadata metadata = 6; + string visibility = 3; + repeated string comments = 4; + Type request = 5; + Type response = 6; + repeated Metadata metadata = 7; } diff --git a/backend/schema/schema_test.go b/backend/schema/schema_test.go index 65feb5fd61..a8f74a86a0 100644 --- a/backend/schema/schema_test.go +++ b/backend/schema/schema_test.go @@ -45,13 +45,13 @@ module todo { when Time } - verb create(todo.CreateRequest) todo.CreateResponse + internal verb create(todo.CreateRequest) todo.CreateResponse +calls todo.destroy +database calls todo.testdb - verb destroy(builtin.HttpRequest) builtin.HttpResponse + public verb destroy(builtin.HttpRequest) builtin.HttpResponse +ingress http GET /todo/destroy/{name} - verb scheduled(Unit) Unit + internal verb scheduled(Unit) Unit +cron */10 * * 1-10,11-31 * * * } @@ -186,7 +186,7 @@ func TestParsing(t *testing.T) { data CreateListResponse {} // Create a new list - verb createList(todo.CreateListRequest) todo.CreateListResponse + internal verb createList(todo.CreateListRequest) todo.CreateListResponse +calls todo.createList } `, @@ -198,9 +198,10 @@ func TestParsing(t *testing.T) { &Data{Name: "CreateListRequest"}, &Data{Name: "CreateListResponse"}, &Verb{Name: "createList", - Comments: []string{"Create a new list"}, - Request: &Ref{Module: "todo", Name: "CreateListRequest"}, - Response: &Ref{Module: "todo", Name: "CreateListResponse"}, + Visibility: "internal", + Comments: []string{"Create a new list"}, + Request: &Ref{Module: "todo", Name: "CreateListRequest"}, + Response: &Ref{Module: "todo", Name: "CreateListResponse"}, Metadata: []Metadata{ &MetadataCalls{Calls: []*Ref{{Module: "todo", Name: "createList"}}}, }, @@ -210,10 +211,10 @@ func TestParsing(t *testing.T) { }, }}, {name: "InvalidRequestRef", - input: `module test { verb test(InvalidRequest) InvalidResponse}`, + input: `module test { internal verb test(InvalidRequest) InvalidResponse}`, errors: []string{ - "1:25-25: unknown reference \"InvalidRequest\"", - "1:41-41: unknown reference \"InvalidResponse\""}}, + "1:34-34: unknown reference \"InvalidRequest\"", + "1:50-50: unknown reference \"InvalidResponse\""}}, {name: "InvalidRef", input: `module test { data Data { user user.User }}`, errors: []string{ @@ -231,18 +232,19 @@ func TestParsing(t *testing.T) { "1:35-35: unknown reference \"verb\"", }}, {name: "KeywordAsName", - input: `module int { data String { name String } verb verb(String) String }`, + input: `module int { data String { name String } internal verb verb(String) String }`, errors: []string{"1:14-14: data structure name \"String\" is a reserved word"}}, {name: "BuiltinRef", - input: `module test { verb myIngress(HttpRequest) HttpResponse }`, + input: `module test { public verb myIngress(HttpRequest) HttpResponse }`, expected: &Schema{ Modules: []*Module{{ Name: "test", Decls: []Decl{ &Verb{ - Name: "myIngress", - Request: &Ref{Module: "builtin", Name: "HttpRequest", TypeParameters: []Type{&String{}}}, - Response: &Ref{Module: "builtin", Name: "HttpResponse", TypeParameters: []Type{&String{}, &String{}}}, + Name: "myIngress", + Visibility: "public", + Request: &Ref{Module: "builtin", Name: "HttpRequest", TypeParameters: []Type{&String{}}}, + Response: &Ref{Module: "builtin", Name: "HttpResponse", TypeParameters: []Type{&String{}, &String{}}}, }, }, }}, @@ -401,11 +403,11 @@ module todo { name String when Time } - verb create(todo.CreateRequest) todo.CreateResponse + internal verb create(todo.CreateRequest) todo.CreateResponse +calls todo.destroy +database calls todo.testdb - verb destroy(builtin.HttpRequest) builtin.HttpResponse + public verb destroy(builtin.HttpRequest) builtin.HttpResponse +ingress http GET /todo/destroy/{name} - verb scheduled(Unit) Unit + internal verb scheduled(Unit) Unit +cron */10 * * 1-10,11-31 * * * } ` @@ -486,15 +488,17 @@ var testSchema = MustValidate(&Schema{ }, }, &Verb{Name: "create", - Request: &Ref{Module: "todo", Name: "CreateRequest"}, - Response: &Ref{Module: "todo", Name: "CreateResponse"}, + Visibility: "internal", + Request: &Ref{Module: "todo", Name: "CreateRequest"}, + Response: &Ref{Module: "todo", Name: "CreateResponse"}, Metadata: []Metadata{ &MetadataCalls{Calls: []*Ref{{Module: "todo", Name: "destroy"}}}, &MetadataDatabases{Calls: []*Ref{{Module: "todo", Name: "testdb"}}}, }}, &Verb{Name: "destroy", - Request: &Ref{Module: "builtin", Name: "HttpRequest", TypeParameters: []Type{&Ref{Module: "todo", Name: "DestroyRequest"}}}, - Response: &Ref{Module: "builtin", Name: "HttpResponse", TypeParameters: []Type{&Ref{Module: "todo", Name: "DestroyResponse"}, &String{}}}, + Visibility: "public", + Request: &Ref{Module: "builtin", Name: "HttpRequest", TypeParameters: []Type{&Ref{Module: "todo", Name: "DestroyRequest"}}}, + Response: &Ref{Module: "builtin", Name: "HttpResponse", TypeParameters: []Type{&Ref{Module: "todo", Name: "DestroyResponse"}, &String{}}}, Metadata: []Metadata{ &MetadataIngress{ Type: "http", @@ -508,8 +512,9 @@ var testSchema = MustValidate(&Schema{ }, }, &Verb{Name: "scheduled", - Request: &Unit{Unit: true}, - Response: &Unit{Unit: true}, + Visibility: "internal", + Request: &Unit{Unit: true}, + Response: &Unit{Unit: true}, Metadata: []Metadata{ &MetadataCronJob{ Cron: "*/10 * * 1-10,11-31 * * *", diff --git a/backend/schema/validate_test.go b/backend/schema/validate_test.go index a1c8bc27c8..82f0f9145c 100644 --- a/backend/schema/validate_test.go +++ b/backend/schema/validate_test.go @@ -18,12 +18,13 @@ func TestValidate(t *testing.T) { {name: "TwoModuleCycle", schema: ` module one { - verb one(Empty) Empty + internal verb one(Empty) Empty +calls two.two } module two { - verb two(Empty) Empty + // some comments + internal verb two(Empty) Empty +calls one.one } `, @@ -31,33 +32,33 @@ func TestValidate(t *testing.T) { {name: "ThreeModulesNoCycle", schema: ` module one { - verb one(Empty) Empty + internal verb one(Empty) Empty +calls two.two } module two { - verb two(Empty) Empty + internal verb two(Empty) Empty +calls three.three } module three { - verb three(Empty) Empty + internal verb three(Empty) Empty } `}, {name: "ThreeModulesCycle", schema: ` module one { - verb one(Empty) Empty + internal verb one(Empty) Empty +calls two.two } module two { - verb two(Empty) Empty + internal verb two(Empty) Empty +calls three.three } module three { - verb three(Empty) Empty + internal verb three(Empty) Empty +calls one.one } `, @@ -65,13 +66,13 @@ func TestValidate(t *testing.T) { {name: "TwoModuleCycleDiffVerbs", schema: ` module one { - verb a(Empty) Empty + internal verb a(Empty) Empty +calls two.a - verb b(Empty) Empty + internal verb b(Empty) Empty } module two { - verb a(Empty) Empty + internal verb a(Empty) Empty +calls one.b } `, @@ -79,49 +80,49 @@ func TestValidate(t *testing.T) { {name: "SelfReference", schema: ` module one { - verb a(Empty) Empty + internal verb a(Empty) Empty +calls one.b - verb b(Empty) Empty + internal verb b(Empty) Empty +calls one.a } `}, {name: "ValidIngressRequestType", schema: ` module one { - verb a(HttpRequest) HttpResponse + internal verb a(HttpRequest) HttpResponse +ingress http GET /a } `}, {name: "InvalidIngressRequestType", schema: ` module one { - verb a(Empty) Empty + internal verb a(Empty) Empty +ingress http GET /a } `, errs: []string{ - "3:13-13: ingress verb a: request type Empty must be builtin.HttpRequest", - "3:20-20: ingress verb a: response type Empty must be builtin.HttpRequest", + "3:22-22: ingress verb a: request type Empty must be builtin.HttpRequest", + "3:29-29: ingress verb a: response type Empty must be builtin.HttpRequest", }}, {name: "IngressBodyTypes", schema: ` module one { - verb bytes(HttpRequest) HttpResponse + internal verb bytes(HttpRequest) HttpResponse +ingress http GET /bytes - verb string(HttpRequest) HttpResponse + internal verb string(HttpRequest) HttpResponse +ingress http GET /string - verb data(HttpRequest) HttpResponse + internal verb data(HttpRequest) HttpResponse +ingress http GET /data // Invalid types. - verb any(HttpRequest) HttpResponse + internal verb any(HttpRequest) HttpResponse +ingress http GET /any - verb path(HttpRequest) HttpResponse + internal verb path(HttpRequest) HttpResponse +ingress http GET /path/{invalid} - verb pathMissing(HttpRequest) HttpResponse + internal verb pathMissing(HttpRequest) HttpResponse +ingress http GET /path/{missing} - verb pathFound(HttpRequest) HttpResponse + internal verb pathFound(HttpRequest) HttpResponse +ingress http GET /path/{parameter} data Path { @@ -130,8 +131,8 @@ func TestValidate(t *testing.T) { } `, errs: []string{ - "11:15-15: ingress verb any: request type HttpRequest must have a body of bytes, string, data structure, unit, float, int, bool, map, or array not Any", - "11:33-33: ingress verb any: response type HttpResponse must have a body of bytes, string, data structure, unit, float, int, bool, map, or array not Any", + "11:24-24: ingress verb any: request type HttpRequest must have a body of bytes, string, data structure, unit, float, int, bool, map, or array not Any", + "11:42-42: ingress verb any: response type HttpResponse must have a body of bytes, string, data structure, unit, float, int, bool, map, or array not Any", "14:31-31: ingress verb path: cannot use path parameter \"invalid\" with request type String, expected Data type", "16:31-31: ingress verb pathMissing: request type one.Path does not contain a field corresponding to the parameter \"missing\"", "16:7-7: duplicate http ingress GET /path/{} for 17:6:\"pathFound\" and 15:6:\"pathMissing\"", @@ -141,7 +142,7 @@ func TestValidate(t *testing.T) { schema: ` module one { data Data {} - verb one(HttpRequest<[one.Data]>) HttpResponse<[one.Data], Empty> + internal verb one(HttpRequest<[one.Data]>) HttpResponse<[one.Data], Empty> +ingress http GET /one } `, @@ -149,7 +150,7 @@ func TestValidate(t *testing.T) { {name: "DoubleCron", schema: ` module one { - verb cronjob(Unit) Unit + internal verb cronjob(Unit) Unit +cron * */2 0-23/2,4-5 * * * * +cron * * * * * * * } @@ -162,7 +163,7 @@ func TestValidate(t *testing.T) { schema: ` module one { data Data {} - verb one(HttpRequest<[one.Data]>) HttpResponse<[one.Data], Empty> + internal verb one(HttpRequest<[one.Data]>) HttpResponse<[one.Data], Empty> +ingress http GET /one +ingress http GET /two } @@ -174,9 +175,9 @@ func TestValidate(t *testing.T) { {name: "CronOnNonEmptyVerb", schema: ` module one { - verb verbWithWrongInput(Empty) Unit + internal verb verbWithWrongInput(Empty) Unit +cron * * * * * * * - verb verbWithWrongOutput(Unit) Empty + internal verb verbWithWrongOutput(Unit) Empty +cron * * * * * * * } `, @@ -191,7 +192,7 @@ func TestValidate(t *testing.T) { data Data {} } module one { - verb a(HttpRequest) HttpResponse + internal verb a(HttpRequest) HttpResponse +ingress http GET /a } `}, diff --git a/backend/schema/verb.go b/backend/schema/verb.go index b3db1ad0dc..6ab461e46b 100644 --- a/backend/schema/verb.go +++ b/backend/schema/verb.go @@ -10,14 +10,23 @@ import ( schemapb "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1/schema" ) +type Visibility string + +const ( + Public Visibility = "public" + Internal Visibility = "internal" + Private Visibility = "private" +) + type Verb struct { Pos Position `parser:"" protobuf:"1,optional"` - Comments []string `parser:"@Comment*" protobuf:"3"` - Name string `parser:"'verb' @Ident" protobuf:"2"` - Request Type `parser:"'(' @@ ')'" protobuf:"4"` - Response Type `parser:"@@" protobuf:"5"` - Metadata []Metadata `parser:"@@*" protobuf:"6"` + Comments []string `parser:"@Comment*" protobuf:"4"` + Visibility string `parser:"@('public' | 'internal' | 'private')?" protobuf:"3"` + Name string `parser:"'verb' @Ident" protobuf:"2"` + Request Type `parser:"'(' @@ ')'" protobuf:"5"` + Response Type `parser:"@@" protobuf:"6"` + Metadata []Metadata `parser:"@@*" protobuf:"7"` } var _ Decl = (*Verb)(nil) @@ -41,7 +50,7 @@ func (v *Verb) GetName() string { return v.Name } func (v *Verb) String() string { w := &strings.Builder{} fmt.Fprint(w, encodeComments(v.Comments)) - fmt.Fprintf(w, "verb %s(%s) %s", v.Name, v.Request, v.Response) + fmt.Fprintf(w, "%s verb %s(%s) %s", v.Visibility, v.Name, v.Request, v.Response) fmt.Fprint(w, indent(encodeMetadata(v.Metadata))) return w.String() } @@ -77,22 +86,24 @@ func (v *Verb) GetMetadataCronJob() optional.Option[*MetadataCronJob] { func (v *Verb) ToProto() proto.Message { return &schemapb.Verb{ - Pos: posToProto(v.Pos), - Name: v.Name, - Comments: v.Comments, - Request: typeToProto(v.Request), - Response: typeToProto(v.Response), - Metadata: metadataListToProto(v.Metadata), + Pos: posToProto(v.Pos), + Name: v.Name, + Visibility: v.Visibility, + Comments: v.Comments, + Request: typeToProto(v.Request), + Response: typeToProto(v.Response), + Metadata: metadataListToProto(v.Metadata), } } func VerbFromProto(s *schemapb.Verb) *Verb { return &Verb{ - Pos: posFromProto(s.Pos), - Name: s.Name, - Comments: s.Comments, - Request: typeToSchema(s.Request), - Response: typeToSchema(s.Response), - Metadata: metadataListToSchema(s.Metadata), + Pos: posFromProto(s.Pos), + Name: s.Name, + Visibility: s.Visibility, + Comments: s.Comments, + Request: typeToSchema(s.Request), + Response: typeToSchema(s.Response), + Metadata: metadataListToSchema(s.Metadata), } } diff --git a/buildengine/build_go_test.go b/buildengine/build_go_test.go index 6750230e80..0be1e927a8 100644 --- a/buildengine/build_go_test.go +++ b/buildengine/build_go_test.go @@ -34,26 +34,30 @@ func TestGenerateGoModule(t *testing.T) { &schema.Data{Name: "EchoRequest"}, &schema.Data{Name: "EchoResponse"}, &schema.Verb{ - Name: "echo", - Request: &schema.Ref{Name: "EchoRequest"}, - Response: &schema.Ref{Name: "EchoResponse"}, + Name: "echo", + Visibility: "internal", + 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{}, + Name: "sink", + Visibility: "internal", + Request: &schema.Ref{Name: "SinkReq"}, + Response: &schema.Unit{}, }, &schema.Data{Name: "SourceResp"}, &schema.Verb{ - Name: "source", - Request: &schema.Unit{}, - Response: &schema.Ref{Name: "SourceResp"}, + Name: "source", + Visibility: "internal", + Request: &schema.Unit{}, + Response: &schema.Ref{Name: "SourceResp"}, }, &schema.Verb{ - Name: "nothing", - Request: &schema.Unit{}, - Response: &schema.Unit{}, + Name: "nothing", + Visibility: "internal", + Request: &schema.Unit{}, + Response: &schema.Unit{}, }, }}, {Name: "test"}, @@ -69,7 +73,7 @@ import ( var _ = context.Background -//ftl:export +//ftl:internal type Color string const ( Red Color = "Red" @@ -77,7 +81,7 @@ const ( Green Color = "Green" ) -//ftl:export +//ftl:internal type ColorInt int const ( RedInt ColorInt = 0 @@ -91,7 +95,7 @@ type EchoRequest struct { type EchoResponse struct { } -//ftl:export +//ftl:internal 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()") } @@ -99,7 +103,7 @@ func Echo(context.Context, EchoRequest) (EchoResponse, error) { type SinkReq struct { } -//ftl:export +//ftl:internal func Sink(context.Context, SinkReq) error { panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/ftl.CallSink()") } @@ -107,12 +111,12 @@ func Sink(context.Context, SinkReq) error { type SourceResp struct { } -//ftl:export +//ftl:internal 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:export +//ftl:internal func Nothing(context.Context) error { panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/ftl.CallEmpty()") } @@ -161,7 +165,7 @@ type Req struct { type Resp struct { } -//ftl:export +//ftl:internal func Call(context.Context, Req) (Resp, error) { panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/ftl.Call()") } diff --git a/buildengine/build_kotlin.go b/buildengine/build_kotlin.go index 2bdb685a64..6cc13bf333 100644 --- a/buildengine/build_kotlin.go +++ b/buildengine/build_kotlin.go @@ -171,13 +171,13 @@ var scaffoldFuncs = scaffolder.FuncMap{ _ = schema.VisitExcludingMetadataChildren(m, func(n schema.Node, next func() error) error { switch n.(type) { case *schema.Data: - imports.Add("xyz.block.ftl.Export") + imports.Append("xyz.block.ftl.Export", "xyz.block.ftl.Visibility") case *schema.Enum: imports.Add("xyz.block.ftl.Export") case *schema.Verb: - imports.Append("xyz.block.ftl.Context", "xyz.block.ftl.Ignore", "xyz.block.ftl.Export") + imports.Append("xyz.block.ftl.Context", "xyz.block.ftl.Ignore", "xyz.block.ftl.Export", "xyz.block.ftl.Visibility") case *schema.Time: imports.Add("java.time.OffsetDateTime") diff --git a/buildengine/build_kotlin_test.go b/buildengine/build_kotlin_test.go index 45b40e8561..1645b9ae03 100644 --- a/buildengine/build_kotlin_test.go +++ b/buildengine/build_kotlin_test.go @@ -110,13 +110,14 @@ package ftl.test import java.time.OffsetDateTime import xyz.block.ftl.Export +import xyz.block.ftl.Visibility -@Export +@Export(Visibility.INTERNAL) data class ParamTestData( val t: T, ) -@Export +@Export(Visibility.INTERNAL) data class TestRequest( val field: Long, ) @@ -124,7 +125,7 @@ data class TestRequest( /** * Response comments */ -@Export +@Export(Visibility.INTERNAL) data class TestResponse( val int: Long, val float: Float, @@ -194,8 +195,9 @@ package ftl.test import xyz.block.ftl.Context import xyz.block.ftl.Export import xyz.block.ftl.Ignore +import xyz.block.ftl.Visibility -@Export +@Export(Visibility.INTERNAL) data class Request( val data: Long, ) @@ -203,7 +205,7 @@ data class Request( /** * TestVerb comments */ -@Export +@Export(Visibility.INTERNAL) @Ignore fun testVerb(context: Context, req: Request): ftl.builtin.Empty = throw NotImplementedError("Verb stubs should not be called directly, instead use context.call(::testVerb, ...)") @@ -234,11 +236,12 @@ func TestGenerateBuiltins(t *testing.T) { package ftl.builtin import xyz.block.ftl.Export +import xyz.block.ftl.Visibility /** * HTTP request structure used for HTTP ingress verbs. */ -@Export +@Export(Visibility.INTERNAL) data class HttpRequest( val method: String, val path: String, @@ -251,7 +254,7 @@ data class HttpRequest( /** * HTTP response structure used for HTTP ingress verbs. */ -@Export +@Export(Visibility.INTERNAL) data class HttpResponse( val status: Long, val headers: Map>, @@ -259,7 +262,7 @@ data class HttpResponse( val error: Error? = null, ) -@Export +@Export(Visibility.INTERNAL) class Empty ` bctx := buildContext{ @@ -300,8 +303,9 @@ package ftl.test import xyz.block.ftl.Context import xyz.block.ftl.Export import xyz.block.ftl.Ignore +import xyz.block.ftl.Visibility -@Export +@Export(Visibility.INTERNAL) @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, ...)") @@ -362,26 +366,27 @@ package ftl.test import xyz.block.ftl.Context import xyz.block.ftl.Export import xyz.block.ftl.Ignore +import xyz.block.ftl.Visibility -@Export +@Export(Visibility.INTERNAL) data class SinkReq( val data: Long, ) -@Export +@Export(Visibility.INTERNAL) @Ignore fun sink(context: Context, req: SinkReq): Unit = throw NotImplementedError("Verb stubs should not be called directly, instead use context.callSink(::sink, ...)") -@Export +@Export(Visibility.INTERNAL) data class SourceResp( val data: Long, ) -@Export +@Export(Visibility.INTERNAL) @Ignore fun source(context: Context): SourceResp = throw NotImplementedError("Verb stubs should not be called directly, instead use context.callSource(::source, ...)") -@Export +@Export(Visibility.INTERNAL) @Ignore fun nothing(context: Context): Unit = throw NotImplementedError("Verb stubs should not be called directly, instead use context.callEmpty(::nothing, ...)") diff --git a/buildengine/testdata/projects/alpha/alpha.go b/buildengine/testdata/projects/alpha/alpha.go index b17ab8fe8c..b2bd45354f 100644 --- a/buildengine/testdata/projects/alpha/alpha.go +++ b/buildengine/testdata/projects/alpha/alpha.go @@ -17,7 +17,7 @@ type EchoResponse struct { Message string `json:"message"` } -//ftl:export +//ftl:internal func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { ftl.Call(ctx, other.Echo, other.EchoRequest{}) return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil diff --git a/buildengine/testdata/projects/alpha/go.sum b/buildengine/testdata/projects/alpha/go.sum index 7ac30c81bb..9bddd3462f 100644 --- a/buildengine/testdata/projects/alpha/go.sum +++ b/buildengine/testdata/projects/alpha/go.sum @@ -128,14 +128,14 @@ 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.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= -modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +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.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= -modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= -modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= -modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +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.8 h1:nGKglNx9K5v0As+zF0/Gcl1kMkmaU1XynYyq92PbsC8= +modernc.org/sqlite v1.29.8/go.mod h1:lQPm27iqa4UNZpmr4Aor0MH0HkCLbt1huYDfWylLZFk= 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= diff --git a/buildengine/testdata/projects/another/another.go b/buildengine/testdata/projects/another/another.go index 73c82e9d76..8ae04ea80a 100644 --- a/buildengine/testdata/projects/another/another.go +++ b/buildengine/testdata/projects/another/another.go @@ -15,7 +15,7 @@ type EchoResponse struct { Message string `json:"message"` } -//ftl:export +//ftl:internal func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil } diff --git a/buildengine/testdata/projects/echokotlin/src/main/kotlin/ftl/echo/Echo.kt b/buildengine/testdata/projects/echokotlin/src/main/kotlin/ftl/echo/Echo.kt index 3028a3451f..c9d7ee9d2c 100644 --- a/buildengine/testdata/projects/echokotlin/src/main/kotlin/ftl/echo/Echo.kt +++ b/buildengine/testdata/projects/echokotlin/src/main/kotlin/ftl/echo/Echo.kt @@ -3,11 +3,12 @@ package ftl.echo import xyz.block.ftl.Context import xyz.block.ftl.Method import xyz.block.ftl.Export +import xyz.block.ftl.Visibility data class EchoRequest(val name: String? = "anonymous") data class EchoResponse(val message: String) -@Export +@Export(Visibility.INTERNAL) fun echo(context: Context, req: EchoRequest): EchoResponse { return EchoResponse(message = "Hello, ${req.name}!") } diff --git a/buildengine/testdata/projects/external/external.go b/buildengine/testdata/projects/external/external.go index 92ae63c4fd..0107a82804 100644 --- a/buildengine/testdata/projects/external/external.go +++ b/buildengine/testdata/projects/external/external.go @@ -13,7 +13,7 @@ type ExternalResponse struct { // External returns the current month as an external type. // -//ftl:export +//ftl:internal func Time(ctx context.Context, req ExternalRequest) (ExternalResponse, error) { return ExternalResponse{Month: time.Now().Month()}, nil } diff --git a/buildengine/testdata/projects/externalkotlin/src/main/kotlin/ftl/externalkotlin/ExternalKotlin.kt b/buildengine/testdata/projects/externalkotlin/src/main/kotlin/ftl/externalkotlin/ExternalKotlin.kt index 277e37e232..cc2210ca80 100644 --- a/buildengine/testdata/projects/externalkotlin/src/main/kotlin/ftl/externalkotlin/ExternalKotlin.kt +++ b/buildengine/testdata/projects/externalkotlin/src/main/kotlin/ftl/externalkotlin/ExternalKotlin.kt @@ -3,6 +3,7 @@ package ftl.externalkotlin import com.google.type.DayOfWeek import xyz.block.ftl.Context import xyz.block.ftl.Export +import xyz.block.ftl.Visibility import xyz.block.ftl.v1.schema.Optional class InvalidInput(val field: String) : Exception() @@ -11,7 +12,7 @@ data class ExternalRequest(val name: String?, val dayOfWeek: DayOfWeek) data class ExternalResponse(val message: String) @Throws(InvalidInput::class) -@Export +@Export(Visibility.INTERNAL) fun external(context: Context, req: ExternalRequest): ExternalResponse { return ExternalResponse(message = "Hello, ${req.name ?: "anonymous"}!") } diff --git a/buildengine/testdata/projects/highgoversion/highgoversion.go b/buildengine/testdata/projects/highgoversion/highgoversion.go index 8e924ce826..8f49cfdb09 100644 --- a/buildengine/testdata/projects/highgoversion/highgoversion.go +++ b/buildengine/testdata/projects/highgoversion/highgoversion.go @@ -15,7 +15,7 @@ type EchoResponse struct { Message string `json:"message"` } -//ftl:export +//ftl:internal func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil } diff --git a/buildengine/testdata/projects/other/other.go b/buildengine/testdata/projects/other/other.go index 295d1a98c2..5b8666c3ee 100644 --- a/buildengine/testdata/projects/other/other.go +++ b/buildengine/testdata/projects/other/other.go @@ -15,7 +15,7 @@ type EchoResponse struct { Message string `json:"message"` } -//ftl:export +//ftl:internal func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil } diff --git a/buildengine/watch_test.go b/buildengine/watch_test.go index cf05602270..565f7f25b3 100644 --- a/buildengine/watch_test.go +++ b/buildengine/watch_test.go @@ -16,7 +16,6 @@ import ( ) const pollFrequency = time.Millisecond * 500 -const halfPollFrequency = time.Millisecond * 250 func TestWatch(t *testing.T) { if testing.Short() { @@ -29,14 +28,14 @@ func TestWatch(t *testing.T) { w := NewWatcher() events, topic := startWatching(ctx, t, w, dir) - time.Sleep(pollFrequency + halfPollFrequency) // midway between file scans + time.Sleep(pollFrequency * 2) // double the poll frequency // Initiate a bunch of changes. err := ftl("init", "go", dir, "one") assert.NoError(t, err) err = ftl("init", "go", dir, "two") assert.NoError(t, err) - time.Sleep(pollFrequency) + time.Sleep(pollFrequency * 2) // Delete a module err = os.RemoveAll(filepath.Join(dir, "two")) @@ -45,7 +44,7 @@ func TestWatch(t *testing.T) { // Change a module. updateModFile(t, filepath.Join(dir, "one")) - time.Sleep(pollFrequency) + time.Sleep(pollFrequency * 2) topic.Close() allEvents := []WatchEvent{} @@ -94,7 +93,7 @@ func TestWatchWithBuildModifyingFiles(t *testing.T) { events, topic := startWatching(ctx, t, w, dir) - time.Sleep(pollFrequency + halfPollFrequency) // midway between file scans + time.Sleep(pollFrequency * 2) // double the poll frequency // Change a file in a module, within a transaction transaction := w.GetTransaction(filepath.Join(dir, "one")) @@ -107,7 +106,7 @@ func TestWatchWithBuildModifyingFiles(t *testing.T) { err = transaction.End() assert.NoError(t, err) - time.Sleep(pollFrequency) + time.Sleep(pollFrequency * 2) topic.Close() allEvents := []WatchEvent{} @@ -135,7 +134,7 @@ func TestWatchWithBuildAndUserModifyingFiles(t *testing.T) { w := NewWatcher() events, topic := startWatching(ctx, t, w, dir) - time.Sleep(pollFrequency + halfPollFrequency) // midway between file scans + time.Sleep(pollFrequency * 2) // double the poll frequency // Change a file in a module, within a transaction transaction := w.GetTransaction(filepath.Join(dir, "one")) @@ -155,7 +154,7 @@ func TestWatchWithBuildAndUserModifyingFiles(t *testing.T) { err = transaction.End() assert.NoError(t, err) - time.Sleep(pollFrequency) + time.Sleep(pollFrequency * 2) topic.Close() allEvents := []WatchEvent{} diff --git a/examples/go/echo/echo.go b/examples/go/echo/echo.go index 175eeaca47..30fcc32ff0 100644 --- a/examples/go/echo/echo.go +++ b/examples/go/echo/echo.go @@ -23,7 +23,7 @@ type EchoResponse struct { // Echo returns a greeting with the current time. // -//ftl:export +//ftl:internal func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { tresp, err := ftl.Call(ctx, time.Time, time.TimeRequest{}) if err != nil { diff --git a/examples/go/time/time.go b/examples/go/time/time.go index 91c55bce04..1eeb9ccce0 100644 --- a/examples/go/time/time.go +++ b/examples/go/time/time.go @@ -12,7 +12,7 @@ type TimeResponse struct { // Time returns the current time. // -//ftl:export +//ftl:internal func Time(ctx context.Context, req TimeRequest) (TimeResponse, error) { return TimeResponse{Time: time.Now()}, nil } diff --git a/frontend/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts b/frontend/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts index a05866dc2c..20d18d9c66 100644 --- a/frontend/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts +++ b/frontend/src/protos/xyz/block/ftl/v1/schema/schema_pb.ts @@ -2030,22 +2030,27 @@ export class Verb extends Message { name = ""; /** - * @generated from field: repeated string comments = 3; + * @generated from field: string visibility = 3; + */ + visibility = ""; + + /** + * @generated from field: repeated string comments = 4; */ comments: string[] = []; /** - * @generated from field: xyz.block.ftl.v1.schema.Type request = 4; + * @generated from field: xyz.block.ftl.v1.schema.Type request = 5; */ request?: Type; /** - * @generated from field: xyz.block.ftl.v1.schema.Type response = 5; + * @generated from field: xyz.block.ftl.v1.schema.Type response = 6; */ response?: Type; /** - * @generated from field: repeated xyz.block.ftl.v1.schema.Metadata metadata = 6; + * @generated from field: repeated xyz.block.ftl.v1.schema.Metadata metadata = 7; */ metadata: Metadata[] = []; @@ -2060,10 +2065,11 @@ export class Verb extends Message { { no: 31634, name: "runtime", kind: "message", T: VerbRuntime, opt: true }, { no: 1, name: "pos", kind: "message", T: Position, opt: true }, { no: 2, name: "name", kind: "scalar", T: 9 /* ScalarType.STRING */ }, - { no: 3, name: "comments", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, - { no: 4, name: "request", kind: "message", T: Type }, - { no: 5, name: "response", kind: "message", T: Type }, - { no: 6, name: "metadata", kind: "message", T: Metadata, repeated: true }, + { no: 3, name: "visibility", kind: "scalar", T: 9 /* ScalarType.STRING */ }, + { no: 4, name: "comments", kind: "scalar", T: 9 /* ScalarType.STRING */, repeated: true }, + { no: 5, name: "request", kind: "message", T: Type }, + { no: 6, name: "response", kind: "message", T: Type }, + { no: 7, name: "metadata", kind: "message", T: Metadata, repeated: true }, ]); static fromBinary(bytes: Uint8Array, options?: Partial): Verb { diff --git a/go-runtime/compile/external-module-template/_ftl/go/modules/{{ range .NonMainModules }}{{ push .Name . }}{{ end }}/external_module.go b/go-runtime/compile/external-module-template/_ftl/go/modules/{{ range .NonMainModules }}{{ push .Name . }}{{ end }}/external_module.go index c300ab83c2..aea4cc3c2e 100644 --- a/go-runtime/compile/external-module-template/_ftl/go/modules/{{ range .NonMainModules }}{{ push .Name . }}{{ end }}/external_module.go +++ b/go-runtime/compile/external-module-template/_ftl/go/modules/{{ range .NonMainModules }}{{ push .Name . }}{{ end }}/external_module.go @@ -14,7 +14,7 @@ var _ = context.Background {{- range .Decls }} {{if is "Enum" . }} {{$enumName := .Name -}} -//ftl:export +//ftl:internal type {{.Name|title}} {{ type $ .Type }} const ( {{- range .Variants }} @@ -36,7 +36,7 @@ type {{.Name|title}} {{.Comments|comment }} {{if .Comments}}// {{end -}} -//ftl:export +//ftl:internal {{- if and (eq (type $ .Request) "ftl.Unit") (eq (type $ .Response) "ftl.Unit")}} func {{.Name|title}}(context.Context) error { panic("Verb stubs should not be called directly, instead use github.com/TBD54566975/ftl/runtime-go/ftl.CallEmpty()") diff --git a/go-runtime/compile/parser.go b/go-runtime/compile/parser.go index 6443816822..1ff6d3b0d8 100644 --- a/go-runtime/compile/parser.go +++ b/go-runtime/compile/parser.go @@ -8,14 +8,13 @@ import ( "strings" "github.com/alecthomas/participle/v2" - "github.com/alecthomas/participle/v2/lexer" "github.com/TBD54566975/ftl/backend/schema" ) // This file contains a parser for Go FTL directives. // -// eg. //ftl:ingress GET /foo/bar +// eg. //ftl:public http GET /foo/bar type directiveWrapper struct { Directive directive `parser:"'ftl' ':' @@"` @@ -24,27 +23,25 @@ type directiveWrapper struct { //sumtype:decl type directive interface{ directive() } -type directiveExport struct { - Pos lexer.Position +type directiveVisibility struct { + Pos schema.Position `parser:"" protobuf:"1,optional"` - Export bool `parser:"@'export'"` + Visibility schema.Visibility `parser:"@('public' | 'internal' | 'private')"` + Type string `parser:"@('http')?"` + Method string `parser:"@('GET' | 'POST' | 'PUT' | 'DELETE')?"` + Path []schema.IngressPathComponent `parser:"('/' @@)*"` } -func (*directiveExport) directive() {} -func (d *directiveExport) String() string { return "ftl:export" } - -type directiveIngress struct { - Pos schema.Position - - Type string `parser:"'ingress' @('http')?"` - Method string `parser:"@('GET' | 'POST' | 'PUT' | 'DELETE')"` - Path []schema.IngressPathComponent `parser:"('/' @@)+"` -} - -func (*directiveIngress) directive() {} -func (d *directiveIngress) String() string { +func (*directiveVisibility) directive() {} +func (d *directiveVisibility) String() string { w := &strings.Builder{} - fmt.Fprintf(w, "ftl:ingress %s", d.Method) + fmt.Fprintf(w, "ftl:%s", d.Visibility) + if d.Type != "" { + fmt.Fprintf(w, " %s", d.Type) + } + if d.Method != "" { + fmt.Fprintf(w, " %s", d.Method) + } for _, p := range d.Path { fmt.Fprintf(w, "/%s", p) } @@ -68,7 +65,7 @@ var directiveParser = participle.MustBuild[directiveWrapper]( participle.Elide("Whitespace"), participle.Unquote(), participle.UseLookahead(2), - participle.Union[directive](&directiveExport{}, &directiveIngress{}, &directiveCronJob{}), + participle.Union[directive](&directiveVisibility{}, &directiveCronJob{}), participle.Union[schema.IngressPathComponent](&schema.IngressPathLiteral{}, &schema.IngressPathParameter{}), ) diff --git a/go-runtime/compile/schema.go b/go-runtime/compile/schema.go index a40db0b982..4d1ff95174 100644 --- a/go-runtime/compile/schema.go +++ b/go-runtime/compile/schema.go @@ -364,16 +364,16 @@ func visitGenDecl(pctx *parseContext, node *ast.GenDecl) { } for _, dir := range directives { switch dir.(type) { - case *directiveExport: + case *directiveVisibility: if len(node.Specs) != 1 { - pctx.errors.add(errorf(node, "error parsing ftl export directive: expected "+ + pctx.errors.add(errorf(node, "error parsing ftl visibility directive: expected "+ "exactly one type declaration")) return } if pctx.module.Name == "" { pctx.module.Name = pctx.pkg.Name } else if pctx.module.Name != pctx.pkg.Name && strings.TrimPrefix(pctx.pkg.Name, "ftl/") != pctx.module.Name { - pctx.errors.add(errorf(node, "type export directive must be in the module package")) + pctx.errors.add(errorf(node, "type visibility directive must be in the module package")) return } if t, ok := node.Specs[0].(*ast.TypeSpec); ok { @@ -387,7 +387,7 @@ func visitGenDecl(pctx *parseContext, node *ast.GenDecl) { visitTypeSpec(pctx, t) } - case *directiveIngress, *directiveCronJob: + case *directiveCronJob: } } return @@ -465,29 +465,27 @@ func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb) { pctx.errors.add(err) } var metadata []schema.Metadata + var visibility string isVerb := false for _, dir := range directives { switch dir := dir.(type) { - case *directiveExport: + case *directiveVisibility: isVerb = true if pctx.module.Name == "" { pctx.module.Name = pctx.pkg.Name } else if pctx.module.Name != pctx.pkg.Name { - pctx.errors.add(errorf(node, "function export directive must be in the module package")) + pctx.errors.add(errorf(node, "function visibility directive must be in the module package")) } - - case *directiveIngress: - isVerb = true - typ := dir.Type - if typ == "" { - typ = "http" + visibility = string(dir.Visibility) + if dir.Type != "" { + metadata = append(metadata, &schema.MetadataIngress{ + Pos: dir.Pos, + Type: dir.Type, + Method: dir.Method, + Path: dir.Path, + }) } - metadata = append(metadata, &schema.MetadataIngress{ - Pos: dir.Pos, - Type: typ, - Method: dir.Method, - Path: dir.Path, - }) + case *directiveCronJob: isVerb = true metadata = append(metadata, &schema.MetadataCronJob{ @@ -510,7 +508,7 @@ func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb) { fnt := pctx.pkg.TypesInfo.Defs[node.Name].(*types.Func) //nolint:forcetypeassert sig := fnt.Type().(*types.Signature) //nolint:forcetypeassert if sig.Recv() != nil { - pctx.errors.add(errorf(node, "ftl:export cannot be a method")) + pctx.errors.add(errorf(node, "ftl: cannot be a method")) return nil } params := sig.Params() @@ -540,12 +538,13 @@ func visitFuncDecl(pctx *parseContext, node *ast.FuncDecl) (verb *schema.Verb) { "unsupported response type %q", results.At(0).Type())) } verb = &schema.Verb{ - Pos: goPosToSchemaPos(node.Pos()), - Comments: visitComments(node.Doc), - Name: strcase.ToLowerCamel(node.Name.Name), - Request: reqV, - Response: resV, - Metadata: metadata, + Pos: goPosToSchemaPos(node.Pos()), + Comments: visitComments(node.Doc), + Name: strcase.ToLowerCamel(node.Name.Name), + Visibility: visibility, + Request: reqV, + Response: resV, + Metadata: metadata, } pctx.nativeNames[verb] = node.Name.Name pctx.module.Decls = append(pctx.module.Decls, verb) diff --git a/go-runtime/compile/schema_test.go b/go-runtime/compile/schema_test.go index 914f42c664..001a969306 100644 --- a/go-runtime/compile/schema_test.go +++ b/go-runtime/compile/schema_test.go @@ -113,13 +113,13 @@ func TestExtractModuleSchema(t *testing.T) { data SourceResp { } - verb nothing(Unit) Unit + internal verb nothing(Unit) Unit - verb sink(one.SinkReq) Unit + internal verb sink(one.SinkReq) Unit - verb source(Unit) one.SourceResp + internal verb source(Unit) one.SourceResp - verb verb(one.Req) one.Resp + internal verb verb(one.Req) one.Resp } ` assert.Equal(t, expected, actual.String()) @@ -154,12 +154,12 @@ func TestExtractModuleSchemaTwo(t *testing.T) { user two.User } - verb callsTwo(two.Payload) two.Payload + internal verb callsTwo(two.Payload) two.Payload +calls two.two - verb returnsUser(Unit) two.UserResponse + internal verb returnsUser(Unit) two.UserResponse - verb two(two.Payload) two.Payload + internal verb two(two.Payload) two.Payload } ` assert.Equal(t, normaliseString(expected), normaliseString(actual.String())) @@ -171,17 +171,23 @@ func TestParseDirectives(t *testing.T) { input string expected directive }{ - {name: "Export", input: "ftl:export", expected: &directiveExport{Export: true}}, - {name: "Ingress", input: `ftl:ingress GET /foo`, expected: &directiveIngress{ - Method: "GET", + {name: "Private", input: "ftl:private", expected: &directiveVisibility{Visibility: schema.Private}}, + {name: "Internal", input: "ftl:internal", expected: &directiveVisibility{Visibility: schema.Internal}}, + {name: "Public", input: "ftl:public", expected: &directiveVisibility{Visibility: schema.Public}}, + {name: "Public Ingress Foo", input: "ftl:public http GET /foo", expected: &directiveVisibility{ + Visibility: schema.Public, + Type: "http", + Method: "GET", Path: []schema.IngressPathComponent{ &schema.IngressPathLiteral{ Text: "foo", }, }, }}, - {name: "Ingress", input: `ftl:ingress GET /test_path/{something}/987-Your_File.txt%7E%21Misc%2A%28path%29info%40abc%3Fxyz`, expected: &directiveIngress{ - Method: "GET", + {name: "Public Ingress TestPath", input: `ftl:public http GET /test_path/{something}/987-Your_File.txt%7E%21Misc%2A%28path%29info%40abc%3Fxyz`, expected: &directiveVisibility{ + Visibility: schema.Public, + Type: "http", + Method: "GET", Path: []schema.IngressPathComponent{ &schema.IngressPathLiteral{ Text: "test_path", @@ -279,16 +285,6 @@ func TestErrorReporting(t *testing.T) { ) } -func TestDuplicateVerbNames(t *testing.T) { - if testing.Short() { - t.SkipNow() - } - pwd, _ := os.Getwd() - _, _, schemaErrs, err := ExtractModuleSchema("testdata/duplicateverbs") - assert.NoError(t, err) - assert.EqualError(t, errors.Join(genericizeErrors(schemaErrs)...), filepath.Join(pwd, `testdata/duplicateverbs/duplicateverbs.go`)+`:23:1-1: verb "Time" already exported`) -} - func genericizeErrors(schemaErrs []*schema.Error) []error { errs := make([]error, len(schemaErrs)) for i, schemaErr := range schemaErrs { diff --git a/go-runtime/compile/testdata/duplicateverbs/duplicateverbs.go b/go-runtime/compile/testdata/duplicateverbs/duplicateverbs.go deleted file mode 100644 index c50eb7514d..0000000000 --- a/go-runtime/compile/testdata/duplicateverbs/duplicateverbs.go +++ /dev/null @@ -1,25 +0,0 @@ -package duplicateverbs - -import ( - "context" - "time" -) - -type TimeRequest struct { - Name string -} -type TimeResponse struct { - Time time.Time -} - -// Time returns the current time. -// -//ftl:export -func Time(ctx context.Context, req TimeRequest) (TimeResponse, error) { - return TimeResponse{Time: time.Now()}, nil -} - -//ftl:export -func Time(ctx context.Context, req TimeRequest) (TimeResponse, error) { - return TimeResponse{Time: time.Now()}, nil -} diff --git a/go-runtime/compile/testdata/duplicateverbs/ftl.toml b/go-runtime/compile/testdata/duplicateverbs/ftl.toml deleted file mode 100644 index 24c06df36b..0000000000 --- a/go-runtime/compile/testdata/duplicateverbs/ftl.toml +++ /dev/null @@ -1,2 +0,0 @@ -module = "duplicateverbs" -language = "go" diff --git a/go-runtime/compile/testdata/duplicateverbs/go.mod b/go-runtime/compile/testdata/duplicateverbs/go.mod deleted file mode 100644 index 96a4d40762..0000000000 --- a/go-runtime/compile/testdata/duplicateverbs/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module ftl/duplicateverbs - -go 1.22.2 - -replace github.com/TBD54566975/ftl => ../../../.. diff --git a/go-runtime/compile/testdata/duplicateverbs/go.sum b/go-runtime/compile/testdata/duplicateverbs/go.sum deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/go-runtime/compile/testdata/failing/failing.go b/go-runtime/compile/testdata/failing/failing.go index 17c24b0e2a..fc446d984b 100644 --- a/go-runtime/compile/testdata/failing/failing.go +++ b/go-runtime/compile/testdata/failing/failing.go @@ -21,7 +21,7 @@ func WrongDirective(ctx context.Context, req Request) (Response, error) { return Response{}, nil } -//ftl:export +//ftl:internal func BadCalls(ctx context.Context, req Request) (Response, error) { ftl.Call(ctx, lib.OtherFunc, lib.Request{}) ftl.Call(ctx, "failing", "failingVerb", req) @@ -29,53 +29,53 @@ func BadCalls(ctx context.Context, req Request) (Response, error) { return Response{}, nil } -//ftl:export +//ftl:internal func TooManyParams(ctx context.Context, req Request, req2 Request) (Response, error) { return Response{}, nil } -//ftl:export +//ftl:internal func WrongParamOrder(first Request, second string) (Response, error) { return Response{}, nil } -//ftl:export +//ftl:internal func UnitSecondParam(ctx context.Context, unit ftl.Unit) (Response, error) { return Response{}, nil } -//ftl:export +//ftl:internal func NoParams() (Response, error) { return Response{}, nil } -//ftl:export +//ftl:internal func TooManyReturn(ctx context.Context, req Request) (Response, Response, error) { return "", Response{}, nil } -//ftl:export +//ftl:internal func NoReturn(ctx context.Context, req Request) { } -//ftl:export +//ftl:internal func NoError(ctx context.Context, req Request) Response { return Response{} } -//ftl:export +//ftl:internal func WrongResponse(ctx context.Context, req Request) (string, ftl.Unit) { return "", ftl.Unit{} } // Duplicate // -//ftl:export +//ftl:internal func WrongResponse(ctx context.Context, req Request) (string, ftl.Unit) { return "", ftl.Unit{} } -//ftl:export +//ftl:internal type BadStruct struct { unexported string } diff --git a/go-runtime/compile/testdata/one/one.go b/go-runtime/compile/testdata/one/one.go index 164916de61..808914e0c7 100644 --- a/go-runtime/compile/testdata/one/one.go +++ b/go-runtime/compile/testdata/one/one.go @@ -9,7 +9,7 @@ import ( "github.com/TBD54566975/ftl/go-runtime/ftl" ) -//ftl:export +//ftl:internal type Color string const ( @@ -20,7 +20,7 @@ const ( // Comments about ColorInt. // -//ftl:export +//ftl:internal type ColorInt int const ( @@ -31,7 +31,7 @@ const ( GreenInt ColorInt = 2 ) -//ftl:export +//ftl:internal type SimpleIota int const ( @@ -40,7 +40,7 @@ const ( Two ) -//ftl:export +//ftl:internal type IotaExpr int const ( @@ -77,7 +77,7 @@ var configValue = ftl.Config[Config]("configValue") var secretValue = ftl.Secret[string]("secretValue") var testDb = ftl.PostgresDatabase("testDb") -//ftl:export +//ftl:internal func Verb(ctx context.Context, req Req) (Resp, error) { return Resp{}, nil } @@ -88,19 +88,19 @@ const YellowInt ColorInt = 3 type SinkReq struct{} -//ftl:export +//ftl:internal func Sink(ctx context.Context, req SinkReq) error { return nil } type SourceResp struct{} -//ftl:export +//ftl:internal func Source(ctx context.Context) (SourceResp, error) { return SourceResp{}, nil } -//ftl:export +//ftl:internal func Nothing(ctx context.Context) error { return nil } diff --git a/go-runtime/compile/testdata/two/two.go b/go-runtime/compile/testdata/two/two.go index c33fa3d644..57f1f145e1 100644 --- a/go-runtime/compile/testdata/two/two.go +++ b/go-runtime/compile/testdata/two/two.go @@ -6,7 +6,7 @@ import ( "github.com/TBD54566975/ftl/go-runtime/ftl" ) -//ftl:export +//ftl:internal type TwoEnum string const ( @@ -15,7 +15,7 @@ const ( Green TwoEnum = "Green" ) -//ftl:export +//ftl:internal type Exported struct { } @@ -31,17 +31,17 @@ type UserResponse struct { User User } -//ftl:export +//ftl:internal func Two(ctx context.Context, req Payload[string]) (Payload[string], error) { return Payload[string]{}, nil } -//ftl:export +//ftl:internal func CallsTwo(ctx context.Context, req Payload[string]) (Payload[string], error) { return ftl.Call(ctx, Two, req) } -//ftl:export +//ftl:internal func ReturnsUser(ctx context.Context) (UserResponse, error) { return UserResponse{ User: User{ diff --git a/go-runtime/scaffolding/{{ .Name | camel | lower }}/{{ .Name | camel | lower }}.go.tmpl b/go-runtime/scaffolding/{{ .Name | camel | lower }}/{{ .Name | camel | lower }}.go.tmpl index 6ce86d10ef..c0296598fd 100644 --- a/go-runtime/scaffolding/{{ .Name | camel | lower }}/{{ .Name | camel | lower }}.go.tmpl +++ b/go-runtime/scaffolding/{{ .Name | camel | lower }}/{{ .Name | camel | lower }}.go.tmpl @@ -15,7 +15,7 @@ type EchoResponse struct { Message string `json:"message"` } -//ftl:export +//ftl:internal func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil -} \ No newline at end of file +} diff --git a/integration/testdata/go/database/echo.go b/integration/testdata/go/database/echo.go index 21cd719a6f..091a839806 100644 --- a/integration/testdata/go/database/echo.go +++ b/integration/testdata/go/database/echo.go @@ -14,7 +14,7 @@ type InsertRequest struct { type InsertResponse struct{} -//ftl:export +//ftl:internal func Insert(ctx context.Context, req InsertRequest) (InsertResponse, error) { err := persistRequest(ctx, req) if err != nil { diff --git a/integration/testdata/go/externalcalls/echo.go b/integration/testdata/go/externalcalls/echo.go index 725abf8809..992d248fe8 100644 --- a/integration/testdata/go/externalcalls/echo.go +++ b/integration/testdata/go/externalcalls/echo.go @@ -17,12 +17,12 @@ type EchoResponse struct { Message string `json:"message"` } -//ftl:export +//ftl:internal func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { return EchoResponse{Message: fmt.Sprintf("Hello, %s!", req.Name.Default("anonymous"))}, nil } -//ftl:export +//ftl:internal func Call(ctx context.Context, req EchoRequest) (EchoResponse, error) { res, err := ftl.Call(ctx, echo2.Echo, echo2.EchoRequest{Name: req.Name}) if err != nil { diff --git a/integration/testdata/go/httpingress/echo.go b/integration/testdata/go/httpingress/echo.go index 32085d845b..d24ac558fe 100644 --- a/integration/testdata/go/httpingress/echo.go +++ b/integration/testdata/go/httpingress/echo.go @@ -23,8 +23,7 @@ type GetResponse struct { Nested Nested `json:"nested"` } -//ftl:export -//ftl:ingress http GET /users/{userId}/posts/{postId} +//ftl:public http GET /users/{userId}/posts/{postId} func Get(ctx context.Context, req builtin.HttpRequest[GetRequest]) (builtin.HttpResponse[GetResponse, string], error) { return builtin.HttpResponse[GetResponse, string]{ Headers: map[string][]string{"Get": {"Header from FTL"}}, @@ -46,8 +45,7 @@ type PostResponse struct { Success bool `json:"success"` } -//ftl:export -//ftl:ingress http POST /users +//ftl:public http POST /users func Post(ctx context.Context, req builtin.HttpRequest[PostRequest]) (builtin.HttpResponse[PostResponse, string], error) { return builtin.HttpResponse[PostResponse, string]{ Status: 201, @@ -63,8 +61,7 @@ type PutRequest struct { type PutResponse struct{} -//ftl:export -//ftl:ingress http PUT /users/{userId} +//ftl:public http PUT /users/{userId} func Put(ctx context.Context, req builtin.HttpRequest[PutRequest]) (builtin.HttpResponse[builtin.Empty, string], error) { return builtin.HttpResponse[builtin.Empty, string]{ Headers: map[string][]string{"Put": {"Header from FTL"}}, @@ -78,8 +75,7 @@ type DeleteRequest struct { type DeleteResponse struct{} -//ftl:export -//ftl:ingress http DELETE /users/{userId} +//ftl:public http DELETE /users/{userId} func Delete(ctx context.Context, req builtin.HttpRequest[DeleteRequest]) (builtin.HttpResponse[builtin.Empty, string], error) { return builtin.HttpResponse[builtin.Empty, string]{ Status: 200, @@ -90,8 +86,7 @@ func Delete(ctx context.Context, req builtin.HttpRequest[DeleteRequest]) (builti type HtmlRequest struct{} -//ftl:export -//ftl:ingress http GET /html +//ftl:public http GET /html func Html(ctx context.Context, req builtin.HttpRequest[HtmlRequest]) (builtin.HttpResponse[string, string], error) { return builtin.HttpResponse[string, string]{ Headers: map[string][]string{"Content-Type": {"text/html; charset=utf-8"}}, @@ -99,44 +94,37 @@ func Html(ctx context.Context, req builtin.HttpRequest[HtmlRequest]) (builtin.Ht }, nil } -//ftl:export -//ftl:ingress http POST /bytes +//ftl:public http POST /bytes func Bytes(ctx context.Context, req builtin.HttpRequest[[]byte]) (builtin.HttpResponse[[]byte, string], error) { return builtin.HttpResponse[[]byte, string]{Body: ftl.Some(req.Body)}, nil } -//ftl:export -//ftl:ingress http GET /empty +//ftl:public http GET /empty func Empty(ctx context.Context, req builtin.HttpRequest[ftl.Unit]) (builtin.HttpResponse[ftl.Unit, string], error) { return builtin.HttpResponse[ftl.Unit, string]{Body: ftl.Some(ftl.Unit{})}, nil } -//ftl:export -//ftl:ingress http GET /string +//ftl:public http GET /string func String(ctx context.Context, req builtin.HttpRequest[string]) (builtin.HttpResponse[string, string], error) { return builtin.HttpResponse[string, string]{Body: ftl.Some(req.Body)}, nil } -//ftl:export -//ftl:ingress http GET /int +//ftl:public http GET /int func Int(ctx context.Context, req builtin.HttpRequest[int]) (builtin.HttpResponse[int, string], error) { return builtin.HttpResponse[int, string]{Body: ftl.Some(req.Body)}, nil } -//ftl:export -//ftl:ingress http GET /float +//ftl:public http GET /float func Float(ctx context.Context, req builtin.HttpRequest[float64]) (builtin.HttpResponse[float64, string], error) { return builtin.HttpResponse[float64, string]{Body: ftl.Some(req.Body)}, nil } -//ftl:export -//ftl:ingress http GET /bool +//ftl:public http GET /bool func Bool(ctx context.Context, req builtin.HttpRequest[bool]) (builtin.HttpResponse[bool, string], error) { return builtin.HttpResponse[bool, string]{Body: ftl.Some(req.Body)}, nil } -//ftl:export -//ftl:ingress http GET /error +//ftl:public http GET /error func Error(ctx context.Context, req builtin.HttpRequest[ftl.Unit]) (builtin.HttpResponse[ftl.Unit, string], error) { return builtin.HttpResponse[ftl.Unit, string]{ Status: 500, @@ -144,8 +132,7 @@ func Error(ctx context.Context, req builtin.HttpRequest[ftl.Unit]) (builtin.Http }, nil } -//ftl:export -//ftl:ingress http GET /array/string +//ftl:public http GET /array/string func ArrayString(ctx context.Context, req builtin.HttpRequest[[]string]) (builtin.HttpResponse[[]string, string], error) { return builtin.HttpResponse[[]string, string]{ Body: ftl.Some(req.Body), @@ -156,8 +143,7 @@ type ArrayType struct { Item string `json:"item"` } -//ftl:export -//ftl:ingress http POST /array/data +//ftl:public http POST /array/data func ArrayData(ctx context.Context, req builtin.HttpRequest[[]ArrayType]) (builtin.HttpResponse[[]ArrayType, string], error) { return builtin.HttpResponse[[]ArrayType, string]{ Body: ftl.Some(req.Body), diff --git a/integration/testdata/go/pubsub/echo.go b/integration/testdata/go/pubsub/echo.go index 4a57b85c4a..ed95b912e7 100644 --- a/integration/testdata/go/pubsub/echo.go +++ b/integration/testdata/go/pubsub/echo.go @@ -12,7 +12,7 @@ type EchoResponse struct { Name string } -//ftl:export +//ftl:internal func Echo(ctx context.Context, req EchoRequest) (EchoResponse, error) { err := ftl.CallSink(ctx, Sink, SinkRequest{}) if err != nil { @@ -33,7 +33,7 @@ type SourceResponse struct { Name string } -//ftl:export +//ftl:internal func Source(ctx context.Context) (SourceResponse, error) { return SourceResponse{ Name: "source", @@ -42,7 +42,7 @@ func Source(ctx context.Context) (SourceResponse, error) { type SinkRequest struct{} -//ftl:export +//ftl:internal func Sink(ctx context.Context, req SinkRequest) error { return nil } 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 index a7a3c7677d..940f3f7bd5 100644 --- 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 @@ -16,11 +16,11 @@ import {{$import}} {{- if is "Data" . }} {{- if and (eq $moduleName "builtin") (eq .Name "Empty")}} {{.Comments|comment -}} -@Export +@Export(Visibility.INTERNAL) class Empty {{- else if .Fields}} {{.Comments|comment -}} -@Export +@Export(Visibility.INTERNAL) data class {{.Name|title}} {{- if .TypeParameters}}< {{- range $i, $tp := .TypeParameters}} @@ -34,7 +34,7 @@ data class {{.Name|title}} {{end}} {{- else if is "Verb" . }} -{{.Comments|comment -}}@Export +{{.Comments|comment -}}@Export(Visibility.INTERNAL) @Ignore {{- if and (eq (type $ .Request) "Unit") (eq (type $ .Response) "Unit")}} fun {{.Name|lowerCamel}}(context: Context): Unit = throw diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Export.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Export.kt index 460fe14c49..389c313d5a 100644 --- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Export.kt +++ b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Export.kt @@ -1,6 +1,27 @@ package xyz.block.ftl +enum class Visibility { + PUBLIC, INTERNAL, PRIVATE +} + +enum class Ingress { + HTTP, NONE +} + +enum class Method { + GET, POST, PUT, DELETE, NONE +} + +/** + * This annotation can be used to mark classes or functions for export with specified visibility, + * type of ingress, HTTP method, and a routing path. + */ @Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @MustBeDocumented -annotation class Export +annotation class Export( + val visibility: Visibility, + val ingress: Ingress = Ingress.NONE, + val method: Method = Method.NONE, + val path: String = "", +) 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/Json.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Json.kt new file mode 100644 index 0000000000..3eb502a197 --- /dev/null +++ b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/Json.kt @@ -0,0 +1,10 @@ +package xyz.block.ftl + + +/** + * 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/schemaextractor/ExtractSchemaRule.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt index abff829d5f..6b957b5d56 100644 --- 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 @@ -58,10 +58,13 @@ 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.Export +import xyz.block.ftl.Visibility +import xyz.block.ftl.Ingress import xyz.block.ftl.Json import xyz.block.ftl.Method import xyz.block.ftl.Cron +import xyz.block.ftl.schemaextractor.SchemaExtractor.Companion.errorAtPosition import xyz.block.ftl.schemaextractor.SchemaExtractor.Companion.extractModuleName import xyz.block.ftl.v1.schema.Array import xyz.block.ftl.v1.schema.Config @@ -145,7 +148,7 @@ class ExtractSchemaRule(config: DetektConfig) : Rule(config) { } when (val element = annotationEntry.parent.parent) { - is KtNamedFunction -> extractor.addVerbToSchema(element) + is KtNamedFunction -> extractor.addVerbToSchema(element, annotationEntry) is KtClass -> { when { element.isData() -> extractor.addDataToSchema(element) @@ -220,9 +223,9 @@ class SchemaExtractor( } } - fun addVerbToSchema(verb: KtNamedFunction) { + fun addVerbToSchema(verb: KtNamedFunction, exportAnnotation: KtAnnotationEntry) { validateVerb(verb) - addDecl(verb.extractModuleName(), Decl(verb = extractVerb(verb))) + addDecl(verb.extractModuleName(), Decl(verb = extractVerb(verb, exportAnnotation))) } fun addDataToSchema(data: KtClass) { @@ -319,7 +322,50 @@ class SchemaExtractor( } } - private fun extractVerb(verb: KtNamedFunction): Verb { + private data class ExportAnnotation( + val visibility: Visibility?, + val ingress: Ingress?, + val method: Method?, + val path: String? + ) + + private fun extractExportAnnotation(verb: KtNamedFunction, bindingContext: BindingContext): ExportAnnotation { + val exportAnnotation = verb.annotationEntries.firstOrNull { + bindingContext.get(BindingContext.ANNOTATION, it)?.fqName?.asString() == Export::class.qualifiedName + } + + var visibility: Visibility? = null + var ingress: Ingress? = null + var method: Method? = null + var path: String? = null + + exportAnnotation?.valueArguments?.forEach { arg -> + when { + arg.getArgumentName()?.asName?.asString() == "visibility" || arg.getArgumentExpression()?.getType(bindingContext)?.fqNameOrNull()?.asString() == Visibility::class.qualifiedName -> { + visibility = kotlin.runCatching { Visibility.valueOf(arg.getArgumentExpression()?.text?.substringAfterLast('.') ?: "") }.getOrNull() + } + arg.getArgumentName()?.asName?.asString() == "ingress" || arg.getArgumentExpression()?.getType(bindingContext)?.fqNameOrNull()?.asString() == Ingress::class.qualifiedName -> { + ingress = kotlin.runCatching { Ingress.valueOf(arg.getArgumentExpression()?.text?.substringAfterLast('.') ?: "") }.getOrNull() + } + arg.getArgumentName()?.asName?.asString() == "method" || arg.getArgumentExpression()?.getType(bindingContext)?.fqNameOrNull()?.asString() == Method::class.qualifiedName -> { + method = kotlin.runCatching { Method.valueOf(arg.getArgumentExpression()?.text?.substringAfterLast('.') ?: "") }.getOrNull() + } + arg.getArgumentName()?.asName?.asString() == "path" || arg.getArgumentExpression()?.getType(bindingContext)?.fqNameOrNull()?.asString() == String::class.qualifiedName -> { + path = arg.getArgumentExpression()?.text?.trim('"') + } + } + } + + return ExportAnnotation(visibility, ingress, method, path) + } + + private fun extractVerb(verb: KtNamedFunction, exportAnnotation: KtAnnotationEntry): Verb { + val exportFields = extractExportAnnotation(verb, bindingContext) + + requireNotNull(exportFields.visibility) { + verb.errorAtPosition("${verb.name} must have a valid visibility argument.") + } + val requestRef = verb.valueParameters.takeIf { it.size > 1 }?.last()?.let { return@let it.typeReference?.resolveType()?.toSchemaType(it.getPosition(), it.textLength) } ?: Type(unit = Unit()) @@ -329,8 +375,8 @@ class SchemaExtractor( } ?: 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)) } + extractIngress(verb, requestRef, returnRef, exportFields)?.apply { metadata.add(Metadata(ingress = this)) } + extractCron(verb)?.apply { metadata.add(Metadata(cronJob = this)) } extractCalls(verb)?.apply { metadata.add(Metadata(calls = this)) } require(verb.name != null) { verb.errorAtPosition("verbs must be named") @@ -342,6 +388,7 @@ class SchemaExtractor( response = returnRef, metadata = metadata, comments = verb.comments(), + visibility = exportFields.visibility.toString().lowercase(), ) } @@ -379,56 +426,51 @@ class SchemaExtractor( } } - private fun extractIngress(verb: KtNamedFunction, requestType: Type, responseType: Type): MetadataIngress? { + private fun extractIngress(verb: KtNamedFunction, requestType: Type, responseType: Type, exportFields: ExportAnnotation): MetadataIngress? { return verb.annotationEntries.firstOrNull { - bindingContext.get(BindingContext.ANNOTATION, it)?.fqName?.asString() == HttpIngress::class.qualifiedName + bindingContext.get(BindingContext.ANNOTATION, it)?.fqName?.asString() == Export::class.qualifiedName }?.let { annotationEntry -> - require(requestType.ref != null) { + requireNotNull(exportFields.ingress) { + return null + } + + require(exportFields.ingress == Ingress.HTTP) { + annotationEntry.errorAtPosition("ingress must be HTTP") + } + + // Validate the request and response types + requireNotNull(requestType.ref) { annotationEntry.errorAtPosition("ingress ${verb.name} request must be a data class") } - require(responseType.ref != null) { + requireNotNull(responseType.ref) { 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") + annotationEntry.errorAtPosition("@Export http ingress ${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 + annotationEntry.errorAtPosition("@Export http ingress ${verb.name} response must be ftl.builtin.HttpResponse") } - val methodArg = args.first.single().getArgumentExpression()?.text?.substringAfter(".") - require(methodArg != null) { - annotationEntry.errorAtPosition("could not extract method from ${verb.name} @HttpIngress annotation") + requireNotNull(exportFields.method) { + annotationEntry.errorAtPosition("HTTP ingress must specify METHOD") } - 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") + val pathArgText = annotationEntry.valueArguments.getOrNull(3)?.getArgumentExpression()?.text?.substringAfterLast(".") + val pathArg = pathArgText?.let { + extractPathComponents(it.trim('"') ?: "") } MetadataIngress( - type = "http", + type = exportFields.ingress.toString().lowercase(), path = pathArg ?: emptyList(), - method = methodArg ?: "", + method = exportFields.method.toString(), pos = annotationEntry.getPosition(), ) } } - private fun extractCron(verb: KtNamedFunction, requestType: Type, responseType: Type): MetadataCronJob? { + private fun extractCron(verb: KtNamedFunction): MetadataCronJob? { return verb.annotationEntries.firstOrNull { bindingContext.get(BindingContext.ANNOTATION, it)?.fqName?.asString() == Cron::class.qualifiedName }?.let { annotationEntry -> @@ -709,7 +751,7 @@ class SchemaExtractor( Type( ref = Ref( - name = refName ?: "", + name = refName, module = fqName.extractModuleName(), pos = position, typeParameters = this.arguments.map { it.type.toSchemaType(position, tokenLength) }.toList(), @@ -732,6 +774,12 @@ class SchemaExtractor( return type } + private fun KtAnnotationEntry.findArgument(name: String): ValueArgument? { + return this.valueArguments.find { + it.getArgumentName()?.asName?.asString() == name + } + } + private fun KotlinType.toClassDescriptor(): ClassDescriptor = requireNotNull(this.unwrap().constructor.declarationDescriptor as? ClassDescriptor) 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 index 2a6f86eb9d..e6ad404296 100644 --- 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 @@ -72,11 +72,12 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { import ftl.time.TimeResponse import xyz.block.ftl.Json import xyz.block.ftl.Context - import xyz.block.ftl.HttpIngress + import xyz.block.ftl.Visibility import xyz.block.ftl.Method import xyz.block.ftl.Module import xyz.block.ftl.Export + class InvalidInput(val field: String) : Exception() data class MapValue(val value: String) @@ -98,8 +99,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { * Echoes the given message. */ @Throws(InvalidInput::class) - @Export - @HttpIngress(Method.GET, "/echo") + @Export(Visibility.PUBLIC, Ingress.HTTP, Method.GET, "/echo") fun echo(context: Context, req: HttpRequest>): HttpResponse { callTime(context) @@ -110,7 +110,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { ) } - @Export + @Export(Visibility.INTERNAL) fun empty(context: Context, req: Empty): Empty { return builtin.Empty() } @@ -123,13 +123,13 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { return context.call(::verb, builtin.Empty()) } - @Export + @Export(Visibility.INTERNAL) fun sink(context: Context, req: Empty) {} - @Export + @Export(Visibility.INTERNAL) fun source(context: Context): Empty {} - @Export + @Export(Visibility.INTERNAL) fun emptyVerb(context: Context) {} """ ExtractSchemaRule(Config.empty).compileAndLintWithContext(env, code) @@ -229,6 +229,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { Decl( verb = Verb( name = "echo", + visibility = "public", comments = listOf( """Echoes the given message.""" ), @@ -302,6 +303,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { Decl( verb = Verb( name = "empty", + visibility = "internal", request = Type( ref = Ref( name = "Empty", @@ -319,6 +321,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { Decl( verb = Verb( name = "sink", + visibility = "internal", request = Type( ref = Ref( name = "Empty", @@ -333,6 +336,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { Decl( verb = Verb( name = "source", + visibility = "internal", request = Type( unit = Unit() ), @@ -347,6 +351,7 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) { Decl( verb = Verb( name = "emptyVerb", + visibility = "internal", request = Type( unit = Unit() ), @@ -394,7 +399,7 @@ data class EchoResponse(val messages: List) * Echoes the given message. */ @Throws(InvalidInput::class) -@Export +@Export(Visibility.INTERNAL) fun echo(context: Context, req: EchoRequest): EchoResponse { callTime(context) return EchoResponse(messages = listOf(EchoMessage(message = "Hello!"))) @@ -425,8 +430,6 @@ fun callTime(context: Context): TimeResponse { package ftl.echo import xyz.block.ftl.Context -import xyz.block.ftl.HttpIngress -import xyz.block.ftl.Method import xyz.block.ftl.Export /** @@ -439,8 +442,7 @@ data class EchoResponse(val message: String) * Echoes the given message. */ @Throws(InvalidInput::class) -@Export -@HttpIngress(Method.GET, "/echo") +@Export(Visibility.INTERNAL, Ingress.HTTP, Method.GET, "/echo") fun echo(context: Context, req: EchoRequest): EchoResponse { return EchoResponse(messages = listOf(EchoMessage(message = "Hello!"))) } @@ -452,10 +454,10 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { assertErrorsFileContainsExactly( Error( - msg = "@HttpIngress-annotated echo request must be ftl.builtin.HttpRequest", + msg = "@Export http ingress echo request must be ftl.builtin.HttpRequest", ), Error( - msg = "@HttpIngress-annotated echo response must be ftl.builtin.HttpResponse", + msg = "@Export http ingress echo response must be ftl.builtin.HttpResponse", ) ) } @@ -469,7 +471,6 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { package ftl.echo import xyz.block.ftl.Context -import xyz.block.ftl.HttpIngress import xyz.block.ftl.Method import xyz.block.ftl.Export @@ -483,8 +484,7 @@ data class EchoResponse(val message: String) * Echoes the given message. */ @Throws(InvalidInput::class) -@Export -@HttpIngress(Method.GET, "/echo") +@Export(Visibility.INTERNAL, Ingress.HTTP, Method.GET, "/echo") fun echo(context: Context, req: EchoRequest): EchoResponse { return EchoResponse(messages = listOf(EchoMessage(message = "Hello!"))) } @@ -496,10 +496,10 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { assertErrorsFileContainsExactly( Error( - msg = "@HttpIngress-annotated echo request must be ftl.builtin.HttpRequest", + msg = "@Export http ingress echo request must be ftl.builtin.HttpRequest", ), Error( - msg = "@HttpIngress-annotated echo response must be ftl.builtin.HttpResponse", + msg = "@Export http ingress echo response must be ftl.builtin.HttpResponse", ) ) } @@ -530,7 +530,7 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { /** * Comments. */ - @Export + @Export(Visibility.INTERNAL) enum class StringThing(val value: String) { /** * A comment. @@ -543,7 +543,7 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { C("C"), } - @Export + @Export(Visibility.INTERNAL) enum class IntThing(val value: Int) { A(1), B(2), @@ -562,7 +562,7 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { data class Response(val message: String) - @Export + @Export(Visibility.INTERNAL) fun something(context: Context, req: Request): Response { return Response(message = "response") } @@ -650,6 +650,7 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { Decl( verb = Verb( name = "something", + visibility = "internal", request = Type(ref = Ref(name = "Request", module = "things")), response = Type(ref = Ref(name = "Response", module = "things")), ), @@ -690,7 +691,7 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { data class Response(val message: String) - @Export + @Export(Visibility.INTERNAL) fun something(context: Context, req: Request): Response { return Response(message = "response") } @@ -763,6 +764,7 @@ fun echo(context: Context, req: EchoRequest): EchoResponse { Decl( verb = Verb( name = "something", + visibility = "internal", request = Type(ref = Ref(name = "Request", module = "test")), response = Type(ref = Ref(name = "Response", module = "test")), ), 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 index f2d94abda8..de166a89dc 100644 --- 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 @@ -2,9 +2,10 @@ 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.Export +import xyz.block.ftl.Ingress +import xyz.block.ftl.Visibility import java.time.OffsetDateTime data class TimeResponse( @@ -20,14 +21,15 @@ enum class Color { /** * Time returns the current time. */ -@Export -@HttpIngress( +@Export( + Visibility.PUBLIC, + Ingress.HTTP, GET, "/time", ) fun time(context: Context, req: Empty): TimeResponse = throw NotImplementedError("Verb stubs should not be called directly, instead use context.call(TimeModuleClient::time, ...)") -@Export +@Export(Visibility.INTERNAL) 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/testdata/TestModule.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/testdata/TestModule.kt index a372bdd3a9..003dc97f1f 100644 --- 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 @@ -8,7 +8,7 @@ import java.time.OffsetDateTime data class EchoRequest(val user: String) data class EchoResponse(val text: String) -@Export +@Export(Visibility.INTERNAL) fun echo(context: Context, req: EchoRequest): EchoResponse { val time = context.call(::time, Empty()) return EchoResponse("Hello ${req.user}, the time is ${time.time}!") @@ -18,7 +18,7 @@ data class TimeResponse(val time: OffsetDateTime) val staticTime = OffsetDateTime.now() -@Export +@Export(Visibility.INTERNAL) fun time(context: Context, req: Empty): TimeResponse { return TimeResponse(staticTime) } @@ -26,15 +26,14 @@ fun time(context: Context, req: Empty): TimeResponse { data class VerbRequest(val text: String = "") data class VerbResponse(val text: String = "") -@Export -@HttpIngress(Method.GET, "/test") +@Export(Visibility.PUBLIC, Ingress.HTTP, Method.GET, "/test") fun verb(context: Context, req: VerbRequest): VerbResponse { return VerbResponse("test") } -@Export @Ignore +@Export(Visibility.INTERNAL) fun anotherVerb(context: Context, req: VerbRequest): VerbResponse { return VerbResponse("ignored") } 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 index a248761c57..c67bd36d65 100644 --- 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 @@ -3,11 +3,12 @@ package ftl.{{ .Name | camel | lower }} import xyz.block.ftl.Context import xyz.block.ftl.Method import xyz.block.ftl.Export +import xyz.block.ftl.Visibility data class EchoRequest(val name: String? = "anonymous") data class EchoResponse(val message: String) -@Export +@Export(Visibility.INTERNAL) fun echo(context: Context, req: EchoRequest): EchoResponse { return EchoResponse(message = "Hello, ${req.name}!") }