Repository: uber-go/dig
Pull request #280 (2021-06-17 06:33:32)

Add WithInfo and ConstructorInfo
This adds WithInfo and ConstructorInfo, which allow users to get what Dig was able to extract out of the `Provide`d constructors.

Specifically, ConstructorInfo contains ID of the constructor (which is unique among all provided ctors), Inputs (parameters), and Outputs (results) types as Strings.

With these, users can introspect the constructor info that Dig was able to get out using reflection. For example:
```go
info := ConstructorInfo{}
c.Provide(someCtor, WithInfo(&info))

fmt.Println(info.ID) // Will print the ID of someCtor

for _, param := info.Inputs {
    fmt.Println(param) // will print the input parameters types of someCtor
}
 
for _, res := info.Outputs {
    fmt.Println(res) // will print the return types of someCtor
}
```

Refs GO-169 and #254. 

codecov[bot]: # [Codecov](https://codecov.io/gh/uber-go/dig/pull/280?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) Report
> Merging [#280](https://codecov.io/gh/uber-go/dig/pull/280?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (4797a98) into [master](https://codecov.io/gh/uber-go/dig/commit/d3dbd1dfca531847f33d59b198b225865a19071c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) (d3dbd1d) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/uber-go/dig/pull/280/graphs/tree.svg?width=650&height=150&src=pr&token=ARuH7Frwgw&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)](https://codecov.io/gh/uber-go/dig/pull/280?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)

```diff
@@            Coverage Diff             @@
##           master     #280      +/-   ##
==========================================
+ Coverage   97.96%   97.97%   +0.01%     
==========================================
  Files          13       13              
  Lines        1079     1088       +9     
==========================================
+ Hits         1057     1066       +9     
  Misses         16       16              
  Partials        6        6              
```


| [Impacted Files](https://codecov.io/gh/uber-go/dig/pull/280?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go) | Coverage Δ | |
|---|---|---|
| [dig.go](https://codecov.io/gh/uber-go/dig/pull/280/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go#diff-ZGlnLmdv) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/uber-go/dig/pull/280?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/uber-go/dig/pull/280?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Last update [d3dbd1d...4797a98](https://codecov.io/gh/uber-go/dig/pull/280?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=uber-go).

abhinav: One question I have, @sywhang is whether we want to also produce information about invokes? Perhaps that can be a separate thing later based on needs. 

RE: naming: What do you think about ProvideInfo instead of ConstructorInfo? That'll make it easy to name the Invoke variant InvokeInfo if/when we add it.
sywhang: > One question I have, @sywhang is whether we want to also produce information about invokes? Perhaps that can be a separate thing later based on needs.

That sounds reasonable. I can work on that as a follow-up to this. 

Re: names, I'll change it to `ProvideInfo` and rename `FillInfo` to `FillProvideInfo`. 

Patch:
diff --git a/dig.go b/dig.go
index 8af25a8..dc3c236 100644
--- a/dig.go
+++ b/dig.go
@@ -62,6 +62,7 @@ func (f optionFunc) applyOption(c *Container) { f(c) }
 type provideOptions struct {
 	Name  string
 	Group string
+	Info  *ProvideInfo
 }
 
 func (o *provideOptions) Validate() error {
@@ -128,6 +129,75 @@ func Group(group string) ProvideOption {
 	})
 }
 
+// ID is a unique integer representing the constructor node in the dependency graph.
+type ID int
+
+// ProvideInfo provides information about the constructor's inputs and outputs
+// types as strings, as well as the ID of the constructor supplied to the Container.
+// It contains ID for the constructor, as well as slices of Input and Output types,
+// which are Stringers that report the types of the parameters and results respectively.
+type ProvideInfo struct {
+	ID      ID
+	Inputs  []*Input
+	Outputs []*Output
+}
+
+// Input contains information on an input parameter of the constructor.
+type Input struct {
+	t           reflect.Type
+	optional    bool
+	name, group string
+}
+
+func (i *Input) String() string {
+	toks := make([]string, 0, 3)
+	t := i.t.String()
+	if i.optional {
+		toks = append(toks, "optional")
+	}
+	if i.name != "" {
+		toks = append(toks, fmt.Sprintf("name = %q", i.name))
+	}
+	if i.group != "" {
+		toks = append(toks, fmt.Sprintf("group = %q", i.group))
+	}
+
+	if len(toks) == 0 {
+		return t
+	}
+	return fmt.Sprintf("%v[%v]", t, strings.Join(toks, ", "))
+}
+
+// Output contains information on an output produced by the constructor.
+type Output struct {
+	t           reflect.Type
+	name, group string
+}
+
+func (o *Output) String() string {
+	toks := make([]string, 0, 2)
+	t := o.t.String()
+	if o.name != "" {
+		toks = append(toks, fmt.Sprintf("name = %q", o.name))
+	}
+	if o.group != "" {
+		toks = append(toks, fmt.Sprintf("group = %q", o.group))
+	}
+
+	if len(toks) == 0 {
+		return t
+	}
+	return fmt.Sprintf("%v[%v]", t, strings.Join(toks, ", "))
+}
+
+// FillProvideInfo is a ProvideOption that writes info on what Dig was able to get out
+// out of the provided constructor into the provided ProvideInfo.
+func FillProvideInfo(info *ProvideInfo) ProvideOption {
+	return provideOptionFunc(func(opts *provideOptions) {
+		opts.Info = info
+	})
+}
+
 // An InvokeOption modifies the default behavior of Invoke. It's included for
 // future functionality; currently, there are no concrete implementations.
 type InvokeOption interface {
@@ -498,9 +568,34 @@ func (c *Container) provide(ctor interface{}, opts provideOptions) error {
 		}
 		c.isVerifiedAcyclic = true
 	}
-
 	c.nodes = append(c.nodes, n)
 
+	// Record introspection info for caller if Info option is specified
+	if info := opts.Info; info != nil {
+		params := n.ParamList().DotParam()
+		results := n.ResultList().DotResult()
+
+		info.ID = (ID)(n.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
 }
 
@@ -730,7 +825,6 @@ func isFieldOptional(f reflect.StructField) (bool, error) {
 		err = errf(
 			"invalid value %q for %q tag on field %v",
 			tag, _optionalTag, f.Name, err)
-
 	}
 
 	return optional, err


Test patch:
diff --git a/dig_test.go b/dig_test.go
index d991496..a907f61 100644
--- a/dig_test.go
+++ b/dig_test.go
@@ -2990,3 +2990,125 @@ func TestUnexportedFieldsFailures(t *testing.T) {
 			`bad argument 1: invalid value "foo" for "ignore-unexported" tag on field In: strconv.ParseBool: parsing "foo": invalid syntax`)
 	})
 }
