Skip to content

Commit

Permalink
refactor: pull config from FTL runtime abstraction (#1708)
Browse files Browse the repository at this point in the history
This doesn't refactor secrets, but the same pattern can be applied
there.

Also switched to forcing a type assertion on `fakeFTL` as it's an
invariant of the testing system.

cc @matt2e
  • Loading branch information
alecthomas authored Jun 7, 2024
1 parent 5beccf7 commit 6f08f0f
Show file tree
Hide file tree
Showing 9 changed files with 60 additions and 30 deletions.
4 changes: 2 additions & 2 deletions go-runtime/ftl/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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))
}
Expand Down
4 changes: 2 additions & 2 deletions go-runtime/ftl/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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))
Expand Down
23 changes: 22 additions & 1 deletion go-runtime/ftl/ftltest/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ package ftltest

import (
"context"
"encoding/json"
"fmt"
"reflect"
"strings"

"github.com/TBD54566975/ftl/common/configuration"
"github.com/TBD54566975/ftl/go-runtime/internal"
)

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
Expand All @@ -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)
}
Expand Down
31 changes: 12 additions & 19 deletions go-runtime/ftl/ftltest/ftltest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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)
}
Expand Down Expand Up @@ -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, "")
Expand Down Expand Up @@ -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
}
}
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
}
}
8 changes: 5 additions & 3 deletions go-runtime/ftl/map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ 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

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)
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions go-runtime/internal/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
11 changes: 9 additions & 2 deletions go-runtime/internal/impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion go-runtime/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions internal/modulecontext/module_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down

0 comments on commit 6f08f0f

Please sign in to comment.