Skip to content

Commit

Permalink
Add DecorateInfo (#320)
Browse files Browse the repository at this point in the history
This adds `dig.DecorateInfo` and `dig.FillDecorateInfo`, which behave like `dig.ProvideInfo` and `dig.FillProvideInfo`, except it's for `scope.Decorate`. It records the input and output parameters of the decorator function that Dig was able to get out of. 

Example usage:
```
var info DecorateInfo
c.Decorate(dcor, FillDecorateInfo(&info)) {
  ...
}

// info.Input/info.Output are now lists of stringers (dig.Input) that
// record the type/name/group info for each parameter/result types of dcor.

```

As a side note, I considered naming this `FillDecoratorInfo` but since the other option is named `FillProvideInfo`, not `FillProviderInfo`, I'm naming this to `FillDecorateInfo` to keep some level of consistency.

Ref GO-1220, #318
  • Loading branch information
sywhang authored Feb 17, 2022
1 parent 59136bb commit a7e23e3
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 10 deletions.
62 changes: 58 additions & 4 deletions decorate.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,10 +116,37 @@ func (n *decoratorNode) Call(s containerStore) error {

func (n *decoratorNode) ID() dot.CtorID { return n.id }

// DecorateOption modifies the default behavior of Provide.
// Currently, there is no implementation of it yet.
// DecorateOption modifies the default behavior of Decorate.
type DecorateOption interface {
noOptionsYet()
apply(*decorateOptions)
}

type decorateOptions struct {
Info *DecorateInfo
}

// FillDecorateInfo is a DecorateOption that writes info on what Dig was
// able to get out of the provided decorator into the provided DecorateInfo.
func FillDecorateInfo(info *DecorateInfo) DecorateOption {
return fillDecorateInfoOption{info: info}
}

type fillDecorateInfoOption struct{ info *DecorateInfo }

func (o fillDecorateInfoOption) String() string {
return fmt.Sprintf("FillDecorateInfo(%p)", o.info)
}

func (o fillDecorateInfoOption) apply(opts *decorateOptions) {
opts.Info = o.info
}

// DecorateInfo provides information about the decorator's inputs and outputs
// types as strings, as well as the ID of the decorator supplied to the Container.
type DecorateInfo struct {
ID ID
Inputs []*Input
Outputs []*Output
}

// Decorate provides a decorator for a type that has already been provided in the Container.
Expand Down Expand Up @@ -164,7 +191,10 @@ func (c *Container) Decorate(decorator interface{}, opts ...DecorateOption) erro
//
// Similar to a provider, the decorator function gets called *at most once*.
func (s *Scope) Decorate(decorator interface{}, opts ...DecorateOption) error {
_ = opts // there are no options at this time
var options decorateOptions
for _, opt := range opts {
opt.apply(&options)
}

dn, err := newDecoratorNode(decorator, s)
if err != nil {
Expand All @@ -181,6 +211,30 @@ func (s *Scope) Decorate(decorator interface{}, opts ...DecorateOption) error {
}
s.decorators[k] = append(s.decorators[k], dn)
}

if info := options.Info; info != nil {
params := dn.params.DotParam()
results := dn.results.DotResult()
info.ID = (ID)(dn.id)
info.Inputs = make([]*Input, len(params))
info.Outputs = make([]*Output, len(results))

for i, param := range params {
info.Inputs[i] = &Input{
t: param.Type,
optional: param.Optional,
name: param.Name,
group: param.Group,
}
}
for i, res := range results {
info.Outputs[i] = &Output{
t: res.Type,
name: res.Name,
group: res.Group,
}
}
}
return nil
}

Expand Down
50 changes: 46 additions & 4 deletions decorate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ package dig_test

import (
"errors"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -44,11 +45,17 @@ func TestDecorateSuccess(t *testing.T) {
assert.Equal(t, "A", a.name, "expected name to not be decorated yet.")
})

c.RequireDecorate(func(a *A) *A { return &A{name: a.name + "'"} })
var info dig.DecorateInfo

c.RequireDecorate(func(a *A) *A { return &A{name: a.name + "'"} }, dig.FillDecorateInfo(&info))
c.RequireInvoke(func(a *A) {
assert.Equal(t, "A'", a.name, "expected name to equal decorated name.")
})

require.Equal(t, len(info.Inputs), 1)
require.Equal(t, len(info.Outputs), 1)
assert.Equal(t, "*dig_test.A", info.Inputs[0].String())
assert.Equal(t, "*dig_test.A", info.Outputs[0].String())
})

