feat: add low-level dependency injection container API (#9658)
This commit is contained in:
parent
13559f9132
commit
e656d5e28a
|
@ -52,3 +52,5 @@
|
|||
- x/*/client/**/*
|
||||
"Type: ADR":
|
||||
- docs/architecture/**/*
|
||||
"C:container":
|
||||
- container/**/*
|
||||
|
|
16
Makefile
16
Makefile
|
@ -238,11 +238,23 @@ check-test-unit-amino: ARGS=-tags='ledger test_ledger_mock test_amino norace'
|
|||
$(CHECK_TEST_TARGETS): EXTRA_ARGS=-run=none
|
||||
$(CHECK_TEST_TARGETS): run-tests
|
||||
|
||||
SUB_MODULES = $(shell find . -type f -name 'go.mod' -print0 | xargs -0 -n1 dirname | sort)
|
||||
CURRENT_DIR = $(shell pwd)
|
||||
run-tests:
|
||||
ifneq (,$(shell which tparse 2>/dev/null))
|
||||
go test -mod=readonly -json $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES) | tparse
|
||||
@echo "Starting unit tests"; \
|
||||
for module in $(SUB_MODULES); do \
|
||||
cd ${CURRENT_DIR}/$$module; \
|
||||
echo "Running unit tests for module $$module"; \
|
||||
go test -mod=readonly -json $(ARGS) $(TEST_PACKAGES) ./... | tparse; \
|
||||
done
|
||||
else
|
||||
go test -mod=readonly $(ARGS) $(EXTRA_ARGS) $(TEST_PACKAGES)
|
||||
@echo "Starting unit tests"; \
|
||||
for module in $(SUB_MODULES); do \
|
||||
cd ${CURRENT_DIR}/$$module; \
|
||||
echo "Running unit tests for module $$module"; \
|
||||
go test -mod=readonly $(ARGS) $(TEST_PACKAGES) ./... ; \
|
||||
done
|
||||
endif
|
||||
|
||||
.PHONY: run-tests test test-all $(TEST_TARGETS)
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
package container
|
||||
|
||||
import "reflect"
|
||||
|
||||
// ConstructorInfo defines a special constructor type that is defined by
|
||||
// reflection. It should be passed as a value to the Provide function.
|
||||
// Ex:
|
||||
// option.Provide(ConstructorInfo{ ... })
|
||||
type ConstructorInfo struct {
|
||||
// In defines the in parameter types to Fn.
|
||||
In []reflect.Type
|
||||
|
||||
// Out defines the out parameter types to Fn.
|
||||
Out []reflect.Type
|
||||
|
||||
// Fn defines the constructor function.
|
||||
Fn func([]reflect.Value) []reflect.Value
|
||||
|
||||
// Location defines the source code location to be used for this constructor
|
||||
// in error messages.
|
||||
Location Location
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
package container_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/container"
|
||||
)
|
||||
|
||||
type KVStoreKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
type ModuleKey string
|
||||
|
||||
type MsgClientA struct {
|
||||
key ModuleKey
|
||||
}
|
||||
|
||||
type KeeperA struct {
|
||||
key KVStoreKey
|
||||
}
|
||||
|
||||
type KeeperB struct {
|
||||
key KVStoreKey
|
||||
msgClientA MsgClientA
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
Handle func()
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
Run func()
|
||||
}
|
||||
|
||||
func ProvideKVStoreKey(scope container.Scope) KVStoreKey {
|
||||
return KVStoreKey{name: scope.Name()}
|
||||
}
|
||||
|
||||
func ProvideModuleKey(scope container.Scope) ModuleKey {
|
||||
return ModuleKey(scope.Name())
|
||||
}
|
||||
|
||||
func ProvideMsgClientA(_ container.Scope, key ModuleKey) MsgClientA {
|
||||
return MsgClientA{key}
|
||||
}
|
||||
|
||||
type ModuleA struct{}
|
||||
|
||||
func (ModuleA) Provide(key KVStoreKey) (KeeperA, Handler, Command) {
|
||||
return KeeperA{key}, Handler{}, Command{}
|
||||
}
|
||||
|
||||
type ModuleB struct{}
|
||||
|
||||
type BDependencies struct {
|
||||
container.StructArgs
|
||||
|
||||
Key KVStoreKey
|
||||
A MsgClientA
|
||||
}
|
||||
|
||||
type BProvides struct {
|
||||
KeeperB KeeperB
|
||||
Handler Handler
|
||||
Commands []Command
|
||||
}
|
||||
|
||||
func (ModuleB) Provide(dependencies BDependencies) BProvides {
|
||||
return BProvides{
|
||||
KeeperB: KeeperB{
|
||||
key: dependencies.Key,
|
||||
msgClientA: dependencies.A,
|
||||
},
|
||||
Handler: Handler{},
|
||||
Commands: []Command{{}, {}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
t.Skip("Expecting this test to fail for now")
|
||||
require.NoError(t,
|
||||
container.Run(
|
||||
func(handlers map[container.Scope]Handler, commands []Command, a KeeperA, b KeeperB) {
|
||||
// TODO:
|
||||
// require one Handler for module a and a scopes
|
||||
// require 3 commands
|
||||
// require KeeperA have store key a
|
||||
// require KeeperB have store key b and MsgClientA
|
||||
}),
|
||||
container.AutoGroupTypes(reflect.TypeOf(Command{})),
|
||||
container.OnePerScopeTypes(reflect.TypeOf(Handler{})),
|
||||
container.Provide(
|
||||
ProvideKVStoreKey,
|
||||
ProvideModuleKey,
|
||||
ProvideMsgClientA,
|
||||
),
|
||||
container.ProvideWithScope(container.NewScope("a"), wrapProvideMethod(ModuleA{})),
|
||||
container.ProvideWithScope(container.NewScope("b"), wrapProvideMethod(ModuleB{})),
|
||||
)
|
||||
}
|
||||
|
||||
func wrapProvideMethod(module interface{}) container.ConstructorInfo {
|
||||
method := reflect.TypeOf(module).Method(0)
|
||||
methodTy := method.Type
|
||||
var in []reflect.Type
|
||||
var out []reflect.Type
|
||||
|
||||
for i := 1; i < methodTy.NumIn(); i++ {
|
||||
in = append(in, methodTy.In(i))
|
||||
}
|
||||
for i := 0; i < methodTy.NumOut(); i++ {
|
||||
out = append(out, methodTy.Out(i))
|
||||
}
|
||||
|
||||
return container.ConstructorInfo{
|
||||
In: in,
|
||||
Out: out,
|
||||
Fn: func(values []reflect.Value) []reflect.Value {
|
||||
values = append([]reflect.Value{reflect.ValueOf(module)}, values...)
|
||||
return method.Func.Call(values)
|
||||
},
|
||||
Location: container.LocationFromPC(method.Func.Pointer()),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module github.com/cosmos/cosmos-sdk/container
|
||||
|
||||
go 1.16
|
||||
|
||||
require github.com/stretchr/testify v1.7.0
|
|
@ -0,0 +1,11 @@
|
|||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
@ -0,0 +1,19 @@
|
|||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Location describes the source code location of a dependency injection
|
||||
// constructor.
|
||||
type Location interface {
|
||||
isLocation()
|
||||
fmt.Stringer
|
||||
fmt.Formatter
|
||||
}
|
||||
|
||||
// LocationFromPC builds a Location from a function program counter location,
|
||||
// such as that returned by reflect.Value.Pointer() or runtime.Caller().
|
||||
func LocationFromPC(pc uintptr) Location {
|
||||
panic("TODO")
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package container
|
||||
|
||||
import "reflect"
|
||||
|
||||
// Option is a functional option for a container.
|
||||
type Option interface {
|
||||
isOption()
|
||||
}
|
||||
|
||||
// Provide creates a container option which registers the provided dependency
|
||||
// injection constructors. Each constructor will be called at most once with the
|
||||
// exception of scoped constructors which are called at most once per scope
|
||||
// (see Scope).
|
||||
func Provide(constructors ...interface{}) Option {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
// ProvideWithScope creates a container option which registers the provided dependency
|
||||
// injection constructors that are to be run in the provided scope. Each constructor
|
||||
// will be called at most once.
|
||||
func ProvideWithScope(scope Scope, constructors ...interface{}) Option {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
// AutoGroupTypes creates an option which registers the provided types as types which
|
||||
// will automatically get grouped together. For a given type T, T and []T can
|
||||
// be declared as output parameters for constructors as many times within the container
|
||||
// as desired. All of the provided values for T can be retrieved by declaring an
|
||||
// []T input parameter.
|
||||
func AutoGroupTypes(types ...reflect.Type) Option {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
// OnePerScopeTypes creates an option which registers the provided types as types which
|
||||
// can have up to one value per scope. All of the values for a one-per-scope type T
|
||||
// and their respective scopes, can be retrieved by declaring an input parameter map[Scope]T.
|
||||
func OnePerScopeTypes(types ...reflect.Type) Option {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
// Error creates an option which causes the dependency injection container to
|
||||
// fail immediately.
|
||||
func Error(err error) Option {
|
||||
panic("TODO")
|
||||
}
|
||||
|
||||
// Options creates an option which bundles together other options.
|
||||
func Options(opts ...Option) Option {
|
||||
panic("TODO")
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package container
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Run runs the provided invoker function with values provided by the provided
|
||||
// options. It is the single entry point for building and running a dependency
|
||||
// injection container. Invoker should be a function taking one or more
|
||||
// dependencies from the container, optionally returning an error.
|
||||
//
|
||||
// Ex:
|
||||
// Run(func (x int) error { println(x) }, Provide(func() int { return 1 }))
|
||||
func Run(invoker interface{}, opts ...Option) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package container
|
||||
|
||||
// Scope is a special type used to define a provider scope.
|
||||
//
|
||||
// Special scoped constructors can be used with Provide by declaring a
|
||||
// constructor with its first input parameter of type Scope. These constructors
|
||||
// should construct an unique value for each dependency based on scope and will
|
||||
// be called at most once per scope.
|
||||
//
|
||||
// Constructors passed to ProvideWithScope can also declare an input parameter
|
||||
// of type Scope to retrieve their scope.
|
||||
type Scope interface {
|
||||
isScope()
|
||||
|
||||
// Name returns the name of the scope which is unique within a container.
|
||||
Name() string
|
||||
}
|
||||
|
||||
// NewScope creates a new scope with the provided name. Only one scope with a
|
||||
// given name can be created per container.
|
||||
func NewScope(name string) Scope {
|
||||
return &scope{name: name}
|
||||
}
|
||||
|
||||
type scope struct {
|
||||
name string
|
||||
}
|
||||
|
||||
func (s *scope) isScope() {}
|
||||
|
||||
func (s *scope) Name() string {
|
||||
return s.name
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package container
|
||||
|
||||
// StructArgs is a type which can be embedded in another struct to alert the
|
||||
// container that the fields of the struct are dependency inputs/outputs. That
|
||||
// is, the container will not look to resolve a value with StructArgs embedded
|
||||
// directly, but will instead use the struct's fields to resolve or populate
|
||||
// dependencies. Types with embedded StructArgs can be used in both the input
|
||||
// and output parameter positions.
|
||||
type StructArgs struct{}
|
||||
|
||||
func (StructArgs) isStructArgs() {}
|
Loading…
Reference in New Issue