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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,16 +100,18 @@ Pal provides several functions for registering services:
- `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.
- `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.

Pal also provides functions for retrieving services:

- `Invoke[T](ctx, invoker, args...)` - Retrieves or creates an instance of type `T` from the container, factory services may require argumens.
- `Invoke[T](ctx, invoker, args...)` - Retrieves or creates an instance of type `T` from the container, factory services may require arguments.
- `InvokeAs[T, C](ctx, invoker, args...)` - A wrapper around `Inoke`, castes invoked service to specified `C`, returns an error if casging fails.
- `InvokeByInterface[I](ctx, invoker, args...)` - Retrieves the only service that implements the given interface `I`.
Returns an error if there are zero or more than one service implementing the interface or if `I` is not an interface.
**Note:** do not overuse this function as it gets slower the more services you have.
- `Build[S](ctx, invoker)` - Creates an instance of S, resolves its dependencies, injects them into its fields.
- `InjectInto[S](ctx, invoker, *S)` - Resolves S's dependencies and injects them into its fields.
- There are `Named` version of `Invoke` functions which allow to retries service by their names.

All these functions accept nil as invoker, in this case, a Pal instance will be extracted from the context.
Pal automatilly adds itself into contexts paseed to `Init`, `Shutdown` and `Run` under `pal.CtxValue` key.
Expand Down
121 changes: 101 additions & 20 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,57 +16,118 @@ import (
func Provide[T any](value T) *ServiceConst[T] {
validatePointerToStruct(value)

return &ServiceConst[T]{instance: value}
return ProvideNamed(typetostring.GetType[T](), value)
}

// ProvideNamed registers a const as a service with a given name. Acts like Provide but allows to specify a name.
func ProvideNamed[T any](name string, value T) *ServiceConst[T] {
validatePointerToStruct(value)

return &ServiceConst[T]{instance: value, ServiceTyped: ServiceTyped[T]{name: name}}
}

// ProvideFn registers a singleton built with a given function.
func ProvideFn[T any](fn func(ctx context.Context) (T, error)) *ServiceFnSingleton[T] {
return &ServiceFnSingleton[T]{
fn: fn,
}
return ProvideNamedFn(typetostring.GetType[T](), fn)
}

// ProvideFactory0 registers a factory service that is build with a given function with no arguments.
func ProvideFactory0[T any](fn func(ctx context.Context) (T, error)) *ServiceFactory0[T] {
return &ServiceFactory0[T]{
fn: fn,
// ProvideFn registers a singleton built with a given function.
func ProvideNamedFn[T any](name string, fn func(ctx context.Context) (T, error)) *ServiceFnSingleton[T] {
return &ServiceFnSingleton[T]{
fn: fn,
ServiceTyped: ServiceTyped[T]{name: name},
}
}

// ProvideRunner turns the given function into a runner. It will run in the background, and the passed context will
// ProvideRunner turns the given function into an anounumous runner. It will run in the background, and the passed context will
// be canceled on app shutdown.
func ProvideRunner(fn func(ctx context.Context) error) *ServiceRunner {
return &ServiceRunner{fn: fn}
return &ServiceRunner{
fn: fn,
}
}

// ProvideList registers a list of given services.
func ProvideList(services ...ServiceDef) *ServiceList {
return &ServiceList{Services: services}
}

// ProvideFactory0 registers a factory service that is build with a given function with no arguments.
func ProvideFactory0[T any](fn func(ctx context.Context) (T, error)) *ServiceFactory0[T] {
return ProvideNamedFactory0(typetostring.GetType[T](), fn)
}

// ProvideFactory1 registers a factory service that is built in runtime with a given function that takes one argument.
func ProvideFactory1[T any, P1 any](fn func(ctx context.Context, p1 P1) (T, error)) *ServiceFactory1[T, P1] {
return &ServiceFactory1[T, P1]{fn: fn}
return ProvideNamedFactory1(typetostring.GetType[T](), fn)
}

// ProvideFactory2 registers a factory service that is built in runtime with a given function that takes two arguments.
func ProvideFactory2[T any, P1 any, P2 any](fn func(ctx context.Context, p1 P1, p2 P2) (T, error)) *ServiceFactory2[T, P1, P2] {
return &ServiceFactory2[T, P1, P2]{fn: fn}
return ProvideNamedFactory2(typetostring.GetType[T](), fn)
}

// ProvideFactory3 registers a factory service that is built in runtime with a given function that takes three arguments.
func ProvideFactory3[T any, P1 any, P2 any, P3 any](fn func(ctx context.Context, p1 P1, p2 P2, p3 P3) (T, error)) *ServiceFactory3[T, P1, P2, P3] {
return &ServiceFactory3[T, P1, P2, P3]{fn: fn}
return ProvideNamedFactory3(typetostring.GetType[T](), fn)
}

// ProvideFactory4 registers a factory service that is built in runtime with a given function that takes four arguments.
func ProvideFactory4[T any, P1 any, P2 any, P3 any, P4 any](fn func(ctx context.Context, p1 P1, p2 P2, p3 P3, p4 P4) (T, error)) *ServiceFactory4[T, P1, P2, P3, P4] {
return &ServiceFactory4[T, P1, P2, P3, P4]{fn: fn}
return ProvideNamedFactory4(typetostring.GetType[T](), fn)
}

// ProvideFactory5 registers a factory service that is built in runtime with a given function that takes five arguments.
func ProvideFactory5[T any, P1 any, P2 any, P3 any, P4 any, P5 any](fn func(ctx context.Context, p1 P1, p2 P2, p3 P3, p4 P4, p5 P5) (T, error)) *ServiceFactory5[T, P1, P2, P3, P4, P5] {
return &ServiceFactory5[T, P1, P2, P3, P4, P5]{fn: fn}
return ProvideNamedFactory5(typetostring.GetType[T](), fn)
}

// ProvideNamedFactory0 is like ProvideFactory0 but allows to specify a name.
func ProvideNamedFactory0[T any](name string, fn func(ctx context.Context) (T, error)) *ServiceFactory0[T] {
return &ServiceFactory0[T]{
fn: fn,
ServiceTyped: ServiceTyped[T]{name: name},
}
}

// ProvideNamedFactory1 is like ProvideFactory1 but allows to specify a name.
func ProvideNamedFactory1[T any, P1 any](name string, fn func(ctx context.Context, p1 P1) (T, error)) *ServiceFactory1[T, P1] {
return &ServiceFactory1[T, P1]{
fn: fn,
ServiceTyped: ServiceTyped[T]{name: name},
}
}

// ProvideNamedFactory2 is like ProvideFactory2 but allows to specify a name.
func ProvideNamedFactory2[T any, P1 any, P2 any](name string, fn func(ctx context.Context, p1 P1, p2 P2) (T, error)) *ServiceFactory2[T, P1, P2] {
return &ServiceFactory2[T, P1, P2]{
fn: fn,
ServiceTyped: ServiceTyped[T]{name: name},
}
}

// ProvideNamedFactory3 is like ProvideFactory3 but allows to specify a name.
func ProvideNamedFactory3[T any, P1 any, P2 any, P3 any](name string, fn func(ctx context.Context, p1 P1, p2 P2, p3 P3) (T, error)) *ServiceFactory3[T, P1, P2, P3] {
return &ServiceFactory3[T, P1, P2, P3]{
fn: fn,
ServiceTyped: ServiceTyped[T]{name: name},
}
}

// ProvideNamedFactory4 is like ProvideFactory4 but allows to specify a name.
func ProvideNamedFactory4[T any, P1 any, P2 any, P3 any, P4 any](name string, fn func(ctx context.Context, p1 P1, p2 P2, p3 P3, p4 P4) (T, error)) *ServiceFactory4[T, P1, P2, P3, P4] {
return &ServiceFactory4[T, P1, P2, P3, P4]{
fn: fn,
ServiceTyped: ServiceTyped[T]{name: name},
}
}

// ProvideNamedFactory5 is like ProvideFactory5 but allows to specify a name.
func ProvideNamedFactory5[T any, P1 any, P2 any, P3 any, P4 any, P5 any](name string, fn func(ctx context.Context, p1 P1, p2 P2, p3 P3, p4 P4, p5 P5) (T, error)) *ServiceFactory5[T, P1, P2, P3, P4, P5] {
return &ServiceFactory5[T, P1, P2, P3, P4, P5]{
fn: fn,
ServiceTyped: ServiceTyped[T]{name: name},
}
}

// ProvidePal registers all services for the given pal instance
Expand All @@ -86,6 +147,16 @@ func ProvidePal(pal *Pal) *ServiceList {
// if the context does not contain a Pal instance, an error will be returned.
func Invoke[T any](ctx context.Context, invoker Invoker, args ...any) (T, error) {
name := typetostring.GetType[T]()
return InvokeNamed[T](ctx, invoker, name, args...)
}

// MustInvoke is like Invoke but panics if an error occurs.
func MustInvoke[T any](ctx context.Context, invoker Invoker, args ...any) T {
return must(Invoke[T](ctx, invoker, args...))
}

// InvokeNamed is like Invoke but allows to specify a name.
func InvokeNamed[T any](ctx context.Context, invoker Invoker, name string, args ...any) (T, error) {
if invoker == nil {
var err error
invoker, err = FromContext(ctx)
Expand All @@ -107,21 +178,26 @@ func Invoke[T any](ctx context.Context, invoker Invoker, args ...any) (T, error)
return casted, nil
}

// MustInvoke is like Invoke but panics if an error occurs.
func MustInvoke[T any](ctx context.Context, invoker Invoker, args ...any) T {
return must(Invoke[T](ctx, invoker, args...))
// MustInvokeNamed is like InvokeNamed but panics if an error occurs.
func MustInvokeNamed[T any](ctx context.Context, invoker Invoker, name string, args ...any) T {
return must(InvokeNamed[T](ctx, invoker, name, args...))
}

// InvokeAs invokes a service and casts it to the expected type. It returns an error if the cast fails.
// May be useful when invoking a service with an interface type and you want to cast it to a concrete type.
// Invoker may be nil, in this case an instance of Pal will be extracted from the context,
// if the context does not contain a Pal instance, an error will be returned.
func InvokeAs[T any, C any](ctx context.Context, invoker Invoker, args ...any) (*C, error) {
service, err := Invoke[T](ctx, invoker, args...)
name := typetostring.GetType[T]()
return InvokeNamedAs[T, C](ctx, invoker, name, args...)
}

// InvokeNamedAs is like InvokeAs but allows to specify a name.
func InvokeNamedAs[T any, C any](ctx context.Context, invoker Invoker, name string, args ...any) (*C, error) {
service, err := InvokeNamed[T](ctx, invoker, name, args...)
if err != nil {
return nil, err
}

casted, ok := any(service).(*C)
if !ok {
var c *C
Expand All @@ -131,6 +207,11 @@ func InvokeAs[T any, C any](ctx context.Context, invoker Invoker, args ...any) (
return casted, nil
}

// MustInvokeNamedAs is like InvokeNamedAs but panics if an error occurs.
func MustInvokeNamedAs[T any, C any](ctx context.Context, invoker Invoker, name string, args ...any) *C {
return must(InvokeNamedAs[T, C](ctx, invoker, name, args...))
}

// MustInvokeAs is like InvokeAs but panics if an error occurs.
func MustInvokeAs[T any, C any](ctx context.Context, invoker Invoker, args ...any) *C {
return must(InvokeAs[T, C](ctx, invoker, args...))
Expand Down
41 changes: 30 additions & 11 deletions api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,23 +83,17 @@ func TestProvideFactory0(t *testing.T) {
})
}

// TestProvideConst tests the ProvideConst function
func TestProvideConst(t *testing.T) {
// TestProvideNamed tests the ProvideNamed function
func TestProvideNamed(t *testing.T) {
t.Parallel()

t.Run("creates a const service", func(t *testing.T) {
t.Run("creates a const service with a given name", func(t *testing.T) {
t.Parallel()

s := NewMockTestServiceStruct(t)
service := pal.Provide(s)
service := pal.ProvideNamed("test", NewMockTestServiceStruct(t))

assert.NotNil(t, service)
assert.Equal(t, "*github.com/zhulik/pal_test.TestServiceStruct", service.Name())

// Verify that the instance is the same
instance, err := service.Instance(t.Context())
assert.NoError(t, err)
assert.Same(t, s, instance)
assert.Equal(t, "test", service.Name())
})
}

Expand Down Expand Up @@ -139,6 +133,31 @@ func TestInvoke(t *testing.T) {
})
}

func TestInvokeNamed(t *testing.T) {
t.Parallel()

t.Run("invokes a service successfully with a given name", func(t *testing.T) {
t.Parallel()

p := newPal(pal.ProvideNamed("test", NewMockTestServiceStruct(t)))

instance, err := pal.InvokeNamed[TestServiceInterface](t.Context(), p, "test")

assert.NoError(t, err)
assert.NotNil(t, instance)
})

t.Run("returns error when service not found", func(t *testing.T) {
t.Parallel()

p := newPal()

_, err := pal.InvokeNamed[TestServiceInterface](t.Context(), p, "test")

assert.ErrorIs(t, err, pal.ErrServiceNotFound)
})
}

func TestInvokeAs(t *testing.T) {
t.Parallel()

Expand Down
7 changes: 3 additions & 4 deletions service_typed.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package pal

import (
"context"

typetostring "github.com/samber/go-type-to-string"
)

type ServiceTyped[T any] struct {
P *Pal
P *Pal
name string
}

func (c *ServiceTyped[T]) Dependencies() []ServiceDef {
Expand Down Expand Up @@ -46,7 +45,7 @@ func (c *ServiceTyped[T]) Make() any {

// Name returns the name of the service, which is the type name of T.
func (c *ServiceTyped[T]) Name() string {
return typetostring.GetType[T]()
return c.name
}

func (c *ServiceTyped[T]) Arguments() int {
Expand Down