// 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 // [ProvideFunc] and [Provide] 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 [Provide] and // [ProvideFunc] functions. // // Services can be of various types: // - a service with no dependencies can be directly injected inside a // ServiceLocator using [Provide]. // - a service with dependencies on other service should use // [ProvideFunc]. This lets the service configure itself when needed // and makes the developer not think about correctly ordering the // 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 // 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". // // 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. // // 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{})) } // 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 // 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 } // 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] configured service of type %T`, s.typeName, v) s.configured = 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 [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{}, } } // Provide 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 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{ typeName: typeName, configured: true, value: value, } return value } // 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 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{ typeName: typeName, configureFunc: func(l *ServiceLocator) (any, error) { return createFunc(l) }, configured: false, } } // 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 configured (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.checkConfigured(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 } 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.checkConfigured(l) if err != nil { return err } v := slot.value.(T) 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 { var zero T return fmt.Sprintf(`%T`, &zero)[1:] }