From b8e669f2ccf922b572d130e5339867816e17c7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B5=D0=B9=20=D0=91=D0=B0?= =?UTF-8?q?=D0=B4=D1=8F=D0=B5=D0=B2?= Date: Mon, 21 Oct 2024 00:15:51 +0700 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B0=20=D1=87=D0=B0=D1=81=D1=82=D1=8C=20=D0=BE?= =?UTF-8?q?=D1=88=D0=B8=D0=B1=D0=BE=D0=BA=20=D0=BB=D0=B8=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D0=B0.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .golangci.yaml | 4 +- README.md | 11 ++-- cli.go | 157 ++++++++++++++++++++++++----------------------- cli_test.go | 47 +++++++------- config.go | 150 ++++++++++++++++++++++++-------------------- env.go | 61 ++++++++++-------- test/config.json | 10 +-- test/config.yaml | 8 +-- test/data.go | 74 +++++++++++----------- utils.go | 78 ++++++++++++----------- 10 files changed, 324 insertions(+), 276 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index a3c0985..92471d4 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -30,14 +30,14 @@ linters: - errchkjson - errname - errorlint - - exhaustive + # - exhaustive - fatcontext - forcetypeassert - funlen - gci - ginkgolinter - gocheckcompilerdirectives - - gochecknoglobals + # - gochecknoglobals - gochecknoinits - gochecksumtype - gocognit diff --git a/README.md b/README.md index 134e345..23db482 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Like JSON, Yaml, **config** uses tags to define configurations: | cli | Host string `cli:"host database host"` | Maps `Host` to a command line argument: **-host** or **--host** | | default | Port int `default:"8080"` | Defines the port with default value: **8080** | | separator | Path string `json:"path" separator:";"` | Separator is used to split string to a slice | +| usage | Usage string `usage:"host address"` | Usage description | #### 1. Data types @@ -145,11 +146,11 @@ Using **cli** keyword to define configuration name ```golang type Database struct { - Host string `cli:"host database host name"` - Port int `cli:"port database port"` - Username string `cli:"username database username" default:"admin"` - Password string `cli:"password database password" default:"admin"` - Log Log `cli:"log database log configurations"` + Host string `cli:"host" usage:"database host name"` + Port int `cli:"port" usage:"database port"` + Username string `cli:"username" default:"admin" usage:"database username"` + Password string `cli:"password" default:"admin" usage:"database password"` + Log Log `cli:"log" usage:"database log configurations"` } ``` diff --git a/cli.go b/cli.go index 607cb5e..fbf4623 100644 --- a/cli.go +++ b/cli.go @@ -20,7 +20,6 @@ import ( "fmt" "reflect" "strconv" - "strings" "unsafe" ) @@ -35,58 +34,73 @@ func newAnyValue(v reflect.Value) *anyValue { return &anyValue{any: v} } -func (v *anyValue) String() string { - kind := v.any.Kind() +func (av *anyValue) String() string { + kind := av.any.Kind() switch kind { case reflect.Bool: - return strconv.FormatBool(v.any.Bool()) + return strconv.FormatBool(av.any.Bool()) case reflect.String: - return v.any.String() + return av.any.String() case reflect.Int8, reflect.Int16, reflect.Int, reflect.Int32, reflect.Int64: - return strconv.FormatInt(v.any.Int(), 10) + return strconv.FormatInt(av.any.Int(), 10) case reflect.Uint8, reflect.Uint16, reflect.Uint, reflect.Uint32, reflect.Uint64: - return strconv.FormatUint(v.any.Uint(), 10) + return strconv.FormatUint(av.any.Uint(), 10) case reflect.Float32: - return strconv.FormatFloat(v.any.Float(), 'E', -1, 32) + return strconv.FormatFloat(av.any.Float(), 'E', -1, 32) case reflect.Float64: - return strconv.FormatFloat(v.any.Float(), 'E', -1, 64) + return strconv.FormatFloat(av.any.Float(), 'E', -1, 64) } - return fmt.Sprintf("unsupported type: %s", kind.String()) + + return "unsupported type: " + kind.String() } -func (av *anyValue) Set(v string) error { +const ( + Float32Size = 32 + Float64Size = 64 + Int8Size = 8 + Int16Size = 16 + Int32Size = 32 + Int64Size = 64 + Uint8Size = 8 + Uint16Size = 16 + Uint32Size = 32 + UintSize = 32 + Uint64Size = 64 +) + +func (av *anyValue) Set(value string) error { kind := av.any.Kind() switch kind { case reflect.String: - av.any.SetString(v) + av.any.SetString(value) case reflect.Float32: - return SetValueWithFloatX(av.any, v, 32) + return SetValueWithFloatX(av.any, value, Float32Size) case reflect.Float64: - return SetValueWithFloatX(av.any, v, 64) + return SetValueWithFloatX(av.any, value, Float64Size) case reflect.Int8: - return SetValueWithIntX(av.any, v, 8) + return SetValueWithIntX(av.any, value, Int8Size) case reflect.Int16: - return SetValueWithIntX(av.any, v, 16) + return SetValueWithIntX(av.any, value, Int16Size) case reflect.Int, reflect.Int32: - return SetValueWithIntX(av.any, v, 32) + return SetValueWithIntX(av.any, value, Int32Size) case reflect.Int64: - return SetValueWithIntX(av.any, v, 64) + return SetValueWithIntX(av.any, value, Int64Size) case reflect.Uint8: - return SetValueWithUintX(av.any, v, 8) + return SetValueWithUintX(av.any, value, Uint8Size) case reflect.Uint16: - return SetValueWithUintX(av.any, v, 16) + return SetValueWithUintX(av.any, value, Uint16Size) case reflect.Uint, reflect.Uint32: - return SetValueWithUintX(av.any, v, 32) + return SetValueWithUintX(av.any, value, Uint32Size) case reflect.Uint64: - return SetValueWithUintX(av.any, v, 64) + return SetValueWithUintX(av.any, value, Uint64Size) default: return fmt.Errorf("unsupported type: %s", kind.String()) } @@ -109,12 +123,13 @@ func (sv *sliceValue) String() string { return sv.value.String() } -func (sv *sliceValue) Set(v string) error { +func (sv *sliceValue) Set(value string) error { sp := sv.separator if sp == "" { sp = ":" } - return SetValueWithSlice(sv.value, v, sp) + + return SetValueWithSlice(sv.value, value, sp) } // errorHandling is a global flag.ErrorHandling @@ -129,10 +144,12 @@ var usageHandler UsageFunc = nil // Command defines a command line structure type Command struct { - Name string // command name - FlagSet *flag.FlagSet // command arguments - Usage string // command usage description - SubCommands map[string]*Command // sub-commands + Name string // Command name + prefix string // Args prefix + FlagSet *flag.FlagSet // Command arguments + Usage string // Command usage description + SubCommands map[string]*Command // Sub-commands + Args []string // Rest of args after parsing } // NewCLI creates a command with given name, the command will use default @@ -147,9 +164,9 @@ func NewCLI(name string) *Command { return &cmd } -// NewWith creates a command with given name, error handling and customized +// NewCliWith creates a command with given name, error handling and customized // usage function -func NewWith( +func NewCliWith( name string, errHandling flag.ErrorHandling, usageHandling UsageFunc, ) *Command { errorHandling = errHandling @@ -164,6 +181,7 @@ func NewWith( if usageHandler != nil { cmd.FlagSet.Usage = usageHandler(&cmd) } + return &cmd } @@ -188,24 +206,26 @@ func (c *Command) Init(i interface{}) error { } // parseValue parses a reflect.Value object and extracts cli definitions -func (c *Command) parseValue(v reflect.Value) error { - typeOfStruct := v.Type() +func (c *Command) parseValue(value reflect.Value) error { var err error - for i := 0; i < v.NumField() && err == nil; i++ { - valueOfField := v.Field(i) + typeOfStruct := value.Type() + + for i := 0; i < value.NumField() && err == nil; i++ { + valueOfField := value.Field(i) kindOfField := valueOfField.Kind() structOfField := typeOfStruct.Field(i) - if kindOfField == reflect.Ptr { + switch kindOfField { + case reflect.Ptr: if !valueOfField.IsNil() && valueOfField.CanSet() { cmd := c.createSubCommand(structOfField.Tag) err = cmd.Init(valueOfField.Interface()) } - } else if kindOfField == reflect.Struct { + case reflect.Struct: cmd := c.createSubCommand(structOfField.Tag) err = cmd.parseValue(valueOfField) - } else { + default: err = c.addFlag(valueOfField, structOfField) } } @@ -214,25 +234,25 @@ func (c *Command) parseValue(v reflect.Value) error { } // addFlag installs a command flag variable by flag API -func (c *Command) addFlag(v reflect.Value, f reflect.StructField) error { - cmdTag, ok := f.Tag.Lookup("cli") - if !ok || cmdTag == "" { +func (c *Command) addFlag(value reflect.Value, field reflect.StructField) error { + name, ok := field.Tag.Lookup("cli") + if !ok || name == "" { return nil } - firstSpace := strings.Index(cmdTag, " ") - name := cmdTag - usage := "" - if firstSpace > 0 { - name = cmdTag[0:firstSpace] - usage = cmdTag[firstSpace+1:] + if len(c.prefix) > 0 { + name = c.prefix + name } - kind := v.Kind() + usage, _ := field.Tag.Lookup("usage") + + kind := value.Kind() switch kind { case reflect.Bool: - c.FlagSet.BoolVar((*bool)(unsafe.Pointer(v.UnsafeAddr())), name, - false, usage) + c.FlagSet.BoolVar( + (*bool)(unsafe.Pointer(value.UnsafeAddr())), name, false, usage, + ) + return nil case reflect.String, reflect.Int8, @@ -247,10 +267,10 @@ func (c *Command) addFlag(v reflect.Value, f reflect.StructField) error { reflect.Uint64, reflect.Float32, reflect.Float64: - anyValue := newAnyValue(v) + anyValue := newAnyValue(value) c.FlagSet.Var(anyValue, name, usage) case reflect.Slice: - sliceValue := newSliceValue(v, f.Tag.Get("separator")) + sliceValue := newSliceValue(value, field.Tag.Get("separator")) c.FlagSet.Var(sliceValue, name, usage) default: return fmt.Errorf("unsupported type: %s", kind.String()) @@ -261,29 +281,24 @@ func (c *Command) addFlag(v reflect.Value, f reflect.StructField) error { // createSubCommand creates sub-commands func (c *Command) createSubCommand(tag reflect.StructTag) *Command { - cmdTag, ok := tag.Lookup("cli") - if !ok || cmdTag == "" { + name, ok := tag.Lookup("cli") + if !ok || name == "" { return c } cmd := Command{SubCommands: make(map[string]*Command)} - firstSpace := strings.Index(cmdTag, " ") - name := cmdTag - usage := "" - if firstSpace > 0 { - name = cmdTag[0:firstSpace] - usage = cmdTag[firstSpace+1:] - } cmd.Name = name - cmd.FlagSet = flag.NewFlagSet(name, errorHandling) - cmd.Usage = usage + cmd.prefix = c.prefix + name + "-" + cmd.FlagSet = c.FlagSet + cmd.Usage, _ = tag.Lookup("usage") if usageHandler != nil { cmd.FlagSet.Usage = usageHandler(&cmd) } c.SubCommands[name] = &cmd + return &cmd } @@ -291,22 +306,10 @@ func (c *Command) createSubCommand(tag reflect.StructTag) *Command { // The Init(interface{}) function must be called before parsing func (c *Command) Parse(args []string) error { if err := c.FlagSet.Parse(args); err != nil { - return err + return fmt.Errorf("parse flag set: %w", err) } - unprocessed := c.FlagSet.Args() - if len(unprocessed) < 1 { - return nil - } + c.Args = c.FlagSet.Args() - if c.SubCommands == nil { - return fmt.Errorf("unsupported command: %s", unprocessed[0]) - } - - cmd := c.SubCommands[unprocessed[0]] - if cmd == nil { - return fmt.Errorf("unsupported command: %s", unprocessed[0]) - } - - return cmd.Parse(unprocessed[1:]) + return nil } diff --git a/cli_test.go b/cli_test.go index 4b7ed01..2eafb38 100644 --- a/cli_test.go +++ b/cli_test.go @@ -43,23 +43,23 @@ func TestServiceCommand(t *testing.T) { // assert database sub cmd dbCmd := cmd.SubCommands["database"] assert.NotNil(dbCmd, "service cmd should have {database} sub cmd") - assert.NotNil(dbCmd.FlagSet.Lookup("dbHost")) - assert.NotNil(dbCmd.FlagSet.Lookup("dbPort")) - assert.NotNil(dbCmd.FlagSet.Lookup("dbUser")) - assert.NotNil(dbCmd.FlagSet.Lookup("dbPassword")) + assert.NotNil(cmd.FlagSet.Lookup("database-host")) + assert.NotNil(cmd.FlagSet.Lookup("database-port")) + assert.NotNil(cmd.FlagSet.Lookup("database-user")) + assert.NotNil(cmd.FlagSet.Lookup("database-password")) // assert database log sub cmd dbLogCmd := dbCmd.SubCommands["log"] assert.NotNil(dbCmd, "database cmd should have {log} sub cmd") - assert.NotNil(dbLogCmd.FlagSet.Lookup("path")) - assert.NotNil(dbLogCmd.FlagSet.Lookup("level")) + assert.NotNil(cmd.FlagSet.Lookup("database-log-path")) + assert.NotNil(cmd.FlagSet.Lookup("database-log-level")) assert.Equal(0, len(dbLogCmd.SubCommands)) // assert log cmd logCmd := cmd.SubCommands["log"] assert.NotNil(logCmd, "service cmd should have {log} sub cmd") - assert.NotNil(logCmd.FlagSet.Lookup("path")) - assert.NotNil(logCmd.FlagSet.Lookup("level")) + assert.NotNil(cmd.FlagSet.Lookup("log-path")) + assert.NotNil(cmd.FlagSet.Lookup("log-level")) } func TestLoginSubCommand(t *testing.T) { @@ -71,15 +71,15 @@ func TestLoginSubCommand(t *testing.T) { // assert login sub command loginCmd := cmd.SubCommands["login"] assert.NotNil(loginCmd, "service cmd should have {login} sub cmd") - assert.NotNil(loginCmd.FlagSet.Lookup("user")) - assert.NotNil(loginCmd.FlagSet.Lookup("password")) + assert.NotNil(cmd.FlagSet.Lookup("login-user")) + assert.NotNil(cmd.FlagSet.Lookup("login-password")) } func TestLoginCommandWithValues(t *testing.T) { assert := assert.New(t) loginConfig := test.LoginConfig{} cmd := NewCLI("Login") - assert.NoError(cmd.Init(&loginConfig), "Can't init login command") + assert.NoError(cmd.Init(&loginConfig), "init login command failed") username := "test-user" password := "test-passwd" @@ -110,31 +110,36 @@ func TestServiceCommandWithValues(t *testing.T) { loginUser := "login-user" loginPassword := "login-passwd" - serviceArgs := []string{"--hostname", serviceHost, "--port", - strconv.Itoa(servicePort), "log", "-path", serviceLogPath, "-level", - serviceLogLevel} + serviceArgs := []string{ + "--hostname", serviceHost, "--port", strconv.Itoa(servicePort), + "--log-path", serviceLogPath, "--log-level", serviceLogLevel, + } assert.NoError(cmd.Parse(serviceArgs)) assert.Equal(serviceHost, serviceConfig.Host) assert.Equal(servicePort, serviceConfig.Port) assert.Equal(serviceLogPath, serviceConfig.Log.Path) assert.Equal(serviceLogLevel, serviceConfig.Log.Level) - dbCmdArgs := []string{"database", "-dbHost", dbHost, "-dbPort", - strconv.Itoa(dbPort), "-dbUser", dbUser, "-dbPassword", dbPassword} + dbCmdArgs := []string{ + "--database-host", dbHost, "--database-port", strconv.Itoa(dbPort), + "--database-user", dbUser, "--database-password", dbPassword, + } assert.NoError(cmd.Parse(dbCmdArgs)) assert.Equal(dbHost, serviceConfig.DBConfig.Host) assert.Equal(dbPort, serviceConfig.DBConfig.Port) assert.Equal(dbUser, serviceConfig.DBConfig.User) assert.Equal(dbPassword, serviceConfig.DBConfig.Password) - loginCmdArgs := []string{"login", "--user", loginUser, "-password", - loginPassword} + loginCmdArgs := []string{ + "--login-user", loginUser, "--login-password", loginPassword, + } assert.NoError(cmd.Parse(loginCmdArgs)) assert.Equal(loginUser, serviceConfig.Login.User) assert.Equal(loginPassword, serviceConfig.Login.Password) - dbLogCmdArgs := []string{"database", "log", "-path", dbLogPath, "-level", - dbLogLevel} + dbLogCmdArgs := []string{ + "--database-log-path", dbLogPath, "--database-log-level", dbLogLevel, + } assert.NoError(cmd.Parse(dbLogCmdArgs)) assert.Equal(dbLogPath, serviceConfig.DBConfig.Log.Path) assert.Equal(dbLogLevel, serviceConfig.DBConfig.Log.Level) @@ -143,7 +148,7 @@ func TestServiceCommandWithValues(t *testing.T) { func TestVariousTypeCommand(t *testing.T) { assert := assert.New(t) typesConfig := test.TypesConfig{} - cmd := NewWith("Types", flag.ContinueOnError, func(cmd *Command) func() { + cmd := NewCliWith("Types", flag.ContinueOnError, func(cmd *Command) func() { return func() { // Stub } diff --git a/config.go b/config.go index e43bdbc..f98e5cb 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,7 @@ package config import ( "encoding/json" + "errors" "flag" "fmt" "os" @@ -60,11 +61,13 @@ func ParseDefault(i interface{}) error { return parseValue(valueOfStruct) } -func parseValue(v reflect.Value) error { - typeOfStruct := v.Type() +func parseValue(value reflect.Value) error { var err error - for i := 0; i < v.NumField() && err == nil; i++ { - valueOfField := v.Field(i) + + typeOfStruct := value.Type() + + for i := 0; i < value.NumField() && err == nil; i++ { + valueOfField := value.Field(i) kindOfField := valueOfField.Kind() structOfField := typeOfStruct.Field(i) @@ -83,74 +86,82 @@ func parseValue(v reflect.Value) error { continue } - kind := valueOfField.Kind() - switch kind { - case reflect.Bool: - err = SetValueWithBool(valueOfField, defValue) - case reflect.String: - valueOfField.SetString(defValue) - case reflect.Int8: - err = SetValueWithIntX(valueOfField, defValue, 8) - case reflect.Int16: - err = SetValueWithIntX(valueOfField, defValue, 16) - case reflect.Int, reflect.Int32: - err = SetValueWithIntX(valueOfField, defValue, 32) - case reflect.Int64: - err = SetValueWithIntX(valueOfField, defValue, 64) - case reflect.Uint8: - err = SetValueWithUintX(valueOfField, defValue, 8) - case reflect.Uint16: - err = SetValueWithUintX(valueOfField, defValue, 16) - case reflect.Uint, reflect.Uint32: - err = SetValueWithUintX(valueOfField, defValue, 32) - case reflect.Uint64: - err = SetValueWithUintX(valueOfField, defValue, 64) - case reflect.Float32: - err = SetValueWithFloatX(valueOfField, defValue, 32) - case reflect.Float64: - err = SetValueWithFloatX(valueOfField, defValue, 64) - case reflect.Slice: - sp, ok := structOfField.Tag.Lookup("separator") - if !ok { - sp = ":" - } - err = SetValueWithSlice(valueOfField, defValue, sp) + err = setValue(valueOfField, defValue, structOfField) + } - default: - return fmt.Errorf("unsupported type: %s", kind.String()) + return err +} + +func setValue(value reflect.Value, defValue string, sf reflect.StructField) error { + var err error + + switch value.Kind() { + case reflect.Bool: + err = SetValueWithBool(value, defValue) + case reflect.String: + value.SetString(defValue) + case reflect.Int8: + err = SetValueWithIntX(value, defValue, Int8Size) + case reflect.Int16: + err = SetValueWithIntX(value, defValue, Int16Size) + case reflect.Int, reflect.Int32: + err = SetValueWithIntX(value, defValue, Int32Size) + case reflect.Int64: + err = SetValueWithIntX(value, defValue, Int64Size) + case reflect.Uint8: + err = SetValueWithUintX(value, defValue, Uint8Size) + case reflect.Uint16: + err = SetValueWithUintX(value, defValue, Uint16Size) + case reflect.Uint, reflect.Uint32: + err = SetValueWithUintX(value, defValue, Uint32Size) + case reflect.Uint64: + err = SetValueWithUintX(value, defValue, Uint64Size) + case reflect.Float32: + err = SetValueWithFloatX(value, defValue, Float32Size) + case reflect.Float64: + err = SetValueWithFloatX(value, defValue, Float64Size) + case reflect.Slice: + sp, ok := sf.Tag.Lookup("separator") + if !ok { + sp = ":" } + + err = SetValueWithSlice(value, defValue, sp) + default: + err = fmt.Errorf("unsupported type: %s", value.Kind().String()) } return err } // ParseCli parses given structure interface and set it with command line input -func ParseCli(i interface{}) error { - cli := NewCLI(os.Args[0]) - if err := cli.Init(i); err != nil { - return err +func ParseCli(out interface{}, name string, args []string) ([]string, error) { + cli := NewCLI(name) + if err := cli.Init(out); err != nil { + return nil, err } - if err := cli.Parse(os.Args[1:]); err != nil { - return err - } - return nil + + err := cli.Parse(args) + + return cli.Args, err } // ParseConfig parses given structure interface and set it with default // configuration file. -// configFlag is a command line flag to tell where to locate configure file. +// The configFlag is a command line flag to tell where to locate configure file. // If the config file doesn't exist, the default config fill will be searched // under the same folder with the fixed order: config.json, config.yaml and // config.properties -func ParseConfig(i interface{}, configFlag string) error { +func ParseConfig(out interface{}, configFlag string) error { configFile := flag.String(configFlag, "", "Specify configuration file") flag.Parse() - return ParseConfigFile(i, *configFile) + + return ParseConfigFile(out, *configFile) } // ParseConfigFile parses given structure interface and set its value with // the specified configuration file -func ParseConfigFile(i interface{}, configFile string) error { +func ParseConfigFile(out interface{}, configFile string) error { var err error if configFile == "" { configFile, err = getDefaultConfigFile() @@ -166,11 +177,11 @@ func ParseConfigFile(i interface{}, configFile string) error { switch configType { case JSONConfigType: - err = parseJSON(i, configFile) + err = parseJSON(out, configFile) case YamlConfigType: - err = parseYaml(i, configFile) + err = parseYaml(out, configFile) case PropConfigType: - err = parseProp(i, configFile) + err = parseProp(out, configFile) default: err = fmt.Errorf("unsupported config file: %s", configFile) } @@ -179,28 +190,36 @@ func ParseConfigFile(i interface{}, configFile string) error { } // parseJSON parses JSON file and set structure with its value -func parseJSON(i interface{}, jsonFile string) error { +func parseJSON(out interface{}, jsonFile string) error { raw, err := os.ReadFile(jsonFile) if err != nil { return fmt.Errorf("open json config file: %s", err.Error()) } - return json.Unmarshal(raw, i) + if err := json.Unmarshal(raw, out); err != nil { + return fmt.Errorf("json unmarshal: %w", err) + } + + return nil } // parseYaml parses Yaml file and set structure with its value -func parseYaml(i interface{}, yamlFile string) error { +func parseYaml(out interface{}, yamlFile string) error { raw, err := os.ReadFile(yamlFile) if err != nil { return fmt.Errorf("open yaml config file: %s", err.Error()) } - return yaml.Unmarshal(raw, i) + if err := yaml.Unmarshal(raw, out); err != nil { + return fmt.Errorf("unmarshal yaml: %w", err) + } + + return nil } // parseProp parses Properties file and set structure with its value -func parseProp(_ interface{}, _ /*propFile*/ string) error { - return fmt.Errorf("properties config is not implemented") +func parseProp(_ interface{}, _ /* The propFile */ string) error { + return errors.New("properties config is not implemented") } // getDefaultConfigFile returns a existing default config file. The checking @@ -214,19 +233,19 @@ func getDefaultConfigFile() (string, error) { path := filepath.Dir(exe) + string(filepath.Separator) - // check json config + // Check json config jsonConfig := path + DefaultJSONConfig if _, err := os.Stat(jsonConfig); err == nil { return jsonConfig, nil } - // check yaml config + // Check yaml config yamlConfig := path + DefaultYamlConfig if _, err := os.Stat(yamlConfig); err == nil { return yamlConfig, nil } - // check prop config + // Check prop config propConfig := path + DefaultPropConfig if _, err := os.Stat(propConfig); err == nil { return propConfig, nil @@ -239,11 +258,12 @@ func getDefaultConfigFile() (string, error) { // corresponding type: json, yaml or properties func getConfigFileType(configFile string) (string, error) { ext := filepath.Ext(configFile) - if ext == ".json" { + switch ext { + case ".json": return JSONConfigType, nil - } else if ext == ".yaml" || ext == ".yml" { + case ".yaml": return YamlConfigType, nil - } else if ext == ".properties" || ext == ".prop" { + case ".properties", ".prop": return PropConfigType, nil } diff --git a/env.go b/env.go index 35d0423..b6aa375 100644 --- a/env.go +++ b/env.go @@ -17,6 +17,7 @@ package config import ( "fmt" + "io" "os" "reflect" ) @@ -27,6 +28,11 @@ func ParseEnv(i interface{}) error { return ParseEnvWith(i, "") } +// UsageEnv prints usage description of config i to out +func UsageEnv(out io.Writer, i interface{}) { + // STub +} + // ParseWith parses with given structure interface and environment name prefix // It is normally used in nested structure. // For example: we have such structure @@ -59,15 +65,17 @@ func ParseEnvWith(i interface{}, prefix string) error { } // parseValue parses a reflect.Value object -func parseValueEnv(v reflect.Value, prefix string) error { - typeOfStruct := v.Type() +func parseValueEnv(value reflect.Value, prefix string) error { var err error - for i := 0; i < v.NumField() && err == nil; i++ { - valueOfField := v.Field(i) + + typeOfStruct := value.Type() + + for i := 0; i < value.NumField() && err == nil; i++ { + valueOfField := value.Field(i) kindOfField := valueOfField.Kind() structOfField := typeOfStruct.Field(i) - // recursively unmarshal if value is ptr type + // Recursively unmarshal if value is ptr type if kindOfField == reflect.Ptr { if !valueOfField.IsNil() && valueOfField.CanSet() { err = ParseEnvWith(valueOfField.Interface(), @@ -90,8 +98,8 @@ func parseValueEnv(v reflect.Value, prefix string) error { } // setFieldValue sets a reflect.Value with environment value -func setFieldValueEnv(v reflect.Value, f reflect.StructField, prefix string) error { - envName := f.Tag.Get("env") +func setFieldValueEnv(value reflect.Value, field reflect.StructField, prefix string) error { + envName := field.Tag.Get("env") if envName == "" { return nil } @@ -101,51 +109,54 @@ func setFieldValueEnv(v reflect.Value, f reflect.StructField, prefix string) err return nil } - if !v.CanSet() { - return fmt.Errorf("%s: can't be set", f.Name) + if !value.CanSet() { + return fmt.Errorf("%s: can't be set", field.Name) } var err error - kind := v.Kind() + + kind := value.Kind() switch kind { case reflect.Bool: - err = SetValueWithBool(v, envValue) + err = SetValueWithBool(value, envValue) case reflect.String: - v.SetString(envValue) + value.SetString(envValue) case reflect.Int8: - err = SetValueWithIntX(v, envValue, 8) + err = SetValueWithIntX(value, envValue, Int8Size) case reflect.Int16: - err = SetValueWithIntX(v, envValue, 16) + err = SetValueWithIntX(value, envValue, Int16Size) case reflect.Int, reflect.Int32: - err = SetValueWithIntX(v, envValue, 32) + err = SetValueWithIntX(value, envValue, Int32Size) case reflect.Int64: - err = SetValueWithIntX(v, envValue, 64) + err = SetValueWithIntX(value, envValue, Int64Size) case reflect.Uint8: - err = SetValueWithUintX(v, envValue, 8) + err = SetValueWithUintX(value, envValue, Uint8Size) case reflect.Uint16: - err = SetValueWithUintX(v, envValue, 16) + err = SetValueWithUintX(value, envValue, Uint16Size) case reflect.Uint, reflect.Uint32: - err = SetValueWithUintX(v, envValue, 32) + err = SetValueWithUintX(value, envValue, Uint32Size) case reflect.Uint64: - err = SetValueWithUintX(v, envValue, 64) + err = SetValueWithUintX(value, envValue, Uint64Size) case reflect.Float32: - err = SetValueWithFloatX(v, envValue, 32) + err = SetValueWithFloatX(value, envValue, Float32Size) case reflect.Float64: - err = SetValueWithFloatX(v, envValue, 64) + err = SetValueWithFloatX(value, envValue, Float64Size) case reflect.Slice: - sp, ok := f.Tag.Lookup("separator") + sp, ok := field.Tag.Lookup("separator") if !ok { sp = ":" } - err = SetValueWithSlice(v, envValue, sp) + + err = SetValueWithSlice(value, envValue, sp) default: return fmt.Errorf("unsupported type: %s", kind.String()) } if err != nil { - return fmt.Errorf("%s: %s", f.Name, err.Error()) + return fmt.Errorf("%s: %s", field.Name, err.Error()) } + return nil } diff --git a/test/config.json b/test/config.json index 12bb410..94c82ac 100644 --- a/test/config.json +++ b/test/config.json @@ -1,10 +1,10 @@ { - "dbHost": "test-db-host", - "dbPort": 9090, - "dbUser": "test-db-user", - "dbPassword": "test-db-password", + "host": "test-db-host", + "port": 9090, + "user": "test-db-user", + "password": "test-db-password", "log": { "path": "/var/log/db", "level": "error" } -} +} \ No newline at end of file diff --git a/test/config.yaml b/test/config.yaml index 31588d2..c94ad03 100644 --- a/test/config.yaml +++ b/test/config.yaml @@ -1,7 +1,7 @@ -dbHost: test-db-host -dbPort: 9090 -dbUser: test-db-user -dbPassword: test-db-password +host: test-db-host +port: 9090 +user: test-db-user +password: test-db-password log: path: /var/log/db level: error diff --git a/test/data.go b/test/data.go index d429e91..7d07e2c 100644 --- a/test/data.go +++ b/test/data.go @@ -16,59 +16,59 @@ package test type DBConfig struct { - Host string `json:"dbHost" yaml:"dbHost" env:"HOST" cli:"dbHost database server hostname"` - Port int `json:"dbPort" yaml:"dbPort" env:"PORT" cli:"dbPort database server port"` - User string `json:"dbUser" yaml:"dbUser" env:"USER" cli:"dbUser database username"` - Password string `json:"dbPassword" yaml:"dbPassword" env:"PASSWORD" cli:"dbPassword database user password"` - Log LogConfig `json:"log" yaml:"log" env:"LOG_" cli:"log database log configuration"` + Host string `cli:"host" env:"HOST" json:"host" usage:"database server hostname" yaml:"host"` + Port int `cli:"port" env:"PORT" json:"port" usage:"database server port" yaml:"port"` + User string `cli:"user" env:"USER" json:"user" usage:"database username" yaml:"user"` + Password string `cli:"password" env:"PASSWORD" json:"password" usage:"database user password" yaml:"password"` + Log LogConfig `cli:"log" env:"LOG_" json:"log" usage:"database log configuration" yaml:"log"` } type LoginConfig struct { - User string `json:"user" yaml:"user" env:"USER" prop:"user" cli:"user login username"` - Password string `json:"password" yaml:"password" env:"PASSWORD" prop:"password" cli:"password login password"` + User string `cli:"user" env:"USER" json:"user" prop:"user" usage:"login username" yaml:"user"` + Password string `cli:"password" env:"PASSWORD" json:"password" prop:"password" usage:"login password" yaml:"password"` } type LogConfig struct { - Path string `json:"path" yaml:"path" env:"PATH" prop:"path" cli:"path log path"` - Level string `json:"level" yaml:"level" env:"LEVEL" porp:"level" cli:"level log level {debug|warning|error}"` + Path string `cli:"path" env:"PATH" json:"path" prop:"path" usage:"log path" yaml:"path"` + Level string `cli:"level" env:"LEVEL" json:"level" prop:"level" usage:"log level {debug|warning|error}" yaml:"level"` } type ServiceConfig struct { - Host string `env:"CONFIG_TEST_SERVICE_HOST" cli:"hostname service hostname"` - Port int `env:"CONFIG_TEST_SERVICE_PORT" cli:"port service port"` - DBConfig DBConfig `env:"CONFIG_TEST_SERVICE_DB_" cli:"database database configuration"` - Login *LoginConfig `env:"CONFIG_TEST_SERVICE_LOGIN_" cli:"login login user and password"` - Log LogConfig `env:"CONFIG_TEST_SERVICE_LOG_" cli:"log service log configuration"` + Host string `cli:"hostname" env:"CONFIG_TEST_SERVICE_HOST" usage:"service hostname"` + Port int `cli:"port" env:"CONFIG_TEST_SERVICE_PORT" usage:"service port"` + DBConfig DBConfig `cli:"database" env:"CONFIG_TEST_SERVICE_DB_" usage:"database configuration"` + Login *LoginConfig `cli:"login" env:"CONFIG_TEST_SERVICE_LOGIN_" usage:"login user and password"` + Log LogConfig `cli:"log" env:"CONFIG_TEST_SERVICE_LOG_" usage:"service log configuration"` } type TypesConfig struct { - BoolValue bool `env:"CONFIG_TEST_BOOL" cli:"bool boolean value"` - StrValue string `env:"CONFIG_TEST_STR" cli:"str string value"` - Int8Value int8 `env:"CONFIG_TEST_INT8" cli:"int8 int8 value"` - Int16Value int16 `env:"CONFIG_TEST_INT16" cli:"int16 int16 value"` - IntValue int `env:"CONFIG_TEST_INT" cli:"int int value"` - Int32Value int32 `env:"CONFIG_TEST_INT32" cli:"int32 int32 value"` - Int64Value int64 `env:"CONFIG_TEST_INT64" cli:"int64 int64 value"` - Uint8Value uint8 `env:"CONFIG_TEST_UINT8" cli:"uint8 uint8 value"` - Uint16Value uint16 `env:"CONFIG_TEST_UINT16" cli:"uint16 uint16 value"` - UintValue uint `env:"CONFIG_TEST_UINT" cli:"uint uint value"` - Uint32Value uint32 `env:"CONFIG_TEST_UINT32" cli:"uint32 uint32 value"` - Uint64Value uint64 `env:"CONFIG_TEST_UINT64" cli:"uint64 uint64 value"` - Float32Value float32 `env:"CONFIG_TEST_FLOAT32" cli:"float32 float32 value"` - Float64Value float64 `env:"CONFIG_TEST_FLOAT64" cli:"float64 float64 value"` + BoolValue bool `cli:"bool" env:"CONFIG_TEST_BOOL" usage:"boolean value"` + StrValue string `cli:"str" env:"CONFIG_TEST_STR" usage:"string value"` + Int8Value int8 `cli:"int8" env:"CONFIG_TEST_INT8" usage:"int8 value"` + Int16Value int16 `cli:"int16" env:"CONFIG_TEST_INT16" usage:"int16 value"` + IntValue int `cli:"int" env:"CONFIG_TEST_INT" usage:"int value"` + Int32Value int32 `cli:"int32" env:"CONFIG_TEST_INT32" usage:"int32 value"` + Int64Value int64 `cli:"int64" env:"CONFIG_TEST_INT64" usage:"int64 value"` + Uint8Value uint8 `cli:"uint8" env:"CONFIG_TEST_UINT8" usage:"uint8 value"` + Uint16Value uint16 `cli:"uint16" env:"CONFIG_TEST_UINT16" usage:"uint16 value"` + UintValue uint `cli:"uint" env:"CONFIG_TEST_UINT" usage:"uint value"` + Uint32Value uint32 `cli:"uint32" env:"CONFIG_TEST_UINT32" usage:"uint32 value"` + Uint64Value uint64 `cli:"uint64" env:"CONFIG_TEST_UINT64" usage:"uint64 value"` + Float32Value float32 `cli:"float32" env:"CONFIG_TEST_FLOAT32" usage:"float32 value"` + Float64Value float64 `cli:"float64" env:"CONFIG_TEST_FLOAT64" usage:"float64 value"` } type DefValueConfig struct { - BoolValue bool `env:"CONFIG_TEST_BOOL" cli:"bool boolean value" default:"true"` - IntValue int `env:"CONFIG_TEST_INT" cli:"int int value" default:"123"` - Float64Value float64 `env:"CONFIG_TEST_FLOAT64" cli:"float64 float64 value" default:"123.4567"` - StrValue string `env:"CONFIG_TEST_STR" cli:"str string value" default:"default-string"` - SliceValue []string `env:"CONFIG_TEST_SLICE" cli:"slice slice values" default:"xx:yy:zz"` - NoDefValue string `env:"CONFIG_TEST_NO_DEFVALUE" cli:"nodefvalue no default value"` + BoolValue bool `cli:"bool" default:"true" env:"CONFIG_TEST_BOOL" usage:"boolean value"` + IntValue int `cli:"int" default:"123" env:"CONFIG_TEST_INT" usage:"int value"` + Float64Value float64 `cli:"float64" default:"123.4567" env:"CONFIG_TEST_FLOAT64" usage:"float64 value"` + StrValue string `cli:"str" default:"default-string" env:"CONFIG_TEST_STR" usage:"string value"` + SliceValue []string `cli:"slice" default:"xx:yy:zz" env:"CONFIG_TEST_SLICE" usage:"slice values"` + NoDefValue string `cli:"nodefvalue" env:"CONFIG_TEST_NO_DEFVALUE" usage:"no default value"` } type SlicesConfig struct { - Paths []string `env:"CONFIG_TEST_SLICES_PATHS" cli:"paths multiple path"` - Debugs []string `env:"CONFIG_TEST_SLICES_DEBUG" cli:"debugs multiple debug" separator:";"` - Values []int `env:"CONFIG_TEST_SLICES_VALUES" cli:"values multiple value" separator:","` + Paths []string `cli:"paths" env:"CONFIG_TEST_SLICES_PATHS" usage:"multiple path"` + Debugs []string `cli:"debugs" env:"CONFIG_TEST_SLICES_DEBUG" separator:";" usage:"multiple debug"` + Values []int `cli:"values" env:"CONFIG_TEST_SLICES_VALUES" separator:"," usage:"multiple value"` } diff --git a/utils.go b/utils.go index e206eb6..cee833f 100644 --- a/utils.go +++ b/utils.go @@ -22,80 +22,88 @@ import ( "strings" ) -func SetValueWithBool(v reflect.Value, boolValue string) error { - value, err := strconv.ParseBool(boolValue) +func SetValueWithBool(value reflect.Value, boolValue string) error { + b, err := strconv.ParseBool(boolValue) if err != nil { - return err + return fmt.Errorf("parse bool: %w", err) } - v.SetBool(value) + value.SetBool(b) + return nil } -func SetValueWithFloatX(v reflect.Value, floatValue string, bitSize int) error { - value, err := strconv.ParseFloat(floatValue, bitSize) +func SetValueWithFloatX(value reflect.Value, floatValue string, bitSize int) error { + f, err := strconv.ParseFloat(floatValue, bitSize) if err != nil { - return err + return fmt.Errorf("parse float: %w", err) } - v.SetFloat(value) + value.SetFloat(f) + return nil } -func SetValueWithIntX(v reflect.Value, intValue string, bitSize int) error { - value, err := strconv.ParseInt(intValue, 10, bitSize) +func SetValueWithIntX(value reflect.Value, intValue string, bitSize int) error { + v, err := strconv.ParseInt(intValue, 10, bitSize) if err != nil { - return err + return fmt.Errorf("parse int: %w", err) } - v.SetInt(value) + value.SetInt(v) + return nil } -func SetValueWithUintX(v reflect.Value, uintValue string, bitSize int) error { - value, err := strconv.ParseUint(uintValue, 10, bitSize) +func SetValueWithUintX(value reflect.Value, uintValue string, bitSize int) error { + v, err := strconv.ParseUint(uintValue, 10, bitSize) if err != nil { - return err + return fmt.Errorf("parse uint: %w", err) } - v.SetUint(value) + value.SetUint(v) + return nil } -func SetValueWithSlice(v reflect.Value, slice string, separator string) error { - data := strings.Split(slice, separator) +func SetValueWithSlice(value reflect.Value, slice string, sep string) error { + data := strings.Split(slice, sep) + size := len(data) if size > 0 { - slice := reflect.MakeSlice(v.Type(), size, size) - for i := 0; i < size; i++ { - ele := slice.Index(i) - kind := ele.Kind() + slice := reflect.MakeSlice(value.Type(), size, size) + + for index := range size { var err error + + ele := slice.Index(index) + + kind := ele.Kind() switch kind { case reflect.Bool: - err = SetValueWithBool(ele, data[i]) + err = SetValueWithBool(ele, data[index]) case reflect.String: - ele.SetString(data[i]) + ele.SetString(data[index]) case reflect.Uint8: - err = SetValueWithUintX(ele, data[i], 8) + err = SetValueWithUintX(ele, data[index], Uint8Size) case reflect.Uint16: - err = SetValueWithUintX(ele, data[i], 16) + err = SetValueWithUintX(ele, data[index], Uint16Size) case reflect.Uint, reflect.Uint32: - err = SetValueWithUintX(ele, data[i], 32) + err = SetValueWithUintX(ele, data[index], Uint32Size) case reflect.Uint64: - err = SetValueWithUintX(ele, data[i], 64) + err = SetValueWithUintX(ele, data[index], Uint64Size) case reflect.Int8: - err = SetValueWithIntX(ele, data[i], 8) + err = SetValueWithIntX(ele, data[index], Int8Size) case reflect.Int16: - err = SetValueWithIntX(ele, data[i], 16) + err = SetValueWithIntX(ele, data[index], Int16Size) case reflect.Int, reflect.Int32: - err = SetValueWithIntX(ele, data[i], 32) + err = SetValueWithIntX(ele, data[index], Int32Size) case reflect.Int64: - err = SetValueWithIntX(ele, data[i], 64) + err = SetValueWithIntX(ele, data[index], Int64Size) case reflect.Float32: - err = SetValueWithFloatX(ele, data[i], 32) + err = SetValueWithFloatX(ele, data[index], Float32Size) case reflect.Float64: - err = SetValueWithFloatX(ele, data[i], 64) + err = SetValueWithFloatX(ele, data[index], Float64Size) default: return fmt.Errorf("unsupported type: %s", kind.String()) } @@ -105,7 +113,7 @@ func SetValueWithSlice(v reflect.Value, slice string, separator string) error { } } - v.Set(slice) + value.Set(slice) } return nil