Skip to content

Commit 045ba96

Browse files
committed
WIP: [PoC] fx.Evaluate
**This is just a PoC at this time and not yet ready for review.** ## Details This change introduces the `fx.Evalute` option, which allows for dynamic generation of provides, invokes, decorates, and other evaluates based on the state of existing dependencies. It addresses one of the more significant remaining pain points with usage of Fx in large applications and ecosystems: the ability to programmatically generate parts of the dependency graph. Concretely, this makes the following possible: ``` fx.Evaluate(func(cfg *Config) fx.Option { if cfg.Environment == "production" { return fx.Provide(func(*sql.DB) Repository { return &sqlRepository{db: db} }), } else { return fx.Provide(func() Repository { return &memoryRepository{} }) } }), fx.Provide(func(...) *sql.DB { ... }), ``` With fx.Evaluate, the dependency on `*sql.DB` is present in the graph only in production environments. In development environments, the dependency connection is absent, and therefore the database connection is never established. (Today, attempting to write an Fx module that switches on the backend like this will result in the SQL connection being established even in development environments.) **TODO** - [ ] Decide whether nilling out the slice is the right approach for tracking what's been already run - [ ] fx.Module handling - [ ] fx.WithLogger handling - [ ] Testing with every other feature - [ ] Documentation
1 parent 0ad8a04 commit 045ba96

File tree

4 files changed

+310
-0
lines changed

4 files changed

+310
-0
lines changed

app.go

+17
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,23 @@ func New(opts ...Option) *App {
504504
return app
505505
}
506506