+
+func TestProvideInfoOption(t *testing.T) {
+	t.Parallel()
+	t.Run("two outputs", func(t *testing.T) {
+		type type1 struct{}
+		type type2 struct{}
+		ctor := func() (*type1, *type2) {
+			return &type1{}, &type2{}
+		}
+
+		c := New()
+		var info ProvideInfo
+		require.NoError(t, c.Provide(ctor, FillProvideInfo(&info)))
+
+		assert.Empty(t, info.Inputs)
+		assert.Equal(t, 2, len(info.Outputs))
+
+		assert.Equal(t, "*dig.type1", info.Outputs[0].String())
+		assert.Equal(t, "*dig.type2", info.Outputs[1].String())
+	})
+
+	t.Run("two inputs and one output", func(t *testing.T) {
+		type type1 struct{}
+		type type2 struct{}
+		type type3 struct{}
+		ctor := func(*type1, *type2) *type3 {
+			return &type3{}
+		}
+		c := New()
+		var info ProvideInfo
+		require.NoError(t, c.Provide(ctor, Name("n"), FillProvideInfo(&info)))
+
+		assert.Equal(t, 2, len(info.Inputs))
+		assert.Equal(t, 1, len(info.Outputs))
+
+		assert.Equal(t, `*dig.type3[name = "n"]`, info.Outputs[0].String())
+		assert.Equal(t, "*dig.type1", info.Inputs[0].String())
+		assert.Equal(t, "*dig.type2", info.Inputs[1].String())
+	})
+
+	t.Run("two inputs, output and error", func(t *testing.T) {
+		type type1 struct{}
+		type GatewayParams struct {
+			In
+
+			WriteToConn  *io.Writer `name:"rw" optional:"true"`
+			ReadFromConn *io.Reader `name:"ro"`
+			ConnNames    []string   `group:"server"`
+		}
+
+		type type3 struct{}
+
+		ctor := func(*type1, GatewayParams) (*type3, error) {
+			return &type3{}, nil
+		}
+		c := New()
+		var info ProvideInfo
+		require.NoError(t, c.Provide(ctor, FillProvideInfo(&info)))
+
+		assert.Equal(t, 4, len(info.Inputs))
+		assert.Equal(t, 1, len(info.Outputs))
+
+		assert.Equal(t, "*dig.type3", info.Outputs[0].String())
+		assert.Equal(t, "*dig.type1", info.Inputs[0].String())
+		assert.Equal(t, `*io.Writer[optional, name = "rw"]`, info.Inputs[1].String())
+		assert.Equal(t, `*io.Reader[name = "ro"]`, info.Inputs[2].String())
+		assert.Equal(t, `[]string[group = "server"]`, info.Inputs[3].String())
+	})
+
+	t.Run("two inputs, two outputs", func(t *testing.T) {
+		type type1 struct{}
+		type type2 struct{}
+		type type3 struct{}
+		type type4 struct{}
+		ctor := func(*type1, *type2) (*type3, *type4) {
+			return &type3{}, &type4{}
+		}
+		c := New()
+		info := ProvideInfo{}
+		require.NoError(t, c.Provide(ctor, Group("g"), FillProvideInfo(&info)))
+
+		assert.Equal(t, 2, len(info.Inputs))
+		assert.Equal(t, 2, len(info.Outputs))
+
+		assert.Equal(t, "*dig.type1", info.Inputs[0].String())
+		assert.Equal(t, "*dig.type2", info.Inputs[1].String())
+
+		assert.Equal(t, `*dig.type3[group = "g"]`, info.Outputs[0].String())
+		assert.Equal(t, `*dig.type4[group = "g"]`, info.Outputs[1].String())
+	})
+
+	t.Run("two ctors", func(t *testing.T) {
+		type type1 struct{}
+		type type2 struct{}
+		type type3 struct{}
+		type type4 struct{}
+		ctor1 := func(*type1) *type2 {
+			return &type2{}
+		}
+		ctor2 := func(*type3) *type4 {
+			return &type4{}
+		}
+		c := New()
+		info1 := ProvideInfo{}
+		info2 := ProvideInfo{}
+		require.NoError(t, c.Provide(ctor1, FillProvideInfo(&info1)))
+		require.NoError(t, c.Provide(ctor2, FillProvideInfo(&info2)))
+
+		assert.NotEqual(t, info1.ID, info2.ID)
+
+		assert.Equal(t, 1, len(info1.Inputs))
+		assert.Equal(t, 1, len(info1.Outputs))
+		assert.Equal(t, 1, len(info2.Inputs))
+		assert.Equal(t, 1, len(info2.Outputs))
+
+		assert.Equal(t, "*dig.type1", info1.Inputs[0].String())
+		assert.Equal(t, "*dig.type2", info1.Outputs[0].String())
+
+		assert.Equal(t, "*dig.type3", info2.Inputs[0].String())
+		assert.Equal(t, "*dig.type4", info2.Outputs[0].String())
+	})
+}