From 16ce42d26c52a37ab328eb4119350b3b3a3f70d4 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: Tue, 22 Oct 2024 20:08:12 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=B8=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=B2=20=D0=BF=D0=BE?= =?UTF-8?q?=D1=82=D0=BE=D0=BA=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D0=BE?= =?UTF-8?q?=D0=B2.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .golangci.yaml | 23 ++++++++++ cli.go | 13 ++++++ cli_test.go | 16 ++++--- env.go | 117 +++++++++++++++++++++++++++++++++++++++++++++---- env_test.go | 7 ++- 5 files changed, 161 insertions(+), 15 deletions(-) diff --git a/.golangci.yaml b/.golangci.yaml index 0460a2f..5f9114a 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -184,6 +184,29 @@ linters-settings: - name: unexported-return disabled: true + varnamelen: + # The longest distance, in source lines, that is being considered a "small scope". + # Variables used in at most this many lines will be ignored. + # Default: 5 + max-distance: 6 + # The minimum length of a variable's name that is considered "long". + # Variable names that are at least this long will be ignored. + # Default: 3 + min-name-length: 2 + # Ignore "ok" variables that hold the bool return value of a type assertion. + # Default: false + ignore-type-assert-ok: true + # Ignore "ok" variables that hold the bool return value of a map index. + # Default: false + ignore-map-index-ok: true + # Ignore "ok" variables that hold the bool return value of a channel receive. + # Default: false + ignore-chan-recv-ok: true + # Optional list of variable names that should be ignored completely. + # Default: [] + ignore-names: + - err + # output configuration options output: # The formats used to render issues. diff --git a/cli.go b/cli.go index cd08d73..3eb537d 100644 --- a/cli.go +++ b/cli.go @@ -18,6 +18,7 @@ package config import ( "flag" "fmt" + "io" "reflect" "strconv" "unsafe" @@ -313,3 +314,15 @@ func (c *Command) Parse(args []string) error { return nil } + +// PrintUsage prints command description +func (c *Command) PrintUsage(w io.Writer) error { + if _, err := w.Write([]byte(c.Usage + "\n")); err != nil { + return fmt.Errorf("write usage: %w", err) + } + + c.FlagSet.SetOutput(w) + c.FlagSet.Usage() + + return nil +} diff --git a/cli_test.go b/cli_test.go index 60e4a09..34b8d41 100644 --- a/cli_test.go +++ b/cli_test.go @@ -17,6 +17,7 @@ package config import ( "flag" + "os" "strconv" "testing" @@ -29,9 +30,7 @@ func TestServiceCommand(t *testing.T) { serviceConfig := test.ServiceConfig{} cmd := NewCLI("Service") err := cmd.Init(&serviceConfig) - if err != nil { - t.Errorf("Can't init service command. %s", err.Error()) - } + assert.NoError(err, "init service command") // assert service cmd assert.NotNil(cmd.FlagSet) @@ -62,6 +61,15 @@ func TestServiceCommand(t *testing.T) { assert.NotNil(cmd.FlagSet.Lookup("log-level")) } +func TestPrintUsage(t *testing.T) { + assert := assert.New(t) + serviceConfig := test.ServiceConfig{} + cmd := NewCLI("Service") + err := cmd.Init(&serviceConfig) + assert.NoError(err, "init service command") + assert.NoError(cmd.PrintUsage(os.Stdout)) +} + func TestLoginSubCommand(t *testing.T) { assert := assert.New(t) serviceConfig := test.ServiceConfig{Login: &test.LoginConfig{}} @@ -256,5 +264,3 @@ func TestCommandWithSlices(t *testing.T) { assert.Equal(200, conf.Values[1]) assert.Equal(300, conf.Values[2]) } - -func TestUsage(t *testing.T) {} diff --git a/env.go b/env.go index 015d6d5..8e38eb8 100644 --- a/env.go +++ b/env.go @@ -16,21 +16,60 @@ package config import ( + "bytes" "fmt" "io" "os" "reflect" + "strings" ) +// ParseValueEnvFunc func to parse env value +type ParseValueEnvFunc func(value reflect.Value, prefix string) error + // Parse parses given structure interface, extracts environment definitions // from its tag and sets structure with defined environment variables -func ParseEnv(i interface{}) error { - return ParseEnvWith(i, "") +func ParseEnv(out interface{}) error { + return ParseEnvWith(out, "", parseValueEnv) } // UsageEnv prints usage description of config i to out -func UsageEnv(out io.Writer, i interface{}) { - // STub +func UsageEnv(out io.Writer, in interface{}) error { + if _, err := out.Write([]byte("Environment Usage:\n")); err != nil { + return fmt.Errorf("write usage: %w", err) + } + + buf := bytes.NewBufferString("") + + if err := ParseEnvWith(in, "", func(value reflect.Value, prefix string) error { + return usageValueEnv(buf, value, prefix) + }); err != nil { + return err + } + + var tabPos int + + for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") { + if pos := strings.Index(line, "\t"); pos > tabPos { + tabPos = pos + } + } + + const TabSize = 8 + + for _, line := range strings.Split(strings.TrimRight(buf.String(), "\n"), "\n") { + if pos := strings.Index(line, "\t"); pos >= 0 { + if count := tabPos/TabSize - pos/TabSize; count > 0 { + line = line[:pos] + strings.Repeat("\t", count) + line[pos:] + } + } + + if _, err := out.Write([]byte(line + "\n")); err != nil { + return fmt.Errorf("write usage: %w", err) + } + } + + return nil } // ParseWith parses with given structure interface and environment name prefix @@ -47,8 +86,8 @@ func UsageEnv(out io.Writer, i interface{}) { // // The Server.DB.Host will be mapped to environment variable: DB_HOST which is // concatenated from DB tag in Server struct and Host tag in Database struct -func ParseEnvWith(i interface{}, prefix string) error { - ptrRef := reflect.ValueOf(i) +func ParseEnvWith(out interface{}, prefix string, parser ParseValueEnvFunc) error { + ptrRef := reflect.ValueOf(out) if ptrRef.IsNil() || ptrRef.Kind() != reflect.Ptr { return fmt.Errorf("%w: %s", @@ -61,7 +100,44 @@ func ParseEnvWith(i interface{}, prefix string) error { errExpectStructPointerInsteadOf, valueOfStruct.Kind().String()) } - return parseValueEnv(valueOfStruct, prefix) + return parser(valueOfStruct, prefix) +} + +func usageValueEnv(out io.Writer, value reflect.Value, prefix string) error { + var err error + + 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 + if kindOfField == reflect.Ptr { + if !valueOfField.IsNil() && valueOfField.CanSet() { + err = ParseEnvWith( + valueOfField.Interface(), + prefix+structOfField.Tag.Get("env"), + func(value reflect.Value, prefix string) error { + return usageValueEnv(out, value, prefix) + }, + ) + } else { + continue + } + } else if kindOfField == reflect.Struct { + err = usageValueEnv(out, valueOfField, prefix+structOfField.Tag.Get("env")) + } + + if err != nil { + return err + } + + err = usageFieldValueEnv(out, structOfField, prefix) + } + + return err } // parseValue parses a reflect.Value object @@ -78,8 +154,11 @@ func parseValueEnv(value reflect.Value, prefix string) error { // Recursively unmarshal if value is ptr type if kindOfField == reflect.Ptr { if !valueOfField.IsNil() && valueOfField.CanSet() { - err = ParseEnvWith(valueOfField.Interface(), - prefix+structOfField.Tag.Get("env")) + err = ParseEnvWith( + valueOfField.Interface(), + prefix+structOfField.Tag.Get("env"), + parseValueEnv, + ) } else { continue } @@ -160,3 +239,23 @@ func setFieldValueEnv(value reflect.Value, field reflect.StructField, prefix str return nil } + +func usageFieldValueEnv(out io.Writer, field reflect.StructField, prefix string) error { + envName := field.Tag.Get("env") + if envName == "" { + return nil + } + + envName = prefix + envName + msg := " " + envName + "\t" + field.Tag.Get("usage") + + if def := field.Tag.Get("default"); def != "" { + msg += " (default: " + def + ")" + } + + if _, err := out.Write([]byte(msg + "\n")); err != nil { + return fmt.Errorf("write usage: %w", err) + } + + return nil +} diff --git a/env_test.go b/env_test.go index fedb65d..f630805 100644 --- a/env_test.go +++ b/env_test.go @@ -75,7 +75,7 @@ func TestLoginConfigEnvWithPrefix(t *testing.T) { defer func() { _ = os.Unsetenv("DB_PASSWORD") }() loginConfig := test.LoginConfig{} - assert.NoError(ParseEnvWith(&loginConfig, "DB_")) + assert.NoError(ParseEnvWith(&loginConfig, "DB_", parseValueEnv)) assert.Equal(LOGIN_USER, loginConfig.User) assert.Equal(LOGIN_PASSWORD, loginConfig.Password) } @@ -142,6 +142,11 @@ func TestServiceConfigEnv(t *testing.T) { assert.Equal(DB_LOG_LEVEL, serviceConfig.DBConfig.Log.Level) } +func TestEnvUsage(t *testing.T) { + conf := test.ServiceConfig{} + assert.NoError(t, UsageEnv(os.Stdout, &conf)) +} + func TestServiceLoginConfigEnv(t *testing.T) { var err error