507+
// At this point, we can run the fx.Evaluates (if any).
508+
// As long as there's at least one evaluate per iteration,
509+
// we'll have to keep unwinding.
510+
//
511+
// Keep evaluating until there are no more evaluates to run.
512+
for app.root.evaluateAll() > 0 {
513+
// TODO: is communicating the number of evalutes the best way?
514+
if app.err != nil {
515+
return app
516+
}
517+
518+
// TODO: fx.Module inside evaluates needs to build subscopes.
519+
app.root.provideAll()
520+
app.err = multierr.Append(app.err, app.root.decorateAll())
521+
// TODO: fx.WithLogger allowed inside an evaluate?
522+
}
523+
507524
if err := app.root.invokeAll(); err != nil {
508525
app.err = err
509526

evaluate.go

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// Copyright (c) 2024 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package fx
22+
23+
import (
24+
"fmt"
25+
"reflect"
26+
"strings"
27+
28+
"go.uber.org/fx/internal/fxreflect"
29+
)
30+
31+
// Evaluate specifies one or more evaluation functions.
32+
// These are functions that accept dependencies from the graph
33+
// and return an fx.Option.
34+
// They may have the following signatures:
35+
//
36+
// func(...) fx.Option
37+
// func(...) (fx.Option, error)
38+
//
39+
// These functions are run after provides and decorates.
40+
// The resulting options are applied to the graph,
41+
// and may introduce new provides, invokes, decorates, or evaluates.
42+
//
43+
// The effect of this is that parts of the graph can be dynamically generated
44+
// based on dependency values.
45+
//
46+
// For example, a function with a dependency on a configuration struct
47+
// could conditionally provide different implementations based on the value.
48+
//
49+
// fx.Evaluate(func(cfg *Config) fx.Option {
50+
// if cfg.Environment == "production" {
51+
// return fx.Provide(func(*sql.DB) Repository {
52+
// return &sqlRepository{db: db}
53+
// }),
54+
// } else {
55+
// return fx.Provide(func() Repository {
56+
// return &memoryRepository{}
57+
// })
58+
// }
59+
// })
60+
//
61+
// This is different from a normal provide that inspects the configuration
62+
// because the dependency on '*sql.DB' is completely absent in the graph
63+
// if the configuration is not "production".
64+
func Evaluate(fns ...any) Option {
65+
return evaluateOption{
66+
Targets: fns,
67+
Stack: fxreflect.CallerStack(1, 0),
68+
}
69+
}
70+
71+
type evaluateOption struct {
72+
Targets []any
73+
Stack fxreflect.Stack
74+
}
75+
76+
func (o evaluateOption) apply(mod *module) {
77+
for _, target := range o.Targets {
78+
mod.evaluates = append(mod.evaluates, evaluate{
79+
Target: target,
80+
Stack: o.Stack,
81+
})
82+
}
83+
}
84+
85+
func (o evaluateOption) String() string {
86+
items := make([]string, len(o.Targets))
87+
for i, target := range o.Targets {
88+
items[i] = fxreflect.FuncName(target)
89+
}
90+
return fmt.Sprintf("fx.Evaluate(%s)", strings.Join(items, ", "))
91+
}
92+
93+
type evaluate struct {
94+
Target any
95+
Stack fxreflect.Stack
96+
}
97+
98+
func runEvaluate(m *module, e evaluate) (err error) {
99+
target := e.Target
100+
defer func() {
101+
if err != nil {
102+
err = fmt.Errorf("fx.Evaluate(%v) from:\n%+vFailed: %w", target, e.Stack, err)
103+
}
104+
}()
105+
106+
// target is a function returning (Option, error).
107+
// Use reflection to build a function with the same parameters,
108+
// and invoke that in the container.
109+
targetV := reflect.ValueOf(target)
110+
targetT := targetV.Type()
111+
inTypes := make([]reflect.Type, targetT.NumIn())
112+
for i := range targetT.NumIn() {
113+
inTypes[i] = targetT.In(i)
114+
}
115+
outTypes := []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}
116+
117+
// TODO: better way to extract information from the container
118+
var opt Option
119+
invokeFn := reflect.MakeFunc(
120+
reflect.FuncOf(inTypes, outTypes, false),
121+
func(args []reflect.Value) []reflect.Value {
122+
out := targetV.Call(args)
123+
switch len(out) {
124+
case 2:
125+
if err, _ := out[1].Interface().(error); err != nil {
126+
return []reflect.Value{reflect.ValueOf(err)}
127+
}
128+
129+
fallthrough
130+
case 1:
131+
opt, _ = out[0].Interface().(Option)
132+
133+
default:
134+
panic("TODO: validation")
135+
}
136+
137+
return []reflect.Value{
138+
reflect.Zero(reflect.TypeOf((*error)(nil)).Elem()),
139+
}
140+
},
141+
).Interface()
142+
if err := m.scope.Invoke(invokeFn); err != nil {
143+
return err
144+
}
145+
146+
if opt == nil {
147+
// Assume no-op.
148+
return nil
149+
}
150+
151+
opt.apply(m)
152+
return nil
153+
}

