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
58 changes: 36 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ a few rules described below.
rather than conflict.

- Testability:
- Pal provides tools to simplify testing, such as the ability to register mock services using ProvideConst.
- Pal provides tools to simplify testing, such as the ability to register mock services using Provide.
- The container design allows for easy swapping of real implementations with test doubles.
- Services can be tested in isolation by creating a test container with only the necessary dependencies.

Expand Down Expand Up @@ -76,7 +76,7 @@ a few rules described below.
- [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection) — specific implementation of the Inversion
of Control pattern where objects receive their dependencies through constructor arguments, method calls, or property
setters rather than creating them themselves.
- Container — a registry of services within the app. It is responsible for managing service lifecycle.
- Container — a registry of services within the app (used internally by Pal; also available for advanced use).
- Service — is an interface that defines a set of methods or operations. Concrete implementations of these services are
responsible for providing specific functionalities within the application. Services can perform tasks on their own or
can be used by other services. Pal recognizes a few types of services:
Expand All @@ -102,13 +102,14 @@ a few rules described below.

## API Functions

Pal provides several functions for registering services:
Pal provides several functions for registering services. They return interfaces (`Hookable` or `ServiceDef`) so the default path stays implementation-agnostic:

- `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}[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.
- `Provide[T any](value T) Hookable[T]` - Registers an instance of a service; chain `ToInit` / `ToShutdown` / `ToHealthCheck` as needed.
- `ProvideFn[I any, T any](fn func(ctx context.Context) (T, error)) Hookable[T]` - Registers a singleton built with the provided function.
- `ProvideFactory{0-5}[...](...) ServiceDef` - Registers a factory service created with the provided function (0–5 args).
- `ProvideRunner(fn) ServiceDef` - Registers an anonymous background runner.
- `ProvideList(...ServiceDef) 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 of `Invoke` functions if you want to give your services explicit names.

Pal also provides functions for retrieving services:

Expand All @@ -122,8 +123,7 @@ Pal also provides functions for retrieving services:
- There are `Named` versions of `Invoke` functions that allow retrieving services by their explicit names.

All these functions accept nil as invoker, in this case, a Pal instance will be extracted from the context.
Pal automatically adds itself into contexts passed to `Init`, `Shutdown`, and `Run` under the `pal.CtxValue` key.
You can extract it manually with `pal.FromContext`
Pal automatically stores itself in contexts passed to `Init`, `Shutdown`, and `Run`. Use `pal.FromContext` / `pal.WithPal` to read or write it.

## Service Types

Expand All @@ -146,7 +146,7 @@ Singleton services are created once during application initialization and reused
pal.Provide[MyService](&MyServiceImpl{})

