From 20826ee64351e34b2fcbe0d169e7e7f56ded556e Mon Sep 17 00:00:00 2001 From: Paul Balogh Date: Sun, 19 May 2024 17:04:43 -0500 Subject: [PATCH] Refactoring commands and configs as well update linters --- .github/workflows/ci.yml | 6 +- .github/workflows/linter.yml | 4 +- .golangci.yml | 14 ++- cmd/migrate.go | 188 ++++++++++++++++++++--------------- cmd/root.go | 92 ++++++++++------- cmd/serve.go | 96 ++++-------------- cmd/version.go | 20 ++-- go.mod | 2 +- internal/api/api.go | 33 +++--- internal/api/api_test.go | 4 +- internal/api/config.go | 19 ---- internal/app/app.go | 17 ++-- internal/app/context.go | 7 +- internal/config/config.go | 16 +++ internal/db/config.go | 28 ------ internal/db/db.go | 4 +- internal/db/place_test.go | 4 +- internal/server/server.go | 91 +++++++++++++++++ 18 files changed, 349 insertions(+), 296 deletions(-) delete mode 100644 internal/api/config.go create mode 100644 internal/config/config.go delete mode 100644 internal/db/config.go create mode 100644 internal/server/server.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 953d9f0..ece0cbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Install Go(lang) uses: actions/setup-go@v5 with: - go-version: 1.21.x + go-version: 1.22.x - name: Run unit tests run: go test -test.short -v -cover -race ./... @@ -29,14 +29,14 @@ jobs: - name: Install Go(lang) uses: actions/setup-go@v5 with: - go-version: 1.21.x + go-version: 1.22.x - name: Build server binary run: | go version make build-only - name: Install k6 run: | - curl https://github.com/grafana/k6/releases/download/v0.48.0/k6-v0.48.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1 + curl https://github.com/grafana/k6/releases/download/v0.51.0/k6-v0.51.0-linux-amd64.tar.gz -L | tar xvz --strip-components 1 - name: k6 Compliance run: | echo "DatabaseURI: "${GITHUB_WORKSPACE}/gorm.db"" > config.yaml diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index b7f4360..ad84686 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -13,7 +13,7 @@ jobs: - name: Install Go(lang) uses: actions/setup-go@v5 with: - go-version: 1.21.x + go-version: 1.22.x - name: Check module dependencies run: | go version @@ -30,7 +30,7 @@ jobs: - name: Install Go uses: actions/setup-go@v5 with: - go-version: 1.21.x + go-version: 1.22.x - name: Retrieve golangci-lint version run: | echo "Version=$(head -n 1 "${GITHUB_WORKSPACE}/.golangci.yml" | tr -d '# ')" >> $GITHUB_OUTPUT diff --git a/.golangci.yml b/.golangci.yml index b51eb56..7d50780 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,4 +1,4 @@ -# v1.55.2 +# v1.58.2 # Please don't remove the first line. It is used in CI to determine the golangci-lint version. run: timeout: 5m @@ -12,13 +12,21 @@ issues: - gocognit - funlen - lll - - path: (cmd|env|migrations)\/ + - path: (env|migrations)\/ linters: - gochecknoglobals linters-settings: + errcheck: + check-type-assertsions: true + disable-default-exclusions: true + exclude-functions: + - io/ioutil.ReadFile + - io.Copy(*bytes.Buffer) + - io.Copy(os.Stdout) govet: - check-shadowing: true + enable: + shadow funlen: lines: 80 statements: 60 diff --git a/cmd/migrate.go b/cmd/migrate.go index f282a70..743d682 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -1,112 +1,136 @@ package cmd import ( + "fmt" + "log/slog" "sort" + "github.com/weesvc/weesvc-gorilla/internal/config" + + "github.com/spf13/viper" + "github.com/jinzhu/gorm" "github.com/pkg/errors" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" "github.com/weesvc/weesvc-gorilla/internal/app" "github.com/weesvc/weesvc-gorilla/internal/migrations" ) -var migrateCmd = &cobra.Command{ - Use: "migrate", - Short: "Readies the application database", - RunE: func(cmd *cobra.Command, args []string) error { - number, _ := cmd.Flags().GetInt("number") - dryRun, _ := cmd.Flags().GetBool("dry-run") +func newMigrateCommand(config *config.Config) *cobra.Command { + migrateCmd := &cobra.Command{ + Use: "migrate", + Short: "Readies the application database", + RunE: func(cmd *cobra.Command, _ []string) error { + svc, err := app.New(config) + if err != nil { + return err + } + defer func() { + _ = svc.Close() + }() + return migrate(cmd, svc) + }, + } - if dryRun { - logrus.Info("=== DRY RUN ===") - } + migrateCmd.Flags().Int("number", -1, "the migration to run forwards until; if not set, will run all migrations") + migrateCmd.Flags().Bool("dry-run", false, "print out migrations to be applied without running them") - sort.Slice(migrations.Migrations, func(i, j int) bool { - return migrations.Migrations[i].Number < migrations.Migrations[j].Number - }) + migrateCmd.PersistentFlags().StringVar(&config.Dialect, "dialect", "sqlite3", "config file") + migrateCmd.PersistentFlags().StringVar(&config.DatabaseURI, "database-uri", "", "config file") - a, err := app.New() - if err != nil { - return err - } - defer func() { - _ = a.Close() - }() - - // Make sure Migration table is there - logrus.Debug("ensuring migrations table is present") - if err := a.Database.AutoMigrate(&migrations.Migration{}).Error; err != nil { - return errors.Wrap(err, "unable to automatically migrate migrations table") - } + _ = viper.BindPFlag("Dialect", migrateCmd.PersistentFlags().Lookup("dialect")) + _ = viper.BindPFlag("DatabaseURI", migrateCmd.PersistentFlags().Lookup("database-uri")) - var latest migrations.Migration - if err := a.Database.Order("number desc").First(&latest).Error; err != nil && !gorm.IsRecordNotFoundError(err) { - return errors.Wrap(err, "unable to find latest migration") - } - - noMigrationsApplied := latest.Number == 0 + return migrateCmd +} - if noMigrationsApplied && len(migrations.Migrations) == 0 { - logrus.Info("no migrations to apply") - return nil - } +func migrate(cmd *cobra.Command, app *app.App) error { + number, _ := cmd.Flags().GetInt("number") + dryRun, _ := cmd.Flags().GetBool("dry-run") - if latest.Number >= migrations.Migrations[len(migrations.Migrations)-1].Number { - logrus.Info("no migrations to apply") - return nil - } + if dryRun { + slog.Info("=== DRY RUN ===") + } - if number == -1 { - number = int(migrations.Migrations[len(migrations.Migrations)-1].Number) - } + runMigrations, err := shouldRunMigrations(app, number) + if err != nil { + return err + } + if !runMigrations { + return nil + } - if uint(number) <= latest.Number && latest.Number > 0 { - logrus.Info("no migrations to apply; number is less than or equal to latest migration") - return nil + for _, migration := range migrations.Migrations { + if migration.Number > uint(number) { + break } - for _, migration := range migrations.Migrations { - if migration.Number > uint(number) { - break - } + logger := slog.With("migration_number", migration.Number) + logger.Info(fmt.Sprintf("applying migration %q", migration.Name)) - logger := logrus.WithField("migration_number", migration.Number) - logger.Infof("applying migration %q", migration.Name) - - if dryRun { - continue - } - - tx := a.Database.Begin() - - if err := migration.Forwards(tx); err != nil { - logger.WithError(err).Error("unable to apply migration, rolling back") - if err := tx.Rollback().Error; err != nil { - logger.WithError(err).Error("unable to rollback...") - } - break - } - - if err := tx.Commit().Error; err != nil { - logger.WithError(err).Error("unable to commit transaction") - break - } - - if err := a.Database.Create(migration).Error; err != nil { - logger.WithError(err).Error("unable to create migration record") - break - } + if dryRun { + continue } + if ok := applyMigration(app, logger, migration); !ok { + break + } + } - return nil - }, + return nil } -func init() { - rootCmd.AddCommand(migrateCmd) +func applyMigration(app *app.App, logger *slog.Logger, migration *migrations.Migration) bool { + tx := app.Database.Begin() + if err := migration.Forwards(tx); err != nil { + logger.With("err", err).Error("unable to apply migration, rolling back") + if err := tx.Rollback().Error; err != nil { + logger.With("err", err).Error("unable to rollback...") + } + return false + } + + if err := tx.Commit().Error; err != nil { + logger.With("err", err).Error("unable to commit transaction") + return false + } + + if err := app.Database.Create(migration).Error; err != nil { + logger.With("err", err).Error("unable to create migration record") + return false + } + return true +} - migrateCmd.Flags().Int("number", -1, "the migration to run forwards until; if not set, will run all migrations") - migrateCmd.Flags().Bool("dry-run", false, "print out migrations to be applied without running them") +func shouldRunMigrations(app *app.App, number int) (bool, error) { + sort.Slice(migrations.Migrations, func(i, j int) bool { + return migrations.Migrations[i].Number < migrations.Migrations[j].Number + }) + + // Make sure Migration table is there + if err := app.Database.AutoMigrate(&migrations.Migration{}).Error; err != nil { + return false, errors.Wrap(err, "unable to automatically migrate migrations table") + } + + var latest migrations.Migration + if err := app.Database.Order("number desc").First(&latest).Error; err != nil && !gorm.IsRecordNotFoundError(err) { + return false, errors.Wrap(err, "unable to find latest migration") + } + + noMigrationsApplied := latest.Number == 0 + if (noMigrationsApplied && len(migrations.Migrations) == 0) || + (latest.Number >= migrations.Migrations[len(migrations.Migrations)-1].Number) { + slog.Info("no migrations to apply") + return false, nil + } + + if number == -1 { + number = int(migrations.Migrations[len(migrations.Migrations)-1].Number) + } + if uint(number) <= latest.Number && latest.Number > 0 { + slog.Info("no migrations to apply; number is less than or equal to latest migration") + return false, nil + } + + return true, nil } diff --git a/cmd/root.go b/cmd/root.go index eaba6e9..631cd75 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,62 +2,82 @@ package cmd import ( - "fmt" + "errors" + "log/slog" "os" + "strings" + + "github.com/weesvc/weesvc-gorilla/internal/config" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var rootCmd = &cobra.Command{ - Use: "weesvc", - Short: "WeeService Application", - Run: func(cmd *cobra.Command, args []string) { - if err := cmd.Usage(); err != nil { - fmt.Println(err) - os.Exit(1) - } - }, -} - // Execute parses and runs the command line. func Execute() { - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) + if err := newRootCommand().Execute(); err != nil { + slog.Error(err.Error()) os.Exit(1) } } -var ( - configFile string - verbose bool -) +func newRootCommand() *cobra.Command { + configFile := "" + cfg := config.NewConfig() -func init() { - cobra.OnInitialize(initConfig) - rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file (default is config.yaml)") - rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose output") + rootCmd := &cobra.Command{ + Use: "weesvc", + Short: "WeeService Application", + PersistentPreRunE: func(_ *cobra.Command, _ []string) error { + return initConfig(configFile, cfg) + }, + Run: func(cmd *cobra.Command, _ []string) { + if err := cmd.Usage(); err != nil { + slog.Error(err.Error()) + os.Exit(1) + } + }, + } - if err := viper.BindPFlag("Verbose", rootCmd.PersistentFlags().Lookup("verbose")); err != nil { - fmt.Println(err) - os.Exit(1) + rootCmd.PersistentFlags().StringVar(&configFile, "config", "", "config file") + rootCmd.PersistentFlags().BoolVar(&cfg.Verbose, "verbose", false, "verbose output") + + _ = viper.BindPFlag("Verbose", rootCmd.PersistentFlags().Lookup("verbose")) + + subCommands := []func(*config.Config) *cobra.Command{ + newServeCommand, + newMigrateCommand, + newVersionCommand, } + for _, sc := range subCommands { + rootCmd.AddCommand(sc(cfg)) + } + + return rootCmd } -func initConfig() { - if configFile != "" { - viper.SetConfigFile(configFile) - } else { - viper.SetConfigName("config") - viper.AddConfigPath(".") - viper.AddConfigPath("/etc/weesvc") - viper.AddConfigPath("$HOME/.weesvc") +func initConfig(configFile string, config *config.Config) error { + viper.SetConfigFile(configFile) + viper.AddConfigPath(".") + + if err := viper.ReadInConfig(); err != nil { + // If not found, we'll use defaults and suffer consequences otherwise + if !errors.As(err, &viper.ConfigFileNotFoundError{}) { + return err + } } + // Enable overrides by environment variable + // E.g. WEESVC_DIALECT will override `Dialect` within the configuration! + viper.SetEnvPrefix("WEESVC") + // Hashes and dots should be treated as underscores + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) viper.AutomaticEnv() - if err := viper.ReadInConfig(); err != nil { - fmt.Printf("unable to read config: %v\n", err) - os.Exit(1) + // Populate our configuration object + if err := viper.Unmarshal(config); err != nil { + return err } + + return nil } diff --git a/cmd/serve.go b/cmd/serve.go index 37e07b9..c24b3ee 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -1,90 +1,30 @@ package cmd import ( - "context" - "fmt" - "net/http" - "os" - "os/signal" - "sync" - "time" - - "github.com/gorilla/handlers" - "github.com/gorilla/mux" - "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "github.com/spf13/viper" - "github.com/weesvc/weesvc-gorilla/internal/api" - "github.com/weesvc/weesvc-gorilla/internal/app" -) - -func serveAPI(ctx context.Context, api *api.API) { - cors := handlers.CORS( - handlers.AllowedOrigins([]string{"*"}), - handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "OPTIONS"}), - ) - - router := mux.NewRouter() - api.Init(router.PathPrefix("/api").Subrouter()) + "github.com/weesvc/weesvc-gorilla/internal/config" - s := &http.Server{ - Addr: fmt.Sprintf(":%d", api.Config.Port), - Handler: cors(router), - ReadTimeout: 2 * time.Minute, - } - - done := make(chan struct{}) - go func() { - <-ctx.Done() - //nolint:contextcheck - if err := s.Shutdown(context.Background()); err != nil { - logrus.Error(err) - } - close(done) - }() + "github.com/weesvc/weesvc-gorilla/internal/server" +) - logrus.Infof("serving api at http://127.0.0.1:%d", api.Config.Port) - if err := s.ListenAndServe(); err != http.ErrServerClosed { - logrus.Error(err) +func newServeCommand(config *config.Config) *cobra.Command { + serveCmd := &cobra.Command{ + Use: "serve", + Short: "Starts the application server", + RunE: func(_ *cobra.Command, _ []string) error { + return server.StartServer(config) + }, } - <-done -} - -var serveCmd = &cobra.Command{ - Use: "serve", - Short: "Starts the application server", - RunE: func(cmd *cobra.Command, args []string) error { - a, err := app.New() - if err != nil { - return err - } - - api := api.New(a) - - ctx, cancel := context.WithCancel(context.Background()) - go func() { - ch := make(chan os.Signal, 1) - signal.Notify(ch, os.Interrupt) - <-ch - logrus.Info("signal caught. shutting down...") - cancel() - }() + serveCmd.PersistentFlags().IntVarP(&config.Port, "api-port", "p", 9092, "port to access the api") + serveCmd.PersistentFlags().StringVar(&config.Dialect, "dialect", "sqlite3", "database dialect") + serveCmd.PersistentFlags().StringVar(&config.DatabaseURI, "database-uri", "", "database connection string") - var wg sync.WaitGroup - - wg.Add(1) - go func() { - defer wg.Done() - defer cancel() - serveAPI(ctx, api) - }() - - wg.Wait() - return nil - }, -} + _ = viper.BindPFlag("Port", serveCmd.PersistentFlags().Lookup("api-port")) + _ = viper.BindPFlag("Dialect", serveCmd.PersistentFlags().Lookup("dialect")) + _ = viper.BindPFlag("DatabaseURI", serveCmd.PersistentFlags().Lookup("database-uri")) -func init() { - rootCmd.AddCommand(serveCmd) + return serveCmd } diff --git a/cmd/version.go b/cmd/version.go index 17b6322..3380de0 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,19 +3,19 @@ package cmd import ( "fmt" + "github.com/weesvc/weesvc-gorilla/internal/config" + "github.com/spf13/cobra" "github.com/weesvc/weesvc-gorilla/internal/env" ) -var versionCmd = &cobra.Command{ - Use: "version", - Short: "Print the version number", - Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("WeeService %v (%v)\n", env.Version, env.Revision) - }, -} - -func init() { - rootCmd.AddCommand(versionCmd) +func newVersionCommand(_ *config.Config) *cobra.Command { + return &cobra.Command{ + Use: "version", + Short: "Print the version number", + Run: func(_ *cobra.Command, _ []string) { + fmt.Printf("WeeService %v (%v)\n", env.Version, env.Revision) + }, + } } diff --git a/go.mod b/go.mod index ea73c3d..fa0acdb 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/gorilla/mux v1.8.1 github.com/jinzhu/gorm v1.9.16 github.com/pkg/errors v0.9.1 - github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.9.0 @@ -59,6 +58,7 @@ require ( github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shirou/gopsutil/v3 v3.23.11 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect diff --git a/internal/api/api.go b/internal/api/api.go index 26534fb..94ced71 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -13,7 +13,6 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" - "github.com/sirupsen/logrus" "github.com/weesvc/weesvc-gorilla/internal/app" ) @@ -31,14 +30,12 @@ func (r *statusCodeRecorder) WriteHeader(statusCode int) { // API represents the context for the public interface. type API struct { - App *app.App - Config *Config + App *app.App } // New creates a new API instance. func New(a *app.App) (api *API) { api = &API{App: a} - api.Config = initConfig() return api } @@ -78,18 +75,18 @@ func (a *API) handler(f func(*app.Context, http.ResponseWriter, *http.Request) e } duration := time.Since(beginTime) - ctx.Logger.WithFields(logrus.Fields{ - "duration": duration, - "status_code": statusCode, - "remote_address": ctx.RemoteAddress, - "trace_id": ctx.TraceID, - }).Info(r.Method + " " + r.URL.RequestURI()) + ctx.Logger.With( + "duration", duration, + "status_code", statusCode, + "remote_address", ctx.RemoteAddress, + "trace_id", ctx.TraceID, + ).Info(r.Method + " " + r.URL.RequestURI()) }() defer func() { if r := recover(); r != nil { - ctx.Logger.Error(fmt.Errorf("%v: %s", r, debug.Stack())) - http.Error(w, "internal server error", http.StatusInternalServerError) + ctx.Logger.Error(fmt.Sprintf("%v: %s", r, debug.Stack())) + http.Error(w, "internal api error", http.StatusInternalServerError) } }() @@ -105,8 +102,8 @@ func (a *API) handler(f func(*app.Context, http.ResponseWriter, *http.Request) e case errors.As(err, &uerr): handleUserError(ctx, w, uerr) default: - ctx.Logger.Error(err) - http.Error(w, "internal server error", http.StatusInternalServerError) + ctx.Logger.Error(err.Error()) + http.Error(w, "internal api error", http.StatusInternalServerError) } } }) @@ -120,8 +117,8 @@ func handleValidationError(ctx *app.Context, w http.ResponseWriter, verr *app.Va } if err != nil { - ctx.Logger.Error(err) - http.Error(w, "internal server error", http.StatusInternalServerError) + ctx.Logger.Error(err.Error()) + http.Error(w, "internal api error", http.StatusInternalServerError) } } @@ -133,8 +130,8 @@ func handleUserError(ctx *app.Context, w http.ResponseWriter, uerr *app.UserErro } if err != nil { - ctx.Logger.Error(err) - http.Error(w, "internal server error", http.StatusInternalServerError) + ctx.Logger.Error(err.Error()) + http.Error(w, "internal api error", http.StatusInternalServerError) } } diff --git a/internal/api/api_test.go b/internal/api/api_test.go index d51b436..aa05db0 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -3,6 +3,8 @@ package api import ( "net/http" "testing" + + "github.com/weesvc/weesvc-gorilla/internal/app" ) func TestAddressForRequest(t *testing.T) { @@ -20,7 +22,7 @@ func TestAddressForRequest(t *testing.T) { expected: "[::1]", }, } - fixture := API{Config: &Config{}} + fixture := API{App: &app.App{}} for _, tc := range testCases { tc := tc // pin t.Run(tc.remoteAddr, func(t *testing.T) { diff --git a/internal/api/config.go b/internal/api/config.go deleted file mode 100644 index fc59991..0000000 --- a/internal/api/config.go +++ /dev/null @@ -1,19 +0,0 @@ -package api - -import "github.com/spf13/viper" - -// Config provides external settings. -type Config struct { - // The port to bind the web application server to - Port int -} - -func initConfig() *Config { - config := &Config{ - Port: viper.GetInt("Port"), - } - if config.Port == 0 { - config.Port = 9092 - } - return config -} diff --git a/internal/app/app.go b/internal/app/app.go index 55279d4..4f54a25 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -2,34 +2,31 @@ package app import ( - "github.com/sirupsen/logrus" + "log/slog" + "github.com/weesvc/weesvc-gorilla/internal/config" "github.com/weesvc/weesvc-gorilla/internal/db" ) // App defines the main application state and behaviors. type App struct { + Config *config.Config Database *db.Database } // NewContext creates context to bind to an incoming request. func (a *App) NewContext() *Context { return &Context{ - Logger: logrus.New(), + Logger: slog.Default(), Database: a.Database, } } // New constructs a new instance of the application. -func New() (app *App, err error) { - app = &App{} +func New(config *config.Config) (app *App, err error) { + app = &App{Config: config} - dbConfig, err := db.InitConfig() - if err != nil { - return nil, err - } - - app.Database, err = db.New(dbConfig) + app.Database, err = db.New(config) if err != nil { return nil, err } diff --git a/internal/app/context.go b/internal/app/context.go index 99088a0..6396986 100644 --- a/internal/app/context.go +++ b/internal/app/context.go @@ -1,22 +1,23 @@ package app import ( + "log/slog" + "github.com/google/uuid" - "github.com/sirupsen/logrus" "github.com/weesvc/weesvc-gorilla/internal/db" ) // Context provides for a request-scoped context. type Context struct { - Logger logrus.FieldLogger + Logger *slog.Logger RemoteAddress string TraceID uuid.UUID Database *db.Database } // WithLogger associates the provided logger to the request context. -func (ctx *Context) WithLogger(logger logrus.FieldLogger) *Context { +func (ctx *Context) WithLogger(logger *slog.Logger) *Context { ret := *ctx ret.Logger = logger return &ret diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..77bf270 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,16 @@ +// Package config provides the central configuration definition for application and services. +package config + +// Config provides database connection information. +type Config struct { + Dialect string + DatabaseURI string + Verbose bool + // The port to bind the web application api to + Port int +} + +// NewConfig returns an initialized, but empty, configuration object. +func NewConfig() *Config { + return &Config{} +} diff --git a/internal/db/config.go b/internal/db/config.go deleted file mode 100644 index d59358c..0000000 --- a/internal/db/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package db - -import ( - "fmt" - - "github.com/spf13/viper" -) - -// Config provides database connection information. -type Config struct { - Dialect string - DatabaseURI string - Verbose bool -} - -// InitConfig initializes the database configuration from external settings. -func InitConfig() (*Config, error) { - viper.SetDefault("Dialect", "sqlite3") - config := &Config{ - Dialect: viper.GetString("Dialect"), - DatabaseURI: viper.GetString("DatabaseURI"), - Verbose: viper.GetBool("Verbose"), - } - if config.DatabaseURI == "" { - return nil, fmt.Errorf("DatabaseURI must be set") - } - return config, nil -} diff --git a/internal/db/db.go b/internal/db/db.go index bb78545..eb5f344 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -5,6 +5,8 @@ import ( "github.com/jinzhu/gorm" "github.com/pkg/errors" + "github.com/weesvc/weesvc-gorilla/internal/config" + // Initialize supported dialects _ "github.com/jinzhu/gorm/dialects/postgres" _ "github.com/jinzhu/gorm/dialects/sqlite" @@ -16,7 +18,7 @@ type Database struct { } // New creates a new instance of the data access object given configuration settings. -func New(config *Config) (*Database, error) { +func New(config *config.Config) (*Database, error) { db, err := gorm.Open(config.Dialect, config.DatabaseURI) if err != nil { return nil, errors.Wrapf(err, "unable to connect to %s database", config.Dialect) diff --git a/internal/db/place_test.go b/internal/db/place_test.go index 6cb0981..d721595 100644 --- a/internal/db/place_test.go +++ b/internal/db/place_test.go @@ -5,6 +5,8 @@ import ( "log" "testing" + "github.com/weesvc/weesvc-gorilla/internal/config" + "github.com/weesvc/weesvc-gorilla/internal/model" "github.com/weesvc/weesvc-gorilla/testhelpers" @@ -118,7 +120,7 @@ func setupDatabase(t *testing.T) *Database { log.Fatal(err) } - placeDB, err := New(&Config{ + placeDB, err := New(&config.Config{ DatabaseURI: pgContainer.ConnectionString, Dialect: "postgres", Verbose: true, diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..fcbee81 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,91 @@ +package server + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "sync" + "time" + + "github.com/weesvc/weesvc-gorilla/internal/config" + + "github.com/pkg/errors" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + + "github.com/weesvc/weesvc-gorilla/internal/api" + "github.com/weesvc/weesvc-gorilla/internal/app" +) + +// StartServer sets up HTTP routes and starts the application server. +func StartServer(config *config.Config) error { + a, err := app.New(config) + if err != nil { + return err + } + + api := api.New(a) + + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, os.Interrupt) + <-ch + slog.Info("signal caught. shutting down...") + cancel() + }() + + var wg sync.WaitGroup + + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + serveAPI(ctx, a, api) + }() + + wg.Wait() + return nil +} + +func serveAPI(ctx context.Context, app *app.App, api *api.API) { + cors := handlers.CORS( + handlers.AllowedOrigins([]string{"*"}), + handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "OPTIONS"}), + ) + + router := mux.NewRouter() + api.Init(router.PathPrefix("/api").Subrouter()) + + s := &http.Server{ + Addr: fmt.Sprintf(":%d", app.Config.Port), + Handler: cors(router), + ReadTimeout: 2 * time.Minute, + } + + done := make(chan struct{}) + go func() { + <-ctx.Done() + //nolint:contextcheck + if err := s.Shutdown(context.Background()); err != nil { + slog.Error(err.Error()) + } + close(done) + }() + + slog.Info(fmt.Sprintf("serving api at http://127.0.0.1:%d", app.Config.Port)) + err := s.ListenAndServe() + if errors.Is(err, http.ErrServerClosed) { + slog.Info("server shutdown complete") + } else if err != nil { + slog.Error("server error", slog.Any("err", err)) + os.Exit(1) + } + + <-done +}