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/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"` } 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 struct { name string init func(*Program) any } type Program struct { BootstrapParser *flags.Parser Parser *flags.Parser Logger zerolog.Logger ConfigFileOption ConfigFile InfoOptions InfoOptions LoggingOptions *orusapi.LoggingOptions DatabaseOptions database.Options XbusOptions XbusOptions DB *sqlx.DB ServeCmd *ServeCmd MigrateCmd *MigrateCmd GenerateConfigCmd *GenerateConfigCmd hasDB bool dbMigrateSource source.Driver setupXbusActors func(*Program) []XbusActorFactory xbusActorNames []string setupHandler func(*Program) http.Handler setupSubcommands []subcommand } type Option func(program *Program) func WithHandler(factory func(*Program) http.Handler) Option { return func(program *Program) { program.setupHandler = factory } } func WithXbusActors(factories func(*Program) []XbusActorFactory) Option { return func(program *Program) { 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 WithDatabase(migrateSource source.Driver) Option { return func(program *Program) { program.hasDB = true program.dbMigrateSource = migrateSource } } func WithSubcommand(name string, init func(*Program) any) Option { return func(program *Program) { program.setupSubcommands = append(program.setupSubcommands, subcommand{ name: name, init: init, }) } } func NewProgram(name string, options ...Option) *Program { bootstrapParser := flags.NewNamedParser(name, flags.IgnoreUnknown) parser := flags.NewNamedParser(name, flags.HelpFlag|flags.PassDoubleDash) program := Program{ 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) 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) } } return &program } func (program *Program) 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 if fe, ok := err.(*flags.Error); ok { if fe.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) 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) 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") } }