Skip to content

Commit a15d198

Browse files
authored
Add Export option to Provide (uber-go#306)
This adds Export option to Provide, which specifies that the provided constructor should be made available to all the Scopes in the same scope tree. For example: c := New() s1 := c.Scope("child 1") s2 := c.Scope("child 2") s2.Provide(func() *A { ... }) c.Invoke(func(a *A) { ... }) // errors s1.Invoke(func(a *A) { ...}) // errors will error out on Invoke because constructor providing *A is provided to the s2 only. With Export option, the child can provide this to all the Scopes in effect: c := New() s1 := c.Scope("child 1") s2 := c.Scope("child 2") s2.Provide(func() *A { ... }, Export(true)) c.Invoke(func(a *A) { ... }) // works! s1.Invoke(func(a *A) { ... }) // works! The implementation is quite simple - if this option is set, we provide it to the root Container. That provides the constructor visibility to all the scopes in effect.
1 parent cd97524 commit a15d198

File tree

4 files changed

+109
-0
lines changed

4 files changed

+109
-0
lines changed

Diff for: provide.go

+35
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type provideOptions struct {
4343
Info *ProvideInfo
4444
As []interface{}
4545
Location *digreflect.Func
46+
Exported bool
4647
}
4748

4849
func (o *provideOptions) Validate() error {
@@ -301,6 +302,34 @@ func (o provideLocationOption) applyProvideOption(opts *provideOptions) {
301302
opts.Location = o.loc
302303
}
303304

305+
// Export is a ProvideOption which specifies that the provided function should
306+
// be made available to all Scopes available in the application, regardless
307+
// of which Scope it was provided from. By default, it is false.
308+
//
309+
// For example,
310+
// c := New()
311+
// s1 := c.Scope("child 1")
312+
// s2:= c.Scope("child 2")
313+
// s1.Provide(func() *bytes.Buffer { ... })
314+
// does not allow the constructor returning *bytes.Buffer to be made available to
315+
// the root Container c or its sibling Scope s2.
316+
//
317+
// With Export, you can make this constructor available to all the Scopes:
318+
// s1.Provide(func() *bytes.Buffer { ... }, Export(true))
319+
func Export(export bool) ProvideOption {
320+
return provideExportOption{exported: export}
321+
}
322+
323+
type provideExportOption struct{ exported bool }
324+
325+
func (o provideExportOption) String() string {
326+
return fmt.Sprintf("Export(%v)", o.exported)
327+
}
328+
329+
func (o provideExportOption) applyProvideOption(opts *provideOptions) {
330+
opts.Exported = o.exported
331+
}
332+
304333
// provider encapsulates a user-provided constructor.
305334
type provider interface {
306335
// ID is a unique numerical identifier for this provider.
@@ -395,6 +424,12 @@ func (s *Scope) Provide(constructor interface{}, opts ...ProvideOption) error {
395424
}
396425

397426
func (s *Scope) provide(ctor interface{}, opts provideOptions) (err error) {
427+
// If Export option is provided to the constructor, this should be injected to the
428+
// root-level Scope (Container) to allow it to propagate to all other Scopes.
429+
if opts.Exported {
430+
s = s.rootScope()
431+
}
432+
398433
// For all scopes affected by this change,
399434
// take a snapshot of the current graph state before
400435
// we start making changes to it as we may need to

Diff for: provide_test.go

+5
Original file line numberDiff line numberDiff line change
@@ -86,3 +86,8 @@ func TestLocationForPCString(t *testing.T) {
8686
opt := LocationForPC(reflect.ValueOf(func() {}).Pointer())
8787
assert.Contains(t, fmt.Sprint(opt), `LocationForPC("go.uber.org/dig".TestLocationForPCString.func1 `)
8888
}
89+
90+
func TestExportString(t *testing.T) {
91+
assert.Equal(t, fmt.Sprint(Export(true)), "Export(true)")
92+
assert.Equal(t, fmt.Sprint(Export(false)), "Export(false)")
93+
}

Diff for: scope.go

+9
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,15 @@ func (s *Scope) cycleDetectedError(cycle []int) error {
244244
return errCycleDetected{Path: path, scope: s}
245245
}
246246

247+
// Returns the root Scope that can be reached from this Scope.
248+
func (s *Scope) rootScope() *Scope {
249+
curr := s
250+
for curr.parentScope != nil {
251+
curr = curr.parentScope
252+
}
253+
return curr
254+
}
255+
247256
// String representation of the entire Scope
248257
func (s *Scope) String() string {
249258
b := &bytes.Buffer{}

Diff for: scope_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,32 @@ func TestScopedOperations(t *testing.T) {
104104
assert.NoError(t, scope.Invoke(func(a *A) {}))
105105
}
106106
})
107+
108+
t.Run("provide with Export", func(t *testing.T) {
109+
// Scope tree:
110+
// root
111+
// / \
112+
// c1 c2
113+
// | / \
114+
// gc1 gc2 gc3 <-- Provide(func() *A)
115+
116+
root := New()
117+
var allScopes []*Scope
118+
119+
allScopes = append(allScopes, root.Scope("child 1"), root.Scope("child 2"))
120+
allScopes = append(allScopes, allScopes[0].Scope("grandchild 1"), allScopes[1].Scope("grandchild 2"), allScopes[1].Scope("grandchild 3"))
121+
122+
type A struct{}
123+
// provide to the leaf Scope with Export option set.
124+
require.NoError(t, allScopes[len(allScopes)-1].Provide(func() *A {
125+
return &A{}
126+
}, Export(true)))
127+
128+
// since constructor was provided with Export option, this should let all the Scopes below should see it.
129+
for _, scope := range allScopes {
130+
assert.NoError(t, scope.Invoke(func(a *A) {}))
131+
}
132+
})
107133
}
108134

109135
func TestScopeFailures(t *testing.T) {
@@ -183,6 +209,40 @@ func TestScopeFailures(t *testing.T) {
183209
}
184210
})
185211

212+
t.Run("introduce a cycle with Export option", func(t *testing.T) {
213+
// what root and child1 sees:
214+
// A <- B C
215+
// | ^
216+
// |_________|
217+
//
218+
// what child2 sees:
219+
// A <- B <- C
220+
// | ^
221+
// |_________|
222+
223+
type A struct{}
224+
type B struct{}
225+
type C struct{}
226+
newA := func(*C) *A { return &A{} }
227+
newB := func(*A) *B { return &B{} }
228+
newC := func(*B) *C { return &C{} }
229+
230+
root := New()
231+
child1 := root.Scope("child 1")
232+
child2 := root.Scope("child 2")
233+
234+
// A <- B made available to all Scopes with root provision.
235+
require.NoError(t, root.Provide(newA))
236+
237+
// B <- C made available to only child 2 with private provide.
238+
require.NoError(t, child2.Provide(newB))
239+
240+
// C <- A made available to all Scopes with Export provide.
241+
err := child1.Provide(newC, Export(true))
242+
assert.Error(t, err, "expected a cycle to be introduced in child 2")
243+
assert.Contains(t, err.Error(), "In Scope child 2")
244+
})
245+
186246
t.Run("private provides do not propagate upstream", func(t *testing.T) {
187247
type A struct{}
188248

0 commit comments

Comments
 (0)