Skip to content

Commit

Permalink
feat: add low-level dependency injection container API (#9658)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaronc authored Aug 12, 2021
1 parent 13559f9 commit e656d5e
Show file tree
Hide file tree
Showing 11 changed files with 309 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,5 @@
- x/*/client/**/*
"Type: ADR":
- docs/architecture/**/*
"C:container":
- container/**/*
16 changes: 14 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
22 changes: 22 additions & 0 deletions container/constructor_info.go
Original file line number Diff line number Diff line change
@@ -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 changes: 128 additions & 0 deletions container/container_test.go
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions container/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/cosmos/cosmos-sdk/container

go 1.16

require github.com/stretchr/testify v1.7.0
11 changes: 11 additions & 0 deletions container/go.sum
Original file line number Diff line number Diff line change
@@ -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 changes: 19 additions & 0 deletions container/location.go
Original file line number Diff line number Diff line change
@@ -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 changes: 50 additions & 0 deletions container/option.go
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions container/run.go
Original file line number Diff line number Diff line change
@@ -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 changes: 33 additions & 0 deletions container/scope.go
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions container/struct_args.go
Original file line number Diff line number Diff line change
@@ -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() {}

0 comments on commit e656d5e

Please sign in to comment.