From ce2ec715be1d7f5265c6dda40bbf0fe96dd6922b 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: Sun, 4 May 2025 13:02:38 +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=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D1=8F=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=D0=B0=20=D0=BE=D0=BF=D0=B8=D1=81?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3?= =?UTF-8?q?=D1=83=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B2=20markdown.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/test.yaml | 7 ---- cspell.config.yaml | 18 ++++++++-- env.go | 72 +++++++++++++++++++++++++++++++------- env_test.go | 5 +++ makefile | 12 ++----- test/data.go | 4 +-- 6 files changed, 85 insertions(+), 33 deletions(-) diff --git a/.gitea/workflows/test.yaml b/.gitea/workflows/test.yaml index c5d9d24..3bb8082 100644 --- a/.gitea/workflows/test.yaml +++ b/.gitea/workflows/test.yaml @@ -19,15 +19,8 @@ jobs: - name: set-up dependencies run: | - go install github.com/kisielk/errcheck@latest - go install honnef.co/go/tools/cmd/staticcheck@latest - go install github.com/sashamelentyev/usestdlibvars@latest - go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest make vendor - - name: lint - run: make lint - - name: golangci-lint uses: https://github.com/golangci/golangci-lint-action@v7 with: diff --git a/cspell.config.yaml b/cspell.config.yaml index 21cf35e..16aa2f9 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -1,10 +1,24 @@ version: "0.2" ignorePaths: [] -dictionaryDefinitions: [] -dictionaries: [] +dictionaryDefinitions: + - name: custom-dictionary + path: ./.cspell/custom-dictionary.txt + addWords: true +dictionaries: + - custom-dictionary words: [] ignoreWords: + - GOCMD + - GOPATH + - GOTEST + - GOVET + - Txterm + - covermode + - coverprofile - davecgh - difflib + - gocov - pmezard + - setaf + - tput import: [] diff --git a/env.go b/env.go index 190df1f..4bdfb22 100644 --- a/env.go +++ b/env.go @@ -27,13 +27,16 @@ import ( // ParseValueEnvFunc func to parse env value type ParseValueEnvFunc func(value reflect.Value, prefix string) error +// ParseFieldEnvFunc func to parse env field +type ParseFieldEnvFunc func(out io.Writer, field reflect.StructField, prefix string) error + // Parse parses given structure interface, extracts environment definitions // from its tag and sets structure with defined environment variables func ParseEnv(out interface{}) error { return ParseEnvWith(out, "", parseValueEnv) } -// UsageEnv prints usage description of config i to out +// UsageEnv prints usage description of config in to out in text 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) @@ -42,7 +45,7 @@ func UsageEnv(out io.Writer, in interface{}) error { buf := bytes.NewBufferString("") if err := ParseEnvWith(in, "", func(value reflect.Value, prefix string) error { - return usageValueEnv(buf, value, prefix) + return usageValueEnv(buf, value, prefix, usageFieldEnv) }); err != nil { return err } @@ -72,6 +75,21 @@ func UsageEnv(out io.Writer, in interface{}) error { return nil } +// MarkdownEnv prints description of config in to out in markdown +func MarkdownEnv(out io.Writer, in interface{}) error { + msg := "# Environment variables\n\n" + msg += "| Name | Default | Required | Usage |\n" + msg += "| :--- | :------ | :------: | :---- |\n" + + if _, err := out.Write([]byte(msg)); err != nil { + return fmt.Errorf("write markdown: %w", err) + } + + return ParseEnvWith(in, "", func(value reflect.Value, prefix string) error { + return usageValueEnv(out, value, prefix, markdownFieldEnv) + }) +} + // ParseWith parses with given structure interface and environment name prefix // It is normally used in nested structure. // For example: we have such structure @@ -103,7 +121,9 @@ func ParseEnvWith(out interface{}, prefix string, parser ParseValueEnvFunc) erro return parser(valueOfStruct, prefix) } -func usageValueEnv(out io.Writer, value reflect.Value, prefix string) error { +func usageValueEnv( + out io.Writer, value reflect.Value, prefix string, usageField ParseFieldEnvFunc, +) error { var err error typeOfStruct := value.Type() @@ -120,22 +140,18 @@ func usageValueEnv(out io.Writer, value reflect.Value, prefix string) error { valueOfField.Interface(), prefix+structOfField.Tag.Get("env"), func(value reflect.Value, prefix string) error { - return usageValueEnv(out, value, prefix) + return usageValueEnv(out, value, prefix, usageField) }, ) } else { continue } } else if kindOfField == reflect.Struct { - err = usageValueEnv(out, valueOfField, prefix+structOfField.Tag.Get("env")) + err = usageValueEnv(out, valueOfField, prefix+structOfField.Tag.Get("env"), usageField) } - if err != nil { - return err - } - - if kindOfField != reflect.Struct { - err = usageFieldValueEnv(out, structOfField, prefix) + if err == nil && kindOfField != reflect.Struct { + err = usageField(out, structOfField, prefix) } } @@ -256,7 +272,7 @@ func setSimpleEnvValue(value reflect.Value, field reflect.StructField, str strin return err } -func usageFieldValueEnv(out io.Writer, field reflect.StructField, prefix string) error { +func usageFieldEnv(out io.Writer, field reflect.StructField, prefix string) error { envName := field.Tag.Get("env") if envName == "" { return nil @@ -267,6 +283,8 @@ func usageFieldValueEnv(out io.Writer, field reflect.StructField, prefix string) if def := field.Tag.Get("default"); def != "" { msg += " (default: " + def + ")" + } else if req := field.Tag.Get("required"); req == "true" { + msg += " (required)" } if _, err := out.Write([]byte(msg + "\n")); err != nil { @@ -275,3 +293,33 @@ func usageFieldValueEnv(out io.Writer, field reflect.StructField, prefix string) return nil } + +func markdownFieldEnv(out io.Writer, field reflect.StructField, prefix string) error { + const mark = "✅" + + envName := field.Tag.Get("env") + if envName == "" { + return nil + } + + envName = prefix + envName + msg := "| " + envName + " | " + + if def := field.Tag.Get("default"); def != "" { + msg += def + } + + msg += " | " + + if req := field.Tag.Get("required"); req == "true" { + msg += mark + } + + msg += " | " + strings.ReplaceAll(field.Tag.Get("usage"), "|", "|") + " |\n" + + if _, err := out.Write([]byte(msg)); err != nil { + return fmt.Errorf("write markdown: %w", err) + } + + return nil +} diff --git a/env_test.go b/env_test.go index f630805..d1c27da 100644 --- a/env_test.go +++ b/env_test.go @@ -147,6 +147,11 @@ func TestEnvUsage(t *testing.T) { assert.NoError(t, UsageEnv(os.Stdout, &conf)) } +func TestEnvMarkdown(t *testing.T) { + conf := test.ServiceConfig{} + assert.NoError(t, MarkdownEnv(os.Stdout, &conf)) +} + func TestServiceLoginConfigEnv(t *testing.T) { var err error diff --git a/makefile b/makefile index 401d8ae..1286528 100644 --- a/makefile +++ b/makefile @@ -82,11 +82,8 @@ endif ## Lint lint: ## Run all available linters. - go vet ./... - errcheck ./... - staticcheck ./... -# usestdlibvars ./... - shadow ./... + @golangci-lint version + @golangci-lint run @$(ECHO_CMD) "Lint\t\t${GREEN}[OK]${RESET}" .PHONY:lint @@ -97,11 +94,6 @@ golangci-lint-install: ## Install golangci-lint util curl -sfL ${GOLANGCI_URL} | sh -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION} .PHONY:lint-golangci-install -golangci-lint: ## Run golangci-lint linter - @golangci-lint run - @$(ECHO_CMD) "GolangCI Lint\t${GREEN}[OK]${RESET}" -.PHONY:golangci-lint - ## Help help: ## Show this help. diff --git a/test/data.go b/test/data.go index 98839fe..9ccccff 100644 --- a/test/data.go +++ b/test/data.go @@ -39,8 +39,8 @@ type LogConfig struct { } type ServiceConfig struct { - Host string `cli:"hostname" env:"CONFIG_TEST_SERVICE_HOST" usage:"service hostname"` - Port int `cli:"port" env:"CONFIG_TEST_SERVICE_PORT" usage:"service port"` + Host string `cli:"hostname" env:"CONFIG_TEST_SERVICE_HOST" required:"true" usage:"service hostname"` + Port int `cli:"port" env:"CONFIG_TEST_SERVICE_PORT" required:"true" 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"`