evaluate_test.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) 2025 Uber Technologies, Inc.
2+
//
3+
// Permission is hereby granted, free of charge, to any person obtaining a copy
4+
// of this software and associated documentation files (the "Software"), to deal
5+
// in the Software without restriction, including without limitation the rights
6+
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
// copies of the Software, and to permit persons to whom the Software is
8+
// furnished to do so, subject to the following conditions:
9+
//
10+
// The above copyright notice and this permission notice shall be included in
11+
// all copies or substantial portions of the Software.
12+
//
13+
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
// THE SOFTWARE.
20+
21+
package fx_test
22+
23+
import (
24+
"bytes"
25+
"io"
26+
"testing"
27+
28+
"github.com/stretchr/testify/assert"
29+
"go.uber.org/fx"
30+
"go.uber.org/fx/fxtest"
31+
)
32+
33+
func TestEvaluate(t *testing.T) {
34+
t.Run("ProvidesOptions", func(t *testing.T) {
35+
type t1 struct{}
36+
type t2 struct{}
37+
38+
var evaluated, provided, invoked bool
39+
app := fxtest.New(t,
40+
fx.Evaluate(func() fx.Option {
41+
evaluated = true
42+
return fx.Provide(func() t1 {
43+
provided = true
44+
return t1{}
45+
})
46+
}),
47+
fx.Provide(func(t1) t2 { return t2{} }),
48+
fx.Invoke(func(t2) {
49+
invoked = true
50+
}),
51+
)
52+
defer app.RequireStart().RequireStop()
53+
54+
assert.True(t, evaluated, "Evaluated function was not called")
55+
assert.True(t, provided, "Provided function was not called")
56+
assert.True(t, invoked, "Invoked function was not called")
57+
})
58+
59+
t.Run("OptionalDependency", func(t *testing.T) {
60+
type Config struct{ Dev bool }
61+
62+
newBufWriter := func(b *bytes.Buffer) io.Writer {
63+
return b
64+
}
65+
66+
newDiscardWriter := func() io.Writer {
67+
return io.Discard
68+
}
69+
70+
newWriter := func(cfg Config) fx.Option {
71+
if cfg.Dev {
72+
return fx.Provide(newDiscardWriter)
73+
}
74+
75+
return fx.Provide(newBufWriter)
76+
}
77+
78+
t.Run("NoDependency", func(t *testing.T) {
79+
var got io.Writer
80+
app := fxtest.New(t,
81+
fx.Evaluate(newWriter),
82+
fx.Provide(
83+
func() *bytes.Buffer {
84+
t.Errorf("unexpected call to *bytes.Buffer")
85+
return nil
86+
},
87+
),
88+
fx.Supply(Config{Dev: true}),
89+
fx.Populate(&got),
90+
)
91+
defer app.RequireStart().RequireStop()
92+
93+
assert.NotNil(t, got)
94+
_, _ = io.WriteString(got, "hello")
95+
})
96+
97+
t.Run("WithDependency", func(t *testing.T) {
98+
var (
99+
buf bytes.Buffer
100+
got io.Writer
101+
)
102+
app := fxtest.New(t,
103+
fx.Evaluate(newWriter),
104+
fx.Supply(&buf, Config{Dev: false}),
105+
fx.Populate(&got),
106+
)
107+
defer app.RequireStart().RequireStop()
108+
109+
assert.NotNil(t, got)
110+
_, _ = io.WriteString(got, "hello")
111+
assert.Equal(t, "hello", buf.String())
112+
})
113+
})
114+
}

module.go

+26
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ type module struct {
125125
provides []provide
126126
invokes []invoke
127127
decorators []decorator
128+
evaluates []evaluate
128129
modules []*module
129130
app *App
130131
log fxevent.Logger
@@ -174,6 +175,7 @@ func (m *module) provideAll() {
174175
for _, p := range m.provides {
175176
m.provide(p)
176177
}
178+
m.provides = nil
177179

178180
for _, m := range m.modules {
179181
m.provideAll()
@@ -264,6 +266,7 @@ func (m *module) installAllEventLoggers() {
264266
}
265267
}
266268
m.fallbackLogger = nil
269+
m.logConstructor = nil
267270
} else if m.parent != nil {
268271
m.log = m.parent.log
269272
}
@@ -308,6 +311,7 @@ func (m *module) invokeAll() error {
308311
return err
309312
}
310313
}
314+
m.invokes = nil
311315

312316
return nil
313317
}
@@ -334,6 +338,7 @@ func (m *module) decorateAll() error {
334338
return err
335339
}
336340
}
341+
m.decorators = nil
337342

338343
for _, m := range m.modules {
339344
if err := m.decorateAll(); err != nil {
@@ -405,3 +410,24 @@ func (m *module) replace(d decorator) error {
405410
})
406411
return err
407412
}
413+
414+
func (m *module) evaluateAll() (count int) {
415+
for _, e := range m.evaluates {
416+
m.evaluate(e)
417+
count++
418+
}
419+
m.evaluates = nil
420+
421+
for _, m := range m.modules {
422+
count += m.evaluateAll()
423+
}
424+
425+
return count
426+
}
427+
428+
func (m *module) evaluate(e evaluate) {
429+
// TODO: events
430+
if err := runEvaluate(m, e); err != nil {
431+
m.app.err = err
432+
}
433+
}

0 commit comments

Comments
 (0)