-
Christophe de Vienne authoredChristophe de Vienne authored
program.go 7.77 KiB
package cmd
import (
"errors"
"fmt"
"net/http"
"os"
"strings"
"github.com/golang-migrate/migrate/v4/source"
"github.com/jessevdk/go-flags"
"github.com/jmoiron/sqlx"
"github.com/rs/zerolog"
"orus.io/orus-io/go-orusapi"
"orus.io/orus-io/go-orusapi/auth"
"orus.io/orus-io/go-orusapi/database"
)
var ErrInvalidConfiguration = errors.New("invalid configuration")
type ConfigFile struct {
ConfigFile string `long:"config" short:"c" env:"CONFIG" no-ini:"t" description:"A configuration file"`
}
//nolint:lll
type InfoOptions struct {
Environment string `long:"environment" env:"ENVIRONMENT" ini-name:"environment" default:"default" description:"A environment name, used in sentry and prometheus"`
}
type XbusActorFactory struct {
Name string
Factory any
}
type subcommand[E any] struct {
name string
init func(*Program[E]) any
}
type Program[E any] struct {
Version Version
BootstrapParser *flags.Parser
Parser *flags.Parser
Logger zerolog.Logger
ConfigFileOption ConfigFile
InfoOptions InfoOptions
LoggingOptions *orusapi.LoggingOptions
TokenOptions *auth.TokenOptions
DatabaseOptions database.Options
XbusOptions XbusOptions
RednerOptions RednerOptions
Ext E
DB *sqlx.DB
ServeCmd *ServeCmd[E]
MigrateCmd *MigrateCmd[E]
GenerateConfigCmd *GenerateConfigCmd
hasDB bool
dbMigrateSource source.Driver
middlewares []Middleware
setupXbusActors func(*Program[E]) []XbusActorFactory
xbusActorNames []string
setupHandler func(*Program[E]) http.Handler
setupSubcommands []subcommand[E]
postInit []func(*Program[E])
}
type Option[E any] func(program *Program[E])
func WithHandler[E any](factory func(*Program[E]) http.Handler) Option[E] {
return func(program *Program[E]) {
program.setupHandler = factory
}
}
type Middleware interface {
Middleware(http.Handler) (http.Handler, error)
}
type middlewareFunc func(http.Handler) (http.Handler, error)
func (f middlewareFunc) Middleware(next http.Handler) (http.Handler, error) {
return f(next)
}
type middlewareFuncNoErr func(http.Handler) http.Handler
func (f middlewareFuncNoErr) Middleware(next http.Handler) (http.Handler, error) {
return f(next), nil
}
func WithMiddleware[E any](middleware any) Option[E] {
var m Middleware
switch f := middleware.(type) {
case func(http.Handler) http.Handler:
m = middlewareFuncNoErr(f)
case func(http.Handler) (http.Handler, error):
m = middlewareFunc(f)
default:
panic(fmt.Errorf("invalid middleware type: %t", middleware))
}
return func(program *Program[E]) {
program.middlewares = append(program.middlewares, m)
}
}
func WithXbusActors[E any](factories func(*Program[E]) []XbusActorFactory) Option[E] {
return func(program *Program[E]) {
program.setupXbusActors = factories
g, err := program.Parser.AddGroup("Xbus", "Xbus options", &program.XbusOptions)
if err != nil {
panic(err)
}
g.Namespace = "xbus"
g.EnvNamespace = "XBUS"
SetupXbusCmd(program)
}
}
func WithTokenOptions[E any]() Option[E] {
return func(program *Program[E]) {
program.TokenOptions = auth.NewTokenOptions()
if _, err := program.Parser.AddGroup("Token", "Token Options", program.TokenOptions); err != nil {
panic(err)
}
}
}
func WithDatabase[E any](migrateSource source.Driver) Option[E] {
return func(program *Program[E]) {
program.hasDB = true
program.dbMigrateSource = migrateSource
}
}
func WithSubcommand[E any](name string, init func(*Program[E]) any) Option[E] {
return func(program *Program[E]) {
program.setupSubcommands = append(program.setupSubcommands, subcommand[E]{
name: name,
init: init,
})
}
}
type Version struct {
Version string
APIVersion string
Hash string
Branch string
HgTopic string
Build string
}
func NewProgram(name string, version Version, options ...Option[struct{}]) *Program[struct{}] {
return NewProgramWithExt(name, version, struct{}{}, options...)
}
func NewProgramWithExt[E any](name string, version Version, ext E, options ...Option[E]) *Program[E] {
bootstrapParser := flags.NewNamedParser(name, flags.IgnoreUnknown)
parser := flags.NewNamedParser(name, flags.HelpFlag|flags.PassDoubleDash)
program := Program[E]{
Ext: ext,
Version: version,
BootstrapParser: bootstrapParser,
Parser: parser,
Logger: orusapi.DefaultLogger(os.Stdout),
}
for _, opt := range options {
opt(&program)
}
program.LoggingOptions = orusapi.MustLoggingOptions(
orusapi.NewLoggingOptions(&program.Logger, os.Stdout))
bootstrapParser.NamespaceDelimiter = "-"
bootstrapParser.EnvNamespaceDelimiter = "_"
bootstrapParser.EnvNamespace = strings.ToUpper(name)
if _, err := bootstrapParser.AddGroup(
"Configuration", "Configuration file", &program.ConfigFileOption,
); err != nil {
panic(err)
}
if _, err := parser.AddGroup("Configuration", "Configuration file", &program.ConfigFileOption); err != nil {
panic(err)
}
if _, err := parser.AddGroup("Info", "Info options", &program.InfoOptions); err != nil {
panic(err)
}
{
g, err := parser.AddGroup("Logging", "Logging options", program.LoggingOptions)
if err != nil {
panic(err)
}
g.Namespace = "log"
g.EnvNamespace = "LOG"
}
program.ServeCmd = SetupServeCmd(&program)
program.GenerateConfigCmd = SetupGenerateConfigCmd(&program)
_ = SetupVersionCmd(&program)
if program.hasDB {
g, err := parser.AddGroup("Database", "Database options", &program.DatabaseOptions)
if err != nil {
panic(err)
}
g.Namespace = "db"
g.EnvNamespace = "DB"
program.MigrateCmd = SetupMigrateCmd(&program)
}
for _, subcmd := range program.setupSubcommands {
if _, err := parser.AddCommand(subcmd.name, subcmd.name, "", subcmd.init(&program)); err != nil {
panic(err)
}
}
for _, pi := range program.postInit {
pi(&program)
}
return &program
}
func PostInit[E any](pi func(program *Program[E])) Option[E] {
return func(program *Program[E]) {
program.postInit = append(program.postInit, pi)
}
}
func (program *Program[E]) ParseArgs(args []string) int {
if _, err := program.BootstrapParser.ParseArgs(args); err != nil {
program.Logger.Err(err).Msg("could not parse command line")
return 1
}
if program.ConfigFileOption.ConfigFile != "" {
program.Logger.Debug().Str("configfile", program.ConfigFileOption.ConfigFile).Msg("parsing configuration file")
iniParser := flags.NewIniParser(program.Parser)
if err := iniParser.ParseFile(program.ConfigFileOption.ConfigFile); err != nil {
program.Logger.Err(err).Msg("")
return 1
}
}
if _, err := program.Parser.Parse(); err != nil {
code := 1
var flagsErr *flags.Error
if errors.As(err, &flagsErr) {
if flagsErr.Type == flags.ErrHelp {
code = 0
// this error actually contains a help message for the user
// so we print it on the console
fmt.Println(err)
} else {
program.Logger.Error().Msg(err.Error())
}
} else {
program.Logger.Err(err).Msg("")
}
return code
}
return 0
}
func (program *Program[E]) EnsureDB(automigrate bool) error {
if !program.hasDB {
return nil
}
program.Logger.Debug().Msg("Connecting to the database...")
if automigrate {
if err := database.AutoMigrate(
program.DatabaseOptions.DSN, program.dbMigrateSource, program.Logger,
); err != nil {
return err
}
} else {
if err := database.IsUptodate(
program.DatabaseOptions.DSN, program.dbMigrateSource,
); err != nil {
return err
}
}
db, err := program.DatabaseOptions.Open()
if err != nil {
return err
}
if err := db.Ping(); err != nil {
return err
}
program.DB = db
program.Logger.Info().Msg("Connected to the database")
return nil
}
func (program *Program[E]) CloseDB() {
if !program.hasDB || program.DB == nil {
return
}
if err := program.DB.Close(); err != nil {
program.Logger.Err(err).Msg("could not close database connection properly")
}
}