Skip to content

Commit

Permalink
Support simultanous name and group tags
Browse files Browse the repository at this point in the history
As per Dig issue:
uber-go#380

In order to support Fx feature requests

uber-go/fx#998
uber-go/fx#1036

We need to be able to drop the restriction, both in terms of options
dig.Name and dig.Group and dig.Out struct annotations on `name` and
`group` being mutually exclusive.

In a future PR, this can then be exploited to populate value group maps
where the 'name' tag becomes the key of a map[string][T]
  • Loading branch information
jquirke committed Mar 9, 2023
1 parent 7f9f0b8 commit d587461
Show file tree
Hide file tree
Showing 5 changed files with 282 additions and 74 deletions.
2 changes: 1 addition & 1 deletion decorate.go
Expand Up @@ -288,7 +288,7 @@ func findResultKeys(r resultList) ([]key, error) {
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
197 changes: 182 additions & 15 deletions dig_test.go
Expand Up @@ -749,6 +749,53 @@ func TestEndToEndSuccess(t *testing.T) {
assert.ElementsMatch(t, actualStrs, expectedStrs, "list of strings provided must match")
})

t.Run("multiple As with Group and Name", func(t *testing.T) {
c := digtest.New(t)
expectedNames := []string{"inst1", "inst2"}
expectedStrs := []string{"foo", "bar"}
for i, s := range expectedStrs {
s := s
c.RequireProvide(func() *bytes.Buffer {
return bytes.NewBufferString(s)
}, dig.Group("buffs"), dig.Name(expectedNames[i]),
dig.As(new(io.Reader), new(io.Writer)))
}

type in struct {
dig.In

Reader1 io.Reader `name:"inst1"`
Reader2 io.Reader `name:"inst2"`
Readers []io.Reader `group:"buffs"`
Writers []io.Writer `group:"buffs"`
}

var actualStrs []string
var actualStrsName []string

c.RequireInvoke(func(got in) {
require.Len(t, got.Readers, 2)
buf := make([]byte, 3)
for i, r := range got.Readers {
_, err := r.Read(buf)
require.NoError(t, err)
actualStrs = append(actualStrs, string(buf))
// put the text back
got.Writers[i].Write(buf)
}
_, err := got.Reader1.Read(buf)
require.NoError(t, err)
actualStrsName = append(actualStrsName, string(buf))
_, err = got.Reader2.Read(buf)
require.NoError(t, err)
actualStrsName = append(actualStrsName, string(buf))
require.Len(t, got.Writers, 2)
})

assert.ElementsMatch(t, actualStrs, expectedStrs, "list of strings provided must match")
assert.ElementsMatch(t, actualStrsName, expectedStrs, "names: list of strings provided must match")
})

t.Run("As same interface", func(t *testing.T) {
c := digtest.New(t)
c.RequireProvide(func() io.Reader {
Expand Down Expand Up @@ -1098,6 +1145,48 @@ func TestGroups(t *testing.T) {
})
})

t.Run("values are provided; coexist with name", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))

type out struct {
dig.Out

Value int `group:"val"`
}

type out2 struct {
dig.Out

Value int `name:"inst1" group:"val"`
}

provide := func(i int) {
c.RequireProvide(func() out {
return out{Value: i}
})
}

provide(1)
provide(2)
provide(3)

c.RequireProvide(func() out2 {
return out2{Value: 4}
})

type in struct {
dig.In

SingleValue int `name:"inst1"`
Values []int `group:"val"`
}

c.RequireInvoke(func(i in) {
assert.Equal(t, []int{1, 2, 3, 4}, i.Values)
assert.Equal(t, 4, i.SingleValue)
})
})

t.Run("groups are provided via option", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))

Expand All @@ -1122,6 +1211,36 @@ func TestGroups(t *testing.T) {
})
})

t.Run("groups are provided via option; coexist with name", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))

provide := func(i int) {
c.RequireProvide(func() int {
return i
}, dig.Group("val"))
}

provide(1)
provide(2)
provide(3)

c.RequireProvide(func() int {
return 4
}, dig.Group("val"), dig.Name("inst1"))

type in struct {
dig.In

SingleValue int `name:"inst1"`
Values []int `group:"val"`
}

