@ -1,20 +1,20 @@
// The [sl] package has two main concepts, the [ServiceLocator] itself is the
// main object that one should pass around through the application. A
// [ServiceLocator] has a list of slots that can be filled with
// [ InjectLazy] and [InjectValu e] and retrieved with [Use]. Slots should
// [ ProvideFunc] and [Provid e] and retrieved with [Use]. Slots should
// be unique by type and they can only be created with the [NewSlot] function.
//
// The usual way to use this module is to make slots for Go interfaces and
// then pass implementations using the [ InjectValu e] and
// [ InjectLazy ] functions.
// then pass implementations using the [ Provid e] and
// [ ProvideFunc ] functions.
//
// Services can be of various types:
// - a service with no dependencies can be directly injected inside a
// ServiceLocator using [ InjectValu e].
// ServiceLocator using [ Provid e].
// - a service with dependencies on other service should use
// [ InjectLazy]. This lets the service to initializ e itself when needed
// [ ProvideFunc]. This lets the service configur e itself when needed
// and makes the developer not think about correctly ordering the
// initialization of its dependencies
// recursive configuration of its dependencies.
// - a service can also be private, in this case the slot for a service
// should be a private field in the service package. This kind of
// services should also provide a way to inject them into a
@ -40,8 +40,16 @@ import (
var Logger * log . Logger = log . New ( os . Stderr , "[service locator] " , log . Lmsgprefix )
// slot is just a "typed" unique "symbol".
//
// This must be defined like so and not for example "struct{ typeName string }"
// because we might want to have more slots for the same type.
type slot [ T any ] * struct { }
type Hook [ T any ] func ( * ServiceLocator , T ) error
// hook is just a typed unique symbol
type hook [ T any ] * struct { }
// NewSlot is the only way to create instances of the slot type. Each instance
// is unique.
//
@ -51,85 +59,110 @@ func NewSlot[T any]() slot[T] {
return slot [ T ] ( new ( struct { } ) )
}
// slotEntry represents a service that can lazily initialized
// (using "createFunc"). Once initialized the instance is kept in the "value"
// NewHook is the only way to create instances of the hook type. Each instance
// is unique.
//
// This lets you have a service dispatch an hook
func NewHook [ T any ] ( ) hook [ T ] {
return hook [ T ] ( new ( struct { } ) )
}
// slotEntry represents a service that can lazily configured
// (using "configureFunc"). Once configured the instance is kept in the "value"
// field and "created" will always be "true". The field "typeName" just for
// debugging purposes.
type slotEntry struct {
// typeName is just used for debugging purposes
typeName string
createFunc func ( * ServiceLocator ) ( any , error )
created bool
// configureFunc is used by lazily provided slot values to tell how to
// configure them self when required
configureFunc func ( * ServiceLocator ) ( any , error )
// configured tells if this slot is already configured
configured bool
// value for this slot
value any
}
func ( s * slotEntry ) checkInitialized ( l * ServiceLocator ) error {
if ! s . created {
v , err := s . createFunc ( l )
// checkConfigured tries to call configure on this slot entry if not already configured
func ( s * slotEntry ) checkConfigured ( l * ServiceLocator ) error {
if ! s . configured {
v , err := s . configureFunc ( l )
if err != nil {
return err
}
Logger . Printf ( ` [slot: %s] initialized valu e of type %T` , s . typeName , v )
Logger . Printf ( ` [slot: %s] configured servic e of type %T` , s . typeName , v )
s . c reat ed = true
s . c onfigu red = true
s . value = v
}
return nil
}
type hookEntry struct {
// typeName is just used for debugging purposes
typeName string
// listeners is a list of functions to call when this hook is called
listeners [ ] func ( * ServiceLocator , any ) error
}
// ServiceLocator is the main context passed around to retrive service
// instances, the interface uses generics so to inject and retrive service
// instances you should use the functions [InjectValue], [InjectLazy] and
// [Use].
// instances you should use the functions [Provide], [ProvideFunc] and [Use].
// This is essentially a dictionary indexed by slots that are them self just
// typed unique symbols
type ServiceLocator struct {
providers map [ any ] * slotEntry
hooks map [ any ] * hookEntry
}
// New creates a new [ServiceLocator] context to pass around in the application.
func New ( ) * ServiceLocator {
return & ServiceLocator {
providers : map [ any ] * slotEntry { } ,
hooks : map [ any ] * hookEntry { } ,
}
}
// InjectValu e will inject a concrete instance inside the ServiceLocator "l"
// for the given "slotKey". This should be used for injecting "static"
// services, for instances whose construction depend on other services you
// should use the [InjectLazy ] function.
// Provid e will inject a concrete instance inside the ServiceLocator "l" for
// the given "slotKey". This should be used for injecting "static" services, for
// instances whose construction depend on other services you should use the
// [ProvideFunc ] function.
//
// This is generic over "T" to check that instances for the given slot type
// check as "T" can also be an interface.
func InjectValue [ T any ] ( l * ServiceLocator , slotKey slot [ T ] , value T ) T {
Logger . Printf ( ` [slot: %s] injected value of type %T ` , getTypeName [ T ] ( ) , value )
func Provide [ T any ] ( l * ServiceLocator , slotKey slot [ T ] , value T ) T {
typeName := getTypeName [ T ] ( )
Logger . Printf ( ` [slot: %s] provided value of type %T ` , typeName , value )
l . providers [ slotKey ] = & slotEntry {
getTypeName [ T ] ( ) ,
nil ,
true ,
value ,
typeName : typeName ,
configured : true ,
value : value ,
}
return value
}
// InjectLazy will inject an instance inside the given ServiceLocator
// ProvideFunc will inject an instance inside the given ServiceLocator
// and "slotKey" that is created only when requested with a call to the
// [Use] function.
//
// This is generic over "T" to check that instances for the given slot type
// check as "T" can also be an interface.
func InjectLazy [ T any ] ( l * ServiceLocator , slotKey slot [ T ] , createFunc func ( * ServiceLocator ) ( T , error ) ) {
Logger . Printf ( ` [slot: %s] injected lazy ` , getTypeName [ T ] ( ) )
func ProvideFunc [ T any ] ( l * ServiceLocator , slotKey slot [ T ] , createFunc func ( * ServiceLocator ) ( T , error ) ) {
typeName := getTypeName [ T ] ( )
Logger . Printf ( ` [slot: %s] inject lazy provider ` , typeName )
l . providers [ slotKey ] = & slotEntry {
createFunc : func ( l * ServiceLocator ) ( any , error ) {
return createFunc ( l )
} ,
created : false ,
value : nil ,
typeName : getTypeName [ T ] ( ) ,
typeName : typeName ,
configureFunc : func ( l * ServiceLocator ) ( any , error ) { return createFunc ( l ) } ,
configured : false ,
}
}
@ -137,7 +170,7 @@ func InjectLazy[T any](l *ServiceLocator, slotKey slot[T], createFunc func(*Serv
// the provided ServiceLocator instance.
//
// If the ServiceLocator does not have a value for the slot key, or if the
// value wasn't correctly initializ ed (in the case of a lazy slot), an error
// value wasn't correctly configur ed (in the case of a lazy slot), an error
// is returned.
func Use [ T any ] ( l * ServiceLocator , slotKey slot [ T ] ) ( T , error ) {
var zero T
@ -147,7 +180,7 @@ func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
return zero , fmt . Errorf ( ` no injected value for type %s ` , getTypeName [ T ] ( ) )
}
err := slot . check Initializ ed( l )
err := slot . check Configur ed( l )
if err != nil {
return zero , err
}
@ -158,24 +191,84 @@ func Use[T any](l *ServiceLocator, slotKey slot[T]) (T, error) {
return v , nil
}
func Invoke [ T any ] ( l * ServiceLocator , slotKey slot [ T ] ) error {
func MustUse [ T any ] ( l * ServiceLocator , slotKey slot [ T ] ) T {
v , err := Use ( l , slotKey )
if err != nil {
log . Fatal ( err )
}
return v
}
func Invoke [ T any ] ( l * ServiceLocator , slotKey slot [ T ] ) error {
slot , ok := l . providers [ slotKey ]
if ! ok {
return fmt . Errorf ( ` no injected value for type %s ` , getTypeName [ T ] ( ) )
}
err := slot . check Initializ ed( l )
err := slot . check Configur ed( l )
if err != nil {
return err
}
v := slot . value . ( T )
Logger . Printf ( ` [slot: %s] using slot with value of type %T ` , getTypeName [ T ] ( ) , v )
Logger . Printf ( ` [slot: %s] invoked slot with value of type %T ` , getTypeName [ T ] ( ) , v )
return nil
}
func MustInvoke [ T any ] ( l * ServiceLocator , slotKey slot [ T ] ) {
if err := Invoke ( l , slotKey ) ; err != nil {
log . Fatal ( err )
}
}
func ProvideHook [ T any ] ( l * ServiceLocator , hookKey hook [ T ] , listeners ... Hook [ T ] ) {
typeName := getTypeName [ T ] ( )
Logger . Printf ( ` [hook: %s] injecting hooks ` , typeName )
// cast type safe listeners to internal untyped version to put inside the hook map
anyListeners := make ( [ ] func ( * ServiceLocator , any ) error , len ( listeners ) )
for i , l := range listeners {
ll := l
anyListeners [ i ] = func ( l * ServiceLocator , a any ) error {
t , ok := a . ( T )
if ! ok {
panic ( ` illegal state ` )
}
return ll ( l , t )
}
}
l . hooks [ hookKey ] = & hookEntry {
typeName : typeName ,
listeners : anyListeners ,
}
}
func UseHook [ T any ] ( l * ServiceLocator , hookKey hook [ T ] , value T ) error {
hookEntry , ok := l . hooks [ hookKey ]
if ! ok {
return fmt . Errorf ( ` no injected hooks for hook of type %s ` , hookEntry . typeName )
}
Logger . Printf ( ` [hook: %s] calling hook with value of type %T ` , hookEntry . typeName , value )
for _ , hookFunc := range hookEntry . listeners {
if err := hookFunc ( l , value ) ; err != nil {
return err
}
}
return nil
}
func MustUseHook [ T any ] ( l * ServiceLocator , hookKey hook [ T ] , value T ) {
if err := UseHook ( l , hookKey , value ) ; err != nil {
log . Fatal ( err )
}
}
// getTypeName is a trick to get the name of a type (even if it is an
// interface type)
func getTypeName [ T any ] ( ) string {