Skip to content

Commit

Permalink
require fields to be present if not tagged with optional
Browse files Browse the repository at this point in the history
  • Loading branch information
fumoboy007 committed Oct 29, 2020
1 parent 685da7c commit 54b0569
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 32 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ import (
)

type Config struct {
Host string
Port int
Host string
Port int
Password string `env:",optional"`
}

func main() {
Expand All @@ -53,12 +54,18 @@ func main() {
}
fmt.Println(cfg.Host)
fmt.Println(cfg.Port)
fmt.Println(cfg.Password)
// Output:
// 127.0.0.1
// 8080
//
}
```

In the above code, the `Password` field is tagged as `optional` whereas the other fields are untagged.
If a field is not tagged as `optional`, it is a required field, so `env.Load()` will return an error if
the corresponding environment variable is missing.

### Environment Variable Names

When go-env populates a struct from environment variables, it uses the following rules to match
Expand Down
72 changes: 61 additions & 11 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ type (
// Set sets the object with a string value.
Set(value string) error
}

options struct {
optional bool
secret bool
}
)

var (
Expand Down Expand Up @@ -123,7 +128,7 @@ func (l *Loader) Load(structPtr interface{}) error {
continue
}

name, secret := getName(ft.Tag.Get(TagName), ft.Name)
name, options := getName(ft.Tag.Get(TagName), ft.Name)
if name == "-" {
continue
}
Expand All @@ -133,7 +138,7 @@ func (l *Loader) Load(structPtr interface{}) error {
if value, ok := l.lookup(name); ok {
logValue := value
if l.log != nil {
if secret {
if options.secret {
l.log("set %v with $%v=\"***\"", ft.Name, name)
} else {
l.log("set %v with $%v=\"%v\"", ft.Name, name, logValue)
Expand All @@ -142,6 +147,8 @@ func (l *Loader) Load(structPtr interface{}) error {
if err := setValue(f, value); err != nil {
return fmt.Errorf("error reading \"%v\": %v", ft.Name, err)
}
} else if !options.optional {
return fmt.Errorf("missing required environment variable \"%v\"", name)
}
}
return nil
Expand All @@ -159,18 +166,61 @@ func indirect(v reflect.Value) reflect.Value {
return v
}

// getName generates the environment variable name from a struct field tag and the field name.
func getName(tag string, field string) (string, bool) {
name := strings.TrimSuffix(tag, ",secret")
nameLen := len(name)
// getName extracts the environment variable name and options from the given struct field tag or if unspecified,
// generates the environment variable name from the given field name.
func getName(tag string, field string) (string, options) {
name, options := getOptions(tag)
if name == "" {
name = camelCaseToUpperSnakeCase(field)
}
return name, options
}

// If the `,secret` suffix was found, it would have been trimmed, so the length should be different.
secret := nameLen < len(tag)
// getOptions extracts the environment variable name and options from the given struct field tag.
func getOptions(tag string) (string, options) {
var options options

if nameLen == 0 {
name = camelCaseToUpperSnakeCase(field)
optionNamesAndPointers := []struct {
name string
pointer *bool
}{
{"optional", &options.optional},
{"secret", &options.secret},
}

trimmedTag := tag
// We do not know the order that the options will be specified in, so we need to do extra checking.
// `O(n^2)` but `n` is really small.
outerLoop:
for {
for i, optionNameAndPointer := range optionNamesAndPointers {
var option bool
if trimmedTag, option = getOption(trimmedTag, optionNameAndPointer.name); option {
*optionNameAndPointer.pointer = option

// We found the option, so remove it from the slice and retry the rest of the options.
optionNamesAndPointers[i] = optionNamesAndPointers[len(optionNamesAndPointers)-1]
optionNamesAndPointers = optionNamesAndPointers[:len(optionNamesAndPointers)-1]
continue outerLoop
}
}

// We checked for all the options and none were specified, so we are done.
break
}
return name, secret

return trimmedTag, options
}

// getOption checks whether the given struct field tag contains the suffix for the given option and
// returns the tag without the suffix.
func getOption(tag string, optionName string) (string, bool) {
trimmedTag := strings.TrimSuffix(tag, ","+optionName)

// If the suffix for the option was found, it would have been trimmed, so the length should be different.
option := len(trimmedTag) < len(tag)

return trimmedTag, option
}

// camelCaseToUpperSnakeCase converts a name from camelCase format into UPPER_SNAKE_CASE format.
Expand Down
66 changes: 49 additions & 17 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,25 +150,30 @@ func Test_camelCaseToUpperSnakeCase(t *testing.T) {

func Test_getName(t *testing.T) {
tests := []struct {
tag string
tg string
field string
name string
secret bool
tag string
tg string
field string
name string
options options
}{
{"t1", "", "Name", "NAME", false},
{"t2", "", "MyName", "MY_NAME", false},
{"t3", "NaME", "Name", "NaME", false},
{"t4", "NaME,secret", "Name", "NaME", true},
{"t5", ",secret", "Name", "NAME", true},
{"t6", "NameWith,Comma", "Name", "NameWith,Comma", false},
{"t7", "NameWith,Comma,secret", "Name", "NameWith,Comma", true},
{"t1", "", "Name", "NAME", options{optional: false, secret: false}},
{"t2", "", "MyName", "MY_NAME", options{optional: false, secret: false}},
{"t3", "NaME", "Name", "NaME", options{optional: false, secret: false}},
{"t4", "NaME,optional", "Name", "NaME", options{optional: true, secret: false}},
{"t5", "NaME,secret", "Name", "NaME", options{optional: false, secret: true}},
{"t6", ",optional", "Name", "NAME", options{optional: true, secret: false}},
{"t7", ",secret", "Name", "NAME", options{optional: false, secret: true}},
{"t8", ",optional,secret", "Name", "NAME", options{optional: true, secret: true}},
{"t9", ",secret,optional", "Name", "NAME", options{optional: true, secret: true}},
{"t10", "NameWith,Comma", "Name", "NameWith,Comma", options{optional: false, secret: false}},
{"t11", "NameWith,Comma,optional", "Name", "NameWith,Comma", options{optional: true, secret: false}},
{"t12", "NameWith,Comma,optional,secret", "Name", "NameWith,Comma", options{optional: true, secret: true}},
}

for _, test := range tests {
name, secret := getName(test.tg, test.field)
name, options := getName(test.tg, test.field)
assert.Equal(t, test.name, name, test.tag)
assert.Equal(t, test.secret, secret, test.tag)
assert.Equal(t, test.options, options, test.tag)
}
}

Expand Down Expand Up @@ -207,7 +212,18 @@ func mockLookup2(name string) (string, bool) {

func mockLookup3(name string) (string, bool) {
data := map[string]string{
"PORT": "a8080",
"HOST": "localhost",
"PORT": "a8080", // invalid `int`
"URL": "http://example.com",
}
value, ok := data[name]
return value, ok
}

func mockLookup4(name string) (string, bool) {
data := map[string]string{
"PORT": "8080",
"URL": "http://example.com",
}
value, ok := data[name]
return value, ok
Expand Down Expand Up @@ -235,6 +251,12 @@ type Config3 struct {
Embedded
}

type Config4 struct {
Host string `env:",optional"`
Port int
Embedded
}

func TestLoader_Load(t *testing.T) {
l := NewWithLookup("", mockLookup, nil)

Expand Down Expand Up @@ -267,12 +289,22 @@ func TestLoader_Load(t *testing.T) {
var cfg3 Config1
l = NewWithLookup("", mockLookup3, nil)
err = l.Load(&cfg3)
assert.NotNil(t, err)
assert.EqualError(t, err, "error reading \"Port\": strconv.ParseInt: parsing \"a8080\": invalid syntax")

var cfg4 Config3
l = NewWithLookup("", mockLookup3, nil)
err = l.Load(&cfg4)
assert.NotNil(t, err)
assert.EqualError(t, err, "error reading \"Port\": strconv.ParseInt: parsing \"a8080\": invalid syntax")

var cfg5 Config1
l = NewWithLookup("T_", mockLookup4, nil)
err = l.Load(&cfg5)
assert.EqualError(t, err, "missing required environment variable \"T_HOST\"")

var cfg6 Config4
l = NewWithLookup("", mockLookup4, nil)
err = l.Load(&cfg6)
assert.Nil(t, err)
}

func TestNew(t *testing.T) {
Expand Down
7 changes: 5 additions & 2 deletions example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ package env_test

import (
"fmt"
"github.com/qiangxue/go-env"
"log"
"os"

"github.com/qiangxue/go-env"
)

type Config struct {
Host string
Port int
Password string `env:",secret"`
Password string `env:",optional,secret"`
}

func Example_one() {
Expand All @@ -23,9 +24,11 @@ func Example_one() {
}
fmt.Println(cfg.Host)
fmt.Println(cfg.Port)
fmt.Println(cfg.Password)
// Output:
// 127.0.0.1
// 8080
//
}

func Example_two() {
Expand Down

0 comments on commit 54b0569

Please sign in to comment.