You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
5.2 KiB
Go
167 lines
5.2 KiB
Go
2 years ago
|
// 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 [InjectValue] 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 [InjectValue] and
|
||
|
// [InjectLazy] functions.
|
||
|
//
|
||
|
// Services can be of various types:
|
||
|
// - a service with no dependencies can be directly injected inside a
|
||
|
// ServiceLocator using [InjectValue].
|
||
|
// - a service with dependencies on other service should use
|
||
|
// [InjectLazy]. This lets the service to initialize itself when needed
|
||
|
// and makes the developer not think about correctly ordering the
|
||
|
// initialization 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
|
||
|
// ServiceLocator.
|
||
|
// - a package can also just provide a slot with some value. This is useful
|
||
|
// for using the ServiceLocator to easily pass around values, effectively
|
||
|
// threating slots just as dynamically scoped variables.
|
||
|
package sl
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"os"
|
||
|
)
|
||
|
|
||
|
// Logger is the debug logger
|
||
|
//
|
||
|
// TODO: in the future this will be disabled and discard by default.
|
||
|
//
|
||
|
// As this is the service locator module it was meaning less to pass this
|
||
|
// through the ServiceLocator itself (without making the whole module more
|
||
|
// complex)
|
||
|
var Logger *log.Logger = log.New(os.Stderr, "[service locator] ", log.Lmsgprefix)
|
||
|
|
||
|
// slot is just a "typed" unique "symbol".
|
||
|
type slot[T any] *struct{}
|
||
|
|
||
|
// NewSlot is the only way to create instances of the slot type. Each instance
|
||
|
// is unique.
|
||
|
//
|
||
|
// This then lets you attach a service instance of type "T" to a
|
||
|
// [ServiceLocator] object.
|
||
|
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"
|
||
|
// field and "created" will always be "true". The field "typeName" just for
|
||
|
// debugging purposes.
|
||
|
type slotEntry struct {
|
||
|
typeName string
|
||
|
|
||
|
createFunc func(*ServiceLocator) (any, error)
|
||
|
created bool
|
||
|
|
||
|
value any
|
||
|
}
|
||
|
|
||
|
func (s *slotEntry) checkInitialized(l *ServiceLocator) error {
|
||
|
if !s.created {
|
||
|
v, err := s.createFunc(l)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
Logger.Printf(`[slot: %s] initialized value of type %T`, s.typeName, v)
|
||
|
|
||
|
s.created = true
|
||
|
s.value = v
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// 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].
|
||
|
type ServiceLocator struct {
|
||
|
providers map[any]*slotEntry
|
||
|
}
|
||
|
|
||
|
// New creates a new [ServiceLocator] context to pass around in the application.
|
||
|
func New() *ServiceLocator {
|
||
|
return &ServiceLocator{
|
||
|
providers: map[any]*slotEntry{},
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// InjectValue 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.
|
||
|
//
|
||
|
// 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)
|
||
|
|
||
|
l.providers[slotKey] = &slotEntry{
|
||
|
getTypeName[T](),
|
||
|
nil,
|
||
|
true,
|
||
|
value,
|
||
|
}
|
||
|
return value
|
||
|
}
|
||
|
|
||
|
// InjectLazy 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]())
|
||
|
|
||
|
l.providers[slotKey] = &slotEntry{
|
||
|
createFunc: func(l *ServiceLocator) (any, error) {
|
||
|
return createFunc(l)
|
||
|
},
|
||
|
created: false,
|
||
|
value: nil,
|
||
|
typeName: getTypeName[T](),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Use retrieves the value of type T associated with the given slot key from
|
||
|
// the provided ServiceLocator instance.
|
||
|
//
|
||
|
// If the ServiceLocator does not have a value for the slot key, or if the
|
||
|
// value wasn't correctly initialized (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
|
||
|
|
||
|
slot, ok := l.providers[slotKey]
|
||
|
if !ok {
|
||
|
return zero, fmt.Errorf(`no injected value for type %s`, getTypeName[T]())
|
||
|
}
|
||
|
|
||
|
err := slot.checkInitialized(l)
|
||
|
if err != nil {
|
||
|
return zero, err
|
||
|
}
|
||
|
|
||
|
v := slot.value.(T)
|
||
|
|
||
|
Logger.Printf(`[slot: %s] using slot with value of type %T`, getTypeName[T](), v)
|
||
|
return v, nil
|
||
|
}
|
||
|
|
||
|
// getTypeName is a trick to get the name of a type (even if it is an
|
||
|
// interface type)
|
||
|
func getTypeName[T any]() string {
|
||
|
var zero T
|
||
|
return fmt.Sprintf(`%T`, &zero)[1:]
|
||
|
}
|