Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort environment variables in usage output #124

Merged
merged 1 commit into from Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 19 additions & 13 deletions cleanenv.go
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"time"
Expand All @@ -26,28 +27,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"
)

Expand Down Expand Up @@ -561,14 +562,16 @@ func GetDescription(cfg interface{}, headerText *string) (string, error) {
return "", err
}

var header, description string
var header string

if headerText != nil {
header = *headerText
} else {
header = "Environment variables:"
}

description := make([]string, 0)

for _, m := range meta {
if len(m.envList) == 0 {
continue
Expand All @@ -584,14 +587,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.
Expand Down
135 changes: 122 additions & 13 deletions cleanenv_test.go
Expand Up @@ -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,
},

Expand All @@ -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,
},

Expand All @@ -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,
},

Expand Down Expand Up @@ -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,
},

Expand Down Expand Up @@ -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",
},

{
Expand All @@ -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",
},

{
Expand All @@ -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",
},

{
Expand All @@ -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 {
Expand All @@ -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"`
Expand Down
20 changes: 10 additions & 10 deletions example_test.go
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -266,8 +266,8 @@ func ExampleUsage() {
// My sweet variables:
// ONE int64
// first parameter
// TWO float64
// second parameter
// THREE string
// third parameter
// TWO float64
// second parameter
}