// Register a singleton service using a factory function
pal.ProvideFn[MyService](func(ctx context.Context) (MyServiceImpl, error) {
pal.ProvideFn[MyService](func(ctx context.Context) (*MyServiceImpl, error) {
return &MyServiceImpl{}, nil
})
```
Expand Down Expand Up @@ -214,9 +214,9 @@ Const services wrap existing instances. They are useful for:
**Registration:**

```go
// Register a const service
// Register an existing instance
existingInstance := &MyServiceImpl{}
pal.ProvideConst[MyService](existingInstance)
pal.Provide[MyService](existingInstance)
```

### Runner Services
Expand Down Expand Up @@ -272,27 +272,30 @@ lifecycle management code closer to the resources it manages.
Hooks can be used with any service type and provide a flexible way to add lifecycle behavior:

```go
// With const services
pal.ProvideConst[MyService](existingInstance).
ToInit(func(ctx context.Context, service MyService, pal *pal.Pal) error {
// With Provide (const / instance services)
pal.Provide[MyService](existingInstance).
ToInit(func(ctx context.Context, service MyService, invoker pal.Invoker) error {
// Custom initialization logic
return service.Connect()
}).
ToShutdown(func(ctx context.Context, service MyService, pal *pal.Pal) error {
ToShutdown(func(ctx context.Context, service MyService, invoker pal.Invoker) error {
// Custom shutdown logic
return service.Disconnect()
}).
ToHealthCheck(func(ctx context.Context, service MyService, pal *pal.Pal) error {
ToHealthCheck(func(ctx context.Context, service MyService, invoker pal.Invoker) error {
// Custom health check logic
return service.Ping()
})

// With function-based services
// With ProvideFn
pal.ProvideFn[MyService](func(ctx context.Context) (*MyServiceImpl, error) {
return &MyServiceImpl{}, nil
}).
ToInit(func(ctx context.Context, service MyService, pal *pal.Pal) error {
ToInit(func(ctx context.Context, service *MyServiceImpl, invoker pal.Invoker) error {
return service.Initialize()
}).
ToShutdown(func(ctx context.Context, service *MyServiceImpl, invoker pal.Invoker) error {
return service.Disconnect()
})
```

Expand All @@ -303,6 +306,17 @@ Examples can be found here:
- [example_container_test.go](./example_container_test.go)
- [example_pal_test.go](./example_pal_test.go)

## Advanced / hackable API

The default path is `Provide*` → `New` → `Run` / `Invoke*`. For power users, these remain exported and may change more freely than the primary API:

- Concrete wrappers: `ServiceConst`, `ServiceFnSingleton`, `ServiceFactory0`–`5`, `ServiceRunner`, `ServiceList`, `ServiceTyped`
- `Container`, `NewContainer`, `Pal.Container()`, `Pal.Services()`, `RunServices`
- Dependency graph: `Pal.TreeJSON()`, `GraphToJSON`, [`pkg/dag`](./pkg/dag)
- Inspect helpers that take a raw DAG (`inspect.DAGToJSON`)

Prefer the interface returns from `Provide*` unless you need to type-assert or construct these types deliberately.

## Example apps

- [Web Server](./examples/web) - Demonstrates how to build a web server using Pal.
Expand Down Expand Up @@ -398,7 +412,7 @@ To get the most out of Pal, follow these best practices:
- Design services as small, focused components with a single responsibility.
- Use interfaces to define service contracts, especially for services that might have multiple implementations.
- Implement the optional lifecycle interfaces ([Initer](./lifecycle_interfaces.go#L39), [Shutdowner](./lifecycle_interfaces.go#L20), [HealthChecker](./lifecycle_interfaces.go#L5), or the [Pal-prefixed](./lifecycle_interfaces.go#L66) alternatives when names clash) when appropriate.
- Use `ProvideConst` and `ProvideFn*` functions with `ToShutdown` hook to register services without dedicated
- Use `Provide` and `ProvideFn` with `ToShutdown` hooks to register services without dedicated
interfaces and struct wrappers.

2. **Dependency Management**:
Expand All @@ -413,7 +427,7 @@ To get the most out of Pal, follow these best practices:

4. **Testing**:
- Create mock implementations of your service interfaces for testing.
- Use `ProvideConst` to register mock services in your tests.
- Use `Provide` to register mock services in your tests.
- Test each service in isolation before testing them together.

5. **Application Structure**:
Expand Down
42 changes: 21 additions & 21 deletions api.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,27 @@ import (
// - An interface, in this case passed value must implement it. Used when T may have multiple implementations like mocks for tests.
// - A pointer to an instance of `T`. For instance,`Provide[*Foo](&Foo{})`. Used when mocking is not required.
// If the passed value implements [Initer] or [PalIniter], the matching init method is called after dependency injection,
// unless a ToInit hook is set on the returned [ServiceConst] (see [ServiceConst.ToInit]).
func Provide[T any](value T) *ServiceConst[T] {
// unless a ToInit hook is set on the returned [Hookable] (see [Hookable.ToInit]).
func Provide[T any](value T) Hookable[T] {
validateNonNilPointer(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] {
func ProvideNamed[T any](name string, value T) Hookable[T] {
validateNonNilPointer(value)

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

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

// ProvideFn registers a singleton built with a given function.
func ProvideNamedFn[I any, T any](name string, fn func(ctx context.Context) (T, error)) *ServiceFnSingleton[I, T] {
// ProvideNamedFn registers a singleton built with a given function under a custom name.
func ProvideNamedFn[I any, T any](name string, fn func(ctx context.Context) (T, error)) Hookable[T] {
validateFactoryFunction[I, T](fn)

return &ServiceFnSingleton[I, T]{
Expand All @@ -44,50 +44,50 @@ func ProvideNamedFn[I any, T any](name string, fn func(ctx context.Context) (T,

// 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 {
func ProvideRunner(fn func(ctx context.Context) error) ServiceDef {
return &ServiceRunner{
fn: fn,
}
}

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

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

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

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

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

// ProvideFactory4 registers a factory service that is built in runtime with a given function that takes four arguments.
func ProvideFactory4[I any, 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[I, T, P1, P2, P3, P4] {
func ProvideFactory4[I any, 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)) ServiceDef {
return ProvideNamedFactory4[I](typetostring.GetType[I](), fn)
}

// ProvideFactory5 registers a factory service that is built in runtime with a given function that takes five arguments.
func ProvideFactory5[I any, 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[I, T, P1, P2, P3, P4, P5] {
func ProvideFactory5[I any, 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)) ServiceDef {
return ProvideNamedFactory5[I](typetostring.GetType[I](), fn)
}

// ProvideNamedFactory0 is like ProvideFactory0 but allows to specify a name.
func ProvideNamedFactory0[I any, T any](name string, fn func(ctx context.Context) (T, error)) *ServiceFactory0[I, T] {
func ProvideNamedFactory0[I any, T any](name string, fn func(ctx context.Context) (T, error)) ServiceDef {
validateFactoryFunction[I, T](fn)
return &ServiceFactory0[I, T]{
fn: fn,
Expand All @@ -96,7 +96,7 @@ func ProvideNamedFactory0[I any, T any](name string, fn func(ctx context.Context
}

// ProvideNamedFactory1 is like ProvideFactory1 but allows to specify a name.
func ProvideNamedFactory1[I any, T any, P1 any](name string, fn func(ctx context.Context, p1 P1) (T, error)) *ServiceFactory1[I, T, P1] {
func ProvideNamedFactory1[I any, T any, P1 any](name string, fn func(ctx context.Context, p1 P1) (T, error)) ServiceDef {
validateFactoryFunction[I, T](fn)

return &ServiceFactory1[I, T, P1]{
Expand All @@ -106,7 +106,7 @@ func ProvideNamedFactory1[I any, T any, P1 any](name string, fn func(ctx context
}

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

return &ServiceFactory2[I, T, P1, P2]{
Expand All @@ -116,7 +116,7 @@ func ProvideNamedFactory2[I any, T any, P1 any, P2 any](name string, fn func(ctx
}

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

return &ServiceFactory3[I, T, P1, P2, P3]{
Expand All @@ -126,7 +126,7 @@ func ProvideNamedFactory3[I any, T any, P1 any, P2 any, P3 any](name string, fn
}

// ProvideNamedFactory4 is like ProvideFactory4 but allows to specify a name.
func ProvideNamedFactory4[I any, 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[I, T, P1, P2, P3, P4] {
func ProvideNamedFactory4[I any, 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)) ServiceDef {
validateFactoryFunction[I, T](fn)

return &ServiceFactory4[I, T, P1, P2, P3, P4]{
Expand All @@ -136,7 +136,7 @@ func ProvideNamedFactory4[I any, T any, P1 any, P2 any, P3 any, P4 any](name str
}

// ProvideNamedFactory5 is like ProvideFactory5 but allows to specify a name.
func ProvideNamedFactory5[I any, 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[I, T, P1, P2, P3, P4, P5] {
func ProvideNamedFactory5[I any, 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)) ServiceDef {
validateFactoryFunction[I, T](fn)

return &ServiceFactory5[I, T, P1, P2, P3, P4, P5]{
Expand All @@ -146,7 +146,7 @@ func ProvideNamedFactory5[I any, T any, P1 any, P2 any, P3 any, P4 any, P5 any](
}

// ProvidePal registers all services for the given pal instance
func ProvidePal(pal *Pal) *ServiceList {
func ProvidePal(pal *Pal) ServiceDef {
services := make([]ServiceDef, 0, len(pal.Services()))
for _, v := range pal.Services() {
if v.Name() != "*github.com/zhulik/pal.Pal" {
Expand Down
2 changes: 1 addition & 1 deletion api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ func TestProvide(t *testing.T) {
t.Parallel()

service := pal.Provide(NewMockRunnerServiceStruct(t)).
ToInit(func(ctx context.Context, service *RunnerServiceStruct, _ *pal.Pal) error {
ToInit(func(ctx context.Context, service *RunnerServiceStruct, _ pal.Invoker) error {
service.MockRunner.EXPECT().Run(ctx).Return(nil)

return nil
Expand Down
13 changes: 9 additions & 4 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ type factoryService interface {
MustFactory() any
}

// Container is responsible for storing services, instances and the dependency graph
// Container is responsible for storing services, instances and the dependency graph.
//
// Advanced: prefer [Pal] for normal apps; Container remains exported for power users
// (custom lifecycles, introspection). May change more freely than Provide/Pal.
type Container struct {
pal *Pal

Expand All @@ -36,7 +39,9 @@ type Container struct {
logger *slog.Logger
}

// NewContainer creates a new Container instance
// NewContainer creates a new Container instance.
//
// Advanced: normal apps use [New]; this constructor is for power users.
func NewContainer(pal *Pal, services ...ServiceDef) *Container {
services = flattenServices(services)

Expand Down Expand Up @@ -147,7 +152,7 @@ func (c *Container) InjectInto(ctx context.Context, target any) error {
for i := 0; i < t.NumField(); i++ {
field := v.Field(i)

tags, err := ParseTag(t.Field(i).Tag.Get("pal"))
tags, err := parseTag(t.Field(i).Tag.Get("pal"))
if err != nil {
return err
}
Expand Down Expand Up @@ -329,7 +334,7 @@ func (c *Container) addDependencyVertex(service ServiceDef, parent ServiceDef) e
continue
}

tags, err := ParseTag(field.Tag.Get("pal"))
tags, err := parseTag(field.Tag.Get("pal"))
if err != nil {
return err
}
Expand Down
3 changes: 2 additions & 1 deletion example_container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ func (s *SimpleServiceImpl) GetMessage() string {
return "Hello from SimpleService"
}

// This example demonstrates how to create a Pal instance with services and use it.
// Example_container demonstrates creating a Pal instance with services and using it.
// (Named for historical reasons; apps go through Pal, not a public Container constructor.)
func Example_container() {
// Create a Pal instance with the service
p := pal.New(
Expand Down
Loading
Loading