From 8c8d996bfc751e72a51daf0b3bb84aef0d640bcf Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 8 Jul 2021 20:55:21 -0400 Subject: [PATCH 01/11] feat: add dependency injection container API --- container/container.go | 46 +++++++++++++++++++++++++ container/container_test.go | 68 +++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 container/container.go create mode 100644 container/container_test.go diff --git a/container/container.go b/container/container.go new file mode 100644 index 00000000000..0537cfc223c --- /dev/null +++ b/container/container.go @@ -0,0 +1,46 @@ +package container + +import "reflect" + +type Option interface { + // TODO + applyOption() +} + +func Provide(constructors ...interface{}) Option { + panic("TODO") +} + +func ProvideWithScope(scope Scope, constructors ...interface{}) Option { + panic("TODO") +} + +func Options(opts ...Option) Option { + panic("TODO") +} + +func DefineGroupTypes(types ...reflect.Type) Option { + panic("TODO") +} + +func Error(err error) Option { + panic("TODO") +} + +type Scope struct { + name string +} + +func NewScope(name string) Scope { + return Scope{name: name} +} + +func (s Scope) Name() string { + return s.name +} + +type StructArgs struct{} + +func Run(invoker interface{}, opts ...Option) error { + panic("TODO") +} diff --git a/container/container_test.go b/container/container_test.go new file mode 100644 index 00000000000..7752c2432d0 --- /dev/null +++ b/container/container_test.go @@ -0,0 +1,68 @@ +package container_test + +import ( + "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{} + +func ProvideKVStoreKey(scope container.Scope) KVStoreKey { + return KVStoreKey{name: scope.Name()} +} + +func ProvideModuleKey(scope container.Scope) ModuleKey { + return ModuleKey(scope.Name()) +} + +func ProvideMsgClientA(key ModuleKey) MsgClientA { + return MsgClientA{key} +} + +func ProvideKeeperA(key KVStoreKey) (KeeperA, Handler) { + return KeeperA{key}, Handler{} +} + +func ProvideKeeperB(key KVStoreKey, a MsgClientA) (KeeperB, Handler) { + return KeeperB{ + key: key, + msgClientA: a, + }, Handler{} +} + +func TestRun(t *testing.T) { + require.NoError(t, + container.Run(func(handlers []Handler) { + // TODO + }), + container.Provide( + ProvideKVStoreKey, + ProvideModuleKey, + ProvideMsgClientA, + ), + container.ProvideWithScope(container.NewScope("a"), ProvideKeeperA), + container.ProvideWithScope(container.NewScope("b"), ProvideKeeperB), + ) +} From 02dcfae1dd3bfee5e3adcc64c013a4989746ba22 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 8 Jul 2021 20:56:29 -0400 Subject: [PATCH 02/11] define handler group type --- container/container_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/container/container_test.go b/container/container_test.go index 7752c2432d0..73f0df04de1 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -1,6 +1,7 @@ package container_test import ( + "reflect" "testing" "github.com/stretchr/testify/require" @@ -57,6 +58,7 @@ func TestRun(t *testing.T) { container.Run(func(handlers []Handler) { // TODO }), + container.DefineGroupTypes(reflect.TypeOf(Handler{})), container.Provide( ProvideKVStoreKey, ProvideModuleKey, From d73f9df4b05d0874cab70e9c209addc5e1cf88dd Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 8 Jul 2021 21:00:21 -0400 Subject: [PATCH 03/11] add go.mod --- container/go.mod | 5 +++++ container/go.sum | 11 +++++++++++ 2 files changed, 16 insertions(+) create mode 100644 container/go.mod create mode 100644 container/go.sum diff --git a/container/go.mod b/container/go.mod new file mode 100644 index 00000000000..f38fd6433f4 --- /dev/null +++ b/container/go.mod @@ -0,0 +1,5 @@ +module github.com/cosmos/cosmos-sdk/container + +go 1.16 + +require github.com/stretchr/testify v1.7.0 diff --git a/container/go.sum b/container/go.sum new file mode 100644 index 00000000000..acb88a48f68 --- /dev/null +++ b/container/go.sum @@ -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= From 32c8ce4d3a0b4b5a66dec93ea337d8e11a6ed9db Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 9 Jul 2021 11:57:24 -0400 Subject: [PATCH 04/11] WIP on API --- container/container.go | 97 ++++++++++++++++++++++++--------- container/container_test.go | 24 +++++---- container/location.go | 104 ++++++++++++++++++++++++++++++++++++ container/option.go | 32 +++++++++++ container/reflect.go | 9 ++++ container/run.go | 5 ++ container/scope.go | 13 +++++ container/struct_args.go | 5 ++ 8 files changed, 256 insertions(+), 33 deletions(-) create mode 100644 container/location.go create mode 100644 container/option.go create mode 100644 container/reflect.go create mode 100644 container/run.go create mode 100644 container/scope.go create mode 100644 container/struct_args.go diff --git a/container/container.go b/container/container.go index 0537cfc223c..6d36233691e 100644 --- a/container/container.go +++ b/container/container.go @@ -2,45 +2,94 @@ package container import "reflect" -type Option interface { - // TODO - applyOption() -} +type container struct { + err error + autoGroupTypes []reflect.Type + autoNameByScopeTypes []reflect.Type -func Provide(constructors ...interface{}) Option { - panic("TODO") -} + providers map[reflect.Type]*node + scopeProviders map[reflect.Type]*scopeNode + nodes []*node + scopeNodes []*scopeNode -func ProvideWithScope(scope Scope, constructors ...interface{}) Option { - panic("TODO") + values map[key]secureValue + scopedValues map[Scope]map[key]reflect.Value + securityContext func(scope Scope, tag string) error } -func Options(opts ...Option) Option { - panic("TODO") +type input struct { + key + Optional bool } -func DefineGroupTypes(types ...reflect.Type) Option { - panic("TODO") +type Output struct { + key + SecurityChecker securityChecker } -func Error(err error) Option { - panic("TODO") +type key struct { + Type reflect.Type } -type Scope struct { - name string +type node struct { + Provider + called bool + values []reflect.Value + err error } -func NewScope(name string) Scope { - return Scope{name: name} +// Provider is a general dependency provider. Its scope parameter is used +// to receive scoped dependencies and gain access to general dependencies within +// its security policy. Access to dependencies provided by this provider can optionally +// be restricted to certain scopes based on SecurityCheckers. +type Provider struct { + // Constructor provides the dependencies + Constructor func(deps []reflect.Value, scope Scope) ([]reflect.Value, error) + + // Needs are the keys for dependencies the constructor needs + Needs []input + + // Needs are the keys for dependencies the constructor provides + Provides []Output + + // Scope is the scope within which the constructor runs + Scope Scope + + IsScopeProvider bool } -func (s Scope) Name() string { - return s.name +type scopeNode struct { + Provider + calledForScope map[Scope]bool + valuesForScope map[Scope][]reflect.Value + errsForScope map[Scope]error } -type StructArgs struct{} +// ScopeProvider provides scoped dependencies. Its constructor function will provide +// dependencies specific to the scope parameter. Instead of providing general dependencies +// with restricted access based on security checkers, ScopeProvider provides potentially different +// dependency instances to different scopes. It is assumed that a scoped provider +// can provide a dependency for any valid scope passed to it, although it can return an error +// to deny access. +type ScopeProvider struct { -func Run(invoker interface{}, opts ...Option) error { - panic("TODO") + // Constructor provides dependencies for the provided scope + Constructor func(scope Scope, deps []reflect.Value) ([]reflect.Value, error) + + // Needs are the keys for dependencies the constructor needs + Needs []input + + // Needs are the keys for dependencies the constructor provides + Provides []key + + // Scope is the scope within which the constructor runs, if it is left empty, + // the constructor runs in the scope it was called with (this only applies to ScopeProvider). + Scope Scope } + +type secureValue struct { + value reflect.Value + securityChecker securityChecker +} + +type securityChecker func(scope Scope) error diff --git a/container/container_test.go b/container/container_test.go index 73f0df04de1..345b74cbdad 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -28,7 +28,13 @@ type KeeperB struct { msgClientA MsgClientA } -type Handler struct{} +type Handler struct { + Handle func() +} + +type Command struct { + Run func() +} func ProvideKVStoreKey(scope container.Scope) KVStoreKey { return KVStoreKey{name: scope.Name()} @@ -42,23 +48,23 @@ func ProvideMsgClientA(key ModuleKey) MsgClientA { return MsgClientA{key} } -func ProvideKeeperA(key KVStoreKey) (KeeperA, Handler) { - return KeeperA{key}, Handler{} +func ProvideKeeperA(key KVStoreKey) (KeeperA, Handler, Command) { + return KeeperA{key}, Handler{}, Command{} } -func ProvideKeeperB(key KVStoreKey, a MsgClientA) (KeeperB, Handler) { +func ProvideKeeperB(key KVStoreKey, a MsgClientA) (KeeperB, Handler, []Command) { return KeeperB{ key: key, msgClientA: a, - }, Handler{} + }, Handler{}, []Command{{}, {}} } func TestRun(t *testing.T) { + t.Skip("Expecting this test to fail for now") require.NoError(t, - container.Run(func(handlers []Handler) { - // TODO - }), - container.DefineGroupTypes(reflect.TypeOf(Handler{})), + container.Run(func(handlers map[container.Scope]Handler, commands []Command) {}), + container.AutoGroupTypes(reflect.TypeOf(Command{})), + container.OnePerScopeTypes(reflect.TypeOf(Handler{})), container.Provide( ProvideKVStoreKey, ProvideModuleKey, diff --git a/container/location.go b/container/location.go new file mode 100644 index 00000000000..900b6aeb0d2 --- /dev/null +++ b/container/location.go @@ -0,0 +1,104 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package container + +import ( + "fmt" + "net/url" + "runtime" + "strings" +) + +type Location struct { + name string + pkg string + file string + line int +} + +func LocationFromPC(pc uintptr) Location { + f := runtime.FuncForPC(pc) + pkgName, funcName := splitFuncName(f.Name()) + fileName, lineNum := f.FileLine(pc) + return Location{ + name: funcName, + pkg: pkgName, + file: fileName, + line: lineNum, + } +} + +// String returns a string representation of the function. +func (f Location) String() string { + return fmt.Sprint(f) +} + +// Format implements fmt.Formatter for Func, printing a single-line +// representation for %v and a multi-line one for %+v. +func (f Location) Format(w fmt.State, c rune) { + if w.Flag('+') && c == 'v' { + // "path/to/package".MyFunction + // path/to/file.go:42 + fmt.Fprintf(w, "%q.%v", f.pkg, f.name) + fmt.Fprintf(w, "\n\t%v:%v", f.file, f.line) + } else { + // "path/to/package".MyFunction (path/to/file.go:42) + fmt.Fprintf(w, "%q.%v (%v:%v)", f.pkg, f.name, f.file, f.line) + } +} + +const _vendor = "/vendor/" + +func splitFuncName(function string) (pname string, fname string) { + if len(function) == 0 { + return + } + + // We have something like "path.to/my/pkg.MyFunction". If the function is + // a closure, it is something like, "path.to/my/pkg.MyFunction.func1". + + idx := 0 + + // Everything up to the first "." after the last "/" is the package name. + // Everything after the "." is the full function name. + if i := strings.LastIndex(function, "/"); i >= 0 { + idx = i + } + if i := strings.Index(function[idx:], "."); i >= 0 { + idx += i + } + pname, fname = function[:idx], function[idx+1:] + + // The package may be vendored. + if i := strings.Index(pname, _vendor); i > 0 { + pname = pname[i+len(_vendor):] + } + + // Package names are URL-encoded to avoid ambiguity in the case where the + // package name contains ".git". Otherwise, "foo/bar.git.MyFunction" would + // mean that "git" is the top-level function and "MyFunction" is embedded + // inside it. + if unescaped, err := url.QueryUnescape(pname); err == nil { + pname = unescaped + } + + return +} diff --git a/container/option.go b/container/option.go new file mode 100644 index 00000000000..b56595ef551 --- /dev/null +++ b/container/option.go @@ -0,0 +1,32 @@ +package container + +import "reflect" + +type Option struct { + prepare func(*container) + provide func(*container) +} + +func Provide(constructors ...interface{}) Option { + panic("TODO") +} + +func ProvideWithScope(scope Scope, constructors ...interface{}) Option { + panic("TODO") +} + +func AutoGroupTypes(types ...reflect.Type) Option { + panic("TODO") +} + +func OnePerScopeTypes(types ...reflect.Type) Option { + panic("TODO") +} + +func Error(err error) Option { + panic("TODO") +} + +func Options(opts ...Option) Option { + panic("TODO") +} diff --git a/container/reflect.go b/container/reflect.go new file mode 100644 index 00000000000..f653b770ad6 --- /dev/null +++ b/container/reflect.go @@ -0,0 +1,9 @@ +package container + +import "reflect" + +type ReflectConstructor struct { + InType, OutTypes []reflect.Type + Fn func([]reflect.Value) []reflect.Value + Location Location +} diff --git a/container/run.go b/container/run.go new file mode 100644 index 00000000000..d2248f4834b --- /dev/null +++ b/container/run.go @@ -0,0 +1,5 @@ +package container + +func Run(invoker interface{}, opts ...Option) error { + panic("TODO") +} diff --git a/container/scope.go b/container/scope.go new file mode 100644 index 00000000000..398885f5054 --- /dev/null +++ b/container/scope.go @@ -0,0 +1,13 @@ +package container + +type Scope struct { + name string +} + +func NewScope(name string) Scope { + return Scope{name: name} +} + +func (s Scope) Name() string { + return s.name +} diff --git a/container/struct_args.go b/container/struct_args.go new file mode 100644 index 00000000000..23b1250ed0c --- /dev/null +++ b/container/struct_args.go @@ -0,0 +1,5 @@ +package container + +type StructArgs struct{} + +func (StructArgs) isStructArgs() {} From b873faf72ab45508adb97701fa49d8b6302186dd Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 9 Jul 2021 12:12:27 -0400 Subject: [PATCH 05/11] WIP --- container/container.go | 6 ++--- container/container_test.go | 44 ++++++++++++++++++++++++++++++++----- container/location.go | 18 +++++++++++---- container/option.go | 5 ++--- container/reflect.go | 6 ++--- container/run.go | 4 +++- container/scope.go | 16 ++++++++++---- 7 files changed, 76 insertions(+), 23 deletions(-) diff --git a/container/container.go b/container/container.go index 6d36233691e..8cd17dfa142 100644 --- a/container/container.go +++ b/container/container.go @@ -32,7 +32,7 @@ type key struct { } type node struct { - Provider + provider called bool values []reflect.Value err error @@ -42,7 +42,7 @@ type node struct { // to receive scoped dependencies and gain access to general dependencies within // its security policy. Access to dependencies provided by this provider can optionally // be restricted to certain scopes based on SecurityCheckers. -type Provider struct { +type provider struct { // Constructor provides the dependencies Constructor func(deps []reflect.Value, scope Scope) ([]reflect.Value, error) @@ -59,7 +59,7 @@ type Provider struct { } type scopeNode struct { - Provider + provider calledForScope map[Scope]bool valuesForScope map[Scope][]reflect.Value errsForScope map[Scope]error diff --git a/container/container_test.go b/container/container_test.go index 345b74cbdad..2c498a85dde 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -48,21 +48,55 @@ func ProvideMsgClientA(key ModuleKey) MsgClientA { return MsgClientA{key} } -func ProvideKeeperA(key KVStoreKey) (KeeperA, Handler, Command) { +type ModuleA struct{} + +func (ModuleA) Provide(key KVStoreKey) (KeeperA, Handler, Command) { return KeeperA{key}, Handler{}, Command{} } -func ProvideKeeperB(key KVStoreKey, a MsgClientA) (KeeperB, Handler, []Command) { +type ModuleB struct{} + +func (ModuleB) Provide(key KVStoreKey, a MsgClientA) (KeeperB, Handler, []Command) { return KeeperB{ key: key, msgClientA: a, }, Handler{}, []Command{{}, {}} } +func wrapProvideMethod(module interface{}) container.ReflectConstructor { + 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.ReflectConstructor{ + 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()), + } +} + 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) {}), + 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( @@ -70,7 +104,7 @@ func TestRun(t *testing.T) { ProvideModuleKey, ProvideMsgClientA, ), - container.ProvideWithScope(container.NewScope("a"), ProvideKeeperA), - container.ProvideWithScope(container.NewScope("b"), ProvideKeeperB), + container.ProvideWithScope(container.NewScope("a"), wrapProvideMethod(ModuleA{})), + container.ProvideWithScope(container.NewScope("b"), wrapProvideMethod(ModuleB{})), ) } diff --git a/container/location.go b/container/location.go index 900b6aeb0d2..12ace9884b9 100644 --- a/container/location.go +++ b/container/location.go @@ -27,7 +27,13 @@ import ( "strings" ) -type Location struct { +type Location interface { + isLocation() + fmt.Stringer + fmt.Formatter +} + +type location struct { name string pkg string file string @@ -38,7 +44,7 @@ func LocationFromPC(pc uintptr) Location { f := runtime.FuncForPC(pc) pkgName, funcName := splitFuncName(f.Name()) fileName, lineNum := f.FileLine(pc) - return Location{ + return &location{ name: funcName, pkg: pkgName, file: fileName, @@ -46,14 +52,18 @@ func LocationFromPC(pc uintptr) Location { } } +func (f *location) isLocation() { + panic("implement me") +} + // String returns a string representation of the function. -func (f Location) String() string { +func (f *location) String() string { return fmt.Sprint(f) } // Format implements fmt.Formatter for Func, printing a single-line // representation for %v and a multi-line one for %+v. -func (f Location) Format(w fmt.State, c rune) { +func (f *location) Format(w fmt.State, c rune) { if w.Flag('+') && c == 'v' { // "path/to/package".MyFunction // path/to/file.go:42 diff --git a/container/option.go b/container/option.go index b56595ef551..38284e83ee8 100644 --- a/container/option.go +++ b/container/option.go @@ -2,9 +2,8 @@ package container import "reflect" -type Option struct { - prepare func(*container) - provide func(*container) +type Option interface { + isOption() } func Provide(constructors ...interface{}) Option { diff --git a/container/reflect.go b/container/reflect.go index f653b770ad6..af792bbb4a1 100644 --- a/container/reflect.go +++ b/container/reflect.go @@ -3,7 +3,7 @@ package container import "reflect" type ReflectConstructor struct { - InType, OutTypes []reflect.Type - Fn func([]reflect.Value) []reflect.Value - Location Location + In, Out []reflect.Type + Fn func([]reflect.Value) []reflect.Value + Location Location } diff --git a/container/run.go b/container/run.go index d2248f4834b..b313b14326a 100644 --- a/container/run.go +++ b/container/run.go @@ -1,5 +1,7 @@ package container +import "fmt" + func Run(invoker interface{}, opts ...Option) error { - panic("TODO") + return fmt.Errorf("not implemented") } diff --git a/container/scope.go b/container/scope.go index 398885f5054..6fdd5850238 100644 --- a/container/scope.go +++ b/container/scope.go @@ -1,13 +1,21 @@ package container -type Scope struct { - name string +type Scope interface { + isScope() + + Name() string } func NewScope(name string) Scope { - return Scope{name: name} + return &scope{name: name} +} + +type scope struct { + name string } -func (s Scope) Name() string { +func (s *scope) isScope() {} + +func (s *scope) Name() string { return s.name } From a26539d9e9d5084b13df062742f13c0ef69d2edb Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Fri, 9 Jul 2021 12:37:15 -0400 Subject: [PATCH 06/11] WIP on API --- container/container.go | 95 -------------------------------- container/container_test.go | 65 +++++++++++++--------- container/location.go | 104 ++---------------------------------- container/option.go | 10 ++++ 4 files changed, 55 insertions(+), 219 deletions(-) delete mode 100644 container/container.go diff --git a/container/container.go b/container/container.go deleted file mode 100644 index 8cd17dfa142..00000000000 --- a/container/container.go +++ /dev/null @@ -1,95 +0,0 @@ -package container - -import "reflect" - -type container struct { - err error - autoGroupTypes []reflect.Type - autoNameByScopeTypes []reflect.Type - - providers map[reflect.Type]*node - scopeProviders map[reflect.Type]*scopeNode - nodes []*node - scopeNodes []*scopeNode - - values map[key]secureValue - scopedValues map[Scope]map[key]reflect.Value - securityContext func(scope Scope, tag string) error -} - -type input struct { - key - Optional bool -} - -type Output struct { - key - SecurityChecker securityChecker -} - -type key struct { - Type reflect.Type -} - -type node struct { - provider - called bool - values []reflect.Value - err error -} - -// Provider is a general dependency provider. Its scope parameter is used -// to receive scoped dependencies and gain access to general dependencies within -// its security policy. Access to dependencies provided by this provider can optionally -// be restricted to certain scopes based on SecurityCheckers. -type provider struct { - // Constructor provides the dependencies - Constructor func(deps []reflect.Value, scope Scope) ([]reflect.Value, error) - - // Needs are the keys for dependencies the constructor needs - Needs []input - - // Needs are the keys for dependencies the constructor provides - Provides []Output - - // Scope is the scope within which the constructor runs - Scope Scope - - IsScopeProvider bool -} - -type scopeNode struct { - provider - calledForScope map[Scope]bool - valuesForScope map[Scope][]reflect.Value - errsForScope map[Scope]error -} - -// ScopeProvider provides scoped dependencies. Its constructor function will provide -// dependencies specific to the scope parameter. Instead of providing general dependencies -// with restricted access based on security checkers, ScopeProvider provides potentially different -// dependency instances to different scopes. It is assumed that a scoped provider -// can provide a dependency for any valid scope passed to it, although it can return an error -// to deny access. -type ScopeProvider struct { - - // Constructor provides dependencies for the provided scope - Constructor func(scope Scope, deps []reflect.Value) ([]reflect.Value, error) - - // Needs are the keys for dependencies the constructor needs - Needs []input - - // Needs are the keys for dependencies the constructor provides - Provides []key - - // Scope is the scope within which the constructor runs, if it is left empty, - // the constructor runs in the scope it was called with (this only applies to ScopeProvider). - Scope Scope -} - -type secureValue struct { - value reflect.Value - securityChecker securityChecker -} - -type securityChecker func(scope Scope) error diff --git a/container/container_test.go b/container/container_test.go index 2c498a85dde..9f881541afb 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -56,34 +56,27 @@ func (ModuleA) Provide(key KVStoreKey) (KeeperA, Handler, Command) { type ModuleB struct{} -func (ModuleB) Provide(key KVStoreKey, a MsgClientA) (KeeperB, Handler, []Command) { - return KeeperB{ - key: key, - msgClientA: a, - }, Handler{}, []Command{{}, {}} -} +type BDependencies struct { + container.StructArgs -func wrapProvideMethod(module interface{}) container.ReflectConstructor { - method := reflect.TypeOf(module).Method(0) - methodTy := method.Type - var in []reflect.Type - var out []reflect.Type + Key KVStoreKey + A MsgClientA +} - 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)) - } +type BProvides struct { + KeeperB KeeperB + Handler Handler + Commands []Command +} - return container.ReflectConstructor{ - In: in, - Out: out, - Fn: func(values []reflect.Value) []reflect.Value { - values = append([]reflect.Value{reflect.ValueOf(module)}, values...) - return method.Func.Call(values) +func (ModuleB) Provide(dependencies BDependencies) BProvides { + return BProvides{ + KeeperB: KeeperB{ + key: dependencies.Key, + msgClientA: dependencies.A, }, - Location: container.LocationFromPC(method.Func.Pointer()), + Handler: Handler{}, + Commands: []Command{{}, {}}, } } @@ -108,3 +101,27 @@ func TestRun(t *testing.T) { container.ProvideWithScope(container.NewScope("b"), wrapProvideMethod(ModuleB{})), ) } + +func wrapProvideMethod(module interface{}) container.ReflectConstructor { + 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.ReflectConstructor{ + 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()), + } +} diff --git a/container/location.go b/container/location.go index 12ace9884b9..faa881e6000 100644 --- a/container/location.go +++ b/container/location.go @@ -1,114 +1,18 @@ -// Copyright (c) 2019 Uber Technologies, Inc. -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -// THE SOFTWARE. - package container import ( "fmt" - "net/url" - "runtime" - "strings" ) +// Location describes the location of dependency injection constructor. type Location interface { isLocation() fmt.Stringer fmt.Formatter } -type location struct { - name string - pkg string - file string - line int -} - +// 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 { - f := runtime.FuncForPC(pc) - pkgName, funcName := splitFuncName(f.Name()) - fileName, lineNum := f.FileLine(pc) - return &location{ - name: funcName, - pkg: pkgName, - file: fileName, - line: lineNum, - } -} - -func (f *location) isLocation() { - panic("implement me") -} - -// String returns a string representation of the function. -func (f *location) String() string { - return fmt.Sprint(f) -} - -// Format implements fmt.Formatter for Func, printing a single-line -// representation for %v and a multi-line one for %+v. -func (f *location) Format(w fmt.State, c rune) { - if w.Flag('+') && c == 'v' { - // "path/to/package".MyFunction - // path/to/file.go:42 - fmt.Fprintf(w, "%q.%v", f.pkg, f.name) - fmt.Fprintf(w, "\n\t%v:%v", f.file, f.line) - } else { - // "path/to/package".MyFunction (path/to/file.go:42) - fmt.Fprintf(w, "%q.%v (%v:%v)", f.pkg, f.name, f.file, f.line) - } -} - -const _vendor = "/vendor/" - -func splitFuncName(function string) (pname string, fname string) { - if len(function) == 0 { - return - } - - // We have something like "path.to/my/pkg.MyFunction". If the function is - // a closure, it is something like, "path.to/my/pkg.MyFunction.func1". - - idx := 0 - - // Everything up to the first "." after the last "/" is the package name. - // Everything after the "." is the full function name. - if i := strings.LastIndex(function, "/"); i >= 0 { - idx = i - } - if i := strings.Index(function[idx:], "."); i >= 0 { - idx += i - } - pname, fname = function[:idx], function[idx+1:] - - // The package may be vendored. - if i := strings.Index(pname, _vendor); i > 0 { - pname = pname[i+len(_vendor):] - } - - // Package names are URL-encoded to avoid ambiguity in the case where the - // package name contains ".git". Otherwise, "foo/bar.git.MyFunction" would - // mean that "git" is the top-level function and "MyFunction" is embedded - // inside it. - if unescaped, err := url.QueryUnescape(pname); err == nil { - pname = unescaped - } - - return + panic("TODO") } diff --git a/container/option.go b/container/option.go index 38284e83ee8..5ba66214058 100644 --- a/container/option.go +++ b/container/option.go @@ -2,18 +2,25 @@ 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. 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. 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 example if func AutoGroupTypes(types ...reflect.Type) Option { panic("TODO") } @@ -22,10 +29,13 @@ func OnePerScopeTypes(types ...reflect.Type) Option { panic("TODO") } +// Error creates an option which causes the dependency injection failure to +// fail immediately. func Error(err error) Option { panic("TODO") } +// Options creates an option which bundles together many other options. func Options(opts ...Option) Option { panic("TODO") } From 9c65775ed695060735c949c705ddb43565f7c203 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 22 Jul 2021 15:21:36 -0400 Subject: [PATCH 07/11] update docs --- container/container_test.go | 17 +++++++++-------- container/location.go | 3 ++- container/option.go | 21 +++++++++++++++------ container/reflect.go | 17 +++++++++++++++-- container/run.go | 7 +++++++ container/scope.go | 12 ++++++++++++ container/struct_args.go | 6 ++++++ 7 files changed, 66 insertions(+), 17 deletions(-) diff --git a/container/container_test.go b/container/container_test.go index 9f881541afb..e90892f11f7 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -44,7 +44,7 @@ func ProvideModuleKey(scope container.Scope) ModuleKey { return ModuleKey(scope.Name()) } -func ProvideMsgClientA(key ModuleKey) MsgClientA { +func ProvideMsgClientA(_ container.Scope, key ModuleKey) MsgClientA { return MsgClientA{key} } @@ -83,13 +83,14 @@ func (ModuleB) Provide(dependencies BDependencies) BProvides { 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.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( diff --git a/container/location.go b/container/location.go index faa881e6000..7b3d42dac11 100644 --- a/container/location.go +++ b/container/location.go @@ -4,7 +4,8 @@ import ( "fmt" ) -// Location describes the location of dependency injection constructor. +// Location describes the source code location of a dependency injection +// constructor. type Location interface { isLocation() fmt.Stringer diff --git a/container/option.go b/container/option.go index 5ba66214058..e1388728643 100644 --- a/container/option.go +++ b/container/option.go @@ -2,40 +2,49 @@ package container import "reflect" -// Option is a functional option for a container +// Option is a functional option for a container. type Option interface { isOption() } // Provide creates a container option which registers the provided dependency -// injection constructors. +// 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. +// 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 example if +// 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 failure to +// 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 many other options. +// Options creates an option which bundles together other options. func Options(opts ...Option) Option { panic("TODO") } diff --git a/container/reflect.go b/container/reflect.go index af792bbb4a1..0dd58af7fd8 100644 --- a/container/reflect.go +++ b/container/reflect.go @@ -2,8 +2,21 @@ package container import "reflect" +// ReflectConstructor defines a special constructor type that is defined by +// reflection. It should be passed as a value to the Provide function. +// Ex: +// option.Provide(ReflectConstructor{ ... }) type ReflectConstructor struct { - In, Out []reflect.Type - Fn func([]reflect.Value) []reflect.Value + // 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 } diff --git a/container/run.go b/container/run.go index b313b14326a..9c410b76a24 100644 --- a/container/run.go +++ b/container/run.go @@ -2,6 +2,13 @@ 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") } diff --git a/container/scope.go b/container/scope.go index 6fdd5850238..f854201ef61 100644 --- a/container/scope.go +++ b/container/scope.go @@ -1,11 +1,23 @@ 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} } diff --git a/container/struct_args.go b/container/struct_args.go index 23b1250ed0c..8c1ae72e827 100644 --- a/container/struct_args.go +++ b/container/struct_args.go @@ -1,5 +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() {} From 211c75666468ed9873a52b6cb6e8b7ea80772ba1 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 26 Jul 2021 11:18:47 -0400 Subject: [PATCH 08/11] rename --- container/{reflect.go => constructor_info.go} | 6 +++--- container/container_test.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename container/{reflect.go => constructor_info.go} (74%) diff --git a/container/reflect.go b/container/constructor_info.go similarity index 74% rename from container/reflect.go rename to container/constructor_info.go index 0dd58af7fd8..b3b5e7b5002 100644 --- a/container/reflect.go +++ b/container/constructor_info.go @@ -2,11 +2,11 @@ package container import "reflect" -// ReflectConstructor defines a special constructor type that is defined by +// 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(ReflectConstructor{ ... }) -type ReflectConstructor struct { +// option.Provide(ConstructorInfo{ ... }) +type ConstructorInfo struct { // In defines the in parameter types to Fn. In []reflect.Type diff --git a/container/container_test.go b/container/container_test.go index e90892f11f7..091ab3ab298 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -103,7 +103,7 @@ func TestRun(t *testing.T) { ) } -func wrapProvideMethod(module interface{}) container.ReflectConstructor { +func wrapProvideMethod(module interface{}) container.ConstructorInfo { method := reflect.TypeOf(module).Method(0) methodTy := method.Type var in []reflect.Type @@ -116,7 +116,7 @@ func wrapProvideMethod(module interface{}) container.ReflectConstructor { out = append(out, methodTy.Out(i)) } - return container.ReflectConstructor{ + return container.ConstructorInfo{ In: in, Out: out, Fn: func(values []reflect.Value) []reflect.Value { From 9ec37b62d86bc0bd619f893dd0244aa8cd5af28b Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 26 Jul 2021 11:26:33 -0400 Subject: [PATCH 09/11] Add unit tests for nested modules --- Makefile | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9d4d589becc..9fc1c2721d2 100644 --- a/Makefile +++ b/Makefile @@ -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) From 125a2752136f56bc16c86f592d1d4c948a2a5229 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 26 Jul 2021 11:28:45 -0400 Subject: [PATCH 10/11] update labeler.yml --- .github/labeler.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/labeler.yml b/.github/labeler.yml index 65310f6f15b..f2771a8da06 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -52,3 +52,5 @@ - x/*/client/**/* "Type: ADR": - docs/architecture/**/* +"C:container": + - container/**/* \ No newline at end of file From 887aca57a57cb360b0fcbb1495e2ef347723e393 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Mon, 26 Jul 2021 11:29:31 -0400 Subject: [PATCH 11/11] update labeler.yml --- .github/labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index f2771a8da06..42d2a50eb9a 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -53,4 +53,4 @@ "Type: ADR": - docs/architecture/**/* "C:container": - - container/**/* \ No newline at end of file + - container/**/*