Skip to content

Commit 1124297

Browse files
Add documentation and tests for soft group values (#909)
This adds documentation regarding soft group values feature and also adds test cases to check this feature works with modules, Supply, Decorate and Annotate. Co-authored-by: Sung Yoon Whang <sungyoon@uber.com>
1 parent d88feb8 commit 1124297

5 files changed

Lines changed: 249 additions & 2 deletions

File tree

annotated_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,18 @@ func TestAnnotatedWrongUsage(t *testing.T) {
444444
)
445445
assert.Contains(t, app.Err().Error(), "embeds a dig.In", "expected error when result types were annotated")
446446
})
447+
448+
t.Run("invalid group option", func(t *testing.T) {
449+
t.Parallel()
450+
451+
app := NewForTest(t,
452+
fx.Provide(
453+
fx.Annotate(func() string { return "sad times" },
454+
fx.ResultTags(`group:"foo,soft"`)),
455+
),
456+
)
457+
assert.Contains(t, app.Err().Error(), "cannot use soft with result value groups", "expected error when invalid group option is provided")
458+
})
447459
}
448460

449461
func TestAnnotatedString(t *testing.T) {
@@ -642,6 +654,34 @@ func TestAnnotate(t *testing.T) {
642654
assert.Contains(t, err.Error(), `missing type: []*fx_test.a[name="a"]`)
643655
})
644656

657+
t.Run("Invoke function with soft group param", func(t *testing.T) {
658+
t.Parallel()
659+
newF := func(foos []int, bar string) {
660+
assert.ElementsMatch(t, []int{10}, foos)
661+
}
662+
app := fxtest.New(t,
663+
fx.Provide(
664+
fx.Annotate(
665+
func() (int, string) { return 10, "hello" },
666+
fx.ResultTags(`group:"foos"`),
667+
),
668+
fx.Annotate(
669+
func() int {
670+
require.FailNow(t, "this function should not be called")
671+
return 20
672+
},
673+
fx.ResultTags(`group:"foos"`),
674+
),
675+
),
676+
fx.Invoke(
677+
fx.Annotate(newF, fx.ParamTags(`group:"foos,soft"`)),
678+
),
679+
)
680+
681+
defer app.RequireStart().RequireStop()
682+
require.NoError(t, app.Err())
683+
})
684+
645685
t.Run("Invoke variadic function with multiple params", func(t *testing.T) {
646686
t.Parallel()
647687

@@ -984,7 +1024,6 @@ func TestAnnotate(t *testing.T) {
9841024
assert.Contains(t, err.Error(), "invalid annotation function func(fx_test.B) string")
9851025
assert.Contains(t, err.Error(), "fx.In structs cannot be annotated")
9861026
})
987-
9881027
}
9891028

9901029
func assertApp(
@@ -1279,7 +1318,6 @@ func TestHookAnnotations(t *testing.T) {
12791318
require.Equal(t, "constructor", <-ch)
12801319
require.Equal(t, "decorated", <-ch)
12811320
})
1282-
12831321
}
12841322

