Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
60 changes: 51 additions & 9 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,43 @@ 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
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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/factories/pinger.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 5 additions & 2 deletions examples/factories/ticker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@ 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.
}

// Init initializes the ticker service.
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
}
Expand Down
12 changes: 12 additions & 0 deletions service_factory0.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
12 changes: 12 additions & 0 deletions service_factory1.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
36 changes: 32 additions & 4 deletions service_factory1_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
})
}
12 changes: 12 additions & 0 deletions service_factory2.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
12 changes: 12 additions & 0 deletions service_factory3.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
12 changes: 12 additions & 0 deletions service_factory4.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
12 changes: 12 additions & 0 deletions service_factory5.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}