// Package helpers working with logging // and other non-main/other help-function. package helpers import ( "encoding/json" "fmt" "io" "os" "path/filepath" "regexp" "strconv" "strings" "time" "github.com/google/uuid" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "github.com/KusoKaihatsuSha/startup/internal/order" ) // Logger output types const ( LogOutNullTS = iota LogOutFastTS LogOutHumanTS ) // Logger Error output types const ( LogErrNullTS = iota + 100 LogErrFastTS LogErrHumanTS ) // LogNull Logger null const ( LogNull = 1000 ) // second as default const ( defaultTimePostfix = "s" ) func init() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix zerolog.SetGlobalLevel(zerolog.DebugLevel) } // ToLog splits notifications into Error and Debug(text). func ToLog(err any, text ...string) { if err == nil || err == "" { return } switch val := err.(type) { case error: if val.Error() != "" { log.Error().Msgf("%s |-> %v", text, val) } default: log.Debug().Msgf("%s |-> %v", text, val) } } // ToLogWithType splits notifications into Error and Debug(text). Using timestamped. func ToLogWithType(err any, typ int) { if err == nil || err == "" { return } if typ == LogNull { std := logger(typ) std.Debug().Msgf("%v", err) return } switch val := err.(type) { case error: if val.Error() != "" { std := logger(typ + LogErrNullTS) std.Error().Msgf("%v", val) } default: std := logger(typ) std.Debug().Msgf("%v", val) } } // logger return Main logger with timestamp. func logger(typ int) zerolog.Logger { switch typ { case LogOutNullTS: return zerolog.New(os.Stdout).With().Logger() case LogErrNullTS: return zerolog.New(os.Stderr).With().Logger() case LogOutFastTS: return zerolog.New(os.Stdout).With().Timestamp().Logger() case LogErrFastTS: return zerolog.New(os.Stderr).With().Timestamp().Logger() case LogOutHumanTS: return zerolog.New(os.Stdout).With().Str("time", time.Now().Format("200601021504")).Logger() case LogErrHumanTS: return zerolog.New(os.Stderr).With().Str("time", time.Now().Format("200601021504")).Logger() case LogNull: return zerolog.New(io.Discard).With().Logger() default: return zerolog.New(os.Stderr).With().Timestamp().Logger() } } // DeleteFile delete file in temp folder. Actually means delete any file by path func DeleteFile(filename string) { filename = separatorCorrect(filename) ToLog(os.RemoveAll(filename)) } // FileExist checking if file exists by path func FileExist(filename string) bool { if filename == "." { return false } _, err := os.Stat(filename) return err == nil } // ValidUUID - validation on the UUID func ValidUUID(v string) string { if tmp, err := uuid.Parse(v); err == nil { return tmp.String() } return uuid.New().String() } // ValidInt - validation on the int func ValidInt(v string) int64 { if tmp, err := strconv.ParseInt(v, 10, 64); err == nil { return tmp } return 0 } // ValidUint - validation on the uint func ValidUint(v string) uint64 { if tmp, err := strconv.ParseUint(v, 10, 64); err == nil { return tmp } return 0 } // ValidFloat - validation on the float func ValidFloat(v string) float64 { if tmp, err := strconv.ParseFloat(v, 64); err == nil { return tmp } return 0 } // ValidBool - validation on the boolean func ValidBool(v string) bool { if tmp, err := strconv.ParseBool(v); err == nil { return tmp } return false } // CloseFile close file. Using with defer func CloseFile(file *os.File) { if err := file.Close(); err != nil { ToLog(err, fmt.Sprintf("file '%s' close error", file.Name())) } } // SettingsFile return map[string]string from the setting file func SettingsFile(filename string) (compare map[string]any) { filename = separatorCorrect(filename) if FileExist(filename) { f, err := os.ReadFile(filename) ToLog(err, fmt.Sprintf("settings file '%s' is not access", filename)) err = json.Unmarshal(f, &compare) ToLog(err, fmt.Sprintf("settings file '%s' parse error", filename)) } return } // CreateFile create file or temp file func CreateFile(filename string) string { filename = separatorCorrect(filename) if filename == "" || filename == "." { file, err := os.CreateTemp("", "*.tmpgo") defer CloseFile(file) ToLog(err, fmt.Sprintf("file '%s' not created", filename)) return file.Name() } else { file, err := os.Create(filename) defer CloseFile(file) ToLog(err, fmt.Sprintf("file '%s' not created", filename)) return file.Name() } } // ValidTempFile - validation type. // create same name file in Temp folder or create random temp file func ValidTempFile(filename string) string { filename = separatorCorrect(filename) _, file := filepath.Split(filename) if filename == "" || filename == "." { return CreateFile(filename) } filename = filepath.Join(os.TempDir(), file) if !FileExist(filename) { return CreateFile(filename) } return filename } // ValidConfigFile - validation type. // create same name file in Temp folder or create random temp file func ValidConfigFile(filename string) string { filename = separatorCorrect(filename) if !FileExist(filename) || filename == "" || filename == "." { return "" } return filename } // ValidFile - validation type. // Create file in not exist. func ValidFile(filename string) string { filename = separatorCorrect(filename) if filename == "" || filename == "." { return CreateFile(filename) } if !FileExist(filename) { CreateFile(filename) } return filename } // ValidDuration - validation on the duration func ValidDuration(v string) time.Duration { if v[0] == '-' || v[0] == '+' { v = v[1:] } l := len(v) - 1 if '0' <= v[l] && v[l] <= '9' { v += defaultTimePostfix } tmp, err := time.ParseDuration(v) ToLog(err, fmt.Sprintf("duration '%s' parse error", v)) return tmp } // ValidURL - validation on the url func ValidURL(v string) string { // trim prefix re := regexp.MustCompile(`^.*(://|^)[^/]+`) trimPrefix := re.FindString(v) re = regexp.MustCompile(`^.*(://|^)`) fullAddress := re.ReplaceAllString(trimPrefix, "") // trim port re = regexp.MustCompile(`^[^/:$]+`) address := re.FindString(fullAddress) // fill address if strings.TrimSpace(address) == "" { return "" } // check ip isIP := false re = regexp.MustCompile(`\d+`) isIPTest := re.ReplaceAllString(address, "") isIPTest = strings.ReplaceAll(isIPTest, ".", "") if strings.TrimSpace(isIPTest) == "" { isIP = true } // correct IP if isIP { re = regexp.MustCompile(`\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}`) addressIP := re.FindString(address) if strings.TrimSpace(addressIP) == "" { return "" } } // check and correct port re = regexp.MustCompile(`:.*`) correctPort := re.FindString(fullAddress) correctPort = strings.Replace(correctPort, ":", "", 1) re = regexp.MustCompile(`\D`) correctPort = re.ReplaceAllString(correctPort, "") correctPort = strings.Replace(correctPort, ":", "", 1) if strings.TrimSpace(correctPort) == "" { return address + ":80" } return address + ":" + correctPort } func separatorCorrect(filename string) string { r := strings.NewReplacer("/", string(os.PathSeparator), "\\", string(os.PathSeparator)) return filepath.Clean(r.Replace(strings.TrimSpace(filename))) } func FileConfExistInStages(stages ...order.Stages) bool { for _, stage := range stages { if stage == order.FILE { return true } } return false } func printDebug(stages ...order.Stages) { fmt.Println("Structure filling order:") for k, stage := range stages { prefix := "" suf := "\n" if k != len(stages)-1 { suf = " ↣ " } if k == 0 { prefix = "\t" } switch stage { case order.FLAG: fmt.Printf("%s%s%s", prefix, "[Flags]", suf) case order.FILE: fmt.Printf("%s%s%s", prefix, "[JSON File]", suf) case order.ENV: fmt.Printf("%s%s%s", prefix, "[Environments]", suf) } } } func PrintDebug(stages ...order.Stages) { for _, stage := range stages { switch stage { case order.NoPreloadConfig: fmt.Printf("%s - Disable find config in other places. Only in list\n", "NoPreloadConfig") printDebug(stages...) return case order.PreloadConfigFlagThenEnv: fmt.Printf("%s - Preload find config in Flag then Env\n", "PreloadConfigFlagThenEnv") printDebug(stages...) return case order.PreloadConfigFlag: fmt.Printf("%s - Preload find config in Flag\n", "PreloadConfigFlag") printDebug(stages...) return case order.PreloadConfigEnv: fmt.Printf("%s - Preload find config in Env\n", "PreloadConfigEnv") printDebug(stages...) return } } if stages == nil { fmt.Printf("%s - only defaults\n", "EMPTY") } else { fmt.Printf("%s - Preload find config in Env then Flag\n", "PreloadConfigEnvThenFlag/Default") printDebug(stages...) } } func PresetPreload(stages ...order.Stages) []order.Stages { for _, v := range stages { switch v { case order.NoPreloadConfig: return stages case order.PreloadConfigEnvThenFlag: return []order.Stages{order.ENV, order.FLAG} case order.PreloadConfigFlagThenEnv: return []order.Stages{order.FLAG, order.ENV} case order.PreloadConfigFlag: return []order.Stages{order.FLAG} case order.PreloadConfigEnv: return []order.Stages{order.ENV} } } return []order.Stages{order.ENV, order.FLAG} }
package tags import ( "encoding/json" "errors" "flag" "fmt" "io" "os" "path/filepath" "reflect" "strings" "testing" "github.com/fatih/color" "github.com/KusoKaihatsuSha/startup/internal/helpers" "github.com/KusoKaihatsuSha/startup/internal/order" "github.com/KusoKaihatsuSha/startup/internal/validation" ) const ( defaultTag = "default" flagTag = "flag" EnvironmentTag = "env" helpTextTag = "help" validationTag = "valid" jsonTag = "json" testTrigger = "-test." ) var durationTypesMap = map[string]string{ "ns": "Nanosecond", "us": "Microsecond", "ms": "Millisecond", "s": "Second", "m": "Minute", "h": "Hour", } // Tags consist information when reading/valid configs type Tags map[string]Tag // Tag of TagInfo store tags Config struct type Tag struct { ConfigFile func(map[string]any) Tag Valid func() any DummyFlags func() Tag Env func() Tag Flag func() Tag FlagSet *flag.FlagSet Flags map[string]*flag.Flag Name string Annotation } // Annotation - store general values type Annotation struct { valid string json string desc string env string def string } type storage struct { Flag bool Store any StoreString string Type reflect.StructField Default string Desc string Env string JSON string Name string } // Set 'flag' interface implementation func (s *storage) Set(value string) error { var err error s.Store, err = comparatorStringType(nil, s.Type, value, s.Flag) s.StoreString = value helpers.ToLog("", fmt.Sprintf("set flag data '%s' %v", value, err)) // skip info and error parse // skip error for custom types return nil } // String - Stringer interface implementation func (s *storage) String() string { return fmt.Sprint(s.Store) } // Fill - filling the 'tag' func Fill[T any](field string, order ...order.Stages) Tag { var t T tagData := Tag{} tagData.Flags = make(map[string]*flag.Flag) tagData.Name = field tagData.FlagSet = flag.NewFlagSet("", flag.ContinueOnError) fieldByName, exist := reflect.TypeOf(t).FieldByName(tagData.Name) if !exist { return Tag{} } if v, ok := fieldByName.Tag.Lookup(helpTextTag); ok { tagData.desc = v } if v, ok := fieldByName.Tag.Lookup(defaultTag); ok { tagData.def = v } if v, ok := fieldByName.Tag.Lookup(EnvironmentTag); ok { tagData.env = strings.ToUpper(v) } if v, ok := fieldByName.Tag.Lookup(jsonTag); ok { tagData.json = v } if v, ok := fieldByName.Tag.Lookup(flagTag); ok { fv := new(storage) fv.Type = fieldByName fv.Flag = true fv.Default = tagData.def fv.Env = tagData.env fv.JSON = tagData.json fv.Desc = tagData.desc fv.Name = v err := fv.Set(tagData.def) helpers.ToLog("", fmt.Sprintf("set flag data '%s' %v", tagData.def, err)) // skip info and error parse // handy bool flag without argument if fv.Type.Type.Name() == "bool" { for i, arg := range os.Args { // only flag if arg == "-"+fv.Name { os.Args[i] = arg + "=true" } } } for _, fTag := range strings.Split(v, ",") { tagData.FlagSet.Var(fv, fTag, tagData.desc) fl := tagData.FlagSet.Lookup(fTag) tagData.Flags[fl.Name] = fl } } if v, ok := fieldByName.Tag.Lookup(validationTag); ok { tagData.valid = v } tagData.ConfigFile = func(m map[string]any) Tag { for k, v := range m { if tagData.json == k { for _, f := range tagData.Flags { f.DefValue = fmt.Sprintf("%v", v) err := f.Value.Set(f.DefValue) helpers.ToLog(err, fmt.Sprintf("set flag data '%s' error", f.DefValue)) } } } return tagData } tagData.DummyFlags = func() Tag { for _, v := range os.Args { if strings.Contains(v, testTrigger) { var _ = func() bool { testing.Init() return true }() break } } for _, f := range tagData.Flags { if ff := flag.Lookup(f.Name); ff == nil { flag.Var(f.Value, f.Name, f.Usage) } } return tagData } tagData.Env = func() Tag { env, ok := os.LookupEnv(tagData.env) if ok { for _, f := range tagData.Flags { err := f.Value.Set(env) helpers.ToLog(err, fmt.Sprintf("set flag data '%s' error", env)) break } } return tagData } tagData.Flag = func() Tag { def := tagData.FlagSet.Output() tagData.FlagSet.SetOutput(io.Discard) for _, arg := range os.Args { err := tagData.FlagSet.Parse([]string{arg}) helpers.ToLogWithType(err, helpers.LogNull) } tagData.FlagSet.SetOutput(def) return tagData } flag.Usage = func() { PrintDefaults(flag.CommandLine, order...) } tagData.Valid = func() any { var stringValueType any var stringValueTypeString string for _, f := range tagData.Flags { stringValueType = f.Value.(*storage).Store stringValueTypeString = f.Value.(*storage).StoreString break } for _, v := range validation.Valids { if tagData.valid == fmt.Sprint(v) { if ret, ok := v.Valid(stringValueTypeString, stringValueType); ok { return ret } } } return stringValueType } return tagData } // PrintDefaults - printing help func PrintDefaults(f *flag.FlagSet, o ...order.Stages) { yellow := color.New(color.FgYellow).SprintfFunc() red := color.New(color.FgRed).SprintfFunc() cyan := color.New(color.FgCyan).SprintfFunc() var def strings.Builder def.WriteString("Order of priority for settings (low -> high): \n") for k, v := range o { if k > 0 { def.WriteString(" --> ") } switch v { case order.FLAG: def.WriteString("Flags") case order.FILE: def.WriteString("Config file (JSON)") case order.ENV: def.WriteString("Environment") } } _, err := fmt.Fprint(f.Output(), yellow("%s \n\n", def.String())) helpers.ToLog(err, fmt.Sprintf("print data '%s' error", def.String())) f.VisitAll(func(lf *flag.Flag) { switch lf.Value.(type) { case *storage: // dummy default: return } var b strings.Builder _, errPrintf := fmt.Fprintf(&b, " %s%s", red("%s", "-"), red("%s", lf.Name)) helpers.ToLog(errPrintf, "print data error") // Two spaces before -; see next two comments. name, usage := flag.UnquoteUsage(lf) if len(name) > 0 { b.WriteString(" ") } // Boolean flags of one ASCII letter are so common we // treat them specially, putting their usage on the same line. if b.Len() <= 4 { // space, space, '-', 'x'. b.WriteString("\t") } else { // Four spaces before the tab triggers good alignment // for both 4- and 8-space tab stops. b.WriteString("\n \t") } vvv := comparatorInfo(lf.Value.(*storage), o...) b.WriteString(strings.ReplaceAll(cyan("%s", usage), "\n", "\n \t")) b.WriteString("\n \t") b.WriteString(fmt.Sprintf("Default value: %v\n \t", lf.DefValue)) b.WriteString(strings.ReplaceAll(yellow("%s", vvv), "\n", "\n \t")) _, errPrint := fmt.Fprint(f.Output(), b.String(), "\n") helpers.ToLog(errPrint, "print data error") }) } func comparatorStringType(flagType flag.Value, reflectType reflect.StructField, stringValue any, onlyForMarshaller bool) (any, error) { reflectTypeName := strings.ToLower(strings.TrimSpace(reflectType.Type.Name())) switch reflectTypeName { case "string": return stringValue, nil case "bool": return helpers.ValidBool(fmt.Sprintf("%v", stringValue)), nil case "duration": return helpers.ValidDuration(fmt.Sprintf("%v", stringValue)), nil case "int8", "int16", "int32", "int64", "rune": return helpers.ValidInt(fmt.Sprintf("%v", stringValue)), nil case "int": return int(helpers.ValidInt(fmt.Sprintf("%v", stringValue))), nil case "uint8", "uint16", "uint32", "uint64": return helpers.ValidUint(fmt.Sprintf("%v", stringValue)), nil case "uint": return uint(helpers.ValidUint(fmt.Sprintf("%v", stringValue))), nil case "float32", "float64": return helpers.ValidFloat(fmt.Sprintf("%v", stringValue)), nil default: if method, ok := reflect.PointerTo(reflectType.Type).MethodByName("UnmarshalText"); ok { in := make([]reflect.Value, method.Type.NumIn()) yyy := reflect.New(reflectType.Type).Interface() in[0] = reflect.ValueOf(yyy) in[1] = reflect.ValueOf([]byte(fmt.Sprintf("%v", stringValue))) method.Func.Call(in)[0].Interface() return in[0].Elem().Interface(), nil } yyy := reflect.New(reflectType.Type).Interface() return reflect.ValueOf(yyy).Elem().Interface(), errors.New("parse error") } } func comparatorInfo(t *storage, o ...order.Stages) string { tt := t.Type.Type.Name() fileName, err := os.Executable() if err != nil { fileName = "appImageBinary" } else { fileName = filepath.Base(fileName) } ret := "" reflectTypeName := strings.ToLower(strings.TrimSpace(tt)) for _, v := range o { switch reflectTypeName { case "string": switch v { case order.FLAG: ret += sample(fileName, t.Name, t.Default) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } case "bool": switch v { case order.FLAG: ret += sampleBool(fileName, t.Name) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } case "duration": switch v { case order.FLAG: ret += durationSample(fileName, t.Name, t.Default) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } case "int8", "int16", "int32", "int64", "rune": switch v { case order.FLAG: ret += sample(fileName, t.Name, t.Default) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } case "int": switch v { case order.FLAG: ret += sample(fileName, t.Name, t.Default) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } case "uint8", "uint16", "uint32", "uint64": switch v { case order.FLAG: ret += sample(fileName, t.Name, t.Default) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } case "uint": switch v { case order.FLAG: ret += sample(fileName, t.Name, t.Default) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } case "float32", "float64": switch v { case order.FLAG: ret += sample(fileName, t.Name, fmt.Sprintf("%f", helpers.ValidFloat(t.Default))) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } default: switch v { case order.FLAG: // ret += sample(fileName, flagName, def, flagOrder) case order.FILE: ret += sampleJson(t.JSON, t.Store) case order.ENV: ret += sampleEnv(t.Env, t.Default) } } } return ret } func sampleEnv(envValue, def string) string { return fmt.Sprintf("Sample environment:\t%s=%s\n", strings.ToUpper(envValue), def) } func sampleJson(jsonValue string, def any) string { m := make(map[string]any, 1) m[jsonValue] = def out, err := json.MarshalIndent(m, "", " ") if err != nil { return "" } return fmt.Sprintf("Sample JSON config:\t\n%s\n", string(out)) } func sample(fileName, flagName, def string) string { return fmt.Sprintf("Sample flag value:\t%s -%s=%s\n", fileName, flagName, def) } func sampleBool(fileName, flagName string) string { return fmt.Sprintf("Sample(TRUE):\t%s -%s\n", fileName, flagName) + fmt.Sprintf("Sample(default):\t%s\n", fileName) + extSample("TRUE", fileName, flagName, "true") + extSample("FALSE", fileName, flagName, "false") + extSample("TRUE", fileName, flagName, "1") + extSample("FALSE", fileName, flagName, "0") + extSample("TRUE", fileName, flagName, "t") + extSample("FALSE", fileName, flagName, "f") } func extSample(value, fileName, flagName, def string) string { return fmt.Sprintf("Sample(%s):\t%s -%s=%s\n", value, fileName, flagName, def) } func durationSample(fileName, flagName, def string) string { return durationSample1(fileName, flagName) + durationSample2(fileName, flagName) + durationSample3(fileName, flagName) } func durationSample1(fileName, flagName string) (print string) { for k, v := range durationTypesMap { print += extSample(v, fileName, flagName, "1"+k) } return } func durationSample2(fileName, flagName string) string { return extSample("1 Hour 2 Minutes and 3 Seconds", fileName, flagName, "1h2m3s") } func durationSample3(fileName, flagName string) string { return extSample("111 Seconds", fileName, flagName, "111") }
package validation import "github.com/KusoKaihatsuSha/startup/internal/helpers" var ( Valids []Valid ) type Valid interface { Valid(string, any) (any, bool) } // Add - add custom validation. Example in interface docs. func Add(value ...Valid) { Valids = append(Valids, value...) } var ( defFileConfigValidation defFileConfigValid = "default_configuration_file" tmpFileValidation tmpFileValid = "tmp_file" fileValidation fileValid = "file" urlValidation urlValid = "url" boolValidation boolValid = "bool" intValidation intValid = "int" floatValidation floatValid = "float" durationValidation durationValid = "duration" uuidValidation uuidValid = "uuid" ) // Default validations type ( defFileConfigValid string tmpFileValid string fileValid string urlValid string boolValid string intValid string floatValid string durationValid string uuidValid string ) func init() { // Will add the default validation checks in the handle of the struct Add( defFileConfigValidation, tmpFileValidation, fileValidation, urlValidation, boolValidation, intValidation, floatValidation, durationValidation, uuidValidation, ) } // Valid Implements default validations func (o defFileConfigValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidConfigFile(value.(string)), true } // Valid Implements default validations func (o tmpFileValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidTempFile(value.(string)), true } // Valid Implements default validations func (o fileValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidFile(value.(string)), true } // Valid Implements default validations func (o urlValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidURL(value.(string)), true } // Valid Implements default validations func (o boolValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidBool(value.(string)), true } // Valid Implements default validations func (o intValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidInt(value.(string)), true } // Valid Implements default validations func (o floatValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidFloat(value.(string)), true } // Valid Implements default validations func (o durationValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidDuration(value.(string)), true } // Valid Implements default validations func (o uuidValid) Valid(stringValue string, value any) (any, bool) { return helpers.ValidUUID(value.(string)), true }
package startup import ( "flag" "fmt" "os" "path/filepath" "reflect" "strings" "sync" "github.com/KusoKaihatsuSha/startup/internal/helpers" "github.com/KusoKaihatsuSha/startup/internal/order" tags "github.com/KusoKaihatsuSha/startup/internal/tag" "github.com/KusoKaihatsuSha/startup/internal/validation" ) var ( this any onceFlags = sync.Once{} ) var DEBUG = false const ( // FLAG Get data from flags FLAG = order.FLAG // FILE Get data from json file FILE = order.FILE // ENV Get data from environments ENV = order.ENV // PreloadConfigEnvThenFlag - Get filepath config from environments and then flags // Default PreloadConfigEnvThenFlag = order.PreloadConfigEnvThenFlag // PreloadConfigFlagThenEnv - Get filepath config from flags and then environments PreloadConfigFlagThenEnv = order.PreloadConfigFlagThenEnv // PreloadConfigFlag - Get filepath config from flags PreloadConfigFlag = order.PreloadConfigFlag // PreloadConfigEnv - Get filepath config from environments PreloadConfigEnv = order.PreloadConfigEnv // NoPreloadConfig - Get filepath config file only from ordered stages NoPreloadConfig = order.NoPreloadConfig ) // Concat structs type temp[T any] struct { Stages []order.Stages tags.Tags CustomerConfiguration T Configuration configuration } /* Configuration consists of settings that are filled in at startup. Default fields: - "Config" - filepath for config file */ type configuration struct { Config string `json:"startup_configuration_file" default:"config.ini" flag:"config" env:"CONFIG" help:"Configuration settings file" valid:"default_configuration_file"` } /* AddValidation using for add custom validation Example: // Custom struct. Struct will be implement in program with selected 'Stages' variable. type Test struct { NewValid []string `json:"new-valid" default:"new valid is default" flag:"valid" text:"-" valid:"test"` } // Custom type. type testValid string // Custom validation. var testValidation testValid = "test" // Custom method. func (o testValid) Valid(stringValue string, value any) (any, bool) { return []string{stringValue + "+++"}, true } // add custom validation func MyFunc() { ... startup.AddValidation(testValidation) ... // Implement all types of configs (Json file -> Environment -> Flags). configurations := startup.Get[Test](startup.FILE, startup.ENV, startup.FLAG) // Test print. fmt.Println(configurations) } Default validations: - `tmp_file` - Check exist inside Temp folder and create if not exist (string in struct) - `file` - Check exist the filepath and create if not exist (string in struct) - `url` - Check url is correct (string in struct) - `bool` - Parse Bool (bool in struct) - `int` - Parse int (int64 in struct) - `float` - Parse float (float64 in struct) - `duration` - Parse duration (time.Duration in struct) - `uuid` - Check uuid. Return new if not exist (string in struct) Caution: flags are reserved: - config */ func AddValidation(value ...validation.Valid) { validation.Add(value...) } /* GetForce will initialize scan the flags, environment and config-file with the right order: - order.FLAG - flag - order.FILE - config file - order.ENV - environment Caution! flags are reserved: - config */ func GetForce[T any](stages ...order.Stages) T { // ---debug--- if DEBUG { helpers.PrintDebug(stages...) } // ---debug--- return get[T](stages...).CustomerConfiguration } func (t *temp[T]) prepare(config tags.Tags) *temp[T] { elements := reflect.ValueOf(&t.CustomerConfiguration).Elem() t.Tags = make(tags.Tags, elements.NumField()) for ii := 0; ii < elements.NumField(); ii++ { name := elements.Type().Field(ii).Name t.Tags[name] = tags.Fill[T](name, t.Stages...) } for configTagName, configTagData := range config { t.Tags[configTagName] = configTagData } return t } func (t *temp[T]) preparePreload() *temp[T] { elements := reflect.ValueOf(&t.Configuration).Elem() t.Tags = make(tags.Tags, elements.NumField()) for ii := 0; ii < elements.NumField(); ii++ { name := elements.Type().Field(ii).Name t.Tags[name] = tags.Fill[configuration](name, t.Stages...) } return t } func (t *temp[T]) fillPreload(fileExist bool) *temp[T] { for _, v := range t.Stages { switch v { case order.FLAG: // ---debug--- if DEBUG && fileExist { printing := "" for _, arg := range os.Args { if strings.Contains(arg, "-config=") { printing = "\tinfo about config file:\tGet filepath from flag '-config'" break } else { printing = "\tinfo about config file:\tFlag '-config' with filepath not set" } } fmt.Println(printing) } // ---debug--- t.flagNoParse() case order.FILE: t.conf() case order.ENV: // ---debug--- if DEBUG && fileExist { if os.Getenv("CONFIG") != "" { fmt.Println("\tinfo about config file:\tGet filepath from environment 'CONFIG'") } else { fmt.Println("\tinfo about config file:\tEnvironment 'CONFIG' with filepath not set") } } // ---debug--- t.env() } } return t } func (t *temp[T]) fill() *temp[T] { for _, v := range t.Stages { switch v { case order.FLAG: t.flag() case order.FILE: t.conf() case order.ENV: t.env() } } return t } func get[T any](stages ...order.Stages) temp[T] { fileExistInStages := helpers.FileConfExistInStages(stages...) preload := (&temp[T]{ Stages: helpers.PresetPreload(stages...), CustomerConfiguration: *new(T), Configuration: configuration{}, }). preparePreload(). fillPreload(fileExistInStages). conf() // ---debug--- if DEBUG && fileExistInStages { cfg := "" switch { case preload.Configuration.Config == "config.ini": if helpers.FileExist(preload.Configuration.Config) { cfg = "default config.ini" } else { cfg = "skipped default config.ini(not exist)" } case len(preload.Configuration.Config) > 0: if helpers.FileExist(preload.Configuration.Config) { // custom config file cfg = filepath.Base(preload.Configuration.Config) } else { cfg = "skipped config file(not exist)" } default: cfg = "not any config file" } fmt.Printf("FILE => %s\n", cfg) } // ---debug--- load := (&temp[T]{ Stages: stages, CustomerConfiguration: *new(T), Configuration: preload.Configuration, }).prepare(preload.Tags) load. fill(). valid() // ---debug--- if DEBUG { fmt.Printf("DATA => %v\n\n", load.CustomerConfiguration) } // ---debug--- return *load } /* Get will initialize scan the flags(one time), environment and config-file with the right order: - order.FLAG - flag - order.FILE - config file - order.ENV - environment Caution! flags are reserved: - config */ func Get[T any](stages ...order.Stages) T { onceFlags.Do( func() { this = get[T](stages...) }) return this.(temp[T]).CustomerConfiguration } func (t *temp[T]) dummy() *temp[T] { for _, v := range t.Tags { v.DummyFlags() } flag.Parse() return t } func (t *temp[T]) flag() *temp[T] { for _, v := range t.Tags { v.Flag() } return t.dummy() } func (t *temp[T]) flagNoParse() *temp[T] { for _, v := range t.Tags { v.Flag() } return t } func (t *temp[T]) env() *temp[T] { for _, v := range t.Tags { v.Env() } return t } // valid check info and make some correcting func (t *temp[T]) valid() *temp[T] { for k, v := range t.Tags { field := reflect.ValueOf(&t.CustomerConfiguration).Elem().FieldByName(k) if field.CanSet() { if valid := v.Valid(); valid != nil { field.Set(reflect.ValueOf(valid)) } else { field.SetZero() } } } return t } // conf get info from the configuration file func (t *temp[T]) conf() *temp[T] { confFile := "" for _, f := range t.Tags["Config"].Flags { if f.Value.String() != "" { confFile = f.Value.String() } } if confFile != "" { reflect.ValueOf(&t.Configuration).Elem().FieldByName("Config").Set(reflect.ValueOf(t.Tags["Config"].Valid())) tmpConfig := helpers.SettingsFile(t.Configuration.Config) for _, v := range t.Tags { v.ConfigFile(tmpConfig) } } return t }