feat: add low-level dependency injection container API (#9658)

This commit is contained in:
Aaron Craelius 2021-08-12 09:07:15 -04:00 committed by GitHub
parent 13559f9132
commit e656d5e28a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 309 additions and 2 deletions

2
.github/labeler.yml vendored
View File

@ -52,3 +52,5 @@
- x/*/client/**/*
"Type: ADR":
- docs/architecture/**/*
"C:container":
- container/**/*

View File

@ -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)

View File

@ -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
}

128
container/container_test.go Normal file
View File

@ -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()),
}
}

5
container/go.mod Normal file
View File

@ -0,0 +1,5 @@
module github.com/cosmos/cosmos-sdk/container
go 1.16
require github.com/stretchr/testify v1.7.0

11
container/go.sum Normal file
View File

@ -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=

19
container/location.go Normal file
View File

@ -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")
}

50
container/option.go Normal file
View File

@ -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")
}

14
container/run.go Normal file
View File

@ -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")
}

33
container/scope.go Normal file
View File

@ -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
}

11
container/struct_args.go Normal file
View File

@ -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() {}