diff --git a/cleanenv.go b/cleanenv.go index 4b1643d..ce0d0d2 100644 --- a/cleanenv.go +++ b/cleanenv.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" "reflect" + "sort" "strconv" "strings" "time" @@ -27,28 +28,28 @@ const ( // Supported tags const ( - // Name of the environment variable or a list of names + // TagEnv name of the environment variable or a list of names TagEnv = "env" - // Value parsing layout (for types like time.Time) + // TagEnvLayout value parsing layout (for types like time.Time) TagEnvLayout = "env-layout" - // Default value + // TagEnvDefault default value TagEnvDefault = "env-default" - // Custom list and map separator + // TagEnvSeparator custom list and map separator TagEnvSeparator = "env-separator" - // Environment variable description + // TagEnvDescription environment variable description TagEnvDescription = "env-description" - // Flag to mark a field as updatable + // TagEnvUpd flag to mark a field as updatable TagEnvUpd = "env-upd" - // Flag to mark a field as required + // TagEnvRequired flag to mark a field as required TagEnvRequired = "env-required" - // Flag to specify prefix for structure fields + // TagEnvPrefix аlag to specify prefix for structure fields TagEnvPrefix = "env-prefix" ) @@ -571,7 +572,7 @@ func GetDescription(cfg interface{}, headerText *string) (string, error) { return "", err } - var header, description string + var header string if headerText != nil { header = *headerText @@ -579,6 +580,8 @@ func GetDescription(cfg interface{}, headerText *string) (string, error) { header = "Environment variables:" } + description := make([]string, 0) + for _, m := range meta { if len(m.envList) == 0 { continue @@ -594,14 +597,17 @@ func GetDescription(cfg interface{}, headerText *string) (string, error) { if m.defValue != nil { elemDescription += fmt.Sprintf(" (default %q)", *m.defValue) } - description += elemDescription + description = append(description, elemDescription) } } - if description != "" { - return header + description, nil + if len(description) == 0 { + return "", nil } - return "", nil + + sort.Strings(description) + + return header + strings.Join(description, ""), nil } // Usage returns a configuration usage help. diff --git a/cleanenv_test.go b/cleanenv_test.go index 68bae1e..0a13fcb 100644 --- a/cleanenv_test.go +++ b/cleanenv_test.go @@ -820,8 +820,8 @@ func TestGetDescription(t *testing.T) { header: nil, want: "Environment variables:" + "\n ONE int\n \tone" + - "\n TWO int\n \ttwo" + - "\n THREE int\n \tthree", + "\n THREE int\n \tthree" + + "\n TWO int\n \ttwo", wantErr: false, }, @@ -830,10 +830,10 @@ func TestGetDescription(t *testing.T) { cfg: &testSeveralEnv{}, header: nil, want: "Environment variables:" + - "\n ONE int\n \tone" + "\n ENO int (alternative to ONE)\n \tone" + - "\n TWO int\n \ttwo" + - "\n OWT int (alternative to TWO)\n \ttwo", + "\n ONE int\n \tone" + + "\n OWT int (alternative to TWO)\n \ttwo" + + "\n TWO int\n \ttwo", wantErr: false, }, @@ -843,8 +843,8 @@ func TestGetDescription(t *testing.T) { header: nil, want: "Environment variables:" + "\n ONE int\n \tone (default \"1\")" + - "\n TWO int\n \ttwo (default \"2\")" + - "\n THREE int\n \tthree (default \"3\")", + "\n THREE int\n \tthree (default \"3\")" + + "\n TWO int\n \ttwo (default \"2\")", wantErr: false, }, @@ -872,8 +872,8 @@ func TestGetDescription(t *testing.T) { header: &header, want: "test header:" + "\n ONE int\n \tone" + - "\n TWO int\n \ttwo" + - "\n THREE int\n \tthree", + "\n THREE int\n \tthree" + + "\n TWO int\n \ttwo", wantErr: false, }, @@ -921,8 +921,9 @@ func TestFUsage(t *testing.T) { usageTexts: nil, want: "Environment variables:" + "\n ONE int\n \tone" + + "\n THREE int\n \tthree" + "\n TWO int\n \ttwo" + - "\n THREE int\n \tthree\n", + "\n", }, { @@ -931,8 +932,9 @@ func TestFUsage(t *testing.T) { usageTexts: nil, want: "test header:" + "\n ONE int\n \tone" + + "\n THREE int\n \tthree" + "\n TWO int\n \ttwo" + - "\n THREE int\n \tthree\n", + "\n", }, { @@ -946,8 +948,9 @@ func TestFUsage(t *testing.T) { want: "test1\ntest2\ntest3\n" + "\nEnvironment variables:" + "\n ONE int\n \tone" + + "\n THREE int\n \tthree" + "\n TWO int\n \ttwo" + - "\n THREE int\n \tthree\n", + "\n", }, { @@ -961,8 +964,9 @@ func TestFUsage(t *testing.T) { want: "test1\ntest2\ntest3\n" + "\ntest header:" + "\n ONE int\n \tone" + + "\n THREE int\n \tthree" + "\n TWO int\n \ttwo" + - "\n THREE int\n \tthree\n", + "\n", }, } for _, tt := range tests { @@ -988,6 +992,111 @@ func TestFUsage(t *testing.T) { } } +func TestFUsageNested(t *testing.T) { + type testNestedEnv struct { + App struct { + Port int `env:"PORT" env-description:"app port"` + Cache struct { + Type string `env:"TYPE" env-description:"cache type"` + Redis struct { + Host string `env:"HOST" env-description:"redis host"` + } `env-prefix:"REDIS_"` + } `env-prefix:"CACHE_"` + } `env-prefix:"APP_"` + Database struct { + Host string `env:"HOST" env-description:"database host"` + } `env-prefix:"DATABASE_"` + } + + customHeader := "test header:" + + tests := []struct { + name string + headerText *string + usageTexts []string + want string + }{ + { + name: "no custom usage", + headerText: nil, + usageTexts: nil, + want: "Environment variables:" + + "\n APP_CACHE_REDIS_HOST string\n \tredis host" + + "\n APP_CACHE_TYPE string\n \tcache type" + + "\n APP_PORT int\n \tapp port" + + "\n DATABASE_HOST string\n \tdatabase host" + + "\n", + }, + + { + name: "custom header", + headerText: &customHeader, + usageTexts: nil, + want: "test header:" + + "\n APP_CACHE_REDIS_HOST string\n \tredis host" + + "\n APP_CACHE_TYPE string\n \tcache type" + + "\n APP_PORT int\n \tapp port" + + "\n DATABASE_HOST string\n \tdatabase host" + + "\n", + }, + + { + name: "custom usages", + headerText: nil, + usageTexts: []string{ + "test1", + "test2", + "test3", + }, + want: "test1\ntest2\ntest3\n" + + "\nEnvironment variables:" + + "\n APP_CACHE_REDIS_HOST string\n \tredis host" + + "\n APP_CACHE_TYPE string\n \tcache type" + + "\n APP_PORT int\n \tapp port" + + "\n DATABASE_HOST string\n \tdatabase host" + + "\n", + }, + + { + name: "custom usages and header", + headerText: &customHeader, + usageTexts: []string{ + "test1", + "test2", + "test3", + }, + want: "test1\ntest2\ntest3\n" + + "\ntest header:" + + "\n APP_CACHE_REDIS_HOST string\n \tredis host" + + "\n APP_CACHE_TYPE string\n \tcache type" + + "\n APP_PORT int\n \tapp port" + + "\n DATABASE_HOST string\n \tdatabase host" + + "\n", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &bytes.Buffer{} + uFuncs := make([]func(), 0, len(tt.usageTexts)) + for _, text := range tt.usageTexts { + uFuncs = append(uFuncs, func(a string) func() { + return func() { + fmt.Fprintln(w, a) + } + }(text)) + } + var cfg testNestedEnv + FUsage(w, &cfg, tt.headerText, uFuncs...)() + gotRaw, _ := ioutil.ReadAll(w) + got := string(gotRaw) + + if got != tt.want { + t.Errorf("wrong output %v, want %v", got, tt.want) + } + }) + } +} + func TestReadConfig(t *testing.T) { type config struct { Number int64 `edn:"number" yaml:"number" env:"TEST_NUMBER" env-default:"1"` diff --git a/example_test.go b/example_test.go index a17bc25..61a1690 100644 --- a/example_test.go +++ b/example_test.go @@ -28,10 +28,10 @@ func ExampleGetDescription() { //Output: Environment variables: // ONE int64 // first parameter - // TWO float64 - // second parameter // THREE string // third parameter + // TWO float64 + // second parameter } // ExampleGetDescription_defaults builds a description text from structure tags with description of default values @@ -53,10 +53,10 @@ func ExampleGetDescription_defaults() { //Output: Environment variables: // ONE int64 // first parameter (default "1") - // TWO float64 - // second parameter (default "2.2") // THREE string // third parameter (default "test") + // TWO float64 + // second parameter (default "2.2") } // ExampleGetDescription_variableList builds a description text from structure tags with description of alternative variables @@ -76,10 +76,10 @@ func ExampleGetDescription_variableList() { //Output: Environment variables: // ONE int64 // first found parameter - // TWO int64 (alternative to ONE) - // first found parameter // THREE int64 (alternative to ONE) // first found parameter + // TWO int64 (alternative to ONE) + // first found parameter } // ExampleGetDescription_customHeaderText builds a description text from structure tags with custom header string @@ -103,10 +103,10 @@ func ExampleGetDescription_customHeaderText() { //Output: Custom header text: // ONE int64 // first parameter - // TWO float64 - // second parameter // THREE string // third parameter + // TWO float64 + // second parameter } // ExampleUpdateEnv updates variables in the configuration structure. @@ -279,8 +279,8 @@ func ExampleUsage() { // My sweet variables: // ONE int64 // first parameter - // TWO float64 - // second parameter // THREE string // third parameter + // TWO float64 + // second parameter }