c.RequireInvoke(func(i in) {
assert.Equal(t, []int{1, 2, 3, 4}, i.Values)
assert.Equal(t, 4, i.SingleValue)
})
})

t.Run("different types may be grouped", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))

Expand Down Expand Up @@ -1453,6 +1572,15 @@ func TestGroups(t *testing.T) {
assert.Contains(t, err.Error(), "flatten can be applied to slices only")
})

t.Run("flatten via option not permitted with name", func(t *testing.T) {
c := digtest.New(t, dig.SetRand(rand.New(rand.NewSource(0))))
err := c.Provide(func() []int {
return []int{1, 2, 3}
}, dig.Group("val,flatten"), dig.Name("meaninglessname"))
require.Error(t, err, "failed to provide")
assert.Contains(t, err.Error(), "flatten can not be applied in conjunction with name")
})

t.Run("a soft value group provider is not called when only that value group is consumed", func(t *testing.T) {
type Param struct {
dig.In
Expand Down Expand Up @@ -1998,21 +2126,6 @@ func TestAsExpectingOriginalType(t *testing.T) {
})
}

func TestProvideIncompatibleOptions(t *testing.T) {
t.Parallel()

t.Run("group and name", func(t *testing.T) {
c := digtest.New(t)
err := c.Provide(func() io.Reader {
t.Fatal("this function must not be called")
return nil
}, dig.Group("foo"), dig.Name("bar"))
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot use named values with value groups: "+
`name:"bar" provided with group:"foo"`)
})
}

type testStruct struct{}

func (testStruct) TestMethod(x int) float64 { return float64(x) }
Expand Down Expand Up @@ -2559,6 +2672,60 @@ func testProvideFailures(t *testing.T, dryRun bool) {
)
})

t.Run("provide multiple instances with the same name and same group", func(t *testing.T) {
c := digtest.New(t, dig.DryRun(dryRun))
type A struct{}
type ret1 struct {
dig.Out
*A `name:"foo" group:"foos"`
}
type ret2 struct {
dig.Out
*A `name:"foo" group:"foos"`
}
c.RequireProvide(func() ret1 {
return ret1{A: &A{}}
})

err := c.Provide(func() ret2 {
return ret2{A: &A{}}
})
require.Error(t, err, "expected error on the second provide")
dig.AssertErrorMatches(t, err,
`cannot provide function "go.uber.org/dig_test".testProvideFailures\S+`,
`dig_test.go:\d+`, // file:line
`cannot provide \*dig_test.A\[name="foo"\] from \[0\].A:`,
`already provided by "go.uber.org/dig_test".testProvideFailures\S+`,
)
})

t.Run("provide multiple instances with the same name but different group", func(t *testing.T) {
c := digtest.New(t, dig.DryRun(dryRun))
type A struct{}
type ret1 struct {
dig.Out
*A `name:"foo" group:"foos"`
}
type ret2 struct {
dig.Out
*A `name:"foo" group:"foosss"`
}
c.RequireProvide(func() ret1 {
return ret1{A: &A{}}
})

err := c.Provide(func() ret2 {
return ret2{A: &A{}}
})
require.Error(t, err, "expected error on the second provide")
dig.AssertErrorMatches(t, err,
`cannot provide function "go.uber.org/dig_test".testProvideFailures\S+`,
`dig_test.go:\d+`, // file:line
`cannot provide \*dig_test.A\[name="foo"\] from \[0\].A:`,
`already provided by "go.uber.org/dig_test".testProvideFailures\S+`,
)
})

t.Run("out with unexported field should error", func(t *testing.T) {
c := digtest.New(t, dig.DryRun(dryRun))

Expand Down
6 changes: 0 additions & 6 deletions provide.go
Expand Up @@ -46,12 +46,6 @@ type provideOptions struct {
}

func (o *provideOptions) Validate() error {
if len(o.Group) > 0 {
if len(o.Name) > 0 {
return newErrInvalidInput(
fmt.Sprintf("cannot use named values with value groups: name:%q provided with group:%q", o.Name, o.Group), nil)
}
}

// Names must be representable inside a backquoted string. The only
// limitation for raw string literals as per
Expand Down

0 comments on commit d587461

Please sign in to comment.