feat(depinject): resolve interface types (#12169)
This commit is contained in:
parent
907df327ed
commit
dd2e432937
@ -95,4 +95,8 @@ type keeperB struct {
|
||||
a KeeperA
|
||||
}
|
||||
|
||||
type KeeperB interface{}
|
||||
type KeeperB interface {
|
||||
isKeeperB()
|
||||
}
|
||||
|
||||
func (k keeperB) isKeeperB() {}
|
||||
|
||||
148
depinject/binding_test.go
Normal file
148
depinject/binding_test.go
Normal file
@ -0,0 +1,148 @@
|
||||
package depinject_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/regen-network/gocuke"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/depinject"
|
||||
)
|
||||
|
||||
func TestBindInterface(t *testing.T) {
|
||||
gocuke.NewRunner(t, &bindingSuite{}).
|
||||
Path("features/bindings.feature").
|
||||
Step(`we try to resolve a "Duck" in global scope`, (*bindingSuite).WeTryToResolveADuckInGlobalScope).
|
||||
Step(`module "(\w+)" wants a "Duck"`, (*bindingSuite).ModuleWantsADuck).
|
||||
Run()
|
||||
}
|
||||
|
||||
type Duck interface {
|
||||
quack()
|
||||
}
|
||||
|
||||
type Mallard struct{}
|
||||
type Canvasback struct{}
|
||||
type Marbled struct{}
|
||||
|
||||
func (duck Mallard) quack() {}
|
||||
func (duck Canvasback) quack() {}
|
||||
func (duck Marbled) quack() {}
|
||||
|
||||
type DuckWrapper struct {
|
||||
Module string
|
||||
Duck Duck
|
||||
}
|
||||
|
||||
func (d DuckWrapper) IsManyPerContainerType() {}
|
||||
|
||||
type Pond struct {
|
||||
Ducks []DuckWrapper
|
||||
}
|
||||
|
||||
type bindingSuite struct {
|
||||
gocuke.TestingT // this gets injected by gocuke
|
||||
|
||||
configs []depinject.Config
|
||||
pond *Pond
|
||||
err error
|
||||
}
|
||||
|
||||
func (s bindingSuite) AnInterfaceDuck() {
|
||||
// we don't need to do anything because this is defined at the type level
|
||||
}
|
||||
|
||||
func (s bindingSuite) TwoImplementationsMallardAndCanvasback() {
|
||||
// we don't need to do anything because this is defined at the type level
|
||||
}
|
||||
|
||||
func (s *bindingSuite) IsProvided(a string) {
|
||||
switch a {
|
||||
case "Mallard":
|
||||
s.addConfig(depinject.Provide(func() Mallard { return Mallard{} }))
|
||||
case "Canvasback":
|
||||
s.addConfig(depinject.Provide(func() Canvasback { return Canvasback{} }))
|
||||
case "Marbled":
|
||||
s.addConfig(depinject.Provide(func() Marbled { return Marbled{} }))
|
||||
default:
|
||||
s.Fatalf("unexpected duck type %s", a)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *bindingSuite) addConfig(config depinject.Config) {
|
||||
s.configs = append(s.configs, config)
|
||||
}
|
||||
|
||||
func (s *bindingSuite) WeTryToResolveADuckInGlobalScope() {
|
||||
s.addConfig(depinject.Provide(func(duck Duck) DuckWrapper {
|
||||
return DuckWrapper{Module: "", Duck: duck}
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *bindingSuite) resolvePond() *Pond {
|
||||
if s.pond != nil {
|
||||
return s.pond
|
||||
}
|
||||
|
||||
s.addConfig(depinject.Provide(func(ducks []DuckWrapper) Pond { return Pond{Ducks: ducks} }))
|
||||
var pond Pond
|
||||
s.err = depinject.Inject(depinject.Configs(s.configs...), &pond)
|
||||
s.pond = &pond
|
||||
return s.pond
|
||||
}
|
||||
|
||||
func (s *bindingSuite) IsResolvedInGlobalScope(typeName string) {
|
||||
pond := s.resolvePond()
|
||||
found := false
|
||||
for _, dw := range pond.Ducks {
|
||||
if dw.Module == "" {
|
||||
require.Contains(s, reflect.TypeOf(dw.Duck).Name(), typeName)
|
||||
found = true
|
||||
}
|
||||
}
|
||||
assert.True(s, found)
|
||||
}
|
||||
|
||||
func (s *bindingSuite) ThereIsAError(expectedErrorMsg string) {
|
||||
s.resolvePond()
|
||||
assert.ErrorContains(s, s.err, expectedErrorMsg)
|
||||
}
|
||||
|
||||
func (s *bindingSuite) ThereIsNoError() {
|
||||
s.resolvePond()
|
||||
assert.NoError(s, s.err)
|
||||
}
|
||||
|
||||
func fullTypeName(typeName string) string {
|
||||
return fmt.Sprintf("github.com/cosmos/cosmos-sdk/depinject_test/depinject_test.%s", typeName)
|
||||
}
|
||||
|
||||
func (s *bindingSuite) ThereIsAGlobalBindingForA(preferredType string, interfaceType string) {
|
||||
s.addConfig(depinject.BindInterface(fullTypeName(interfaceType), fullTypeName(preferredType)))
|
||||
}
|
||||
|
||||
func (s *bindingSuite) ThereIsABindingForAInModule(preferredType string, interfaceType string, moduleName string) {
|
||||
s.addConfig(depinject.BindInterfaceInModule(moduleName, fullTypeName(interfaceType), fullTypeName(preferredType)))
|
||||
}
|
||||
|
||||
func (s *bindingSuite) ModuleWantsADuck(module string) {
|
||||
s.addConfig(depinject.ProvideInModule(module, func(duck Duck) DuckWrapper {
|
||||
return DuckWrapper{Module: module, Duck: duck}
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *bindingSuite) ModuleResolvesA(module string, duckType string) {
|
||||
pond := s.resolvePond()
|
||||
moduleFound := false
|
||||
for _, dw := range pond.Ducks {
|
||||
if dw.Module == module {
|
||||
assert.Contains(s, reflect.TypeOf(dw.Duck).Name(), duckType)
|
||||
moduleFound = true
|
||||
}
|
||||
}
|
||||
assert.True(s, moduleFound)
|
||||
}
|
||||
@ -48,6 +48,48 @@ func provide(ctr *container, key *moduleKey, providers []interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BindInterface defines a container configuration for an explicit interface binding of inTypeName to outTypeName
|
||||
// in global scope. The example below demonstrates a configuration where the container always provides a Canvasback
|
||||
// instance when an interface of type Duck is requested as an input.
|
||||
//
|
||||
// BindInterface(
|
||||
// "github.com/cosmos/cosmos-sdk/depinject_test/depinject_test.Duck",
|
||||
// "github.com/cosmos/cosmos-sdk/depinject_test/depinject_test.Canvasback")
|
||||
func BindInterface(inTypeName string, outTypeName string) Config {
|
||||
return containerConfig(func(ctr *container) error {
|
||||
return bindInterface(ctr, inTypeName, outTypeName, "")
|
||||
})
|
||||
}
|
||||
|
||||
// BindInterfaceInModule defines a container configuration for an explicit interface binding of inTypeName to outTypeName
|
||||
// in the scope of the module with name moduleName. The example below demonstrates a configuration where the container
|
||||
// provides a Canvasback instance when an interface of type Duck is requested as an input, but only in the scope of
|
||||
// "moduleFoo".
|
||||
//
|
||||
// BindInterfaceInModule(
|
||||
// "moduleFoo",
|
||||
// "github.com/cosmos/cosmos-sdk/depinject_test/depinject_test.Duck",
|
||||
// "github.com/cosmos/cosmos-sdk/depinject_test/depinject_test.Canvasback")
|
||||
func BindInterfaceInModule(moduleName string, inTypeName string, outTypeName string) Config {
|
||||
return containerConfig(func(ctr *container) error {
|
||||
return bindInterface(ctr, inTypeName, outTypeName, moduleName)
|
||||
})
|
||||
}
|
||||
|
||||
func bindInterface(ctr *container, inTypeName string, outTypeName string, moduleName string) error {
|
||||
var mk *moduleKey
|
||||
if moduleName != "" {
|
||||
mk = &moduleKey{name: moduleName}
|
||||
}
|
||||
ctr.addBinding(interfaceBinding{
|
||||
interfaceName: inTypeName,
|
||||
implTypeName: outTypeName,
|
||||
moduleKey: mk,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Supply(values ...interface{}) Config {
|
||||
loc := LocationFromCaller(1)
|
||||
return containerConfig(func(ctr *container) error {
|
||||
|
||||
@ -3,18 +3,16 @@ package depinject
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/depinject/internal/graphviz"
|
||||
"github.com/pkg/errors"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type container struct {
|
||||
*debugConfig
|
||||
|
||||
resolvers map[reflect.Type]resolver
|
||||
keyedResolvers map[string]resolver
|
||||
resolvers map[string]resolver
|
||||
interfaceBindings map[string]interfaceBinding
|
||||
|
||||
moduleKeys map[string]*moduleKey
|
||||
|
||||
@ -28,14 +26,24 @@ type resolveFrame struct {
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
// interfaceBinding defines a type binding for interfaceName to type implTypeName when being provided as a
|
||||
// dependency to the module identified by moduleKey. If moduleKey is nil then the type binding is applied globally,
|
||||
// not module-scoped.
|
||||
type interfaceBinding struct {
|
||||
interfaceName string
|
||||
implTypeName string
|
||||
moduleKey *moduleKey
|
||||
resolver resolver
|
||||
}
|
||||
|
||||
func newContainer(cfg *debugConfig) *container {
|
||||
return &container{
|
||||
debugConfig: cfg,
|
||||
resolvers: map[reflect.Type]resolver{},
|
||||
keyedResolvers: map[string]resolver{},
|
||||
moduleKeys: map[string]*moduleKey{},
|
||||
callerStack: nil,
|
||||
callerMap: map[Location]bool{},
|
||||
debugConfig: cfg,
|
||||
resolvers: map[string]resolver{},
|
||||
moduleKeys: map[string]*moduleKey{},
|
||||
interfaceBindings: map[string]interfaceBinding{},
|
||||
callerStack: nil,
|
||||
callerMap: map[Location]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,14 +86,18 @@ func (c *container) call(provider *ProviderDescriptor, moduleKey *moduleKey) ([]
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *container) getResolver(typ reflect.Type, key string) (resolver, error) {
|
||||
if key != "" {
|
||||
if vr, ok := c.keyedResolvers[key]; ok {
|
||||
return vr, nil
|
||||
}
|
||||
func (c *container) getResolver(typ reflect.Type, key *moduleKey) (resolver, error) {
|
||||
c.logf("Resolving %v", typ)
|
||||
|
||||
pr, err := c.getExplicitResolver(typ, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pr != nil {
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
if vr, ok := c.resolvers[typ]; ok {
|
||||
if vr, ok := c.resolverByType(typ); ok {
|
||||
return vr, nil
|
||||
}
|
||||
|
||||
@ -109,8 +121,8 @@ func (c *container) getResolver(typ reflect.Type, key string) (resolver, error)
|
||||
graphNode: typeGraphNode,
|
||||
}
|
||||
|
||||
c.resolvers[elemType] = r
|
||||
c.resolvers[sliceType] = &sliceGroupResolver{r}
|
||||
c.addResolver(elemType, r)
|
||||
c.addResolver(sliceType, &sliceGroupResolver{r})
|
||||
} else if isOnePerModuleType(elemType) {
|
||||
c.logf("Registering resolver for one-per-module type %v", elemType)
|
||||
mapType := reflect.MapOf(stringType, elemType)
|
||||
@ -126,11 +138,63 @@ func (c *container) getResolver(typ reflect.Type, key string) (resolver, error)
|
||||
graphNode: typeGraphNode,
|
||||
}
|
||||
|
||||
c.resolvers[elemType] = r
|
||||
c.resolvers[mapType] = &mapOfOnePerModuleResolver{r}
|
||||
c.addResolver(elemType, r)
|
||||
c.addResolver(mapType, &mapOfOnePerModuleResolver{r})
|
||||
}
|
||||
|
||||
return c.resolvers[typ], nil
|
||||
res, found := c.resolverByType(typ)
|
||||
|
||||
if !found && typ.Kind() == reflect.Interface {
|
||||
matches := map[reflect.Type]reflect.Type{}
|
||||
var resolverType reflect.Type
|
||||
for _, r := range c.resolvers {
|
||||
if r.getType().Kind() != reflect.Interface && r.getType().Implements(typ) {
|
||||
resolverType = r.getType()
|
||||
matches[resolverType] = resolverType
|
||||
}
|
||||
}
|
||||
|
||||
if len(matches) == 1 {
|
||||
res, _ = c.resolverByType(resolverType)
|
||||
c.logf("Implicitly registering resolver %v for interface type %v", resolverType, typ)
|
||||
c.addResolver(typ, res)
|
||||
} else if len(matches) > 1 {
|
||||
return nil, newErrMultipleImplicitInterfaceBindings(typ, matches)
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (c *container) getExplicitResolver(typ reflect.Type, key *moduleKey) (resolver, error) {
|
||||
var pref interfaceBinding
|
||||
var found bool
|
||||
|
||||
// module scoped binding takes precedence
|
||||
pref, found = c.interfaceBindings[bindingKeyFromType(typ, key)]
|
||||
|
||||
// fallback to global scope binding
|
||||
if !found {
|
||||
pref, found = c.interfaceBindings[bindingKeyFromType(typ, nil)]
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if pref.resolver != nil {
|
||||
return pref.resolver, nil
|
||||
}
|
||||
|
||||
res, ok := c.resolverByTypeName(pref.implTypeName)
|
||||
if ok {
|
||||
c.logf("Registering resolver %v for interface type %v by explicit binding", res.getType(), typ)
|
||||
pref.resolver = res
|
||||
return res, nil
|
||||
|
||||
}
|
||||
|
||||
return nil, newErrNoTypeForExplicitBindingFound(pref)
|
||||
}
|
||||
|
||||
var stringType = reflect.TypeOf("")
|
||||
@ -155,7 +219,7 @@ func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (inter
|
||||
return nil, fmt.Errorf("one-per-module type %v can't be used as an input parameter", typ)
|
||||
}
|
||||
|
||||
vr, err := c.getResolver(typ, in.Key)
|
||||
vr, err := c.getResolver(typ, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -197,7 +261,7 @@ func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (inter
|
||||
typ = typ.Elem()
|
||||
}
|
||||
|
||||
vr, err := c.getResolver(typ, out.Key)
|
||||
vr, err := c.getResolver(typ, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -218,11 +282,7 @@ func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (inter
|
||||
graphNode: typeGraphNode,
|
||||
idxInValues: i,
|
||||
}
|
||||
c.resolvers[typ] = vr
|
||||
|
||||
if out.Key != "" {
|
||||
c.keyedResolvers[out.Key] = vr
|
||||
}
|
||||
c.addResolver(typ, vr)
|
||||
}
|
||||
|
||||
c.addGraphEdge(providerGraphNode, vr.typeGraphNode())
|
||||
@ -250,25 +310,20 @@ func (c *container) addNode(provider *ProviderDescriptor, key *moduleKey) (inter
|
||||
|
||||
c.logf("Registering resolver for module-scoped type %v", typ)
|
||||
|
||||
existing, ok := c.resolvers[typ]
|
||||
existing, ok := c.resolverByType(typ)
|
||||
if ok {
|
||||
return nil, errors.Errorf("duplicate provision of type %v by module-scoped provider %s\n\talready provided by %s",
|
||||
typ, provider.Location, existing.describeLocation())
|
||||
}
|
||||
|
||||
typeGraphNode := c.typeGraphNode(typ)
|
||||
mdr := &moduleDepResolver{
|
||||
c.addResolver(typ, &moduleDepResolver{
|
||||
typ: typ,
|
||||
idxInValues: i,
|
||||
node: node,
|
||||
valueMap: map[*moduleKey]reflect.Value{},
|
||||
graphNode: typeGraphNode,
|
||||
}
|
||||
c.resolvers[typ] = mdr
|
||||
|
||||
if out.Key != "" {
|
||||
c.keyedResolvers[out.Key] = mdr
|
||||
}
|
||||
})
|
||||
|
||||
c.addGraphEdge(providerGraphNode, typeGraphNode)
|
||||
}
|
||||
@ -284,16 +339,16 @@ func (c *container) supply(value reflect.Value, location Location) error {
|
||||
typeGraphNode := c.typeGraphNode(typ)
|
||||
c.addGraphEdge(locGrapNode, typeGraphNode)
|
||||
|
||||
if existing, ok := c.resolvers[typ]; ok {
|
||||
if existing, ok := c.resolverByType(typ); ok {
|
||||
return duplicateDefinitionError(typ, location, existing.describeLocation())
|
||||
}
|
||||
|
||||
c.resolvers[typ] = &supplyResolver{
|
||||
c.addResolver(typ, &supplyResolver{
|
||||
typ: typ,
|
||||
value: value,
|
||||
loc: location,
|
||||
graphNode: typeGraphNode,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -321,7 +376,7 @@ func (c *container) resolve(in ProviderInput, moduleKey *moduleKey, caller Locat
|
||||
return reflect.ValueOf(OwnModuleKey{moduleKey}), nil
|
||||
}
|
||||
|
||||
vr, err := c.getResolver(in.Type, in.Key)
|
||||
vr, err := c.getResolver(in.Type, moduleKey)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
@ -431,6 +486,42 @@ func (c container) formatResolveStack() string {
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func fullyQualifiedTypeName(typ reflect.Type) string {
|
||||
pkgType := typ
|
||||
if typ.Kind() == reflect.Pointer || typ.Kind() == reflect.Slice || typ.Kind() == reflect.Map || typ.Kind() == reflect.Array {
|
||||
pkgType = typ.Elem()
|
||||
}
|
||||
return fmt.Sprintf("%s/%v", pkgType.PkgPath(), typ)
|
||||
}
|
||||
|
||||
func bindingKeyFromTypeName(typeName string, key *moduleKey) string {
|
||||
if key == nil {
|
||||
return fmt.Sprintf("%s;", typeName)
|
||||
}
|
||||
return fmt.Sprintf("%s;%s", typeName, key.name)
|
||||
}
|
||||
|
||||
func bindingKeyFromType(typ reflect.Type, key *moduleKey) string {
|
||||
return bindingKeyFromTypeName(fullyQualifiedTypeName(typ), key)
|
||||
}
|
||||
|
||||
func (c *container) addBinding(p interfaceBinding) {
|
||||
c.interfaceBindings[bindingKeyFromTypeName(p.interfaceName, p.moduleKey)] = p
|
||||
}
|
||||
|
||||
func (c *container) addResolver(typ reflect.Type, r resolver) {
|
||||
c.resolvers[fullyQualifiedTypeName(typ)] = r
|
||||
}
|
||||
|
||||
func (c *container) resolverByType(typ reflect.Type) (resolver, bool) {
|
||||
return c.resolverByTypeName(fullyQualifiedTypeName(typ))
|
||||
}
|
||||
|
||||
func (c *container) resolverByTypeName(typeName string) (resolver, bool) {
|
||||
res, found := c.resolvers[typeName]
|
||||
return res, found
|
||||
}
|
||||
|
||||
func markGraphNodeAsUsed(node *graphviz.Node) {
|
||||
node.SetColor("black")
|
||||
node.SetPenWidth("1.5")
|
||||
|
||||
@ -634,45 +634,3 @@ func TestConditionalDebugging(t *testing.T) {
|
||||
require.Empty(t, logs)
|
||||
require.True(t, success)
|
||||
}
|
||||
|
||||
type Duck interface {
|
||||
quack()
|
||||
}
|
||||
|
||||
type AlsoDuck interface {
|
||||
quack()
|
||||
}
|
||||
|
||||
type Mallard struct{}
|
||||
|
||||
func (duck Mallard) quack() {}
|
||||
|
||||
type KeyedOutput struct {
|
||||
depinject.Out
|
||||
Duck Duck `key:"foo"`
|
||||
}
|
||||
|
||||
type KeyedInput struct {
|
||||
depinject.In
|
||||
AlsoDuck AlsoDuck `key:"foo"`
|
||||
}
|
||||
|
||||
type Pond struct {
|
||||
Duck AlsoDuck
|
||||
}
|
||||
|
||||
func TestKeyedInputOutput(t *testing.T) {
|
||||
var pond Pond
|
||||
|
||||
require.NoError(t,
|
||||
depinject.Inject(
|
||||
depinject.Provide(
|
||||
func() KeyedOutput { return KeyedOutput{Duck: Mallard{}} },
|
||||
func(in KeyedInput) Pond {
|
||||
require.NotNil(t, in.AlsoDuck)
|
||||
return Pond{Duck: in.AlsoDuck}
|
||||
}),
|
||||
&pond))
|
||||
|
||||
require.NotNil(t, pond)
|
||||
}
|
||||
|
||||
@ -1,11 +1,70 @@
|
||||
package depinject
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrMultipleImplicitInterfaceBindings defines an error condition where an attempt was made to implicitly bind
|
||||
// Interface to a concrete type, but the container was unable to come to a resolution because multiple Matches
|
||||
// were found.
|
||||
type ErrMultipleImplicitInterfaceBindings struct {
|
||||
error
|
||||
Interface reflect.Type
|
||||
Matches []reflect.Type
|
||||
}
|
||||
|
||||
func newErrMultipleImplicitInterfaceBindings(i reflect.Type, matches map[reflect.Type]reflect.Type) ErrMultipleImplicitInterfaceBindings {
|
||||
var ms []reflect.Type
|
||||
for k := range matches {
|
||||
ms = append(ms, k)
|
||||
}
|
||||
return ErrMultipleImplicitInterfaceBindings{Interface: i, Matches: ms}
|
||||
}
|
||||
|
||||
func (err ErrMultipleImplicitInterfaceBindings) Error() string {
|
||||
matchesStr := ""
|
||||
for _, m := range err.Matches {
|
||||
matchesStr = fmt.Sprintf("%s\n %s", matchesStr, fullyQualifiedTypeName(m))
|
||||
}
|
||||
return fmt.Sprintf("Multiple implementations found for interface %v: %s", err.Interface, matchesStr)
|
||||
}
|
||||
|
||||
// ErrNoTypeForExplicitBindingFound defines an error condition where an explicit binding was specified from Interface
|
||||
// to Implementation but no provider for the requested Implementation was found in the container.
|
||||
type ErrNoTypeForExplicitBindingFound struct {
|
||||
Implementation string
|
||||
Interface string
|
||||
ModuleName string
|
||||
error
|
||||
}
|
||||
|
||||
func newErrNoTypeForExplicitBindingFound(p interfaceBinding) ErrNoTypeForExplicitBindingFound {
|
||||
var moduleName string
|
||||
if p.moduleKey != nil {
|
||||
moduleName = p.moduleKey.name
|
||||
}
|
||||
|
||||
return ErrNoTypeForExplicitBindingFound{
|
||||
Implementation: p.implTypeName,
|
||||
Interface: p.interfaceName,
|
||||
ModuleName: moduleName,
|
||||
}
|
||||
}
|
||||
|
||||
func (err ErrNoTypeForExplicitBindingFound) Error() string {
|
||||
if err.ModuleName != "" {
|
||||
return fmt.Sprintf("No type for explicit binding found. Given the explicit interface binding %s in module %s, a provider of type %s was not found.",
|
||||
err.Interface, err.ModuleName, err.Implementation)
|
||||
} else {
|
||||
return fmt.Sprintf("No type for explicit binding found. Given the explicit interface binding %s, a provider of type %s was not found.",
|
||||
err.Interface, err.Implementation)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func duplicateDefinitionError(typ reflect.Type, duplicateLoc Location, existingLoc string) error {
|
||||
return errors.Errorf("duplicate provision of type %v by %s\n\talready provided by %s",
|
||||
typ, duplicateLoc, existingLoc)
|
||||
|
||||
94
depinject/features/bindings.feature
Normal file
94
depinject/features/bindings.feature
Normal file
@ -0,0 +1,94 @@
|
||||
Feature: interface type resolution
|
||||
|
||||
Background:
|
||||
Given an interface Duck
|
||||
And two implementations Mallard and Canvasback
|
||||
|
||||
Rule: interface types resolve to a concrete type implicitly if there is only one matching implementation
|
||||
Example: only one implementation
|
||||
Given "Mallard" is provided
|
||||
When we try to resolve a "Duck" in global scope
|
||||
Then "Mallard" is resolved in global scope
|
||||
|
||||
Example: two implementations
|
||||
Given "Mallard" is provided
|
||||
* "Canvasback" is provided
|
||||
When we try to resolve a "Duck" in global scope
|
||||
Then there is a "Multiple implementations found" error
|
||||
|
||||
Rule: bindings must point to a real type
|
||||
Example: a bound type is not provided
|
||||
Given "Mallard" is provided
|
||||
And there is a global binding for a "Marbled" "Duck"
|
||||
When we try to resolve a "Duck" in global scope
|
||||
Then there is a "No type for explicit binding" error
|
||||
|
||||
Rule: bindings supersede implicit type resolution
|
||||
Example: global scope
|
||||
Given "Canvasback" is provided
|
||||
And there is a global binding for a "Mallard" "Duck"
|
||||
When we try to resolve a "Duck" in global scope
|
||||
Then there is a "No type for explicit binding" error
|
||||
|
||||
Example: module scope
|
||||
Given "Canvasback" is provided
|
||||
And there is a binding for a "Mallard" "Duck" in module "A"
|
||||
When module "A" wants a "Duck"
|
||||
Then there is a "No type for explicit binding" error
|
||||
|
||||
Rule: bindings in global scope apply to both global and module-scoped resolution (if there is no module-scoped binding)
|
||||
Example: global resolution
|
||||
Given "Mallard" is provided
|
||||
And "Canvasback" is provided
|
||||
And there is a global binding for a "Mallard" "Duck"
|
||||
When we try to resolve a "Duck" in global scope
|
||||
Then "Mallard" is resolved in global scope
|
||||
|
||||
Example: module-scoped resolution
|
||||
Given "Mallard" is provided
|
||||
And "Canvasback" is provided
|
||||
And there is a global binding for a "Mallard" "Duck"
|
||||
When module "A" wants a "Duck"
|
||||
Then module "A" resolves a "Mallard"
|
||||
|
||||
Rule: module-scoped bindings only apply to module-scoped resolution
|
||||
Example: a module-scoped binding doesn't work for global scope
|
||||
Given "Mallard" is provided
|
||||
* "Canvasback" is provided
|
||||
* there is a binding for a "Canvasback" "Duck" in module "A"
|
||||
When we try to resolve a "Duck" in global scope
|
||||
Then there is a "Multiple implementations found" error
|
||||
|
||||
Example: a module-scoped binding works for that module
|
||||
Given "Mallard" is provided
|
||||
* "Canvasback" is provided
|
||||
* there is a binding for a "Canvasback" "Duck" in module "A"
|
||||
When module "A" wants a "Duck"
|
||||
Then module "A" resolves a "Canvasback"
|
||||
|
||||
Example: a module-scoped binding doesn't work for another module
|
||||
Given "Mallard" is provided
|
||||
* "Canvasback" is provided
|
||||
* there is a binding for a "Canvasback" "Duck" in module "A"
|
||||
When module "B" wants a "Duck"
|
||||
Then there is a "Multiple implementations found" error
|
||||
|
||||
# this case is called a "journey" scenario which tests a bunch of things together
|
||||
# most tests should be short and to the point like the ones above but one or two long ones
|
||||
# are good to test more things together &/or do integration tests
|
||||
Example: two module-scoped binding and a global binding
|
||||
Given "Mallard" is provided
|
||||
* "Canvasback" is provided
|
||||
* "Marbled" is provided
|
||||
* there is a global binding for a "Marbled" "Duck"
|
||||
* there is a binding for a "Canvasback" "Duck" in module "A"
|
||||
* there is a binding for a "Mallard" "Duck" in module "B"
|
||||
When module "A" wants a "Duck"
|
||||
* module "B" wants a "Duck"
|
||||
* module "C" wants a "Duck"
|
||||
* we try to resolve a "Duck" in global scope
|
||||
Then there is no error
|
||||
* module "A" resolves a "Canvasback"
|
||||
* module "B" resolves a "Mallard"
|
||||
* module "C" resolves a "Marbled"
|
||||
* "Marbled" is resolved in global scope
|
||||
@ -4,14 +4,21 @@ go 1.18
|
||||
|
||||
require (
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/regen-network/gocuke v0.6.2
|
||||
github.com/stretchr/testify v1.7.1
|
||||
golang.org/x/exp v0.0.0-20220428152302-39d4317da171
|
||||
gotest.tools/v3 v3.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/alecthomas/participle/v2 v2.0.0-alpha7 // indirect
|
||||
github.com/cockroachdb/apd/v3 v3.1.0 // indirect
|
||||
github.com/cucumber/common/gherkin/go/v22 v22.0.0 // indirect
|
||||
github.com/cucumber/common/messages/go/v17 v17.1.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||
github.com/google/go-cmp v0.5.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
pgregory.net/rapid v0.4.7 // indirect
|
||||
)
|
||||
|
||||
@ -1,14 +1,33 @@
|
||||
github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c=
|
||||
github.com/alecthomas/participle/v2 v2.0.0-alpha7/go.mod h1:NumScqsC42o9x+dGj8/YqsIfhrIQjFEOFovxotbBirA=
|
||||
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1 h1:GDQdwm/gAcJcLAKQQZGOJ4knlw+7rfEQQcmwTbt4p5E=
|
||||
github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
|
||||
github.com/cockroachdb/apd/v3 v3.1.0 h1:MK3Ow7LH0W8zkd5GMKA1PvS9qG3bWFI95WaVNfyZJ/w=
|
||||
github.com/cockroachdb/apd/v3 v3.1.0/go.mod h1:6qgPBMXjATAdD/VefbRP9NoSLKjbB4LCoA7gN4LpHs4=
|
||||
github.com/cucumber/common/gherkin/go/v22 v22.0.0 h1:4K8NqptbvdOrjL9DEea6HFjSpbdT9+Q5kgLpmmsHYl0=
|
||||
github.com/cucumber/common/gherkin/go/v22 v22.0.0/go.mod h1:3mJT10B2GGn3MvVPd3FwR7m2u4tLhSRhWUqJU4KN4Fg=
|
||||
github.com/cucumber/common/messages/go/v17 v17.1.1 h1:RNqopvIFyLWnKv0LfATh34SWBhXeoFTJnSrgm9cT/Ts=
|
||||
github.com/cucumber/common/messages/go/v17 v17.1.1/go.mod h1:bpGxb57tDE385Rb2EohgUadLkAbhoC4IyCFi89u/JQI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/regen-network/gocuke v0.6.2 h1:pHviZ0kKAq2U2hN2q3smKNxct6hS0mGByFMHGnWA97M=
|
||||
github.com/regen-network/gocuke v0.6.2/go.mod h1:zYaqIHZobHyd0xOrHGPQjbhGJsuZ1oElx150u2o1xuk=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@ -39,8 +58,11 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
|
||||
gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
|
||||
pgregory.net/rapid v0.4.7 h1:MTNRktPuv5FNqOO151TM9mDTa+XHcX6ypYeISDVD14g=
|
||||
pgregory.net/rapid v0.4.7/go.mod h1:UYpPVyjFHzYBGHIxLFoupi8vwk6rXNzRY9OMvVxFIOU=
|
||||
|
||||
@ -38,6 +38,10 @@ type groupResolver struct {
|
||||
graphNode *graphviz.Node
|
||||
}
|
||||
|
||||
func (g *groupResolver) getType() reflect.Type {
|
||||
return g.sliceType
|
||||
}
|
||||
|
||||
type sliceGroupResolver struct {
|
||||
*groupResolver
|
||||
}
|
||||
|
||||
@ -20,6 +20,10 @@ type moduleDepResolver struct {
|
||||
graphNode *graphviz.Node
|
||||
}
|
||||
|
||||
func (s moduleDepResolver) getType() reflect.Type {
|
||||
return s.typ
|
||||
}
|
||||
|
||||
func (s moduleDepResolver) describeLocation() string {
|
||||
return s.node.provider.Location.String()
|
||||
}
|
||||
|
||||
@ -37,6 +37,10 @@ type onePerModuleResolver struct {
|
||||
graphNode *graphviz.Node
|
||||
}
|
||||
|
||||
func (o *onePerModuleResolver) getType() reflect.Type {
|
||||
return o.mapType
|
||||
}
|
||||
|
||||
type mapOfOnePerModuleResolver struct {
|
||||
*onePerModuleResolver
|
||||
}
|
||||
|
||||
@ -28,12 +28,10 @@ type ProviderDescriptor struct {
|
||||
type ProviderInput struct {
|
||||
Type reflect.Type
|
||||
Optional bool
|
||||
Key string
|
||||
}
|
||||
|
||||
type ProviderOutput struct {
|
||||
Type reflect.Type
|
||||
Key string
|
||||
}
|
||||
|
||||
func ExtractProviderDescriptor(provider interface{}) (ProviderDescriptor, error) {
|
||||
|
||||
@ -24,16 +24,6 @@ type StructOut struct {
|
||||
Y []byte
|
||||
}
|
||||
|
||||
type KeyedIn struct {
|
||||
depinject.In
|
||||
X string `key:"theKey"`
|
||||
}
|
||||
|
||||
type KeyedOut struct {
|
||||
depinject.Out
|
||||
X string `key:"theKey"`
|
||||
}
|
||||
|
||||
func TestExtractProviderDescriptor(t *testing.T) {
|
||||
var (
|
||||
intType = reflect.TypeOf(0)
|
||||
@ -97,20 +87,6 @@ func TestExtractProviderDescriptor(t *testing.T) {
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
name: "keyed input",
|
||||
ctr: func(_ KeyedIn) int { return 0 },
|
||||
wantIn: []depinject.ProviderInput{{Type: stringType, Key: "theKey"}},
|
||||
wantOut: []depinject.ProviderOutput{{Type: intType}},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "keyed output",
|
||||
ctr: func(s string) KeyedOut { return KeyedOut{X: "foo"} },
|
||||
wantIn: []depinject.ProviderInput{{Type: stringType}},
|
||||
wantOut: []depinject.ProviderOutput{{Type: stringType, Key: "theKey"}},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@ -11,4 +11,5 @@ type resolver interface {
|
||||
resolve(*container, *moduleKey, Location) (reflect.Value, error)
|
||||
describeLocation() string
|
||||
typeGraphNode() *graphviz.Node
|
||||
getType() reflect.Type
|
||||
}
|
||||
|
||||
@ -22,6 +22,10 @@ type simpleResolver struct {
|
||||
graphNode *graphviz.Node
|
||||
}
|
||||
|
||||
func (s *simpleResolver) getType() reflect.Type {
|
||||
return s.typ
|
||||
}
|
||||
|
||||
func (s *simpleResolver) describeLocation() string {
|
||||
return s.node.provider.Location.String()
|
||||
}
|
||||
|
||||
@ -120,16 +120,9 @@ func structArgsInTypes(typ reflect.Type) ([]ProviderInput, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var key string
|
||||
keyTag, keyFound := f.Tag.Lookup("key")
|
||||
if keyFound {
|
||||
key = keyTag
|
||||
}
|
||||
|
||||
res = append(res, ProviderInput{
|
||||
Type: f.Type,
|
||||
Optional: optional,
|
||||
Key: key,
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
@ -158,15 +151,8 @@ func structArgsOutTypes(typ reflect.Type) []ProviderOutput {
|
||||
continue
|
||||
}
|
||||
|
||||
var key string
|
||||
keyTag, keyFound := f.Tag.Lookup("key")
|
||||
if keyFound {
|
||||
key = keyTag
|
||||
}
|
||||
|
||||
res = append(res, ProviderOutput{
|
||||
Type: f.Type,
|
||||
Key: key,
|
||||
})
|
||||
}
|
||||
return res
|
||||
|
||||
@ -13,6 +13,10 @@ type supplyResolver struct {
|
||||
graphNode *graphviz.Node
|
||||
}
|
||||
|
||||
func (s supplyResolver) getType() reflect.Type {
|
||||
return s.typ
|
||||
}
|
||||
|
||||
func (s supplyResolver) describeLocation() string {
|
||||
return s.loc.String()
|
||||
}
|
||||
|
||||
@ -211,7 +211,7 @@ func provideModuleBasic() runtime.AppModuleBasicWrapper {
|
||||
type authOutputs struct {
|
||||
depinject.Out
|
||||
|
||||
AccountKeeper keeper.AccountKeeper `key:"cosmos.auth.v1.AccountKeeper"`
|
||||
AccountKeeper keeper.AccountKeeper
|
||||
Module runtime.AppModuleWrapper
|
||||
}
|
||||
|
||||
|
||||
@ -221,7 +221,7 @@ type bankInputs struct {
|
||||
depinject.In
|
||||
|
||||
Config *modulev1.Module
|
||||
AccountKeeper types.AccountKeeper `key:"cosmos.auth.v1.AccountKeeper"`
|
||||
AccountKeeper types.AccountKeeper
|
||||
Cdc codec.Codec
|
||||
Subspace paramtypes.Subspace
|
||||
Key *store.KVStoreKey
|
||||
@ -230,7 +230,7 @@ type bankInputs struct {
|
||||
type bankOutputs struct {
|
||||
depinject.Out
|
||||
|
||||
BankKeeper keeper.Keeper `key:"cosmos.bank.v1.Keeper"`
|
||||
BankKeeper keeper.BaseKeeper
|
||||
Module runtime.AppModuleWrapper
|
||||
}
|
||||
|
||||
|
||||
@ -209,8 +209,8 @@ type feegrantInputs struct {
|
||||
|
||||
Key *store.KVStoreKey
|
||||
Cdc codec.Codec
|
||||
AccountKeeper feegrant.AccountKeeper `key:"cosmos.auth.v1.AccountKeeper"`
|
||||
BankKeeper feegrant.BankKeeper `key:"cosmos.bank.v1.Keeper"`
|
||||
AccountKeeper feegrant.AccountKeeper
|
||||
BankKeeper feegrant.BankKeeper
|
||||
Registry cdctypes.InterfaceRegistry
|
||||
}
|
||||
|
||||
|
||||
@ -197,8 +197,8 @@ type stakingInputs struct {
|
||||
depinject.In
|
||||
|
||||
Config *modulev1.Module
|
||||
AccountKeeper types.AccountKeeper `key:"cosmos.auth.v1.AccountKeeper"`
|
||||
BankKeeper types.BankKeeper `key:"cosmos.bank.v1.Keeper"`
|
||||
AccountKeeper types.AccountKeeper
|
||||
BankKeeper types.BankKeeper
|
||||
Cdc codec.Codec
|
||||
Subspace paramstypes.Subspace
|
||||
Key *store.KVStoreKey
|
||||
@ -208,7 +208,7 @@ type stakingInputs struct {
|
||||
type stakingOutputs struct {
|
||||
depinject.Out
|
||||
|
||||
StakingKeeper *keeper.Keeper `key:"cosmos.staking.v1.Keeper"`
|
||||
StakingKeeper *keeper.Keeper
|
||||
Module runtime.AppModuleWrapper
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user