diff --git a/README.md b/README.md index e866fa7..8f7a045 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ Pal provides several functions for registering services: - `Provide[T any](value T)` - Registers an instance of service. - `ProvideFn[T any](fn func(ctx context.Context) (T, error))` - Registers a singleton service created using the provided function. -- `ProvideFactory{0-5}[T any, {0-5}P any](fn func(ctx context.Context, {0-5}P args) (T, error)))` - Registers a factory service created using the provided function with given amount of arguments. +- `ProvideFactory{0-5}[I any, T any, {0-5}P any](fn func(ctx context.Context, {0-5}P args) (T, error)))` - Registers a factory service created using the provided function with given amount of arguments. - `ProvideList(...ServiceDef)` - Registers multiple services at once, useful when splitting apps into modules, see [example](./examples/web) - There are also `Named` versions of `Provide` functions, they can be used along with `name` tag and `Named` versions `Invoke` functions if you want to give your services explicit names. @@ -151,21 +151,39 @@ argument cannot be explicitdependencies of other services. They are perfect for: **Registration:** ```go // Register a factory service with no arguments -pal.ProvideFactory0[MyService](func(ctx context.Context) (MyService, error) { +pal.ProvideFactory0[MyService](func(ctx context.Context) (*MyServiceImpl, error) { return &MyServiceImpl{}, nil }) // Register a factory service with arguments -pal.ProvideFactory2[MyService](func(ctx context.Context, url string, timeout time.Duration) (MyService, error) { +pal.ProvideFactory2[MyService](func(ctx context.Context, url string, timeout time.Duration) (*MyServiceImpl, error) { return &MyServiceImpl{URL: url, Timeout: timeout}, nil }) ``` **Invocation** -```go -pal.Invoke[MyService](ctx, p, "https://exmaple.com", timeout) -``` +There are 2 ways to invoke a factory service: + +- manual invocation: + ```go + pal.Invoke[MyService](ctx, p, "https://exmaple.com", timeout) + ``` + this way **must never** be using during initialization as Pal does know that your service depends on a factory service and the factory service + may not be yet initialized. +- ivocation using injected factory function: + ```go + type SomeService struct { + ... + // parameters of a factory function must match the parameters of the function passed to pal.ProvideFactory + // but the return value must match the first type argument pal.ProvideFactory + CreateMyService(ctx context.Context, url string, timeout time.Duration) (MyService, error) + ... + } + ``` + + This way pal can see that `SomeService` depends on `MyService` and adjust the intitialization process accordingly. + It is safe to call `CreateMyService` from `MyService.Init()`. ### Const Services Const services wrap existing instances. They are useful for: @@ -243,13 +261,6 @@ pal.ProvideConst[MyService](existingInstance). return service.Ping() }) -// With factory services -pal.ProvideFactory[MyService](&MyServiceImpl{}). - ToInit(func(ctx context.Context, service MyService, pal *pal.Pal) error { - // Each factory instance will be initialized with this hook - return service.Setup() - }) - // With function-based services pal.ProvideFn[MyService](func(ctx context.Context) (*MyServiceImpl, error) { return &MyServiceImpl{}, nil diff --git a/container.go b/container.go index 481c1a5..2029e18 100644 --- a/container.go +++ b/container.go @@ -16,13 +16,19 @@ import ( "github.com/zhulik/pal/pkg/dag" ) +type factoryServiceMaping struct { + Factory any + Service ServiceDef +} + // Container is responsible for storing services, instances and the dependency graph type Container struct { pal *Pal - services map[string]ServiceDef - graph *dag.DAG[string, ServiceDef] - logger *slog.Logger + services map[string]ServiceDef + factories map[string]factoryServiceMaping + graph *dag.DAG[string, ServiceDef] + logger *slog.Logger } // NewContainer creates a new Container instance @@ -30,14 +36,23 @@ func NewContainer(pal *Pal, services ...ServiceDef) *Container { services = flattenServices(services) container := &Container{ - pal: pal, - services: map[string]ServiceDef{}, - graph: dag.New[string, ServiceDef](), - logger: slog.With("palComponent", "Container"), + pal: pal, + services: map[string]ServiceDef{}, + factories: map[string]factoryServiceMaping{}, + graph: dag.New[string, ServiceDef](), + logger: slog.With("palComponent", "Container"), } for _, service := range services { container.addService(service) + if factorier, ok := service.(interface{ Factory() any }); ok { + fn := factorier.Factory() + fnType := reflect.TypeOf(fn) + container.factories[typetostring.GetReflectType(fnType)] = factoryServiceMaping{ + Factory: fn, + Service: service, + } + } } return container @@ -123,6 +138,7 @@ func (c *Container) InjectInto(ctx context.Context, target any) error { } fieldType := t.Field(i).Type + if fieldType == reflect.TypeOf((*slog.Logger)(nil)) && c.pal.config.AttrSetters != nil { c.injectLoggerIntoField(field, target) continue @@ -142,10 +158,19 @@ func (c *Container) InjectInto(ctx context.Context, target any) error { typeName = typetostring.GetReflectType(fieldType) } + if fieldType.Kind() == reflect.Func { + mapping, ok := c.factories[typeName] + if ok { + field.Set(reflect.ValueOf(mapping.Factory)) + } + + continue + } + err = c.injectByName(ctx, typeName, field) if err != nil { if errors.Is(err, ErrServiceNotFound) && !mustInject { - return nil + continue } return err } @@ -277,12 +302,29 @@ func (c *Container) addDependencyVertex(service ServiceDef, parent ServiceDef) e typ := val.Type() for i := 0; i < typ.NumField(); i++ { - dependencyName := typetostring.GetReflectType(typ.Field(i).Type) + field := typ.Field(i) + tags, err := ParseTag(field.Tag.Get("pal")) + if err != nil { + return err + } + + dependencyName := tags[TagName] + + if dependencyName == "" { + dependencyName = typetostring.GetReflectType(field.Type) + } + if childService, ok := c.services[dependencyName]; ok { if err := c.addDependencyVertex(childService, service); err != nil { return err } } + + if factoryMapping, ok := c.factories[dependencyName]; ok { + if err := c.addDependencyVertex(factoryMapping.Service, service); err != nil { + return err + } + } } return nil diff --git a/examples/factories/pinger.go b/examples/factories/pinger.go index a4673b3..f276668 100644 --- a/examples/factories/pinger.go +++ b/examples/factories/pinger.go @@ -34,7 +34,7 @@ func (p *pinger) Shutdown(_ context.Context) error { return nil } -// Ping pings google.com. +// Ping pings given URL. func (p *pinger) Ping(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, "GET", p.URL, nil) if err != nil { diff --git a/examples/factories/ticker.go b/examples/factories/ticker.go index 2293ef7..8c4db12 100644 --- a/examples/factories/ticker.go +++ b/examples/factories/ticker.go @@ -14,7 +14,10 @@ type ticker struct { Pal *pal.Pal - pinger Pinger // pinger is injected by pal, using the Pinger interface. + // CreatePinger is a factory function that creates a pinger service, it is injected by pal. + CreatePinger func(ctx context.Context, url string) (Pinger, error) + + pinger Pinger ticker *time.Ticker // ticker is created in Init and stopped in Shutdown. } @@ -22,7 +25,7 @@ type ticker struct { func (t *ticker) Init(ctx context.Context) error { //nolint:unparam defer t.Logger.Info("ticker initialized") - pinger, err := pal.Invoke[Pinger](ctx, t.Pal, "https://google.com") + pinger, err := t.CreatePinger(ctx, "https://google.com") if err != nil { return err } diff --git a/service_factory0.go b/service_factory0.go index 10e465b..bead2ef 100644 --- a/service_factory0.go +++ b/service_factory0.go @@ -25,3 +25,15 @@ func (c *ServiceFactory0[I, T]) Instance(ctx context.Context, _ ...any) (any, er return instance, nil } + +// Factory returns a function that creates a new instance of the service. +func (c *ServiceFactory0[I, T]) Factory() any { + return func(ctx context.Context) (I, error) { + instance, err := c.Instance(ctx) + if err != nil { + var i I + return i, err + } + return instance.(I), nil + } +} diff --git a/service_factory1.go b/service_factory1.go index 3c77cee..1bf72ef 100644 --- a/service_factory1.go +++ b/service_factory1.go @@ -35,3 +35,15 @@ func (c *ServiceFactory1[I, T, P1]) Instance(ctx context.Context, args ...any) ( return instance, nil } + +// Factory returns a function that creates a new instance of the service. +func (c *ServiceFactory1[I, T, P1]) Factory() any { + return func(ctx context.Context, p1 P1) (I, error) { + instance, err := c.Instance(ctx, p1) + if err != nil { + var i I + return i, err + } + return instance.(I), nil + } +} diff --git a/service_factory1_test.go b/service_factory1_test.go index c596c5d..224372b 100644 --- a/service_factory1_test.go +++ b/service_factory1_test.go @@ -16,11 +16,15 @@ type serviceWithFactoryServiceDependency struct { Dependency *factory1Service } +type serviceWithFactoryFunctionDependency struct { + CreateDependency func(ctx context.Context, name string) (*factory1Service, error) +} + // TestService_Instance tests the Instance method of the service struct -func TestServiceFactory1_Instance(t *testing.T) { +func TestServiceFactory1_Invocation(t *testing.T) { t.Parallel() - t.Run("when called with correct arguments, returns a new instance built with given arguments", func(t *testing.T) { + t.Run("when invoked with correct arguments, returns a new instance built with given arguments", func(t *testing.T) { t.Parallel() service := pal.ProvideFactory1[*factory1Service](func(_ context.Context, name string) (*factory1Service, error) { @@ -49,7 +53,7 @@ func TestServiceFactory1_Instance(t *testing.T) { assert.NotSame(t, instance1, instance2) }) - t.Run("when called with incorrect number of arguments, returns an error", func(t *testing.T) { + t.Run("when invoked with incorrect number of arguments, returns an error", func(t *testing.T) { t.Parallel() service := pal.ProvideFactory1[*factory1Service](func(_ context.Context, name string) (*factory1Service, error) { @@ -67,7 +71,7 @@ func TestServiceFactory1_Instance(t *testing.T) { assert.ErrorIs(t, err, pal.ErrServiceInvalidArgumentsCount) }) - t.Run("when called with incorrect argument type, returns an error", func(t *testing.T) { + t.Run("when invoked with incorrect argument type, returns an error", func(t *testing.T) { t.Parallel() service := pal.ProvideFactory1[*factory1Service](func(_ context.Context, name string) (*factory1Service, error) { @@ -102,4 +106,28 @@ func TestServiceFactory1_Instance(t *testing.T) { assert.ErrorIs(t, err, pal.ErrFactoryServiceDependency) }) + + t.Run("when invoked via injected factory function, returns a new instance built with given arguments", func(t *testing.T) { + t.Parallel() + + service := pal.ProvideFactory1[*factory1Service](func(_ context.Context, name string) (*factory1Service, error) { + return &factory1Service{Name: name}, nil + }) + p := newPal(service) + + ctx := pal.WithPal(t.Context(), p) + + err := p.Init(t.Context()) + assert.NoError(t, err) + + serviceWithFactoryFn := &serviceWithFactoryFunctionDependency{} + err = p.InjectInto(ctx, serviceWithFactoryFn) + + assert.NoError(t, err) + + dependency, err := serviceWithFactoryFn.CreateDependency(ctx, "test") + + assert.NoError(t, err) + assert.Equal(t, "test", dependency.Name) + }) } diff --git a/service_factory2.go b/service_factory2.go index 45a18ae..a563afd 100644 --- a/service_factory2.go +++ b/service_factory2.go @@ -40,3 +40,15 @@ func (c *ServiceFactory2[I, T, P1, P2]) Instance(ctx context.Context, args ...an return instance, nil } + +// Factory returns a function that creates a new instance of the service. +func (c *ServiceFactory2[I, T, P1, P2]) Factory() any { + return func(ctx context.Context, p1 P1, p2 P2) (I, error) { + instance, err := c.Instance(ctx, p1, p2) + if err != nil { + var i I + return i, err + } + return instance.(I), nil + } +} diff --git a/service_factory3.go b/service_factory3.go index b4b0b08..e1b1a25 100644 --- a/service_factory3.go +++ b/service_factory3.go @@ -45,3 +45,15 @@ func (c *ServiceFactory3[I, T, P1, P2, P3]) Instance(ctx context.Context, args . return instance, nil } + +// Factory returns a function that creates a new instance of the service. +func (c *ServiceFactory3[I, T, P1, P2, P3]) Factory() any { + return func(ctx context.Context, p1 P1, p2 P2, p3 P3) (I, error) { + instance, err := c.Instance(ctx, p1, p2, p3) + if err != nil { + var i I + return i, err + } + return instance.(I), nil + } +} diff --git a/service_factory4.go b/service_factory4.go index 7c4b5f4..406140c 100644 --- a/service_factory4.go +++ b/service_factory4.go @@ -50,3 +50,15 @@ func (c *ServiceFactory4[I, T, P1, P2, P3, P4]) Instance(ctx context.Context, ar return instance, nil } + +// Factory returns a function that creates a new instance of the service. +func (c *ServiceFactory4[I, T, P1, P2, P3, P4]) Factory() any { + return func(ctx context.Context, p1 P1, p2 P2, p3 P3, p4 P4) (I, error) { + instance, err := c.Instance(ctx, p1, p2, p3, p4) + if err != nil { + var i I + return i, err + } + return instance.(I), nil + } +} diff --git a/service_factory5.go b/service_factory5.go index 356f276..d39877e 100644 --- a/service_factory5.go +++ b/service_factory5.go @@ -55,3 +55,15 @@ func (c *ServiceFactory5[I, T, P1, P2, P3, P4, P5]) Instance(ctx context.Context return instance, nil } + +// Factory returns a function that creates a new instance of the service. +func (c *ServiceFactory5[I, T, P1, P2, P3, P4, P5]) Factory() any { + return func(ctx context.Context, p1 P1, p2 P2, p3 P3, p4 P4, p5 P5) (I, error) { + instance, err := c.Instance(ctx, p1, p2, p3, p4, p5) + if err != nil { + var i I + return i, err + } + return instance.(I), nil + } +}