Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GetFieldParams and GetFieldParamsWithOptions functions #261

Merged
merged 5 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 101 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,39 @@ 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 {
func GetFieldParams(v interface{}) ([]FieldParams, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing godoc

return GetFieldParamsWithOptions(v, defaultOptions())
}

func GetFieldParamsWithOptions(v interface{}, opts Options) ([]FieldParams, error) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing godoc

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 +205,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 +218,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 +234,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 +287,88 @@ 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
type FieldParams struct {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing godoc

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"`
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