Skip to content

Commit 29dd172

Browse files
authored
param: Search scopes during Build, support value groups (#312)
This significantly changes how we instantiate dependencies for a constructor and adds support for value groups. Previously, for each parameter of a constructor, we attempted to instantiate the value in every scope until one succeeded without dependency errors. Effectively: for param in constructor for scope in scopes_to_root err = scope.instantiate(param) if err != missing_dependencies { return err } This made the behavior less certain because we were relying on fuzzily matching errors. With this change, we search the container for constructors as needed for singular parameters (paramSingle) and for value groups (paramGroupedSlice). Roughly: for param in constructor switch param { case paramSingle: for scope in scopes_to_root { if scope.provides(param) { found = true } } // ... case paramGroupedSlice: for scope in scopes_to_root { providers.append(scope.providers_for(param)) } default: // ... } This is cleaner because it works by searching the container for what it needs rather than trying and letting it fail. This includes a subtle change to constructorNode.Call: previously, when a constructor ran, its results were stored in the containerStore that it used to instantiate its dependencies. Now, results are stored in the containerStore (Scope) to which that constructor was provided. This works because the leaf param types (paramSingle and paramGroupedSlice) will search the scope tree to find the value.
1 parent 08ad091 commit 29dd172

File tree

3 files changed

+130
-36
lines changed

3 files changed

+130
-36
lines changed

constructor.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,12 @@ func (n *constructorNode) Call(c containerStore) error {
148148
if err := n.resultList.ExtractList(receiver, results); err != nil {
149149
return errConstructorFailed{Func: n.location, Reason: err}
150150
}
151-
receiver.Commit(c)
151+
152+
// Commit the result to the original container that this constructor
153+
// was supplied to. The provided constructor is only used for a view of
154+
// the rest of the graph to instantiate the dependencies of this
155+
// container.
156+
receiver.Commit(n.s)
152157
n.called = true
153158

154159
return nil

param.go

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -146,28 +146,11 @@ func (pl paramList) Build(containerStore) (reflect.Value, error) {
146146
// to the underlying constructor.
147147
func (pl paramList) BuildList(c containerStore) ([]reflect.Value, error) {
148148
args := make([]reflect.Value, len(pl.Params))
149-
allContainers := c.storesToRoot()
150149
for i, p := range pl.Params {
151-
// iterate through the tree path of scopes.
152-
containerLoop:
153-
for _, c := range allContainers {
154-
arg, err := p.Build(c)
155-
if err == nil {
156-
args[i] = arg
157-
break containerLoop
158-
}
159-
// If argument has successfully been built, it's possible
160-
// for these errors to occur in child scopes that don't
161-
// contain the given parameter type. We can safely ignore
162-
// these.
163-
// If it's an error other than missing types/dependencies,
164-
// this means some constructor returned an error that must
165-
// be reported.
166-
_, isErrMissingTypes := err.(errMissingTypes)
167-
_, isErrMissingDeps := err.(errMissingDependencies)
168-
if err != nil && !isErrMissingTypes && !isErrMissingDeps {
169-
return nil, err
170-
}
150+
var err error
151+
args[i], err = p.Build(c)
152+
if err != nil {
153+
return nil, err
171154
}
172155
}
173156
return args, nil
@@ -213,12 +196,36 @@ func (ps paramSingle) String() string {
213196

214197
return fmt.Sprintf("%v[%v]", ps.Type, strings.Join(opts, ", "))
215198
}
199+
200+
// searches the given container and its parent for a matching value.
201+
func (ps paramSingle) getValue(c containerStore) (reflect.Value, bool) {
202+
for _, c := range c.storesToRoot() {
203+
if v, ok := c.getValue(ps.Name, ps.Type); ok {
204+
return v, ok
205+
}
206+
}
207+
return _noValue, false
208+
}
209+
216210
func (ps paramSingle) Build(c containerStore) (reflect.Value, error) {
217-
if v, ok := c.getValue(ps.Name, ps.Type); ok {
211+
if v, ok := ps.getValue(c); ok {
218212
return v, nil
219213
}
220214

221-
providers := c.getValueProviders(ps.Name, ps.Type)
215+
// Starting at the given container and working our way up its parents,
216+
// find one that provides this dependency.
217+
//
218+
// Once found, we'll use that container for the rest of the invocation.
219+
// Dependencies of this type will begin searching at that container,
220+
// rather than starting at base.
221+
var providers []provider
222+
for _, container := range c.storesToRoot() {
223+
providers = container.getValueProviders(ps.Name, ps.Type)
224+
if len(providers) > 0 {
225+
break
226+
}
227+
}
228+
222229
if len(providers) == 0 {
223230
if ps.Optional {
224231
return reflect.Zero(ps.Type), nil
@@ -247,7 +254,7 @@ func (ps paramSingle) Build(c containerStore) (reflect.Value, error) {
247254

248255
// If we get here, it's impossible for the value to be absent from the
249256
// container.
250-
v, _ := c.getValue(ps.Name, ps.Type)
257+
v, _ := ps.getValue(c)
251258
return v, nil
252259
}
253260

@@ -479,21 +486,25 @@ func newParamGroupedSlice(f reflect.StructField, c containerStore) (paramGrouped
479486
}
480487

481488
func (pt paramGroupedSlice) Build(c containerStore) (reflect.Value, error) {
482-
for _, n := range c.getGroupProviders(pt.Group, pt.Type.Elem()) {
483-
if err := n.Call(c); err != nil {
484-
return _noValue, errParamGroupFailed{
485-
CtorID: n.ID(),
486-
Key: key{group: pt.Group, t: pt.Type.Elem()},
487-
Reason: err,
489+
var itemCount int
490+
stores := c.storesToRoot()
491+
for _, c := range stores {
492+
providers := c.getGroupProviders(pt.Group, pt.Type.Elem())
493+
itemCount += len(providers)
494+
for _, n := range providers {
495+
if err := n.Call(c); err != nil {
496+
return _noValue, errParamGroupFailed{
497+
CtorID: n.ID(),
498+
Key: key{group: pt.Group, t: pt.Type.Elem()},
499+
Reason: err,
500+
}
488501
}
489502
}
490503
}
491504

492-
items := c.getValueGroup(pt.Group, pt.Type.Elem())
493-
494-
result := reflect.MakeSlice(pt.Type, len(items), len(items))
495-
for i, v := range items {
496-
result.Index(i).Set(v)
505+
result := reflect.MakeSlice(pt.Type, 0, itemCount)
506+
for _, c := range stores {
507+
result = reflect.Append(result, c.getValueGroup(pt.Group, pt.Type.Elem())...)
497508
}
498509
return result, nil
499510
}

scope_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,3 +309,81 @@ func TestScopeFailures(t *testing.T) {
309309
assert.NoError(t, gc.Invoke(func(a *A) {}), "expected Invoke in grandchild container on child's private-provided type to fail")
310310
})
311311
}
312+
313+
func TestScopeValueGroups(t *testing.T) {
314+
t.Run("provide in parent and child", func(t *testing.T) {
315+
type result struct {
316+
Out
317+
318+
Value string `group:"foo"`
319+
}
320+
321+
root := New()
322+
require.NoError(t, root.Provide(func() result {
323+
return result{Value: "a"}
324+
}))
325+
require.NoError(t, root.Provide(func() result {
326+
return result{Value: "b"}
327+
}))
328+
require.NoError(t, root.Provide(func() result {
329+
return result{Value: "c"}
330+
}))
331+
332+
child := root.Scope("child")
333+
require.NoError(t,
334+
child.Provide(func() result {
335+
return result{Value: "d"}
336+
}))
337+
338+
type param struct {
339+
In
340+
341+
Values []string `group:"foo"`
342+
}
343+
344+
t.Run("invoke parent", func(t *testing.T) {
345+
require.NoError(t, root.Invoke(func(i param) {
346+
assert.ElementsMatch(t, []string{"a", "b", "c"}, i.Values)
347+
}), "only values added to parent should be visible")
348+
})
349+
350+
t.Run("invoke child", func(t *testing.T) {
351+
require.NoError(t, child.Invoke(func(i param) {
352+
assert.ElementsMatch(t, []string{"a", "b", "c", "d"}, i.Values)
353+
}), "values added to both, parent and child should be visible")
354+
})
355+
})
356+
357+
t.Run("value group as a parent dependency", func(t *testing.T) {
358+
// Tree:
359+
//
360+
// root defines a function that consumes the value group
361+
// |
362+
// |
363+
// child produces values to the value group
364+
365+
type T1 struct{}
366+
type param struct {
367+
In
368+
369+
Values []string `group:"foo"`
370+
}
371+
372+
root := New()
373+
374+
require.NoError(t, root.Provide(func(p param) T1 {
375+
assert.ElementsMatch(t, []string{"a", "b", "c"}, p.Values)
376+
return T1{}
377+
}))
378+
379+
child := root.Scope("child")
380+
require.NoError(t, child.Provide(func() string { return "a" }, Group("foo")))
381+
require.NoError(t, child.Provide(func() string { return "b" }, Group("foo")))
382+
require.NoError(t, child.Provide(func() string { return "c" }, Group("foo")))
383+
384+
// Invocation in child should see values provided to the child,
385+
// even though the constructor we're invoking is provided in
386+
// the parent.
387+
require.NoError(t, child.Invoke(func(T1) {}))
388+
})
389+
}

0 commit comments

Comments
 (0)