From 01982942a8e4aeb08d822a7d9a2a9dc67e456618 Mon Sep 17 00:00:00 2001 From: Antonio De Lucreziis Date: Wed, 14 Jun 2023 12:41:34 +0200 Subject: [PATCH] chore: refactored sl and added an hooks system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit modified: cmd/dev-server/main.go, cmd/server/main.go Aggiornati per utilizzare il nuovo sistema degli hooks, ora nei "main" vanno solo chiamate "ProvideValue", "Provide" e "ProvideHook" ed una finale di "Invoke" o "Use". Mentre nei vari servizi vanno solo chiamate "Use" o "UseHook" modified: libs/sl/sl.go Rinominati "InjectValue" e "InjectLazy" a "Provide" e "ProvideFunc", altro refactor di varie cose ed aggiunto il concetto degli hook per poter iniettare anche più di un servizio dentro un altro in un preciso ordine. modified: server/config/config.go Lieve refactor ed aggiunta la variabile "NPM_COMMAND" configurabile per la fase di development. modified: server/listautenti/listautenti.go Ora questo è un vero e proprio servizio "state-less", fornisce solo una funzione che monta tutte le route necessarie a questo servizio. Più avanti dipenderà da LDAP quindi forse sarà leggermente più complicato. modified: server/listautenti/listautenti_test.go Aggiornato questo test, ora usa il nuovo sistema e funge di nuovo. deleted: server/routes/routes.go Il sistema degli hook è stato introdotto anche per semplificare questa cosa che ora infatti non serve più. modified: server/server.go Ora il server non chiama più "InjectValue" con i sotto-router ma espone solo un hook che passa i sotto router come parametri, in questo modo è più facile aggiungere e togliere servizi direttamente alla radice quando viene configurata tutta l'applicazione. --- cmd/dev-server/main.go | 32 +++-- cmd/server/main.go | 28 ++-- libs/sl/sl.go | 175 +++++++++++++++++++------ server/config/config.go | 33 +++-- server/listautenti/listautenti.go | 18 +-- server/listautenti/listautenti_test.go | 49 ++++--- server/routes/routes.go | 9 -- server/server.go | 22 +++- 8 files changed, 244 insertions(+), 122 deletions(-) delete mode 100644 server/routes/routes.go diff --git a/cmd/dev-server/main.go b/cmd/dev-server/main.go index 7b2964b..6895d06 100644 --- a/cmd/dev-server/main.go +++ b/cmd/dev-server/main.go @@ -24,9 +24,15 @@ func init() { func main() { l := sl.New() - cfg := sl.InjectValue(l, config.Slot, config.TestingDevelopmentConfig) + // + // Setup the application + // - sl.InjectValue[database.Database](l, database.Slot, &database.Memory{ + // Config + sl.ProvideFunc(l, config.Slot, config.Configure) + + // Database + sl.Provide[database.Database](l, database.Slot, &database.Memory{ Users: []model.User{ { Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534", @@ -43,21 +49,23 @@ func main() { }, }) - sl.InjectLazy(l, server.Slot, server.Configure) - sl.InjectLazy(l, listautenti.Slot, listautenti.Configure) + // Server + sl.ProvideFunc(l, server.Slot, server.Configure) + sl.ProvideHook(l, server.ApiRoutesHook, + listautenti.MountApiRoutesHook, + ) - srv, err := sl.Use(l, server.Slot) - if err != nil { - log.Fatal(err) - } + // + // Start the application + // - go func() { - log.Fatal(srv.Router.Listen(cfg.Host)) - }() + sl.MustInvoke(l, server.Slot) r, w := io.Pipe() - cmd := exec.Command("pnpm", "run", "dev") + cfg := sl.MustUse(l, config.Slot) + + cmd := exec.Command(cfg.NpmCommand, "run", "dev") cmd.Stdout = w go func() { diff --git a/cmd/server/main.go b/cmd/server/main.go index c5ef750..0800f67 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -10,18 +10,22 @@ import ( "git.phc.dm.unipi.it/phc/website/server/config" "git.phc.dm.unipi.it/phc/website/server/database" + "git.phc.dm.unipi.it/phc/website/server/listautenti" "git.phc.dm.unipi.it/phc/website/server/model" ) func main() { l := sl.New() - cfg := sl.InjectValue(l, config.Slot, config.Config{ - Mode: "production", - Host: ":4000", - }) + // + // Setup the application + // + + // Config + sl.Provide(l, config.Slot, config.ExampleProductionConfig) - sl.InjectValue[database.Database](l, database.Slot, &database.Memory{ + // Database + sl.Provide[database.Database](l, database.Slot, &database.Memory{ Users: []model.User{ { Id: "e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534", @@ -38,10 +42,18 @@ func main() { }, }) - srv, err := server.Configure(l) + // Server + sl.ProvideFunc(l, server.Slot, server.Configure) + sl.ProvideHook(l, server.ApiRoutesHook, + listautenti.MountApiRoutesHook, + ) + + // + // Start the application + // + + err := sl.Invoke(l, server.Slot) if err != nil { log.Fatal(err) } - - log.Fatal(srv.Router.Listen(cfg.Host)) } diff --git a/libs/sl/sl.go b/libs/sl/sl.go index fb21076..8b82495 100644 --- a/libs/sl/sl.go +++ b/libs/sl/sl.go @@ -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 [InjectValue] and retrieved with [Use]. Slots should +// [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 [InjectValue] and -// [InjectLazy] functions. +// 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 [InjectValue]. +// ServiceLocator using [Provide]. // - a service with dependencies on other service should use -// [InjectLazy]. This lets the service to initialize itself when needed +// [ProvideFunc]. This lets the service configure 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 value of type %T`, s.typeName, v) + Logger.Printf(`[slot: %s] configured service of type %T`, s.typeName, v) - s.created = true + 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 [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{}, } } -// 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. +// 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 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 initialized (in the case of a lazy slot), an error +// 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 @@ -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.checkInitialized(l) + err := slot.checkConfigured(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.checkInitialized(l) + err := slot.checkConfigured(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 { diff --git a/server/config/config.go b/server/config/config.go index 650a2b6..6c376d4 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -9,37 +9,46 @@ import ( type Config struct { Mode string Host string + + NpmCommand string } var Slot = sl.NewSlot[Config]() +func setFromEnvOrDefault(target *string, m map[string]string, key string, defaultValue string) { + v, ok := m[key] + if ok { + *target = v + } else { + *target = defaultValue + } +} + func Configure(l *sl.ServiceLocator) (Config, error) { - m, err := godotenv.Read(".env") + env, err := godotenv.Read(".env") if err != nil { return Config{}, err } var cfg Config - cfg.Mode = "production" - if v, ok := m["MODE"]; ok { - cfg.Mode = v - } - - cfg.Host = ":4000" - if v, ok := m["HOST"]; ok { - cfg.Host = v - } + setFromEnvOrDefault(&cfg.Mode, env, "MODE", "production") + setFromEnvOrDefault(&cfg.Host, env, "HOST", ":4000") + setFromEnvOrDefault(&cfg.NpmCommand, env, "NPM_COMMAND", "npm") return cfg, nil } -var TestingProductionConfig = Config{ +var ExampleProductionConfig = Config{ Mode: "production", Host: ":4000", + + NpmCommand: "npm", } -var TestingDevelopmentConfig = Config{ +var ExampleDevelopmentConfig = Config{ Mode: "development", Host: ":4000", + + NpmCommand: "npm", } diff --git a/server/listautenti/listautenti.go b/server/listautenti/listautenti.go index 72e33c2..c39d03c 100644 --- a/server/listautenti/listautenti.go +++ b/server/listautenti/listautenti.go @@ -6,25 +6,15 @@ import ( "git.phc.dm.unipi.it/phc/website/libs/sl" "git.phc.dm.unipi.it/phc/website/server/database" - "git.phc.dm.unipi.it/phc/website/server/routes" ) -type ListaUtenti struct{} - -var Slot = sl.NewSlot[*ListaUtenti]() - -func Configure(l *sl.ServiceLocator) (*ListaUtenti, error) { +func MountApiRoutesHook(l *sl.ServiceLocator, api fiber.Router) error { db, err := sl.Use(l, database.Slot) if err != nil { - return nil, err - } - - r, err := sl.Use(l, routes.Root) - if err != nil { - return nil, err + return err } - r.Get("/api/lista-utenti", func(c *fiber.Ctx) error { + api.Get("/lista-utenti", func(c *fiber.Ctx) error { users, err := db.ReadUsers() if err != nil { return err @@ -34,5 +24,5 @@ func Configure(l *sl.ServiceLocator) (*ListaUtenti, error) { return c.JSON(users) }) - return &ListaUtenti{}, nil + return nil } diff --git a/server/listautenti/listautenti_test.go b/server/listautenti/listautenti_test.go index 4f7fa2d..948545d 100644 --- a/server/listautenti/listautenti_test.go +++ b/server/listautenti/listautenti_test.go @@ -10,23 +10,22 @@ import ( "gotest.tools/assert" - "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp" "github.com/valyala/fasthttp/fasthttputil" "git.phc.dm.unipi.it/phc/website/libs/db" "git.phc.dm.unipi.it/phc/website/libs/sl" "git.phc.dm.unipi.it/phc/website/libs/util" + "git.phc.dm.unipi.it/phc/website/server" "git.phc.dm.unipi.it/phc/website/server/config" "git.phc.dm.unipi.it/phc/website/server/database" "git.phc.dm.unipi.it/phc/website/server/listautenti" "git.phc.dm.unipi.it/phc/website/server/model" - "git.phc.dm.unipi.it/phc/website/server/routes" ) func TestApiListaUtenti(t *testing.T) { - r := fiber.New() + memDB := &database.Memory{ Users: []model.User{ { @@ -45,23 +44,35 @@ func TestApiListaUtenti(t *testing.T) { } l := sl.New() - sl.InjectValue(l, config.Slot, config.TestingProductionConfig) - sl.InjectValue[database.Database](l, database.Slot, memDB) - sl.InjectValue(l, routes.Root, fiber.Router(r)) - listautenti.Configure(l) + // Config + sl.Provide(l, config.Slot, config.ExampleProductionConfig) + + // Database + sl.Provide[database.Database](l, database.Slot, memDB) + + // Server + sl.ProvideFunc(l, server.Slot, server.Configure) + sl.ProvideHook(l, server.ApiRoutesHook, + listautenti.MountApiRoutesHook, + ) + + // Initialize server instance + srv, err := sl.Use(l, server.Slot) + assert.NilError(t, err) + + // // Try doing the request + // req, err := http.NewRequest("GET", "http://localhost:4000/api/lista-utenti", nil) - if err != nil { - t.Error(err) - } + assert.NilError(t, err) ln := fasthttputil.NewInmemoryListener() defer ln.Close() go func() { - err := fasthttp.Serve(ln, r.Handler()) + err := fasthttp.Serve(ln, srv.Router.Handler()) if err != nil { panic(fmt.Errorf("failed to serve: %v", err)) } @@ -76,27 +87,23 @@ func TestApiListaUtenti(t *testing.T) { } res, err := client.Do(req) - if err != nil { - t.Error(err) - } + assert.NilError(t, err) body, err := io.ReadAll(res.Body) - if err != nil { - t.Error(err) - } + assert.NilError(t, err) assert.Equal(t, string(body), util.CompactIndentedLines(` [ { - "Id":"claire", + "Id":"e39ad8d5-a087-4cb2-8fd7-5a6ca3f6a534", + "Username":"claire-doe", "FullName":"Claire Doe", - "Nickname":"claire-doe", "Email":"claire.doe@example.org" }, { - "Id":"john", + "Id":"9b7109cd-95a1-41e9-a9f6-001a32c20ca1", + "Username":"john-smith", "FullName":"John Smith", - "Nickname":"john-smith", "Email":"john.smith@example.org" } ] diff --git a/server/routes/routes.go b/server/routes/routes.go deleted file mode 100644 index 0d66280..0000000 --- a/server/routes/routes.go +++ /dev/null @@ -1,9 +0,0 @@ -package routes - -import ( - "git.phc.dm.unipi.it/phc/website/libs/sl" - - "github.com/gofiber/fiber/v2" -) - -var Root = sl.NewSlot[fiber.Router]() diff --git a/server/server.go b/server/server.go index 0817a9a..1283e10 100644 --- a/server/server.go +++ b/server/server.go @@ -1,9 +1,10 @@ package server import ( + "log" + "git.phc.dm.unipi.it/phc/website/libs/sl" - "git.phc.dm.unipi.it/phc/website/server/listautenti" - "git.phc.dm.unipi.it/phc/website/server/routes" + "git.phc.dm.unipi.it/phc/website/server/config" "github.com/gofiber/fiber/v2" ) @@ -12,15 +13,26 @@ type Server struct{ Router *fiber.App } var Slot = sl.NewSlot[*Server]() +var ApiRoutesHook = sl.NewHook[fiber.Router]() + func Configure(l *sl.ServiceLocator) (*Server, error) { + + cfg, err := sl.Use(l, config.Slot) + if err != nil { + return nil, err + } + r := fiber.New(fiber.Config{}) r.Static("/assets", "./out/frontend/assets") - sl.InjectValue(l, routes.Root, fiber.Router(r)) - - if err := sl.Invoke(l, listautenti.Slot); err != nil { + api := r.Group("/api") + if err := sl.UseHook(l, ApiRoutesHook, api); err != nil { return nil, err } + go func() { + log.Fatal(r.Listen(cfg.Host)) + }() + return &Server{r}, nil }