From fa034df3fdab89be6dfde6f169d84b21657ff11b Mon Sep 17 00:00:00 2001 From: jimmykodes Date: Thu, 30 Jul 2020 15:31:27 -0700 Subject: [PATCH 1/3] Add default value option in env tag this will split a tag on a || and use the value following the pipes (if any) as a default value if none of the env var options return a value --- env.go | 15 +++++++++++++-- env_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/env.go b/env.go index 2d76cc4..3f3fae1 100644 --- a/env.go +++ b/env.go @@ -86,7 +86,7 @@ func Unmarshal(es EnvSet, v interface{}) error { return ErrUnexportedField } - envKeys := strings.Split(tag, ",") + envKeys, defaultVal := parseTag(tag) var ( envValue string @@ -99,7 +99,9 @@ func Unmarshal(es EnvSet, v interface{}) error { } } - if !ok { + if !ok && defaultVal != "" { + envValue = defaultVal + } else if !ok { continue } @@ -237,3 +239,12 @@ func Marshal(v interface{}) (EnvSet, error) { return es, nil } + +func parseTag(tag string) ([]string, string) { + tagData := strings.Split(tag, "||") + envKeys := strings.Split(tagData[0], ",") + if len(tagData) > 1 { + return envKeys, tagData[1] + } + return envKeys, "" +} diff --git a/env_test.go b/env_test.go index 1ba2e95..84d4914 100644 --- a/env_test.go +++ b/env_test.go @@ -64,6 +64,15 @@ type UnexportedStruct struct { home string `env:"HOME"` } +type DefaultValueStruct struct { + DefaultString string `env:"MISSING_STRING||found"` + DefaultBool bool `env:"MISSING_BOOL||true"` + DefaultInt int `env:"MISSING_INT||7"` + DefaultDuration time.Duration `env:"MISSING_DURATION||5s"` + DefaultWithOptionsMissing string `env:"MISSING_1,MISSING_2||present"` + DefaultWithOptionsPresent string `env:"MISSING_1,PRESENT||present"` +} + func TestUnmarshal(t *testing.T) { environ := map[string]string{ "HOME": "/home/test", @@ -233,6 +242,30 @@ func TestUnmarshalUnexported(t *testing.T) { } } +func TestUnmarshalDefaultValues(t *testing.T) { + environ := map[string]string { + "PRESENT": "youFoundMe", + } + var defaultValueStruct DefaultValueStruct + err := Unmarshal(environ, &defaultValueStruct) + if err != nil { + t.Errorf("Expected no error but got %s", err) + } + testCases := [][]interface{}{ + {defaultValueStruct.DefaultInt, 7}, + {defaultValueStruct.DefaultBool, true}, + {defaultValueStruct.DefaultString, "found"}, + {defaultValueStruct.DefaultDuration, 5 * time.Second}, + {defaultValueStruct.DefaultWithOptionsMissing, "present"}, + {defaultValueStruct.DefaultWithOptionsPresent, "youFoundMe"}, + } + for _, testCase := range testCases { + if testCase[0] != testCase[1] { + t.Errorf("Expected field value to be '%v' but got '%v'", testCase[0], testCase[1]) + } + } +} + func TestMarshal(t *testing.T) { validStruct := ValidStruct{ Home: "/home/test", From 4b25b379d2236ff5bbb5031a95058b29dc6f1b59 Mon Sep 17 00:00:00 2001 From: jimmykodes Date: Thu, 30 Jul 2020 15:39:45 -0700 Subject: [PATCH 2/3] add default value example to readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index acc5ca5..8613e84 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ type Environment struct { Extras env.EnvSet - Duration time.Duration `env:"TYPE_DURATION"` + Duration time.Duration `env:"TYPE_DURATION"` + DefaultValue string `env:"MISSING_VAR||default_value"` } func main() { From 8b9a491baff5af134c355781dea985cfc61f3718 Mon Sep 17 00:00:00 2001 From: jimmykodes Date: Sun, 2 Aug 2020 20:04:18 -0700 Subject: [PATCH 3/3] Use default= instead of || to specify default vals --- README.md | 5 +++-- env.go | 50 ++++++++++++++++++++++++++++++++++++++------------ env_test.go | 41 ++++++++++++++++++++++++++++++++++------- 3 files changed, 75 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 8613e84..20526d0 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ type Environment struct { Extras env.EnvSet - Duration time.Duration `env:"TYPE_DURATION"` - DefaultValue string `env:"MISSING_VAR||default_value"` + Duration time.Duration `env:"TYPE_DURATION"` + DefaultValue string `env:"MISSING_VAR,default=default_value"` + RequiredValue string `env:"IM_REQUIRED,required=true"` } func main() { diff --git a/env.go b/env.go index 3f3fae1..b3c7aee 100644 --- a/env.go +++ b/env.go @@ -36,6 +36,9 @@ var ( // ErrUnexportedField returned when a field with tag "env" is not exported. ErrUnexportedField = errors.New("field must be exported") + + // ErrMissingRequiredValue returned when a field with required=true contains no value or default + ErrMissingRequiredValue = errors.New("value for this field is required") ) // Unmarshal parses an EnvSet and stores the result in the value pointed to by @@ -86,23 +89,27 @@ func Unmarshal(es EnvSet, v interface{}) error { return ErrUnexportedField } - envKeys, defaultVal := parseTag(tag) + envTag := parseTag(tag) var ( envValue string ok bool ) - for _, envKey := range envKeys { + for _, envKey := range envTag.Keys { envValue, ok = es[envKey] if ok { break } } - if !ok && defaultVal != "" { - envValue = defaultVal - } else if !ok { - continue + if !ok { + if envTag.Default != "" { + envValue = envTag.Default + } else if envTag.Required { + return ErrMissingRequiredValue + } else { + continue + } } err := set(typeField.Type, valueField, envValue) @@ -240,11 +247,30 @@ func Marshal(v interface{}) (EnvSet, error) { return es, nil } -func parseTag(tag string) ([]string, string) { - tagData := strings.Split(tag, "||") - envKeys := strings.Split(tagData[0], ",") - if len(tagData) > 1 { - return envKeys, tagData[1] +type tag struct { + Keys []string + Default string + Required bool +} + +func parseTag(tagString string) tag { + var t tag + envKeys := strings.Split(tagString, ",") + for _, key := range envKeys { + if strings.Contains(key, "=") { + keyData := strings.Split(key, "=") + switch strings.ToLower(keyData[0]) { + case "default": + t.Default = keyData[1] + case "required": + t.Required = strings.ToLower(keyData[1]) == "true" + default: + // just ignoring unsupported keys + continue + } + } else { + t.Keys = append(t.Keys, key) + } } - return envKeys, "" + return t } diff --git a/env_test.go b/env_test.go index 84d4914..e4b036f 100644 --- a/env_test.go +++ b/env_test.go @@ -65,12 +65,19 @@ type UnexportedStruct struct { } type DefaultValueStruct struct { - DefaultString string `env:"MISSING_STRING||found"` - DefaultBool bool `env:"MISSING_BOOL||true"` - DefaultInt int `env:"MISSING_INT||7"` - DefaultDuration time.Duration `env:"MISSING_DURATION||5s"` - DefaultWithOptionsMissing string `env:"MISSING_1,MISSING_2||present"` - DefaultWithOptionsPresent string `env:"MISSING_1,PRESENT||present"` + DefaultString string `env:"MISSING_STRING,default=found"` + DefaultBool bool `env:"MISSING_BOOL,default=true"` + DefaultInt int `env:"MISSING_INT,default=7"` + DefaultDuration time.Duration `env:"MISSING_DURATION,default=5s"` + DefaultWithOptionsMissing string `env:"MISSING_1,MISSING_2,default=present"` + DefaultWithOptionsPresent string `env:"MISSING_1,PRESENT,default=present"` +} + +type RequiredValueStruct struct { + Required string `env:"REQUIRED_VAL,required=true"` + RequiredWithDefault string `env:"REQUIRED_MISSING,default=myValue,required=true"` + NotRequired string `env:"NOT_REQUIRED,required=false"` + InvalidExtra string `env:"INVALID,invalid=invalid"` } func TestUnmarshal(t *testing.T) { @@ -261,11 +268,31 @@ func TestUnmarshalDefaultValues(t *testing.T) { } for _, testCase := range testCases { if testCase[0] != testCase[1] { - t.Errorf("Expected field value to be '%v' but got '%v'", testCase[0], testCase[1]) + t.Errorf("Expected field value to be '%v' but got '%v'", testCase[1], testCase[0]) } } } +func TestUnmarshalRequiredValues(t *testing.T) { + environ := map[string]string{} + var requiredValuesStruct RequiredValueStruct + err := Unmarshal(environ, &requiredValuesStruct) + if err != ErrMissingRequiredValue { + t.Errorf("Expected error 'ErrMissingRequiredValue' but go '%s'", err) + } + environ["REQUIRED_VAL"] = "required" + err = Unmarshal(environ, &requiredValuesStruct) + if err != nil { + t.Errorf("Expected no error but got '%s'", err) + } + if requiredValuesStruct.Required != "required" { + t.Errorf("Expected field value to be '%s' but got '%s'", "required", requiredValuesStruct.Required) + } + if requiredValuesStruct.RequiredWithDefault != "myValue" { + t.Errorf("Expected field value to be '%s' but got '%s'", "myValue", requiredValuesStruct.RequiredWithDefault) + } +} + func TestMarshal(t *testing.T) { validStruct := ValidStruct{ Home: "/home/test",