diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bdc10dc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-present The Plant + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab88cce --- /dev/null +++ b/README.md @@ -0,0 +1,167 @@ +# inject + +The `inject` package is inspired by [codecangsta/inject](https://github.com/codegangsta/inject) and provides simplified public methods for dependency injection in Go. + +## Features + +- `Provide`: Used to provide dependencies through a function. +- `Invoke`: Used to resolve dependencies and invoke a function with them. +- `Resolve`: Used to resolve a dependency by its type. +- `Apply`: Used to apply dependencies to a struct. +- `SetParent`: Used to set the parent injector. +- Thread safety ensured +- Supports injection through the `inject` tag of struct fields + +## Usage + +Here's an example of how to use the `inject` package: + +```go +package inject_test + +import ( + "fmt" + + "github.com/theplant/inject" +) + +// Define interfaces and implementations +type Printer interface { + Print() string +} + +type SimplePrinter struct{} + +func (p *SimplePrinter) Print() string { + return "Printing document" +} + +// New type definition +type DocumentDescription string + +type Document struct { + Injector *inject.Injector `inject:""` // Injector will be provided by default, so you can also get it if needed + + ID string // Not injected + Description DocumentDescription `inject:""` // Exported non-optional field + Printer Printer `inject:""` // Exported non-optional field + Size int64 `inject:"optional"` // Exported optional field + page int `inject:""` // Unexported non-optional field + name string `inject:"optional"` // Unexported optional field +} + +func ExampleInjector() { + inj := inject.New() + + // Provide dependencies + if err := inj.Provide( + func() Printer { + return &SimplePrinter{} + }, + func() string { + return "A simple string" + }, + func() DocumentDescription { + return "A document description" + }, + func() int { + return 42 + }, + ); err != nil { + panic(err) + } + + { + // Resolve dependencies + var printer Printer + if err := inj.Resolve(&printer); err != nil { + panic(err) + } + fmt.Println("Resolved printer:", printer.Print()) + } + + printDoc := func(doc *Document) { + fmt.Printf("Document id: %q\n", doc.ID) + fmt.Printf("Document description: %q\n", doc.Description) + fmt.Printf("Document printer: %q\n", doc.Printer.Print()) + fmt.Printf("Document size: %d\n", doc.Size) + fmt.Printf("Document page: %d\n", doc.page) + fmt.Printf("Document name: %q\n", doc.name) + } + + fmt.Println("-------") + + { + // Invoke a function + results, err := inj.Invoke(func(printer Printer) *Document { + return &Document{ + // This value will be retained as it is not tagged with `inject`, despite string being provided + ID: "idInvoked", + // This value will be overridden since it is tagged with `inject` and DocumentDescription is provided + Description: "DescriptionInvoked", + // This value will be overridden with the same value since it is tagged with `inject` and Printer is provided + Printer: printer, + // This value will be retained since it is tagged with `inject:"optional"` and int64 is not provided + Size: 100, + } + }) + if err != nil { + panic(err) + } + + printDoc(results[0].(*Document)) + } + + fmt.Println("-------") + + { + // Apply dependencies to a struct instance + doc := &Document{} + if err := inj.Apply(doc); err != nil { + panic(err) + } + + printDoc(doc) + } + + fmt.Println("-------") + + { + // Create a child injector and then apply dependencies to a struct instance + child := inject.New() + child.SetParent(inj) + + doc := &Document{} + if err := child.Apply(doc); err != nil { + panic(err) + } + + printDoc(doc) + } + + // Output: + // Resolved printer: Printing document + // ------- + // Document id: "idInvoked" + // Document description: "A document description" + // Document printer: "Printing document" + // Document size: 100 + // Document page: 42 + // Document name: "A simple string" + // ------- + // Document id: "" + // Document description: "A document description" + // Document printer: "Printing document" + // Document size: 0 + // Document page: 42 + // Document name: "A simple string" + // ------- + // Document id: "" + // Document description: "A document description" + // Document printer: "Printing document" + // Document size: 0 + // Document page: 42 + // Document name: "A simple string" +} + +``` diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..da1a555 --- /dev/null +++ b/example_test.go @@ -0,0 +1,146 @@ +package inject_test + +import ( + "fmt" + + "github.com/theplant/inject" +) + +// Define interfaces and implementations +type Printer interface { + Print() string +} + +type SimplePrinter struct{} + +func (p *SimplePrinter) Print() string { + return "Printing document" +} + +// New type definition +type DocumentDescription string + +type Document struct { + Injector *inject.Injector `inject:""` // Injector will be provided by default, so you can also get it if needed + + ID string // Not injected + Description DocumentDescription `inject:""` // Exported non-optional field + Printer Printer `inject:""` // Exported non-optional field + Size int64 `inject:"optional"` // Exported optional field + page int `inject:""` // Unexported non-optional field + name string `inject:"optional"` // Unexported optional field +} + +func ExampleInjector() { + inj := inject.New() + + // Provide dependencies + if err := inj.Provide( + func() Printer { + return &SimplePrinter{} + }, + func() string { + return "A simple string" + }, + func() DocumentDescription { + return "A document description" + }, + func() int { + return 42 + }, + ); err != nil { + panic(err) + } + + { + // Resolve dependencies + var printer Printer + if err := inj.Resolve(&printer); err != nil { + panic(err) + } + fmt.Println("Resolved printer:", printer.Print()) + } + + printDoc := func(doc *Document) { + fmt.Printf("Document id: %q\n", doc.ID) + fmt.Printf("Document description: %q\n", doc.Description) + fmt.Printf("Document printer: %q\n", doc.Printer.Print()) + fmt.Printf("Document size: %d\n", doc.Size) + fmt.Printf("Document page: %d\n", doc.page) + fmt.Printf("Document name: %q\n", doc.name) + } + + fmt.Println("-------") + + { + // Invoke a function + results, err := inj.Invoke(func(printer Printer) *Document { + return &Document{ + // This value will be retained as it is not tagged with `inject`, despite string being provided + ID: "idInvoked", + // This value will be overridden since it is tagged with `inject` and DocumentDescription is provided + Description: "DescriptionInvoked", + // This value will be overridden with the same value since it is tagged with `inject` and Printer is provided + Printer: printer, + // This value will be retained since it is tagged with `inject:"optional"` and int64 is not provided + Size: 100, + } + }) + if err != nil { + panic(err) + } + + printDoc(results[0].(*Document)) + } + + fmt.Println("-------") + + { + // Apply dependencies to a struct instance + doc := &Document{} + if err := inj.Apply(doc); err != nil { + panic(err) + } + + printDoc(doc) + } + + fmt.Println("-------") + + { + // Create a child injector and then apply dependencies to a struct instance + child := inject.New() + child.SetParent(inj) + + doc := &Document{} + if err := child.Apply(doc); err != nil { + panic(err) + } + + printDoc(doc) + } + + // Output: + // Resolved printer: Printing document + // ------- + // Document id: "idInvoked" + // Document description: "A document description" + // Document printer: "Printing document" + // Document size: 100 + // Document page: 42 + // Document name: "A simple string" + // ------- + // Document id: "" + // Document description: "A document description" + // Document printer: "Printing document" + // Document size: 0 + // Document page: 42 + // Document name: "A simple string" + // ------- + // Document id: "" + // Document description: "A document description" + // Document printer: "Printing document" + // Document size: 0 + // Document page: 42 + // Document name: "A simple string" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9f71e7b --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/theplant/inject + +go 1.22.3 + +require ( + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.9.0 + golang.org/x/sync v0.7.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7c27b3d --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/inject.go b/inject.go new file mode 100644 index 0000000..ba6140b --- /dev/null +++ b/inject.go @@ -0,0 +1,254 @@ +package inject + +import ( + "fmt" + "reflect" + "strings" + "sync" + "unsafe" + + "github.com/pkg/errors" + "golang.org/x/sync/singleflight" +) + +var ( + ErrTypeNotProvided = errors.New("type not provided") + ErrTypeAlreadyProvided = errors.New("type already provided") + ErrParentAlreadySet = errors.New("parent already set") +) + +type Injector struct { + mu sync.RWMutex + + values map[reflect.Type]reflect.Value + providers map[reflect.Type]any // value func + parent *Injector + + sfg singleflight.Group +} + +func New() *Injector { + inj := &Injector{ + values: map[reflect.Type]reflect.Value{}, + providers: map[reflect.Type]any{}, + } + inj.Provide(func() *Injector { return inj }) + return inj +} + +func (inj *Injector) SetParent(parent *Injector) error { + inj.mu.RLock() + defer inj.mu.RUnlock() + if inj.parent != nil { + return ErrParentAlreadySet + } + inj.parent = parent + return nil +} + +var typeError = reflect.TypeOf((*error)(nil)).Elem() + +func (inj *Injector) provide(f any) (err error) { + rv := reflect.ValueOf(f) + rt := rv.Type() + if rt.Kind() != reflect.Func { + panic("Provide only accepts a function") + } + + inj.mu.Lock() + defer inj.mu.Unlock() + + setted := []reflect.Type{} + defer func() { + if err != nil { + for _, t := range setted { + delete(inj.providers, t) + } + } + }() + + numOut := rt.NumOut() + for i := 0; i < numOut; i++ { + outType := rt.Out(i) + + // skip error type if it is the last return value + if i == numOut-1 && outType == typeError { + continue + } + + if _, ok := inj.values[outType]; ok { + return errors.Wrap(ErrTypeAlreadyProvided, outType.String()) + } + + if _, ok := inj.providers[outType]; ok { + return errors.Wrap(ErrTypeAlreadyProvided, outType.String()) + } + + inj.providers[outType] = f + setted = append(setted, outType) + } + return nil +} + +func (inj *Injector) invoke(f any) ([]reflect.Value, error) { + rt := reflect.TypeOf(f) + if rt.Kind() != reflect.Func { + panic("Invoke only accepts a function") + } + + numIn := rt.NumIn() + in := make([]reflect.Value, numIn) + for i := 0; i < numIn; i++ { + argType := rt.In(i) + argValue, err := inj.resolve(argType) + if err != nil { + return nil, err + } + in[i] = argValue + } + + outs := reflect.ValueOf(f).Call(in) + + // apply if possible + for _, out := range outs { + unwrapped := unwrapPtr(out) + if unwrapped.Kind() == reflect.Struct { + if err := inj.applyStruct(unwrapped); err != nil { + return nil, err + } + } + } + + numOut := len(outs) + if numOut > 0 && rt.Out(numOut-1) == typeError { + rvErr := outs[numOut-1] + outs = outs[:numOut-1] + if !rvErr.IsNil() { + return outs, rvErr.Interface().(error) + } + } + + return outs, nil +} + +func (inj *Injector) resolve(rt reflect.Type) (reflect.Value, error) { + inj.mu.RLock() + rv := inj.values[rt] + if rv.IsValid() { + inj.mu.RUnlock() + return rv, nil + } + provider, ok := inj.providers[rt] + parent := inj.parent + inj.mu.RUnlock() + + if ok { + // ensure that the provider is only executed once same time + _, err, _ := inj.sfg.Do(fmt.Sprintf("%p", provider), func() (any, error) { + // must recheck the provider, because it may be deleted by prev inj.sfg.Do + inj.mu.RLock() + _, ok := inj.providers[rt] + inj.mu.RUnlock() + if !ok { + return nil, nil + } + + results, err := inj.invoke(provider) + if err != nil { + return nil, err + } + + inj.mu.Lock() + for _, result := range results { + resultType := result.Type() + inj.values[resultType] = result + delete(inj.providers, resultType) + } + inj.mu.Unlock() + + return nil, nil + }) + if err != nil { + return rv, err + } + return inj.resolve(rt) + } + + if parent != nil { + return parent.resolve(rt) + } + + return rv, errors.Wrap(ErrTypeNotProvided, rt.String()) +} + +func unwrapPtr(rv reflect.Value) reflect.Value { + for rv.Kind() == reflect.Ptr { + rv = rv.Elem() + } + return rv +} + +func (inj *Injector) Apply(val any) error { + rv := unwrapPtr(reflect.ValueOf(val)) + if rv.Kind() != reflect.Struct { + panic("Apply only accepts a struct") + } + return inj.applyStruct(rv) +} + +const tagOptional = "optional" + +func (inj *Injector) applyStruct(rv reflect.Value) error { + rt := rv.Type() + + for i := 0; i < rv.NumField(); i++ { + field := rv.Field(i) + structField := rt.Field(i) + if tag, ok := structField.Tag.Lookup("inject"); ok { + if !field.CanSet() { + // If the field is unexported, we need to create a new field that is settable. + field = reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem() + } + dep, err := inj.resolve(field.Type()) + if err != nil { + if errors.Is(err, ErrTypeNotProvided) && strings.TrimSpace(tag) == tagOptional { + continue + } + return err + } + field.Set(dep) + } + } + + return nil +} + +func (inj *Injector) Provide(fs ...any) error { + for _, f := range fs { + if err := inj.provide(f); err != nil { + return err + } + } + return nil +} + +func (inj *Injector) Invoke(f any) ([]any, error) { + results, err := inj.invoke(f) + if err != nil { + return nil, err + } + out := make([]any, len(results)) + for i, result := range results { + out[i] = result.Interface() + } + return out, nil +} + +func (inj *Injector) Resolve(ref any) error { + rv, err := inj.resolve(reflect.TypeOf(ref).Elem()) + if err != nil { + return err + } + reflect.ValueOf(ref).Elem().Set(rv) + return nil +} diff --git a/inject_test.go b/inject_test.go new file mode 100644 index 0000000..efbfd00 --- /dev/null +++ b/inject_test.go @@ -0,0 +1,284 @@ +package inject + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNew(t *testing.T) { + injector := New() + require.NotNil(t, injector) + require.NotNil(t, injector.values) + require.NotNil(t, injector.providers) +} + +func TestSetParent(t *testing.T) { + injector := New() + parent := New() + err := injector.SetParent(parent) + require.NoError(t, err) + require.Equal(t, parent, injector.parent) + err = injector.SetParent(parent) + require.ErrorIs(t, err, ErrParentAlreadySet) +} + +func TestProvide(t *testing.T) { + { + injector := New() + require.Panics(t, func() { + injector.Provide("testNotFunc") + }) + } + { + injector := New() + err := injector.Provide(func() string { return "test" }) + require.NoError(t, err) + } + { + injector := New() + err := injector.Provide(func() (string, int) { return "test", 0 }) + require.NoError(t, err) + require.Len(t, injector.providers, 3) + + err = injector.Provide(func() (int64, int) { return 1, 2 }) + require.ErrorIs(t, err, ErrTypeAlreadyProvided) + require.Len(t, injector.providers, 3) + } + { + injector := New() + err := injector.Provide(func() (string, error) { return "test", nil }) + require.NoError(t, err) + require.Len(t, injector.providers, 2) + + _, err = injector.invoke(func(s string) {}) + require.NoError(t, err) + require.Len(t, injector.providers, 1) + + err = injector.Provide(func() (int64, string) { return 1, "" }) + require.ErrorIs(t, err, ErrTypeAlreadyProvided) + require.Len(t, injector.providers, 1) + } +} + +func TestInvoke(t *testing.T) { + injector := New() + err := injector.Provide(func() string { return "test" }) + require.NoError(t, err) + + require.Panics(t, func() { + injector.invoke("testNotFunc") + }) + + results, err := injector.invoke(func(s string) string { return s }) + require.NoError(t, err) + require.Equal(t, "test", results[0].Interface()) + + { + errTemp := errors.New("temp") + results, err := injector.invoke(func(s string) (string, error) { return "", errTemp }) + require.ErrorIs(t, err, errTemp) + require.Equal(t, "", results[0].Interface()) + require.Len(t, results, 1) + } + + { + results, err := injector.Invoke(func(s string) string { return s }) + require.NoError(t, err) + require.Equal(t, "test", results[0]) + } +} + +func TestResolve(t *testing.T) { + injector := New() + + { + err := injector.Provide(func() string { return "test" }) + require.NoError(t, err) + var str string + err = injector.Resolve(&str) + require.NoError(t, err) + require.Equal(t, "test", str) + } + + { + err := injector.Provide(func() *string { + a := "testPtr" + return &a + }) + require.NoError(t, err) + var str *string + err = injector.Resolve(&str) + require.NoError(t, err) + require.Equal(t, "testPtr", *str) + } + + { + injector := New() + + errTemp := errors.New("temp") + // if error is not nil, the value will be ignored + err := injector.Provide(func() (string, error) { return "test", errTemp }) + require.NoError(t, err) + str := "xxx" + err = injector.Resolve(&str) + require.ErrorIs(t, err, errTemp) + require.Equal(t, "xxx", str) + } +} + +func TestApply(t *testing.T) { + type TestStruct struct { + *Injector `inject:""` + Value string `inject:"" json:"value,omitempty"` + value string `inject:""` + optional0 *int64 `inject:"optional"` + optional1 uint64 `inject:"optional"` + ID string `json:"id,omitempty"` + } + injector := New() + err := injector.Provide( + func() string { return "test" }, + func() uint64 { return 123 }, + ) + require.NoError(t, err) + testStruct := &TestStruct{} + err = injector.Apply(testStruct) + require.NoError(t, err) + require.Equal(t, "test", testStruct.Value) + require.Equal(t, "test", testStruct.value) + require.Nil(t, testStruct.optional0) + require.Equal(t, uint64(123), testStruct.optional1) + require.Equal(t, "", testStruct.ID) + require.Equal(t, injector, testStruct.Injector) + + require.Panics(t, func() { + injector.Apply("testNotStruct") + }) +} + +func TestMultipleProviders(t *testing.T) { + injector := New() + err := injector.Provide(func() string { return "test1" }) + require.NoError(t, err) + err = injector.Provide(func() string { return "test2" }) + require.ErrorIs(t, err, ErrTypeAlreadyProvided) + results, err := injector.invoke(func(s1, s2 string) string { return s1 + s2 }) + require.NoError(t, err) + require.Equal(t, "test1test1", results[0].Interface()) +} + +func TestUnresolvedDependency(t *testing.T) { + injector := New() + err := injector.Provide(func() string { return "test" }) + require.NoError(t, err) + _, err = injector.invoke(func(s string, i int) string { return s }) + require.ErrorIs(t, err, ErrTypeNotProvided) +} + +func TestParentInjection(t *testing.T) { + parent := New() + err := parent.Provide(func() string { return "test" }) + require.NoError(t, err) + child := New() + err = child.SetParent(parent) + require.NoError(t, err) + results, err := child.invoke(func(s string) string { return s }) + require.NoError(t, err) + require.Equal(t, "test", results[0].Interface()) + + // override + err = child.Provide(func() string { return "test2" }) + require.NoError(t, err) + results, err = child.invoke(func(s string) string { return s }) + require.NoError(t, err) + require.Equal(t, "test2", results[0].Interface()) +} + +type TestInterface interface { + Test() string +} + +type TestStruct struct { + Name string +} + +func (t *TestStruct) Test() string { + return t.Name +} + +func TestInterfaceType(t *testing.T) { + injector := New() + + err := injector.Provide(func() TestInterface { return &TestStruct{Name: "hello"} }) + require.NoError(t, err) + var testIface TestInterface + err = injector.Resolve(&testIface) + require.NoError(t, err) + require.NotNil(t, testIface) + require.Equal(t, "hello", testIface.Test()) + + type Visibility string + err = injector.Provide(func() Visibility { return "public" }) + require.NoError(t, err) + var visibility Visibility + err = injector.Resolve(&visibility) + require.NoError(t, err) + require.Equal(t, Visibility("public"), visibility) + + type StructToApply struct { + iface TestInterface `inject:""` + Visibility Visibility `inject:""` + str string `inject:""` + ID string `json:"id,omitempty"` + } + err = injector.Provide(func() string { return "str" }) + require.NoError(t, err) + + structToApply := &StructToApply{} + err = injector.Apply(structToApply) + require.NoError(t, err) + require.Equal(t, "hello", structToApply.iface.Test()) + require.Equal(t, Visibility("public"), structToApply.Visibility) + require.Equal(t, "str", structToApply.str) + require.Equal(t, "", structToApply.ID) +} + +func TestAutoApply(t *testing.T) { + type TestStruct struct { + Injector *Injector `inject:""` + Value string `inject:"" json:"value,omitempty"` + value string `inject:""` + ID string `json:"id,omitempty"` + } + injector := New() + err := injector.Provide( + func() string { return "test" }, + ) + require.NoError(t, err) + results, err := injector.Invoke(func() *TestStruct { + return &TestStruct{ + ID: "testID", + } + }) + require.NoError(t, err) + testStruct := results[0].(*TestStruct) + require.Equal(t, "test", testStruct.Value) + require.Equal(t, "test", testStruct.value) + require.Equal(t, "testID", testStruct.ID) + require.Equal(t, injector, testStruct.Injector) + + { + err = injector.Provide(func() *TestStruct { return &TestStruct{ID: "testID2"} }) + require.NoError(t, err) + + testStruct := &TestStruct{} + err := injector.Resolve(&testStruct) + require.NoError(t, err) + require.Equal(t, "test", testStruct.Value) + require.Equal(t, "test", testStruct.value) + require.Equal(t, "testID2", testStruct.ID) + require.Equal(t, injector, testStruct.Injector) + } +}