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 }