github.com/TBD54566975/ftl@v0.219.0/internal/modulecontext/module_context.go (about)

     1  package modulecontext
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"encoding/json"
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/alecthomas/types/optional"
    11  	_ "github.com/jackc/pgx/v5/stdlib" // SQL driver
    12  
    13  	"github.com/TBD54566975/ftl/backend/schema"
    14  	"github.com/TBD54566975/ftl/internal/reflect"
    15  )
    16  
    17  // Verb is a function that takes a request and returns a response but is not constrained by request/response type like ftl.Verb
    18  //
    19  // It is used for definitions of mock verbs as well as real implementations of verbs to directly execute
    20  type Verb func(ctx context.Context, req any) (resp any, err error)
    21  
    22  // ModuleContext holds the context needed for a module, including configs, secrets and DSNs
    23  //
    24  // ModuleContext is immutable
    25  type ModuleContext struct {
    26  	module    string
    27  	configs   map[string][]byte
    28  	secrets   map[string][]byte
    29  	databases map[string]Database
    30  
    31  	isTesting               bool
    32  	mockVerbs               map[schema.RefKey]Verb
    33  	allowDirectVerbBehavior bool
    34  }
    35  
    36  // Builder is used to build a ModuleContext
    37  type Builder ModuleContext
    38  
    39  type contextKeyModuleContext struct{}
    40  
    41  // NewBuilder creates a new blank Builder for the given module.
    42  func NewBuilder(module string) *Builder {
    43  	return &Builder{
    44  		module:    module,
    45  		configs:   map[string][]byte{},
    46  		secrets:   map[string][]byte{},
    47  		databases: map[string]Database{},
    48  		mockVerbs: map[schema.RefKey]Verb{},
    49  	}
    50  }
    51  
    52  // AddConfigs adds configuration values (as bytes) to the builder
    53  func (b *Builder) AddConfigs(configs map[string][]byte) *Builder {
    54  	for name, data := range configs {
    55  		b.configs[name] = data
    56  	}
    57  	return b
    58  }
    59  
    60  // AddSecrets adds configuration values (as bytes) to the builder
    61  func (b *Builder) AddSecrets(secrets map[string][]byte) *Builder {
    62  	for name, data := range secrets {
    63  		b.secrets[name] = data
    64  	}
    65  	return b
    66  }
    67  
    68  // AddDatabases adds databases to the builder
    69  func (b *Builder) AddDatabases(databases map[string]Database) *Builder {
    70  	for name, db := range databases {
    71  		b.databases[name] = db
    72  	}
    73  	return b
    74  }
    75  
    76  // UpdateForTesting marks the builder as part of a test environment and adds mock verbs and flags for other test features.
    77  func (b *Builder) UpdateForTesting(mockVerbs map[schema.RefKey]Verb, allowDirectVerbBehavior bool) *Builder {
    78  	b.isTesting = true
    79  	for name, verb := range mockVerbs {
    80  		b.mockVerbs[name] = verb
    81  	}
    82  	b.allowDirectVerbBehavior = allowDirectVerbBehavior
    83  	return b
    84  }
    85  
    86  func (b *Builder) Build() ModuleContext {
    87  	return ModuleContext(reflect.DeepCopy(*b))
    88  }
    89  
    90  // FromContext returns the ModuleContext attached to a context.
    91  func FromContext(ctx context.Context) ModuleContext {
    92  	m, ok := ctx.Value(contextKeyModuleContext{}).(ModuleContext)
    93  	if !ok {
    94  		panic("no ModuleContext in context")
    95  	}
    96  	return m
    97  }
    98  
    99  // ApplyToContext returns a Go context.Context with ModuleContext added.
   100  func (m ModuleContext) ApplyToContext(ctx context.Context) context.Context {
   101  	return context.WithValue(ctx, contextKeyModuleContext{}, m)
   102  }
   103  
   104  // GetConfig reads a configuration value for the module.
   105  //
   106  // "value" must be a pointer to a Go type that can be unmarshalled from JSON.
   107  func (m ModuleContext) GetConfig(name string, value any) error {
   108  	data, ok := m.configs[name]
   109  	if !ok {
   110  		return fmt.Errorf("no config value for %q", name)
   111  	}
   112  	return json.Unmarshal(data, value)
   113  }
   114  
   115  // GetSecret reads a secret value for the module.
   116  //
   117  // "value" must be a pointer to a Go type that can be unmarshalled from JSON.
   118  func (m ModuleContext) GetSecret(name string, value any) error {
   119  	data, ok := m.secrets[name]
   120  	if !ok {
   121  		return fmt.Errorf("no secret value for %q", name)
   122  	}
   123  	return json.Unmarshal(data, value)
   124  }
   125  
   126  // GetDatabase gets a database connection
   127  //
   128  // Returns an error if no database with that name is found or it is not the expected type
   129  // When in a testing context (via ftltest), an error is returned if the database is not a test database
   130  func (m ModuleContext) GetDatabase(name string, dbType DBType) (*sql.DB, error) {
   131  	db, ok := m.databases[name]
   132  	if !ok {
   133  		return nil, fmt.Errorf("missing DSN for database %s", name)
   134  	}
   135  	if db.DBType != dbType {
   136  		return nil, fmt.Errorf("database %s does not match expected type of %s", name, dbType)
   137  	}
   138  	if m.isTesting && !db.isTestDB {
   139  		return nil, fmt.Errorf("accessing non-test database %q while testing: try adding ftltest.WithDatabase(db) as an option with ftltest.Context(...)", name)
   140  	}
   141  	return db.db, nil
   142  }
   143  
   144  // BehaviorForVerb returns what to do to execute a verb
   145  //
   146  // This allows module context to dictate behavior based on testing options
   147  // Returning optional.Nil indicates the verb should be executed normally via the controller
   148  func (m ModuleContext) BehaviorForVerb(ref schema.Ref) (optional.Option[VerbBehavior], error) {
   149  	if mock, ok := m.mockVerbs[ref.ToRefKey()]; ok {
   150  		return optional.Some(VerbBehavior(MockBehavior{Mock: mock})), nil
   151  	} else if m.allowDirectVerbBehavior && ref.Module == m.module {
   152  		return optional.Some(VerbBehavior(DirectBehavior{})), nil
   153  	} else if m.isTesting {
   154  		if ref.Module == m.module {
   155  			return optional.None[VerbBehavior](), fmt.Errorf("no mock found: provide a mock with ftltest.WhenVerb(%s, ...) or enable all calls within the module with ftltest.WithCallsAllowedWithinModule()", strings.ToUpper(ref.Name[:1])+ref.Name[1:])
   156  		}
   157  		return optional.None[VerbBehavior](), fmt.Errorf("no mock found: provide a mock with ftltest.WhenVerb(%s.%s, ...)", ref.Module, strings.ToUpper(ref.Name[:1])+ref.Name[1:])
   158  	}
   159  	return optional.None[VerbBehavior](), nil
   160  }
   161  
   162  // VerbBehavior indicates how to execute a verb
   163  type VerbBehavior interface {
   164  	Call(ctx context.Context, verb Verb, request any) (any, error)
   165  }
   166  
   167  // DirectBehavior indicates that the verb should be executed by calling the function directly (for testing)
   168  type DirectBehavior struct{}
   169  
   170  func (DirectBehavior) Call(ctx context.Context, verb Verb, req any) (any, error) {
   171  	return verb(ctx, req)
   172  }
   173  
   174  var _ VerbBehavior = DirectBehavior{}
   175  
   176  // MockBehavior indicates the verb has a mock implementation
   177  type MockBehavior struct {
   178  	Mock Verb
   179  }
   180  
   181  func (b MockBehavior) Call(ctx context.Context, verb Verb, req any) (any, error) {
   182  	return b.Mock(ctx, req)
   183  }