Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 17 commits into from
Aug 12, 2021
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -237,11 +237,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 {
blushi marked this conversation as resolved.
Show resolved Hide resolved
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{}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was simpler to have one struct vs dig.In and dig.Out... Wonder if anyone has an opinion

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In / Out makes it more clear when reading a structure definition, without going into the provider / usage:

  • when I see a struct withdig.In I quickly associate it with a dependencies of some type.
  • when I see a struct with dig.Out I see that those are some dependencies a provider can provide.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to change it to In/Out, but note that I also have structs named Input and Output here to describe the actual constructor inputs/outputs... Any suggestions on naming?


func (StructArgs) isStructArgs() {}