diff --git a/.vscode/settings.json b/.vscode/settings.json index 7e2d3bd..df5bfac 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,14 @@ } }, + "cSpell.words": [ + "ftype", + "nolint", + "goconst", + "TZUTC", + "webserver" + ], + "comment": { "description": "Uncomment to enable goproxy and gosumdb." "go.toolsEnvVars": { diff --git a/internal/config/envconfig.go b/internal/config/envconfig.go new file mode 100644 index 0000000..3eaf00b --- /dev/null +++ b/internal/config/envconfig.go @@ -0,0 +1,272 @@ +package config + +import ( + "flag" + "fmt" + "os" + "reflect" + "strconv" + "strings" +) + +type structInfo struct { + Name string + Alt string + Info string + Key string + Field reflect.Value + Tags reflect.StructTag + Type reflect.Type + DefaultValue interface{} + Secret interface{} +} + +// getEnvString returns string from environment variable +func getEnvString(env string, def string) string { + val := os.Getenv(env) + + if len(val) == 0 { + return def + } + + return "" +} + +// getEnvBool returns boolean from environment variable +func getEnvBool(env string, def bool) (bool, error) { + val := os.Getenv(env) + + if len(val) == 0 { + return def, nil + } + + ret, err := strconv.ParseBool(val) + if err != nil { + return false, fmt.Errorf("Environment variable is not of type bool: %v", env) + } + + return ret, nil +} + +// getEnvInt returns int from environment variable +func getEnvInt(env string, def int) (int, error) { + val := os.Getenv(env) + + if len(val) == 0 { + return def, nil + } + + ret, err := strconv.Atoi(val) + if err != nil { + return 0, fmt.Errorf("Environment variable is not of type int: %v", env) + } + + return ret, nil +} + +// getEnvInt64 return int64 from environment variable +func getEnvInt64(env string, def int64) (int64, error) { + val := os.Getenv(env) + + if len(val) == 0 { + return def, nil + } + + ret, err := strconv.ParseInt(val, 10, 64) + if err != nil { + return 0, fmt.Errorf("Environment variable is not of type int64: %v", env) + } + + return ret, nil +} + +// getEnvFloat64 return float64 from environment variable +func getEnvFloat64(env string, def float64) (float64, error) { + val := os.Getenv(env) + + if len(val) == 0 { + return def, nil + } + + ret, err := strconv.ParseFloat(val, 64) + if err != nil { + return 0, fmt.Errorf("Environment variable is not of type float64: %v", env) + } + + return ret, nil +} + +func getStructInfo(spec interface{}) ([]structInfo, error) { + s := reflect.ValueOf(spec) + + if s.Kind() != reflect.Pointer { + return []structInfo{}, fmt.Errorf("getStructInfo() was sent a %s instead of a pointer to a struct.\n", s.Kind()) + } + + s = s.Elem() + if s.Kind() != reflect.Struct { + return []structInfo{}, fmt.Errorf("getStructInfo() was sent a %s instead of a struct.\n", s.Kind()) + } + typeOfSpec := s.Type() + + infos := make([]structInfo, 0, s.NumField()) + for i := 0; i < s.NumField(); i++ { + f := s.Field(i) + ftype := typeOfSpec.Field(i) + + ignored, _ := strconv.ParseBool(ftype.Tag.Get("ignored")) + if !f.CanSet() || ignored { + continue + } + + for f.Kind() == reflect.Pointer { + if f.IsNil() { + if f.Type().Elem().Kind() != reflect.Struct { + break + } + f.Set(reflect.New(f.Type().Elem())) + } + f = f.Elem() + } + + secret, err := typeConversion(ftype.Type.String(), ftype.Tag.Get("secret")) + if err != nil { + secret = false + } + + var desc string + if len(ftype.Tag.Get("info")) != 0 { + desc = fmt.Sprintf("(%s) %s", strings.ToUpper(ftype.Tag.Get("env")), ftype.Tag.Get("info")) + } else { + desc = fmt.Sprintf("(%s)", strings.ToUpper(ftype.Tag.Get("env"))) + } + + info := structInfo{ + Name: ftype.Name, + Alt: strings.ToUpper(ftype.Tag.Get("env")), + Info: desc, + Key: ftype.Name, + Field: f, + Tags: ftype.Tag, + Type: ftype.Type, + Secret: secret, + } + if info.Alt != "" { + info.Key = info.Alt + } + info.Key = strings.ToUpper(info.Key) + if ftype.Tag.Get("default") != "" { + v, err := typeConversion(ftype.Type.String(), ftype.Tag.Get("default")) + if err != nil { + return []structInfo{}, err + } + info.DefaultValue = v + } + infos = append(infos, info) + } + return infos, nil +} + +func typeConversion(t, v string) (interface{}, error) { + switch t { + case "string": //nolint:goconst + return v, nil + case "int": //nolint:goconst + return strconv.ParseInt(v, 10, 0) + case "int8": + return strconv.ParseInt(v, 10, 8) + case "int16": + return strconv.ParseInt(v, 10, 16) + case "int32": + return strconv.ParseInt(v, 10, 32) + case "int64": + return strconv.ParseInt(v, 10, 64) + case "uint": + return strconv.ParseUint(v, 10, 0) + case "uint16": + return strconv.ParseUint(v, 10, 16) + case "uint32": + return strconv.ParseUint(v, 10, 32) + case "uint64": + return strconv.ParseUint(v, 10, 64) + case "float32": + return strconv.ParseFloat(v, 32) + case "float64": + return strconv.ParseFloat(v, 64) + case "complex64": + return strconv.ParseComplex(v, 64) + case "complex128": + return strconv.ParseComplex(v, 128) + case "bool": //nolint:goconst + return strconv.ParseBool(v) + } + return nil, fmt.Errorf("Unable to identify type.") +} + +func (cfg *Config) parseFlags(cfgInfo []structInfo) { + for _, info := range cfgInfo { + switch info.Type.String() { + case "string": + var dv string + + if info.DefaultValue != nil { + dv = info.DefaultValue.(string) + } + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string) + flag.StringVar(p, info.Name, getEnvString(info.Alt, dv), info.Info) + case "bool": + var dv bool + + if info.DefaultValue != nil { + dv = info.DefaultValue.(bool) + } + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool) + retVal, err := getEnvBool(info.Alt, dv) + if err != nil { + cfg.Log.Error("Error Encountered", "error", err) + os.Exit(1) + } + flag.BoolVar(p, info.Name, retVal, info.Info) + case "int": + var dv int + + if info.DefaultValue != nil { + dv = info.DefaultValue.(int) + } + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int) + retVal, err := getEnvInt(info.Alt, dv) + if err != nil { + cfg.Log.Error("Error Encountered", "error", err) + os.Exit(1) + } + flag.IntVar(p, info.Name, retVal, info.Info) + case "int64": + var dv int64 + + if info.DefaultValue != nil { + dv = info.DefaultValue.(int64) + } + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int64) + retVal, err := getEnvInt64(info.Alt, dv) + if err != nil { + cfg.Log.Error("Error Encountered", "error", err) + os.Exit(1) + } + flag.Int64Var(p, info.Name, retVal, info.Info) + case "float64": + var dv float64 + + if info.DefaultValue != nil { + dv = info.DefaultValue.(float64) + } + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*float64) + retVal, err := getEnvFloat64(info.Alt, dv) + if err != nil { + cfg.Log.Error("Error Encountered", "error", err) + os.Exit(1) + } + flag.Float64Var(p, info.Name, retVal, info.Info) + } + } + flag.Parse() +} diff --git a/internal/config/initialize.go b/internal/config/initialize.go new file mode 100644 index 0000000..42ad7dd --- /dev/null +++ b/internal/config/initialize.go @@ -0,0 +1,36 @@ +package config + +import ( + "os" + "time" +) + +func Init() *Config { + cfg := New() + + cfgInfo, err := getStructInfo(cfg) + if err != nil { + cfg.Log.Error("Unable to initialize program parameters", "error", err) + os.Exit(1) + } + + // get command line flags + cfg.parseFlags(cfgInfo) + + // set logging Level + setLogLevel(cfg) + + // set timezone & time format + cfg.TZUTC, _ = time.LoadLocation("UTC") + cfg.TZLocal, err = time.LoadLocation(cfg.TimeZoneLocal) + if err != nil { + cfg.Log.Error("Unable to parse timezone string", "error", err) + os.Exit(1) + } + + // print running config + printRunningConfig(cfg, cfgInfo) + + // return configuration + return cfg +} diff --git a/internal/config/struct-config.go b/internal/config/struct-config.go new file mode 100644 index 0000000..c4d4c46 --- /dev/null +++ b/internal/config/struct-config.go @@ -0,0 +1,73 @@ +package config + +import ( + "log/slog" + "reflect" + "strconv" + "time" +) + +type Config struct { + // time configuration + TimeFormat string `default:"2006-01-02 15:04:05" env:"TIME_FORMAT"` + TimeZoneLocal string `default:"America/Chicago" env:"TIME_ZONE"` + TZLocal *time.Location `ignored:"true"` + TZUTC *time.Location `ignored:"true"` + + // logging + LogLevel int `default:"50" env:"log_level"` + Log *slog.Logger `ignored:"true"` + + // webserver + WebServerPort int `default:"8080" env:"webserver_port"` + WebServerIP string `default:"0.0.0.0" env:"webserver_ip"` + WebServerReadTimeout int `default:"5" env:"webserver_read_timeout"` + WebServerWriteTimeout int `default:"1" env:"webserver_write_timeout"` + WebServerIdleTimeout int `default:"2" env:"webserver_idle_timeout"` +} + +// New initializes the config variable for use with a prepared set of defaults. +func New() *Config { + return &Config{} +} + +func setLogLevel(cfg *Config) { + logLevel := &slog.LevelVar{} + + switch { + // error + case cfg.LogLevel <= 20: + logLevel.Set(slog.LevelError) + cfg.Log.Info("Log level updated", "level", slog.LevelError) + // warning + case cfg.LogLevel > 20 && cfg.LogLevel <= 40: + logLevel.Set(slog.LevelWarn) + cfg.Log.Info("Log level updated", "level", slog.LevelWarn) + // info + case cfg.LogLevel > 40 && cfg.LogLevel <= 60: + logLevel.Set(slog.LevelInfo) + cfg.Log.Info("Log level updated", "level", slog.LevelInfo) + // debug + case cfg.LogLevel > 60: + logLevel.Set(slog.LevelDebug) + cfg.Log.Info("Log level updated", "level", slog.LevelDebug) + } + // set default logger + slog.SetDefault(cfg.Log) +} + +func printRunningConfig(cfg *Config, cfgInfo []structInfo) { + for _, info := range cfgInfo { + switch info.Type.String() { + case "string": + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*string) + cfg.Log.Debug("Running Configuration", info.Alt, *p) + case "bool": + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*bool) + cfg.Log.Debug("Running Configuration", info.Alt, strconv.FormatBool(*p)) + case "int": + p := reflect.ValueOf(cfg).Elem().FieldByName(info.Name).Addr().Interface().(*int) + cfg.Log.Debug("Running Configuration", info.Alt, strconv.FormatInt(int64(*p), 10)) + } + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..5f00e4c --- /dev/null +++ b/readme.md @@ -0,0 +1,19 @@ +# GoLang Base + +## Table of Contents + +1. [Information](#information) +1. [Usage](#usage) + +## Information + +This is a base repository for starting a GoLang project. It includes a set of linters, rules, and other configurations to make starting a new project easier. + +## Usage + +Clone this repository and run the following command: + + ```bash +PRJCT="example.com/newproject" +go mod init "${PRJCT}" +``` \ No newline at end of file