Skip to content

Commit

Permalink
feat: GetFieldParams and GetFieldParamsWithOptions functions (#261)
Browse files Browse the repository at this point in the history
* Add GetFieldParams and GetFieldParamsWithOptions functions

* Add nested structure to tests

* Fixes after merge with origin/main

* Added missing godocs and fixed golangci-lint
  • Loading branch information
saturn4er committed Jul 25, 2023
1 parent 4f6bb22 commit e7e49c4
Show file tree
Hide file tree
Showing 2 changed files with 160 additions and 43 deletions.
149 changes: 106 additions & 43 deletions env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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{})
Expand All @@ -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
Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -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
Expand Down
54 changes: 54 additions & 0 deletions env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down

0 comments on commit e7e49c4

Please sign in to comment.