// 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]. As slots should be unique 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 required and makes the developer not think the topological sort to put onto the DAG of service 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 also just provide a slot. 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, 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, "[sl] ", 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. The field "typeName" just for debugging purposes. type slotEntry struct { createFunc func(*ServiceLocator) (any, error) created bool value any typeName string } func (s *slotEntry) checkInitialized(l *ServiceLocator) error { if !s.created { v, err := s.createFunc(l) if err != nil { return err } Logger.Printf(`initialized lazy value of type %T for slot of type %s`, v, s.typeName) 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(`injected value of type %T for slot of type %s`, value, getTypeName[T]()) l.providers[slotKey] = &slotEntry{ nil, true, value, getTypeName[T](), } 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(`injected lazy for slot of type %s`, 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(`using slot of type %s 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:] }