t.Run("simple decorate a provider from child scope", func(t *testing.T) {
Expand All @@ -61,14 +68,20 @@ func TestDecorateSuccess(t *testing.T) {
child := c.Scope("child")
child.RequireProvide(func() *A { return &A{name: "A"} }, dig.Export(true))

child.RequireDecorate(func(a *A) *A { return &A{name: a.name + "'"} })
var info dig.DecorateInfo
child.RequireDecorate(func(a *A) *A { return &A{name: a.name + "'"} }, dig.FillDecorateInfo(&info))
c.RequireInvoke(func(a *A) {
assert.Equal(t, "A", a.name, "expected name to equal original name in parent scope")
})

child.RequireInvoke(func(a *A) {
assert.Equal(t, "A'", a.name, "expected name to equal decorated name in child scope")
})

require.Equal(t, len(info.Inputs), 1)
require.Equal(t, len(info.Outputs), 1)
assert.Equal(t, "*dig_test.A", info.Inputs[0].String())
assert.Equal(t, "*dig_test.A", info.Outputs[0].String())
})

t.Run("check parent-provided decorator doesn't need parent to invoke", func(t *testing.T) {
Expand Down Expand Up @@ -183,19 +196,25 @@ func TestDecorateSuccess(t *testing.T) {
c.RequireProvide(func() *A { return &A{Name: "A"} })
c.RequireProvide(func() string { return "b" }, dig.Name("b"))

var info dig.DecorateInfo
c.RequireDecorate(func(b B) C {
return C{
A: &A{
Name: b.A.Name + "'",
},
B: b.B + "'",
}
})
}, dig.FillDecorateInfo(&info))

c.RequireInvoke(func(b B) {
assert.Equal(t, "A'", b.A.Name)
assert.Equal(t, "b'", b.B)
})

require.Equal(t, 2, len(info.Inputs))
require.Equal(t, 2, len(info.Outputs))
assert.Equal(t, "*dig_test.A", info.Inputs[0].String())
assert.Equal(t, `string[name = "b"]`, info.Inputs[1].String())
})

t.Run("decorate with value groups", func(t *testing.T) {
Expand All @@ -216,6 +235,7 @@ func TestDecorateSuccess(t *testing.T) {
c.RequireProvide(func() string { return "cat" }, dig.Group("animals"))
c.RequireProvide(func() string { return "gopher" }, dig.Group("animals"))

var info dig.DecorateInfo
c.RequireDecorate(func(p Params) Result {
animals := p.Animals
for i := 0; i < len(animals); i++ {
Expand All @@ -224,11 +244,14 @@ func TestDecorateSuccess(t *testing.T) {
return Result{
Animals: animals,
}
})
}, dig.FillDecorateInfo(&info))

c.RequireInvoke(func(p Params) {
assert.ElementsMatch(t, []string{"good dog", "good cat", "good gopher"}, p.Animals)
})

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

t.Run("decorate with optional parameter", func(t *testing.T) {
Expand Down Expand Up @@ -562,3 +585,22 @@ func TestDecorateFailure(t *testing.T) {
assert.Contains(t, err.Error(), "*dig_test.A already decorated")
})
}

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

t.Run("nil", func(t *testing.T) {
t.Parallel()

assert.Equal(t, "FillDecorateInfo(0x0)", fmt.Sprint(dig.FillDecorateInfo(nil)))
})

t.Run("not nil", func(t *testing.T) {
t.Parallel()

opt := dig.FillDecorateInfo(new(dig.DecorateInfo))
assert.NotEqual(t, fmt.Sprint(opt), "FillDecorateInfo(0x0)")
assert.Contains(t, fmt.Sprint(opt), "FillDecorateInfo(0x")
})

}
4 changes: 2 additions & 2 deletions provide.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ type ProvideInfo struct {
Outputs []*Output
}

// Input contains information on an input parameter of the constructor.
// Input contains information on an input parameter of a function.
type Input struct {
t reflect.Type
optional bool
Expand All @@ -178,7 +178,7 @@ func (i *Input) String() string {
return fmt.Sprintf("%v[%v]", t, strings.Join(toks, ", "))
}

// Output contains information on an output produced by the constructor.
// Output contains information on an output produced by a function.
type Output struct {
t reflect.Type
name, group string
Expand Down

0 comments on commit a7e23e3

Please sign in to comment.