From e7e49c464796841b50737b73bac531a01d94bf66 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Tue, 25 Jul 2023 15:52:02 +0300 Subject: [PATCH] feat: GetFieldParams and GetFieldParamsWithOptions functions (#261) * Add GetFieldParams and GetFieldParamsWithOptions functions * Add nested structure to tests * Fixes after merge with origin/main * Added missing godocs and fixed golangci-lint --- env.go | 149 +++++++++++++++++++++++++++++++++++++--------------- env_test.go | 54 +++++++++++++++++++ 2 files changed, 160 insertions(+), 43 deletions(-) diff --git a/env.go b/env.go index be8ee53..1c8e70d 100644 --- a/env.go +++ b/env.go @@ -95,6 +95,9 @@ type ParserFunc func(v string) (interface{}, error) // OnSetFn is a hook that can be run when a value is set. type OnSetFn func(tag string, value interface{}, isDefault bool) +// processFieldFn is a function which takes all information about a field and processes it. +type processFieldFn func(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error + // Options for the parser. type Options struct { // Environment keys and values that will be accessible for the service. @@ -161,16 +164,43 @@ func optionsWithEnvPrefix(field reflect.StructField, opts Options) Options { // Parse parses a struct containing `env` tags and loads its values from // environment variables. func Parse(v interface{}) error { - return parseInternal(v, defaultOptions()) + return parseInternal(v, setField, defaultOptions()) } -// Parse parses a struct containing `env` tags and loads its values from +// ParseWithOptions parses a struct containing `env` tags and loads its values from // environment variables. func ParseWithOptions(v interface{}, opts Options) error { - return parseInternal(v, customOptions(opts)) + return parseInternal(v, setField, customOptions(opts)) } -func parseInternal(v interface{}, opts Options) error { +// GetFieldParams parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParams(v interface{}) ([]FieldParams, error) { + return GetFieldParamsWithOptions(v, defaultOptions()) +} + +// GetFieldParamsWithOptions parses a struct containing `env` tags and returns information about +// tags it found. +func GetFieldParamsWithOptions(v interface{}, opts Options) ([]FieldParams, error) { + var result []FieldParams + err := parseInternal( + v, + func(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error { + if fieldParams.OwnKey != "" { + result = append(result, fieldParams) + } + return nil + }, + customOptions(opts), + ) + if err != nil { + return nil, err + } + + return result, nil +} + +func parseInternal(v interface{}, processField processFieldFn, opts Options) error { ptrRef := reflect.ValueOf(v) if ptrRef.Kind() != reflect.Ptr { return newAggregateError(NotStructPtrError{}) @@ -179,10 +209,11 @@ func parseInternal(v interface{}, opts Options) error { if ref.Kind() != reflect.Struct { return newAggregateError(NotStructPtrError{}) } - return doParse(ref, opts) + + return doParse(ref, processField, opts) } -func doParse(ref reflect.Value, opts Options) error { +func doParse(ref reflect.Value, processField processFieldFn, opts Options) error { refType := ref.Type() var agrErr AggregateError @@ -191,7 +222,7 @@ func doParse(ref reflect.Value, opts Options) error { refField := ref.Field(i) refTypeField := refType.Field(i) - if err := doParseField(refField, refTypeField, opts); err != nil { + if err := doParseField(refField, refTypeField, processField, opts); err != nil { if val, ok := err.(AggregateError); ok { agrErr.Errors = append(agrErr.Errors, val.Errors...) } else { @@ -207,27 +238,41 @@ func doParse(ref reflect.Value, opts Options) error { return agrErr } -func doParseField(refField reflect.Value, refTypeField reflect.StructField, opts Options) error { +func doParseField(refField reflect.Value, refTypeField reflect.StructField, processField processFieldFn, opts Options) error { if !refField.CanSet() { return nil } if reflect.Ptr == refField.Kind() && !refField.IsNil() { - return parseInternal(refField.Interface(), optionsWithEnvPrefix(refTypeField, opts)) + return parseInternal(refField.Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) } if reflect.Struct == refField.Kind() && refField.CanAddr() && refField.Type().Name() == "" { - return parseInternal(refField.Addr().Interface(), optionsWithEnvPrefix(refTypeField, opts)) + return parseInternal(refField.Addr().Interface(), processField, optionsWithEnvPrefix(refTypeField, opts)) } - value, err := get(refTypeField, opts) + + params, err := parseFieldParams(refTypeField, opts) if err != nil { return err } - if value != "" { - return set(refField, refTypeField, value, opts.FuncMap) + if err := processField(refField, refTypeField, opts, params); err != nil { + return err } if reflect.Struct == refField.Kind() { - return doParse(refField, optionsWithEnvPrefix(refTypeField, opts)) + return doParse(refField, processField, optionsWithEnvPrefix(refTypeField, opts)) + } + + return nil +} + +func setField(refField reflect.Value, refTypeField reflect.StructField, opts Options, fieldParams FieldParams) error { + value, err := get(fieldParams, opts) + if err != nil { + return err + } + + if value != "" { + return set(refField, refTypeField, value, opts.FuncMap) } return nil @@ -246,71 +291,89 @@ func toEnvName(input string) string { return string(output) } -func get(field reflect.StructField, opts Options) (val string, err error) { - var exists bool - var isDefault bool - var loadFile bool - var unset bool - var notEmpty bool - var expand bool +// FieldParams contains information about parsed field tags. +type FieldParams struct { + OwnKey string + Key string + DefaultValue string + HasDefaultValue bool + Required bool + LoadFile bool + Unset bool + NotEmpty bool + Expand bool +} - required := opts.RequiredIfNoDef +func parseFieldParams(field reflect.StructField, opts Options) (FieldParams, error) { ownKey, tags := parseKeyForOption(field.Tag.Get(opts.TagName)) if ownKey == "" && opts.UseFieldNameByDefault { ownKey = toEnvName(field.Name) } + defaultValue, hasDefaultValue := field.Tag.Lookup("envDefault") + + result := FieldParams{ + OwnKey: ownKey, + Key: opts.Prefix + ownKey, + Required: opts.RequiredIfNoDef, + DefaultValue: defaultValue, + HasDefaultValue: hasDefaultValue, + } + for _, tag := range tags { switch tag { case "": continue case "file": - loadFile = true + result.LoadFile = true case "required": - required = true + result.Required = true case "unset": - unset = true + result.Unset = true case "notEmpty": - notEmpty = true + result.NotEmpty = true case "expand": - expand = true + result.Expand = true default: - return "", newNoSupportedTagOptionError(tag) + return FieldParams{}, newNoSupportedTagOptionError(tag) } } - prefix := opts.Prefix - key := prefix + ownKey - defaultValue, defExists := field.Tag.Lookup("envDefault") - val, exists, isDefault = getOr(key, defaultValue, defExists, opts.Environment) + return result, nil +} + +func get(fieldParams FieldParams, opts Options) (val string, err error) { + var exists, isDefault bool + + val, exists, isDefault = getOr(fieldParams.Key, fieldParams.DefaultValue, fieldParams.HasDefaultValue, opts.Environment) - if expand { + if fieldParams.Expand { val = os.ExpandEnv(val) } - if unset { - defer os.Unsetenv(key) + if fieldParams.Unset { + defer os.Unsetenv(fieldParams.Key) } - if required && !exists && len(ownKey) > 0 { - return "", newEnvVarIsNotSet(key) + if fieldParams.Required && !exists && len(fieldParams.OwnKey) > 0 { + return "", newEnvVarIsNotSet(fieldParams.Key) } - if notEmpty && val == "" { - return "", newEmptyEnvVarError(key) + if fieldParams.NotEmpty && val == "" { + return "", newEmptyEnvVarError(fieldParams.Key) } - if loadFile && val != "" { + if fieldParams.LoadFile && val != "" { filename := val val, err = getFromFile(filename) if err != nil { - return "", newLoadFileContentError(filename, key, err) + return "", newLoadFileContentError(filename, fieldParams.Key, err) } } if opts.OnSet != nil { - if ownKey != "" { - opts.OnSet(key, val, isDefault) + if fieldParams.OwnKey != "" { + opts.OnSet(fieldParams.Key, val, isDefault) } } return val, err diff --git a/env_test.go b/env_test.go index fabbeea..999232b 100644 --- a/env_test.go +++ b/env_test.go @@ -1740,6 +1740,60 @@ func TestErrorIs(t *testing.T) { }) } +type FieldParamsConfig struct { + Simple []string `env:"SIMPLE"` + WithoutEnv string + privateWithEnv string `env:"PRIVATE_WITH_ENV"` //nolint:unused + WithDefault string `env:"WITH_DEFAULT" envDefault:"default"` + Required string `env:"REQUIRED,required"` + File string `env:"FILE,file"` + Unset string `env:"UNSET,unset"` + NotEmpty string `env:"NOT_EMPTY,notEmpty"` + Expand string `env:"EXPAND,expand"` + NestedConfig struct { + Simple []string `env:"SIMPLE"` + } `envPrefix:"NESTED_"` +} + +func TestGetFieldParams(t *testing.T) { + var config FieldParamsConfig + params, err := GetFieldParams(&config) + isNoErr(t, err) + + expectedParams := []FieldParams{ + {OwnKey: "SIMPLE", Key: "SIMPLE"}, + {OwnKey: "WITH_DEFAULT", Key: "WITH_DEFAULT", DefaultValue: "default", HasDefaultValue: true}, + {OwnKey: "REQUIRED", Key: "REQUIRED", Required: true}, + {OwnKey: "FILE", Key: "FILE", LoadFile: true}, + {OwnKey: "UNSET", Key: "UNSET", Unset: true}, + {OwnKey: "NOT_EMPTY", Key: "NOT_EMPTY", NotEmpty: true}, + {OwnKey: "EXPAND", Key: "EXPAND", Expand: true}, + {OwnKey: "SIMPLE", Key: "NESTED_SIMPLE"}, + } + isTrue(t, len(params) == len(expectedParams)) + isTrue(t, areEqual(params, expectedParams)) +} + +func TestGetFieldParamsWithPrefix(t *testing.T) { + var config FieldParamsConfig + + params, err := GetFieldParamsWithOptions(&config, Options{Prefix: "FOO_"}) + isNoErr(t, err) + + expectedParams := []FieldParams{ + {OwnKey: "SIMPLE", Key: "FOO_SIMPLE"}, + {OwnKey: "WITH_DEFAULT", Key: "FOO_WITH_DEFAULT", DefaultValue: "default", HasDefaultValue: true}, + {OwnKey: "REQUIRED", Key: "FOO_REQUIRED", Required: true}, + {OwnKey: "FILE", Key: "FOO_FILE", LoadFile: true}, + {OwnKey: "UNSET", Key: "FOO_UNSET", Unset: true}, + {OwnKey: "NOT_EMPTY", Key: "FOO_NOT_EMPTY", NotEmpty: true}, + {OwnKey: "EXPAND", Key: "FOO_EXPAND", Expand: true}, + {OwnKey: "SIMPLE", Key: "FOO_NESTED_SIMPLE"}, + } + isTrue(t, len(params) == len(expectedParams)) + isTrue(t, areEqual(params, expectedParams)) +} + func isTrue(tb testing.TB, b bool) { tb.Helper()