diff --git a/di_explain.go b/di_explain.go index a40a8e7..106e49a 100644 --- a/di_explain.go +++ b/di_explain.go @@ -165,7 +165,7 @@ func ExplainNamedService(scope Injector, name string) (description ExplainServic ScopeID: serviceScope.ID(), ScopeName: serviceScope.Name(), ServiceName: name, - ServiceType: service.getType(), + ServiceType: service.getServiceType(), ServiceBuildTime: buildTime, Invoked: invoked, Dependencies: newExplainServiceDependencies(_i, newEdgeService(_i.ID(), _i.Name(), name), "dependencies"), diff --git a/di_test.go b/di_test.go index 7e4dc9c..b7c7783 100644 --- a/di_test.go +++ b/di_test.go @@ -515,6 +515,10 @@ func TestInvokeNamed(t *testing.T) { instance2, err2 := InvokeNamed[int](i, "foobar") is.Nil(err2) is.EqualValues(42, instance2) + + instance3, err3 := InvokeNamed[string](i, "foobar") + is.EqualError(err3, "DI: service found, but type mismatch: invoking `string` but registered `int`") + is.EqualValues("", instance3) } func TestMustInvokeNamed(t *testing.T) { diff --git a/invoke.go b/invoke.go index c7b60dc..4009477 100644 --- a/invoke.go +++ b/invoke.go @@ -83,7 +83,7 @@ func invokeByName[T any](i Injector, name string) (T, error) { service, ok := serviceAny.(Service[T]) if !ok { - return empty[T](), serviceNotFound(injector, ErrServiceNotFound, invokerChain) + return empty[T](), serviceTypeMismatch(inferServiceName[T](), serviceAny.(ServiceAny).getTypeName()) } injector.RootScope().opts.onBeforeInvocation(serviceScope, name) @@ -247,6 +247,11 @@ func serviceNotFound(injector Injector, err error, chain []string) error { return fmt.Errorf("%w `%s`, available services: %s", err, name, strings.Join(sortedServiceNames, ", ")) } +// serviceTypeMismatch returns an error indicating that the specified service was found, but its type does not match the expected type. +func serviceTypeMismatch(invoking string, registered string) error { + return fmt.Errorf("DI: service found, but type mismatch: invoking `%s` but registered `%s`", invoking, registered) +} + // getServiceNames formats a list of EdgeService names. func getServiceNames(services []EdgeService) []string { return mAp(services, func(edge EdgeService, _ int) string { diff --git a/service.go b/service.go index 908878a..902462c 100644 --- a/service.go +++ b/service.go @@ -28,7 +28,8 @@ var serviceTypeToIcon = map[ServiceType]string{ type Service[T any] interface { getName() string - getType() ServiceType + getTypeName() string + getServiceType() ServiceType getEmptyInstance() any getInstanceAny(Injector) (any, error) getInstance(Injector) (T, error) @@ -43,7 +44,8 @@ type Service[T any] interface { // Like Service[T] but without the generic type. type ServiceAny interface { getName() string - getType() ServiceType + getTypeName() string + getServiceType() ServiceType getEmptyInstance() any getInstanceAny(Injector) (any, error) // getInstance(Injector) (T, error) @@ -56,7 +58,8 @@ type ServiceAny interface { } type serviceGetName interface{ getName() string } -type serviceGetType interface{ getType() ServiceType } +type serviceGetTypeName interface{ getTypeName() string } +type serviceGetServiceType interface{ getServiceType() ServiceType } type serviceGetEmptyInstance interface{ getEmptyInstance() any } type serviceGetInstanceAny interface{ getInstanceAny(Injector) (any, error) } type serviceGetInstance[T any] interface{ getInstance(Injector) (T, error) } //nolint:unused @@ -73,7 +76,8 @@ type serviceBuildTime interface { } var _ serviceGetName = (Service[int])(nil) -var _ serviceGetType = (Service[int])(nil) +var _ serviceGetTypeName = (Service[int])(nil) +var _ serviceGetServiceType = (Service[int])(nil) var _ serviceGetEmptyInstance = (Service[int])(nil) var _ serviceGetInstanceAny = (Service[int])(nil) var _ serviceIsHealthchecker = (Service[int])(nil) @@ -88,7 +92,7 @@ func inferServiceName[T any]() string { } func inferServiceProviderStacktrace(service ServiceAny) (stacktrace.Frame, bool) { - if service.getType() == ServiceTypeTransient { + if service.getServiceType() == ServiceTypeTransient { return stacktrace.Frame{}, false } else { providerFrame, _ := service.source() @@ -113,7 +117,7 @@ func inferServiceInfo(injector Injector, name string) (serviceInfo, bool) { return serviceInfo{ name: name, - serviceType: serviceAny.(serviceGetType).getType(), + serviceType: serviceAny.(serviceGetServiceType).getServiceType(), serviceBuildTime: buildTime, healthchecker: serviceAny.(serviceIsHealthchecker).isHealthchecker(), shutdowner: serviceAny.(serviceIsShutdowner).isShutdowner(), diff --git a/service_alias.go b/service_alias.go index f92b4bf..efe24de 100644 --- a/service_alias.go +++ b/service_alias.go @@ -17,6 +17,7 @@ var _ serviceClone = (*serviceAlias[int, int])(nil) type serviceAlias[Initial any, Alias any] struct { mu sync.RWMutex name string + typeName string scope Injector targetName string @@ -31,6 +32,7 @@ func newServiceAlias[Initial any, Alias any](name string, scope Injector, target return &serviceAlias[Initial, Alias]{ mu: sync.RWMutex{}, name: name, + typeName: inferServiceName[Alias](), scope: scope, targetName: targetName, @@ -44,7 +46,11 @@ func (s *serviceAlias[Initial, Alias]) getName() string { return s.name } -func (s *serviceAlias[Initial, Alias]) getType() ServiceType { +func (s *serviceAlias[Initial, Alias]) getTypeName() string { + return s.typeName +} + +func (s *serviceAlias[Initial, Alias]) getServiceType() ServiceType { return ServiceTypeAlias } @@ -79,7 +85,8 @@ func (s *serviceAlias[Initial, Alias]) getInstance(i Injector) (Alias, error) { return target, nil default: // should never happen, since invoke() checks the type - return empty[Alias](), fmt.Errorf("DI: could not cast `%s` as `%s`", s.targetName, s.name) + return empty[Alias](), serviceTypeMismatch(inferServiceName[Alias](), inferServiceName[Initial]()) + // return empty[Alias](), fmt.Errorf("DI: could not cast `%s` as `%s`", s.targetName, s.name) } } @@ -161,8 +168,9 @@ func (s *serviceAlias[Initial, Alias]) shutdown(ctx context.Context) error { func (s *serviceAlias[Initial, Alias]) clone() any { return &serviceAlias[Initial, Alias]{ - mu: sync.RWMutex{}, - name: s.name, + mu: sync.RWMutex{}, + name: s.name, + typeName: s.typeName, // scope: s.scope, <-- @TODO: we should inject here the cloned scope targetName: s.targetName, diff --git a/service_alias_test.go b/service_alias_test.go index ceadeca..5936c11 100644 --- a/service_alias_test.go +++ b/service_alias_test.go @@ -29,14 +29,24 @@ func TestServiceAlias_getName(t *testing.T) { is.Equal("foobar1", service1.getName()) } -func TestServiceAlias_getType(t *testing.T) { +func TestServiceAlias_getTypeName(t *testing.T) { + t.Parallel() + is := assert.New(t) + + i := New() + + service1 := newServiceAlias[string, int]("foobar1", i, "foobar2") + is.Equal("int", service1.getTypeName()) +} + +func TestServiceAlias_getServiceType(t *testing.T) { t.Parallel() is := assert.New(t) i := New() service1 := newServiceAlias[string, string]("foobar1", i, "foobar2") - is.Equal(ServiceTypeAlias, service1.getType()) + is.Equal(ServiceTypeAlias, service1.getServiceType()) } func TestServiceAlias_getEmptyInstance(t *testing.T) { @@ -77,7 +87,7 @@ func TestServiceAlias_getInstanceAny(t *testing.T) { // target service found but not convertible type service3 := newServiceAlias[*lazyTestHeathcheckerOK, int]("github.com/samber/do/v2.Healthchecker", i, "int") instance3, err3 := service3.getInstanceAny(i) - is.EqualError(err3, "DI: could not find service `int`, available services: `*github.com/samber/do/v2.lazyTestHeathcheckerOK`, `github.com/samber/do/v2.Healthchecker`, `int`") + is.EqualError(err3, "DI: service found, but type mismatch: invoking `*github.com/samber/do/v2.lazyTestHeathcheckerOK` but registered `int`") is.EqualValues(0, instance3) // @TODO: missing test with child scopes @@ -113,7 +123,7 @@ func TestServiceAlias_getInstance(t *testing.T) { // target service found but not convertible type service3 := newServiceAlias[*lazyTestHeathcheckerOK, int]("github.com/samber/do/v2.Healthchecker", i, "int") instance3, err3 := service3.getInstance(i) - is.EqualError(err3, "DI: could not find service `int`, available services: `*github.com/samber/do/v2.lazyTestHeathcheckerOK`, `github.com/samber/do/v2.Healthchecker`, `int`") + is.EqualError(err3, "DI: service found, but type mismatch: invoking `*github.com/samber/do/v2.lazyTestHeathcheckerOK` but registered `int`") is.EqualValues(0, instance3) // @TODO: missing test with child scopes diff --git a/service_eager.go b/service_eager.go index e5a9033..290bab1 100644 --- a/service_eager.go +++ b/service_eager.go @@ -15,6 +15,7 @@ var _ serviceClone = (*serviceEager[int])(nil) type serviceEager[T any] struct { name string + typeName string instance T providerFrame stacktrace.Frame @@ -28,6 +29,7 @@ func newServiceEager[T any](name string, instance T) *serviceEager[T] { return &serviceEager[T]{ name: name, + typeName: inferServiceName[T](), instance: instance, providerFrame: providerFrame, @@ -41,7 +43,11 @@ func (s *serviceEager[T]) getName() string { return s.name } -func (s *serviceEager[T]) getType() ServiceType { +func (s *serviceEager[T]) getTypeName() string { + return s.typeName +} + +func (s *serviceEager[T]) getServiceType() ServiceType { return ServiceTypeEager } @@ -112,6 +118,7 @@ func (s *serviceEager[T]) shutdown(ctx context.Context) error { func (s *serviceEager[T]) clone() any { return &serviceEager[T]{ name: s.name, + typeName: s.typeName, instance: s.instance, providerFrame: s.providerFrame, diff --git a/service_eager_test.go b/service_eager_test.go index 166c198..d441707 100644 --- a/service_eager_test.go +++ b/service_eager_test.go @@ -68,17 +68,30 @@ func TestServiceEager_getName(t *testing.T) { is.Equal("foobar2", service2.getName()) } -func TestServiceEager_getType(t *testing.T) { +func TestServiceEager_getTypeName(t *testing.T) { t.Parallel() is := assert.New(t) test := eagerTest{foobar: "foobar"} service1 := newServiceEager("foobar1", 42) - is.Equal(ServiceTypeEager, service1.getType()) + is.Equal("int", service1.getTypeName()) service2 := newServiceEager("foobar2", test) - is.Equal(ServiceTypeEager, service2.getType()) + is.Equal("github.com/samber/do/v2.eagerTest", service2.getTypeName()) +} + +func TestServiceEager_getServiceType(t *testing.T) { + t.Parallel() + is := assert.New(t) + + test := eagerTest{foobar: "foobar"} + + service1 := newServiceEager("foobar1", 42) + is.Equal(ServiceTypeEager, service1.getServiceType()) + + service2 := newServiceEager("foobar2", test) + is.Equal(ServiceTypeEager, service2.getServiceType()) } func TestServiceEager_getEmptyInstance(t *testing.T) { diff --git a/service_lazy.go b/service_lazy.go index a4a752f..d2de8a9 100644 --- a/service_lazy.go +++ b/service_lazy.go @@ -18,6 +18,7 @@ var _ serviceClone = (*serviceLazy[int])(nil) type serviceLazy[T any] struct { mu sync.RWMutex name string + typeName string instance T // lazy loading @@ -34,8 +35,9 @@ func newServiceLazy[T any](name string, provider Provider[T]) *serviceLazy[T] { providerFrame, _ := stacktrace.NewFrameFromPtr(reflect.ValueOf(provider).Pointer()) return &serviceLazy[T]{ - mu: sync.RWMutex{}, - name: name, + mu: sync.RWMutex{}, + name: name, + typeName: inferServiceName[T](), built: false, buildTime: 0, @@ -51,7 +53,11 @@ func (s *serviceLazy[T]) getName() string { return s.name } -func (s *serviceLazy[T]) getType() ServiceType { +func (s *serviceLazy[T]) getTypeName() string { + return s.typeName +} + +func (s *serviceLazy[T]) getServiceType() ServiceType { return ServiceTypeLazy } @@ -179,8 +185,9 @@ func (s *serviceLazy[T]) shutdown(ctx context.Context) error { func (s *serviceLazy[T]) clone() any { // reset `build` flag and instance return &serviceLazy[T]{ - mu: sync.RWMutex{}, - name: s.name, + mu: sync.RWMutex{}, + name: s.name, + typeName: s.typeName, built: false, provider: s.provider, diff --git a/service_lazy_test.go b/service_lazy_test.go index 195324a..7a59991 100644 --- a/service_lazy_test.go +++ b/service_lazy_test.go @@ -108,7 +108,7 @@ func TestServiceLazy_getName(t *testing.T) { is.Equal("foobar2", service2.getName()) } -func TestServiceLazy_getType(t *testing.T) { +func TestServiceLazy_getTypeName(t *testing.T) { t.Parallel() is := assert.New(t) @@ -122,10 +122,30 @@ func TestServiceLazy_getType(t *testing.T) { } service1 := newServiceLazy("foobar1", provider1) - is.Equal(ServiceTypeLazy, service1.getType()) + is.Equal("int", service1.getTypeName()) service2 := newServiceLazy("foobar2", provider2) - is.Equal(ServiceTypeLazy, service2.getType()) + is.Equal("github.com/samber/do/v2.lazyTest", service2.getTypeName()) +} + +func TestServiceLazy_getServiceType(t *testing.T) { + t.Parallel() + is := assert.New(t) + + test := lazyTest{foobar: "foobar"} + + provider1 := func(i Injector) (int, error) { + return 42, nil + } + provider2 := func(i Injector) (lazyTest, error) { + return test, nil + } + + service1 := newServiceLazy("foobar1", provider1) + is.Equal(ServiceTypeLazy, service1.getServiceType()) + + service2 := newServiceLazy("foobar2", provider2) + is.Equal(ServiceTypeLazy, service2.getServiceType()) } func TestServiceLazy_getEmptyInstance(t *testing.T) { diff --git a/service_transient.go b/service_transient.go index 75cb820..3b56a7f 100644 --- a/service_transient.go +++ b/service_transient.go @@ -12,7 +12,8 @@ var _ serviceShutdown = (*serviceTransient[int])(nil) var _ serviceClone = (*serviceTransient[int])(nil) type serviceTransient[T any] struct { - name string + name string + typeName string // lazy loading provider Provider[T] @@ -20,7 +21,8 @@ type serviceTransient[T any] struct { func newServiceTransient[T any](name string, provider Provider[T]) *serviceTransient[T] { return &serviceTransient[T]{ - name: name, + name: name, + typeName: inferServiceName[T](), provider: provider, } @@ -30,7 +32,11 @@ func (s *serviceTransient[T]) getName() string { return s.name } -func (s *serviceTransient[T]) getType() ServiceType { +func (s *serviceTransient[T]) getTypeName() string { + return s.typeName +} + +func (s *serviceTransient[T]) getServiceType() ServiceType { return ServiceTypeTransient } @@ -68,7 +74,8 @@ func (s *serviceTransient[T]) shutdown(ctx context.Context) error { func (s *serviceTransient[T]) clone() any { return &serviceTransient[T]{ - name: s.name, + name: s.name, + typeName: s.typeName, provider: s.provider, } diff --git a/service_transient_test.go b/service_transient_test.go index 4c4a163..34b63dc 100644 --- a/service_transient_test.go +++ b/service_transient_test.go @@ -76,7 +76,7 @@ func TestServiceTransient_getName(t *testing.T) { is.Equal("foobar2", service2.getName()) } -func TestServiceTransient_getType(t *testing.T) { +func TestServiceTransient_getTypeName(t *testing.T) { t.Parallel() is := assert.New(t) @@ -90,10 +90,30 @@ func TestServiceTransient_getType(t *testing.T) { } service1 := newServiceTransient("foobar1", provider1) - is.Equal(ServiceTypeTransient, service1.getType()) + is.Equal("int", service1.getTypeName()) service2 := newServiceTransient("foobar2", provider2) - is.Equal(ServiceTypeTransient, service2.getType()) + is.Equal("github.com/samber/do/v2.transientTest", service2.getTypeName()) +} + +func TestServiceTransient_getServiceType(t *testing.T) { + t.Parallel() + is := assert.New(t) + + test := transientTest{foobar: "foobar"} + + provider1 := func(i Injector) (int, error) { + return 42, nil + } + provider2 := func(i Injector) (transientTest, error) { + return test, nil + } + + service1 := newServiceTransient("foobar1", provider1) + is.Equal(ServiceTypeTransient, service1.getServiceType()) + + service2 := newServiceTransient("foobar2", provider2) + is.Equal(ServiceTypeTransient, service2.getServiceType()) } func TestServiceTransient_getEmptyInstance(t *testing.T) {