diff --git a/go-runtime/ftl/config.go b/go-runtime/ftl/config.go index 78bea66131..61ca059051 100644 --- a/go-runtime/ftl/config.go +++ b/go-runtime/ftl/config.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/TBD54566975/ftl/go-runtime/ftl/reflection" - "github.com/TBD54566975/ftl/internal/modulecontext" + "github.com/TBD54566975/ftl/go-runtime/internal" ) // ConfigType is a type that can be used as a configuration value. @@ -33,7 +33,7 @@ func (c ConfigValue[T]) GoString() string { // Get returns the value of the configuration key from FTL. func (c ConfigValue[T]) Get(ctx context.Context) (out T) { - err := modulecontext.FromContext(ctx).GetConfig(c.Name, &out) + err := internal.FromContext(ctx).GetConfig(ctx, c.Name, &out) if err != nil { panic(fmt.Errorf("failed to get %s: %w", c, err)) } diff --git a/go-runtime/ftl/config_test.go b/go-runtime/ftl/config_test.go index 0167019c50..d7984dd463 100644 --- a/go-runtime/ftl/config_test.go +++ b/go-runtime/ftl/config_test.go @@ -7,6 +7,7 @@ import ( "github.com/alecthomas/assert/v2" + "github.com/TBD54566975/ftl/go-runtime/internal" "github.com/TBD54566975/ftl/internal/log" "github.com/TBD54566975/ftl/internal/modulecontext" ) @@ -22,8 +23,7 @@ func TestConfig(t *testing.T) { data, err := json.Marshal(C{"one", "two"}) assert.NoError(t, err) - moduleCtx := modulecontext.NewBuilder("test").AddConfigs(map[string][]byte{"test": data}).Build() - ctx = moduleCtx.ApplyToContext(ctx) + ctx = internal.WithContext(ctx, internal.New(modulecontext.NewBuilder("test").AddConfigs(map[string][]byte{"test": data}).Build())) config := Config[C]("test") assert.Equal(t, C{"one", "two"}, config.Get(ctx)) diff --git a/go-runtime/ftl/ftltest/fake.go b/go-runtime/ftl/ftltest/fake.go index ad07c1a358..c5b3d94b4a 100644 --- a/go-runtime/ftl/ftltest/fake.go +++ b/go-runtime/ftl/ftltest/fake.go @@ -2,10 +2,12 @@ package ftltest import ( "context" + "encoding/json" "fmt" "reflect" "strings" + "github.com/TBD54566975/ftl/common/configuration" "github.com/TBD54566975/ftl/go-runtime/internal" ) @@ -13,6 +15,7 @@ type fakeFTL struct { fsm *fakeFSMManager mockMaps map[uintptr]mapImpl allowMapCalls bool + configValues map[string][]byte } // mapImpl is a function that takes an object and returns an object of a potentially different @@ -22,13 +25,31 @@ type mapImpl func(context.Context) (any, error) func newFakeFTL() *fakeFTL { return &fakeFTL{ fsm: newFakeFSMManager(), - mockMaps: make(map[uintptr]mapImpl), + mockMaps: map[uintptr]mapImpl{}, allowMapCalls: false, + configValues: map[string][]byte{}, } } var _ internal.FTL = &fakeFTL{} +func (f *fakeFTL) setConfig(name string, value any) error { + data, err := json.Marshal(value) + if err != nil { + return err + } + f.configValues[name] = data + return nil +} + +func (f *fakeFTL) GetConfig(ctx context.Context, name string, dest any) error { + data, ok := f.configValues[name] + if !ok { + return fmt.Errorf("config value %q not found: %w", name, configuration.ErrNotFound) + } + return json.Unmarshal(data, dest) +} + func (f *fakeFTL) FSMSend(ctx context.Context, fsm string, instance string, event any) error { return f.fsm.SendEvent(ctx, fsm, instance, event) } diff --git a/go-runtime/ftl/ftltest/ftltest.go b/go-runtime/ftl/ftltest/ftltest.go index 5bae1e7c82..0b392d561e 100644 --- a/go-runtime/ftl/ftltest/ftltest.go +++ b/go-runtime/ftl/ftltest/ftltest.go @@ -26,7 +26,6 @@ import ( ) type OptionsState struct { - configs map[string][]byte secrets map[string][]byte databases map[string]modulecontext.Database mockVerbs map[schema.RefKey]modulecontext.Verb @@ -38,7 +37,6 @@ type Option func(context.Context, *OptionsState) error // Context suitable for use in testing FTL verbs with provided options func Context(options ...Option) context.Context { state := &OptionsState{ - configs: make(map[string][]byte), secrets: make(map[string][]byte), databases: make(map[string]modulecontext.Database), mockVerbs: make(map[schema.RefKey]modulecontext.Verb), @@ -55,7 +53,7 @@ func Context(options ...Option) context.Context { } } - builder := modulecontext.NewBuilder(name).AddConfigs(state.configs).AddSecrets(state.secrets).AddDatabases(state.databases) + builder := modulecontext.NewBuilder(name).AddSecrets(state.secrets).AddDatabases(state.databases) builder = builder.UpdateForTesting(state.mockVerbs, state.allowDirectVerbBehavior, newFakeLeaseClient()) return builder.Build().ApplyToContext(ctx) } @@ -108,8 +106,12 @@ func WithProjectFiles(paths ...string) Option { if err != nil { return fmt.Errorf("could not read configs: %w", err) } + + fftl := internal.FromContext(ctx).(*fakeFTL) //nolint:forcetypeassert for name, data := range configs { - state.configs[name] = data + if err := fftl.setConfig(name, json.RawMessage(data)); err != nil { + return err + } } sm, err := cf.NewDefaultSecretsManagerFromConfig(ctx, paths, "") @@ -140,11 +142,10 @@ func WithConfig[T ftl.ConfigType](config ftl.ConfigValue[T], value T) Option { if config.Module != reflection.Module() { return fmt.Errorf("config %v does not match current module %s", config.Module, reflection.Module()) } - data, err := json.Marshal(value) - if err != nil { + fftl := internal.FromContext(ctx).(*fakeFTL) //nolint:forcetypeassert + if err := fftl.setConfig(config.Name, value); err != nil { return err } - state.configs[config.Name] = data return nil } } @@ -336,12 +337,8 @@ func WithCallsAllowedWithinModule() Option { // ) func WhenMap[T, U any](mapper *ftl.MapHandle[T, U], fake func(context.Context) (any, error)) Option { return func(ctx context.Context, state *OptionsState) error { - someFTL := internal.FromContext(ctx) - fakeFTL, ok := someFTL.(*fakeFTL) - if !ok { - return fmt.Errorf("could not retrieve fakeFTL for saving a mock Map in test") - } - fakeFTL.addMapMock(mapper, fake) + fftl := internal.FromContext(ctx).(*fakeFTL) //nolint:forcetypeassert + fftl.addMapMock(mapper, fake) return nil } } @@ -352,12 +349,8 @@ func WhenMap[T, U any](mapper *ftl.MapHandle[T, U], fake func(context.Context) ( // Any overrides provided by calling WhenMap(...) will take precedence. func WithMapsAllowed() Option { return func(ctx context.Context, state *OptionsState) error { - someFTL := internal.FromContext(ctx) - fakeFTL, ok := someFTL.(*fakeFTL) - if !ok { - return fmt.Errorf("could not retrieve fakeFTL for saving a mock Map in test") - } - fakeFTL.startAllowingMapCalls() + fftl := internal.FromContext(ctx).(*fakeFTL) //nolint:forcetypeassert + fftl.startAllowingMapCalls() return nil } } diff --git a/go-runtime/ftl/map_test.go b/go-runtime/ftl/map_test.go index 14c3aba2c8..38f17f0cea 100644 --- a/go-runtime/ftl/map_test.go +++ b/go-runtime/ftl/map_test.go @@ -6,8 +6,10 @@ import ( "strconv" "testing" - "github.com/TBD54566975/ftl/go-runtime/internal" "github.com/alecthomas/assert/v2" + + "github.com/TBD54566975/ftl/go-runtime/internal" + "github.com/TBD54566975/ftl/internal/modulecontext" ) type intHandle int @@ -15,7 +17,7 @@ type intHandle int func (s intHandle) Get(ctx context.Context) int { return int(s) } func TestMapPanic(t *testing.T) { - ctx := internal.WithContext(context.Background(), internal.New()) + ctx := internal.WithContext(context.Background(), internal.New(modulecontext.Empty("test"))) n := intHandle(1) once := Map(n, func(ctx context.Context, n int) (string, error) { return "", fmt.Errorf("test error %d", n) @@ -26,7 +28,7 @@ func TestMapPanic(t *testing.T) { } func TestMapGet(t *testing.T) { - ctx := internal.WithContext(context.Background(), internal.New()) + ctx := internal.WithContext(context.Background(), internal.New(modulecontext.Empty("test"))) n := intHandle(1) once := Map(n, func(ctx context.Context, n int) (string, error) { return strconv.Itoa(n), nil diff --git a/go-runtime/internal/api.go b/go-runtime/internal/api.go index 980c926d25..5c1e80064d 100644 --- a/go-runtime/internal/api.go +++ b/go-runtime/internal/api.go @@ -20,6 +20,9 @@ type FTL interface { // CallMap calls Get on an instance of an ftl.Map. CallMap(ctx context.Context, mapper any, mapImpl func(context.Context) (any, error)) any + + // GetConfig unmarshals a configuration value into dest. + GetConfig(ctx context.Context, name string, dest any) error } type ftlContextKey struct{} diff --git a/go-runtime/internal/impl.go b/go-runtime/internal/impl.go index 530468a7e5..b2d2e27f9e 100644 --- a/go-runtime/internal/impl.go +++ b/go-runtime/internal/impl.go @@ -13,17 +13,24 @@ import ( "github.com/TBD54566975/ftl/backend/schema" "github.com/TBD54566975/ftl/go-runtime/encoding" "github.com/TBD54566975/ftl/go-runtime/ftl/reflection" + "github.com/TBD54566975/ftl/internal/modulecontext" "github.com/TBD54566975/ftl/internal/rpc" ) // RealFTL is the real implementation of the [internal.FTL] interface using the Controller. -type RealFTL struct{} +type RealFTL struct { + mctx modulecontext.ModuleContext +} // New creates a new [RealFTL] -func New() *RealFTL { return &RealFTL{} } +func New(mctx modulecontext.ModuleContext) *RealFTL { return &RealFTL{mctx: mctx} } var _ FTL = &RealFTL{} +func (r *RealFTL) GetConfig(ctx context.Context, name string, dest any) error { + return r.mctx.GetConfig(name, dest) +} + func (r *RealFTL) FSMSend(ctx context.Context, fsm, instance string, event any) error { client := rpc.ClientFromContext[ftlv1connect.VerbServiceClient](ctx) body, err := encoding.Marshal(event) diff --git a/go-runtime/server/server.go b/go-runtime/server/server.go index b78b7d9685..e3fe954d71 100644 --- a/go-runtime/server/server.go +++ b/go-runtime/server/server.go @@ -33,7 +33,6 @@ type UserVerbConfig struct { // This function is intended to be used by the code generator. func NewUserVerbServer(moduleName string, handlers ...Handler) plugin.Constructor[ftlv1connect.VerbServiceHandler, UserVerbConfig] { return func(ctx context.Context, uc UserVerbConfig) (context.Context, ftlv1connect.VerbServiceHandler, error) { - ctx = internal.WithContext(ctx, internal.New()) verbServiceClient := rpc.Dial(ftlv1connect.NewVerbServiceClient, uc.FTLEndpoint.String(), log.Error) ctx = rpc.ContextWithClient(ctx, verbServiceClient) @@ -48,6 +47,7 @@ func NewUserVerbServer(moduleName string, handlers ...Handler) plugin.Constructo return nil, nil, err } ctx = moduleCtx.ApplyToContext(ctx) + ctx = internal.WithContext(ctx, internal.New(moduleCtx)) err = observability.Init(ctx, moduleName, "HEAD", uc.ObservabilityConfig) if err != nil { diff --git a/internal/modulecontext/module_context.go b/internal/modulecontext/module_context.go index 3e7cf7c4d5..9ecc761615 100644 --- a/internal/modulecontext/module_context.go +++ b/internal/modulecontext/module_context.go @@ -40,6 +40,10 @@ type Builder ModuleContext type contextKeyModuleContext struct{} +func Empty(module string) ModuleContext { + return NewBuilder(module).Build() +} + // NewBuilder creates a new blank Builder for the given module. func NewBuilder(module string) *Builder { return &Builder{