Skip to content

Commit

Permalink
Sort environment variables in usage output (#124)
Browse files Browse the repository at this point in the history
  • Loading branch information
ilyakaznacheev committed Jul 20, 2023
2 parents c35e574 + 9b1184a commit 654da51
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 36 deletions.
32 changes: 19 additions & 13 deletions cleanenv.go
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
"strings"
"time"
Expand All @@ -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"
)

Expand Down Expand Up @@ -571,14 +572,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 @@ -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.
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 @@ -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
}

0 comments on commit 654da51

Please sign in to comment.