package main import ( "context" "errors" "fmt" "io" "log/slog" "os" "os/signal" "git.loyso.art/frx/devsim/internal/api/http" "git.loyso.art/frx/devsim/internal/store" "git.loyso.art/frx/devsim/internal/store/mongo" "git.loyso.art/frx/devsim/internal/store/pg" "git.loyso.art/frx/devsim/pkg/collections" "golang.org/x/sync/errgroup" ) var availableStoreTypes = collections.NewSet([]string{ "pg", "mongo", }...) func main() { ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() log := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{})) var settings applicationSettings settings.fromEnv() err := settings.validate() if err != nil { log.ErrorContext(ctx, "unable to validate settings", slog.Any("err", err)) os.Exit(1) } err = app(ctx, settings, log) if err != nil { log.ErrorContext(ctx, "unable to run app", slog.Any("err", err)) os.Exit(1) } } type mongoSettings struct { DSN string } func (s *mongoSettings) fromEnv() { *s = mongoSettings{ DSN: os.Getenv("DEVSIM_MONGO_DSN"), } } type pgSettings struct { DSN string } func (s *pgSettings) fromEnv() { *s = pgSettings{ DSN: os.Getenv("DEVSIM_PG_DSN"), } } type applicationSettings struct { listenAddr string storeType string pg pgSettings mongo mongoSettings } func (s *applicationSettings) fromEnv() { const webaddr = ":9123" *s = applicationSettings{ listenAddr: valueOrDefault(os.Getenv("DEVSIM_HTTP_ADDR"), webaddr), storeType: os.Getenv("DEVSIM_STORE_TYPE"), } s.pg.fromEnv() s.mongo.fromEnv() } func (s *applicationSettings) validate() (err error) { if !availableStoreTypes.Contains(s.storeType) { err = errors.Join(err, errors.New("store_type value is unsupported")) } switch s.storeType { case "pg": if s.pg.DSN == "" { err = errors.Join(err, errors.New("no postgres dsn provided")) } case "mongo": if s.mongo.DSN == "" { err = errors.Join(err, errors.New("no mongo dsn provided")) } } if s.listenAddr == "" { err = errors.Join(err, errors.New("no listen address provided")) } return err } type namedCloser struct { closer io.Closer name string } func app(ctx context.Context, settings applicationSettings, log *slog.Logger) (err error) { var repo store.Stats var closers []namedCloser switch settings.storeType { case "pg": pgconn, err := pg.Dial(ctx, settings.pg.DSN) if err != nil { return fmt.Errorf("connecting to postgres: %w", err) } repo = pgconn.StatsRepository() closers = append(closers, namedCloser{ name: "postgres", closer: pgconn, }) case "mongo": mongoconn, err := mongo.Dial(ctx, settings.mongo.DSN) if err != nil { return fmt.Errorf("connecting to mongo: %w", err) } repo = mongoconn.StatsRepository() closers = append(closers, namedCloser{ name: "mongo", closer: mongoconn, }) } hb := http.NewHandlersBuilder() hb.MountStatsHandlers(repo, log) httpServer := http.NewServer(settings.listenAddr) closers = append(closers, namedCloser{ name: "http", closer: httpServer, }) httpServer.RegisterHandler(hb.Build()) eg, _ := errgroup.WithContext(ctx) eg.Go(func() error { return httpServer.Run() }) err = eg.Wait() if err != nil { log.ErrorContext(ctx, "unable to proceed the app", slog.Any("err", err)) } for _, closer := range closers { name := closer.name closerUnit := closer.closer errClose := closerUnit.Close() if errClose != nil { log.ErrorContext(ctx, "unable to close component", slog.String("component", name), slog.Any("err", errClose)) } } return nil } func valueOrDefault[x comparable](value, fallback x) x { var v x if value == v { return fallback } return value }