2
0

Исправлена часть ошибок линтера.
All checks were successful
test / test (push) Successful in 52s

This commit is contained in:
Алексей Бадяев 2024-10-21 00:15:51 +07:00
parent 0d4ffa905c
commit b8e669f2cc
Signed by: alexey
GPG Key ID: 686FBC1363E4AFAE
10 changed files with 324 additions and 276 deletions

View File

@ -30,14 +30,14 @@ linters:
- errchkjson
- errname
- errorlint
- exhaustive
# - exhaustive
- fatcontext
- forcetypeassert
- funlen
- gci
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
# - gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit

View File

@ -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"`
}
```

157
cli.go
View File

@ -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
}

View File

@ -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
}

150
config.go
View File

@ -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
}

61
env.go
View File

@ -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
}

View File

@ -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"
}
}
}

View File

@ -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

View File

@ -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"`
}

View File

@ -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