12851323
func TestHookAnnotationFailures(t *testing.T) {

decorate_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ package fx_test
2222

2323
import (
2424
"errors"
25+
"strings"
2526
"testing"
2627

2728
"github.com/stretchr/testify/assert"
@@ -185,6 +186,55 @@ func TestDecorateSuccess(t *testing.T) {
185186
defer app.RequireStart().RequireStop()
186187
})
187188

189+
t.Run("decorator with soft value group", func(t *testing.T) {
190+
app := fxtest.New(t,
191+
fx.Provide(
192+
fx.Annotate(
193+
func() (string, int) { return "cheeseburger", 15 },
194+
fx.ResultTags(`group:"burger"`, `group:"potato"`),
195+
),
196+
),
197+
fx.Provide(
198+
fx.Annotate(
199+
func() (string, int) { return "mushroomburger", 35 },
200+
fx.ResultTags(`group:"burger"`, `group:"potato"`),
201+
),
202+
),
203+
fx.Provide(
204+
fx.Annotate(
205+
func() string {
206+
require.FailNow(t, "should not be called")
207+
return "veggieburger"
208+
},
209+
fx.ResultTags(`group:"burger"`, `group:"potato"`),
210+
),
211+
),
212+
fx.Decorate(
213+
fx.Annotate(
214+
func(burgers []string) []string {
215+
retBurg := make([]string, len(burgers))
216+
for i, burger := range burgers {
217+
retBurg[i] = strings.ToUpper(burger)
218+
}
219+
return retBurg
220+
},
221+
fx.ParamTags(`group:"burger,soft"`),
222+
fx.ResultTags(`group:"burger"`),
223+
),
224+
),
225+
fx.Invoke(
226+
fx.Annotate(
227+
func(burgers []string, fries []int) {
228+
assert.ElementsMatch(t, []string{"CHEESEBURGER", "MUSHROOMBURGER"}, burgers)
229+
},
230+
fx.ParamTags(`group:"burger,soft"`, `group:"potato"`),
231+
),
232+
),
233+
)
234+
defer app.RequireStart().RequireStop()
235+
require.NoError(t, app.Err())
236+
})
237+
188238
t.Run("decorator with optional parameter", func(t *testing.T) {
189239
type Config struct {
190240
Name string

inout.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,71 @@ import "go.uber.org/dig"
163163
// Note that values in a value group are unordered. Fx makes no guarantees
164164
// about the order in which these values will be produced.
165165
//
166+
// To declare a soft relationship between a group and its constructors, use
167+
// the `soft` option on the group tag (`group:"[groupname],soft"`), this
168+
// option can only be used for input parameters, e.g. `fx.In` structures.
169+
// A soft group will be populated only with values from already-executed
170+
// constructors.
171+
//
172+
// type Params struct {
173+
// fx.In
174+
//
175+
// Handlers []Handler `group:"server"`
176+
// Logger *zap.Logger
177+
// }
178+
//
179+
// NewHandlerAndLogger := func() (Handler, *zap.Logger) { ... }
180+
// NewHandler := func() Handler { ... }
181+
// Foo := func(Params) { ... }
182+
//
183+
// app := fx.New(
184+
// fx.Provide(NewHandlerAndLogger),
185+
// fx.Provide(NewHandler),
186+
// fx.Invoke(Foo),
187+
// )
188+
//
189+
// The only constructor called is `NewHandler`, because this also provides
190+
// `*zap.Logger` needed in the `Params` struct received by `foo`
191+
//
192+
// In the next example, the slice `s` isn't populated as the provider would be
193+
// called only because of `strings` soft group value
194+
//
195+
// app := fx.New(
196+
// fx.Provide(
197+
// fx.Annotate(
198+
// func() (string,int) { return "hello" },
199+
// fx.ResultTags(`group:"strings"`),
200+
// ),
201+
// ),
202+
// fx.Invoke(
203+
// fx.Annotate(func(s []string) {
204+
// // s will be an empty slice
205+
// }, fx.ParamTags(`group:"strings,soft"`)),
206+
// ),
207+
// )
208+
//
209+
// In the next example, the slice `s` will be populated because there is a
210+
// consumer for the same type which hasn't a `soft` dependency
211+
//
212+
// app := fx.New(
213+
// fx.Provide(
214+
// fx.Annotate(
215+
// func() string { "hello" },
216+
// fx.ResultTags(`group:"strings"`),
217+
// ),
218+
// ),
219+
// fx.Invoke(
220+
// fx.Annotate(func(b []string) {
221+
// // b will be ["hello"]
222+
// }, fx.ParamTags(`group:"strings"`)),
223+
// ),
224+
// fx.Invoke(
225+
// fx.Annotate(func(s []string) {
226+
// // s will be ["hello"]
227+
// }, fx.ParamTags(`group:"strings,soft"`)),
228+
// ),
229+
// )
230+
//
166231
// # Unexported fields
167232
//
168233
// By default, a type that embeds fx.In may not have any unexported fields. The

module_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,43 @@ func TestModuleSuccess(t *testing.T) {
7676
defer app.RequireStart().RequireStop()
7777
})
7878

79+
t.Run("provide a value to a soft group value from nested modules", func(t *testing.T) {
80+
t.Parallel()
81+
type Param struct {
82+
fx.In
83+
84+
Foos []string `group:"foo,soft"`
85+
Bar int
86+
}
87+
type Result struct {
88+
fx.Out
89+
90+
Foo string `group:"foo"`
91+
Bar int
92+
}
93+
app := fxtest.New(t,
94+
fx.Module("child",
95+
fx.Module("grandchild",
96+
fx.Provide(fx.Annotate(
97+
func() string {
98+
require.FailNow(t, "should not be called")
99+
return "there"
100+
},
101+
fx.ResultTags(`group:"foo"`),
102+
)),
103+
fx.Provide(func() Result {
104+
return Result{Foo: "hello", Bar: 10}
105+
}),
106+
),
107+
),
108+
fx.Invoke(func(p Param) {
109+
assert.ElementsMatch(t, []string{"hello"}, p.Foos)
110+
}),
111+
)
112+
defer app.RequireStart().RequireStop()
113+
require.NoError(t, app.Err())
114+
})
115+
79116
t.Run("invoke from nested module", func(t *testing.T) {
80117
t.Parallel()
81118
invokeRan := false
@@ -271,6 +308,22 @@ func TestModuleFailures(t *testing.T) {
271308
assert.Contains(t, err.Error(), "cannot apply more than one line of ParamTags")
272309
})
273310

311+
t.Run("soft provided to fx.Out struct", func(t *testing.T) {
312+
t.Parallel()
313+
314+
type Result struct {
315+
fx.Out
316+
317+
Bars []int `group:"bar,soft"`
318+
}
319+
app := NewForTest(t,
320+
fx.Provide(func() Result { return Result{Bars: []int{1, 2, 3}} }),
321+
)
322+
err := app.Err()
323+
require.Error(t, err, "failed to create app")
324+
assert.Contains(t, err.Error(), "cannot use soft with result value groups")
325+
})
326+
274327
t.Run("provider in Module fails", func(t *testing.T) {
275328
t.Parallel()
276329

supply_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,45 @@ func TestSupply(t *testing.T) {
153153
require.Error(t, supplied[1].(*fxevent.Supplied).Err)
154154
})
155155

156+
t.Run("SupplyToASoftGroup", func(t *testing.T) {
157+
t.Parallel()
158+
159+
type Param struct {
160+
fx.In
161+
162+
Foos []string `group:"foo,soft"`
163+
Bar []int `group:"bar"`
164+
}
165+
type Result struct {
166+
fx.Out
167+
168+
Foo string `group:"foo"`
169+
Bar int `group:"bar"`
170+
}
171+
app := fxtest.New(t,
172+
fx.Supply(
173+
Result{
174+
Foo: "sad",
175+
Bar: 20,
176+
}),
177+
fx.Supply(
178+
fx.Annotated{
179+
Target: 10,
180+
Group: "bar",
181+
},
182+
fx.Annotated{
183+
Target: "bye",
184+
Group: "foo",
185+
}),
186+
fx.Supply(fx.Annotated{
187+
Target: "hello",
188+
Group: "foo",
189+
}),
190+
fx.Invoke(func(p Param) {
191+
assert.ElementsMatch(t, []string{"sad"}, p.Foos)
192+
}),
193+
)
194+
195+
defer app.RequireStart().RequireStop()
196+
})
156197
}

0 commit comments

Comments
 (0)