Skip to content
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased
- No changes yet.
### Added
- Support for consuming named value groups as `map[string]T` in addition to `[]T`.
Named value groups can now be consumed as maps where names become keys, enabling
both direct named access and map-based access to the same providers.
- Simultaneous `dig.Name()` and `dig.Group()` support, removing previous mutual
exclusivity to enable named value group patterns.
- Comprehensive validation for slice decorators with named value groups, preventing
incompatible patterns and providing clear guidance for correct usage.
- Soft value groups support with map consumption, maintaining consistent behavior
with slice consumption patterns.

## [1.19.0] - 2025-05-13

Expand Down
16 changes: 8 additions & 8 deletions constructor.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,15 +213,15 @@ func (n *constructorNode) Call(c containerStore) (err error) {
// would be made to a containerWriter and defers them until Commit is called.
type stagingContainerWriter struct {
values map[key]reflect.Value
groups map[key][]reflect.Value
groups map[key][]keyedGroupValue
}

var _ containerWriter = (*stagingContainerWriter)(nil)

func newStagingContainerWriter() *stagingContainerWriter {
return &stagingContainerWriter{
values: make(map[key]reflect.Value),
groups: make(map[key][]reflect.Value),
groups: make(map[key][]keyedGroupValue),
}
}

Expand All @@ -233,12 +233,12 @@ func (sr *stagingContainerWriter) setDecoratedValue(_ string, _ reflect.Type, _
digerror.BugPanicf("stagingContainerWriter.setDecoratedValue must never be called")
}

func (sr *stagingContainerWriter) submitGroupedValue(group string, t reflect.Type, v reflect.Value) {
func (sr *stagingContainerWriter) submitGroupedValue(group, mapKey string, t reflect.Type, v reflect.Value) {
k := key{t: t, group: group}
sr.groups[k] = append(sr.groups[k], v)
sr.groups[k] = append(sr.groups[k], keyedGroupValue{key: mapKey, value: v})
}

func (sr *stagingContainerWriter) submitDecoratedGroupedValue(_ string, _ reflect.Type, _ reflect.Value) {
func (sr *stagingContainerWriter) submitDecoratedGroupedValue(_, _ string, _ reflect.Type, _ reflect.Value) {
digerror.BugPanicf("stagingContainerWriter.submitDecoratedGroupedValue must never be called")
}

Expand All @@ -248,9 +248,9 @@ func (sr *stagingContainerWriter) Commit(cw containerWriter) {
cw.setValue(k.name, k.t, v)
}

for k, vs := range sr.groups {
for _, v := range vs {
cw.submitGroupedValue(k.group, k.t, v)
for k, kgvs := range sr.groups {
for _, kgv := range kgvs {
cw.submitGroupedValue(k.group, kgv.key, k.t, kgv.value)
}
}
}
14 changes: 7 additions & 7 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,12 @@ type containerWriter interface {
setDecoratedValue(name string, t reflect.Type, v reflect.Value)

// submitGroupedValue submits a value to the value group with the provided
// name.
submitGroupedValue(name string, t reflect.Type, v reflect.Value)
// name and optional map key.
submitGroupedValue(name, mapKey string, t reflect.Type, v reflect.Value)

// submitDecoratedGroupedValue submits a decorated value to the value group
// with the provided name.
submitDecoratedGroupedValue(name string, t reflect.Type, v reflect.Value)
// with the provided name and optional map key.
submitDecoratedGroupedValue(name, mapKey string, t reflect.Type, v reflect.Value)
}

// containerStore provides access to the Container's underlying data store.
Expand All @@ -109,7 +109,7 @@ type containerStore interface {
// Retrieves all values for the provided group and type.
//
// The order in which the values are returned is undefined.
getValueGroup(name string, t reflect.Type) []reflect.Value
getValueGroup(name string, t reflect.Type) []keyedGroupValue

// Retrieves all decorated values for the provided group and type, if any.
getDecoratedValueGroup(name string, t reflect.Type) (reflect.Value, bool)
Expand Down Expand Up @@ -292,8 +292,8 @@ func (bs byTypeName) Swap(i int, j int) {
bs[i], bs[j] = bs[j], bs[i]
}

func shuffledCopy(rand *rand.Rand, items []reflect.Value) []reflect.Value {
newItems := make([]reflect.Value, len(items))
func shuffledCopy(rand *rand.Rand, items []keyedGroupValue) []keyedGroupValue {
newItems := make([]keyedGroupValue, len(items))
for i, j := range rand.Perm(len(items)) {
newItems[i] = items[j]
}
Expand Down
6 changes: 4 additions & 2 deletions decorate.go
Original file line number Diff line number Diff line change
Expand Up @@ -309,13 +309,15 @@ func findResultKeys(r resultList) ([]key, error) {
case resultSingle:
keys = append(keys, key{t: innerResult.Type, name: innerResult.Name})
case resultGrouped:
if innerResult.Type.Kind() != reflect.Slice {
isMap := innerResult.Type.Kind() == reflect.Map && innerResult.Type.Key().Kind() == reflect.String
isSlice := innerResult.Type.Kind() == reflect.Slice
if !isMap && !isSlice {
return nil, newErrInvalidInput("decorating a value group requires decorating the entire value group, not a single value", nil)
}
keys = append(keys, key{t: innerResult.Type.Elem(), group: innerResult.Group})
case resultObject:
for _, f := range innerResult.Fields {
q = append(q, f.Result)
q = append(q, f.Results...)
}
case resultList:
q = append(q, innerResult.Results...)
Expand Down
99 changes: 99 additions & 0 deletions decorate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,64 @@ func TestDecorateSuccess(t *testing.T) {
}))
})

t.Run("map is treated as an ordinary dependency without group tag, named or unnamed, and passes through multiple scopes", func(t *testing.T) {
type params struct {
dig.In

Strings1 map[string]string
Strings2 map[string]string `name:"strings2"`
}

type childResult struct {
dig.Out

Strings1 map[string]string
Strings2 map[string]string `name:"strings2"`
}

type A map[string]string
type B map[string]string

parent := digtest.New(t)
parent.RequireProvide(func() map[string]string { return map[string]string{"key1": "val1", "key2": "val2"} })
parent.RequireProvide(func() map[string]string { return map[string]string{"key1": "val21", "key2": "val22"} }, dig.Name("strings2"))

parent.RequireProvide(func(p params) A { return A(p.Strings1) })
parent.RequireProvide(func(p params) B { return B(p.Strings2) })

child := parent.Scope("child")

parent.RequireDecorate(func(p params) childResult {
res := childResult{Strings1: make(map[string]string, len(p.Strings1))}
for k, s := range p.Strings1 {
res.Strings1[k] = strings.ToUpper(s)
}
res.Strings2 = p.Strings2
return res
})

child.RequireDecorate(func(p params) childResult {
res := childResult{Strings2: make(map[string]string, len(p.Strings2))}
for k, s := range p.Strings2 {
res.Strings2[k] = strings.ToUpper(s)
}
res.Strings1 = p.Strings1
res.Strings1["key3"] = "newval"
return res
})

require.NoError(t, child.Invoke(func(p params) {
require.Len(t, p.Strings1, 3)
assert.Equal(t, "VAL1", p.Strings1["key1"])
assert.Equal(t, "VAL2", p.Strings1["key2"])
assert.Equal(t, "newval", p.Strings1["key3"])
require.Len(t, p.Strings2, 2)
assert.Equal(t, "VAL21", p.Strings2["key1"])
assert.Equal(t, "VAL22", p.Strings2["key2"])

}))

})
t.Run("decorate values in soft group", func(t *testing.T) {
type params struct {
dig.In
Expand Down Expand Up @@ -394,6 +452,46 @@ func TestDecorateSuccess(t *testing.T) {
assert.Equal(t, `[]string[group = "animals"]`, info.Inputs[0].String())
})

t.Run("decorate with map value groups", func(t *testing.T) {
type Params struct {
dig.In

Animals map[string]string `group:"animals"`
}

type Result struct {
dig.Out

Animals map[string]string `group:"animals"`
}

c := digtest.New(t)
c.RequireProvide(func() string { return "dog" }, dig.Name("animal1"), dig.Group("animals"))
c.RequireProvide(func() string { return "cat" }, dig.Name("animal2"), dig.Group("animals"))
c.RequireProvide(func() string { return "gopher" }, dig.Name("animal3"), dig.Group("animals"))

var info dig.DecorateInfo
c.RequireDecorate(func(p Params) Result {
animals := p.Animals
for k, v := range animals {
animals[k] = "good " + v
}
return Result{
Animals: animals,
}
}, dig.FillDecorateInfo(&info))

c.RequireInvoke(func(p Params) {
assert.Len(t, p.Animals, 3)
assert.Equal(t, "good dog", p.Animals["animal1"])
assert.Equal(t, "good cat", p.Animals["animal2"])
assert.Equal(t, "good gopher", p.Animals["animal3"])
})

require.Equal(t, 1, len(info.Inputs))
assert.Equal(t, `map[string]string[group = "animals"]`, info.Inputs[0].String())
})

t.Run("decorate with optional parameter", func(t *testing.T) {
c := digtest.New(t)

Expand Down Expand Up @@ -919,6 +1017,7 @@ func TestMultipleDecorates(t *testing.T) {
assert.ElementsMatch(t, []int{2, 3, 4}, a.Values)
})
})

}

func TestFillDecorateInfoString(t *testing.T) {
Expand Down
Loading