adding envs tagging/interpolation

This commit is contained in:
brent saner
2024-06-17 04:33:30 -04:00
parent eed9c34ebf
commit b64c318a4a
18 changed files with 734 additions and 1575 deletions

View File

@@ -11,6 +11,7 @@ import (
`sync`
`r00t2.io/goutils/multierr`
`r00t2.io/goutils/structutils`
`r00t2.io/sysutils/errs`
`r00t2.io/sysutils/internal`
`r00t2.io/sysutils/paths`
@@ -195,7 +196,7 @@ func HasEnv(key string) (envIsSet bool) {
- a string (pointer only)
- a struct (pointer only)
- a map
- a map (applied to both keys *and* values)
- a slice
and performs variable substitution on strings from environment variables.
@@ -206,30 +207,22 @@ func HasEnv(key string) (envIsSet bool) {
For structs, the tag name used can be changed by setting the StructTagInterpolate
variable in this submodule; the default is `envsub`.
If the tag value is "-", the field will be skipped.
For map fields within structs, the default is to apply interpolation to both keys and values;
this can be changed with the `no_map_key` and `no_map_value` options (tag values).
Any other tag value(s) are ignored.
For map fields within structs etc., the default is to apply interpolation to both keys and values.
All other tag value(s) are ignored.
For maps and slices, Interpolate will recurse into values (e.g. [][]string will work as expected).
Supported struct tag options:
* `no_map_key` - Do not operate on map keys if they are strings or string pointers.
See also InterpolateOptNoMapKey.
* `no_map_value` - Do not operate on map values if they are strings or string pointers.
See also InterpolateOptNoMapValue.
If s is nil, no interpolation will be performed. No error will be returned.
If s is not a valid/supported type, no interpolation will be performed. No error will be returned.
*/
func Interpolate[T any](s T, opts ...optInterpolate) (err error) {
func Interpolate[T any](s T) (err error) {
var sVal reflect.Value = reflect.ValueOf(s)
var sType reflect.Type = sVal.Type()
var kind reflect.Kind = sType.Kind()
var ptrVal reflect.Value
var ptrType reflect.Type
var ptrKind reflect.Kind
var sVal reflect.Value = reflect.ValueOf(s)
var sType reflect.Type = sVal.Type()
var kind reflect.Kind = sType.Kind()
switch kind {
case reflect.Ptr:
@@ -240,30 +233,30 @@ func Interpolate[T any](s T, opts ...optInterpolate) (err error) {
ptrType = ptrVal.Type()
ptrKind = ptrType.Kind()
if ptrKind == reflect.String {
err = interpolateStringReflect(ptrVal, opts, nil)
err = interpolateStringReflect(ptrVal)
} else {
// Otherwise, it should be a struct ptr.
if ptrKind != reflect.Struct {
return
}
err = interpolateStruct(ptrVal, opts, nil)
err = interpolateStruct(ptrVal)
}
case reflect.Map:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
err = interpolateMap(sVal, opts, nil)
err = interpolateMap(sVal)
case reflect.Slice:
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
return
}
err = interpolateSlice(sVal, opts, nil)
err = interpolateSlice(sVal)
/*
case reflect.Struct:
if sVal.IsZero() || !sVal.IsValid() {
return
}
err = interpolateStruct(sVal, opts, nil)
err = interpolateStruct(sVal)
*/
}
@@ -300,153 +293,11 @@ func InterpolateString(s *string) (err error) {
return
}
/*
PopulateStruct takes (a pointer to) a struct and performs *population* on it.
Unlike the InterpolateStruct function, this *completely populates* (or *replaces*)
a field's value with the specified environment variable; no *substitution* is performed.
You can change the tag name used by changing the StructTagPopulate variable in this module;
the default is `envpop`.
Tag value format:
<tag>:"<VAR NAME>[,<option>,<option>...]"
e.g.
envpop:"SOMEVAR"
envpop:"OTHERVAR,force"
envpop:"OTHERVAR,allow_empty"
envpop:"OTHERVAR,force,allow_empty"
If the tag value is "-", or <VAR NAME> is not provided, the field will be explicitly skipped.
(This is the default behavior for struct fields not tagged with `envpop`.)
Recognized options:
* force - Existing field values that are non-empty strings or non-nil pointers are normally skipped; this option always replaces them.
* allow_empty - Normally no replacement will be performed if the specified variable is undefined/not found.
This option allows an empty string to be used instead.
Not very useful for string fields, but potentially useful for string pointer fields.
e.g.:
struct{
// If this is an empty string, it will be replaced with the value of $CWD.
CurrentDir string `envpop:"CWD"`
// This would only populate with $USER if the pointer is nil.
UserName *string `envpop:"USER"`
// This will *always* replace the field's value with the value of $DISPLAY,
// even if not an empty string.
// Note the `force` option.
Display string `envpop:"DISPLAY,force"`
// Likewise, even if not nil, this field's value would be replaced with the value of $SHELL.
Shell *string `envpop:"SHELL,force"`
// This field will be untouched if non-nil, otherwise it will be a pointer to an empty string
// if FOOBAR is undefined.
NonExistentVar *string `envpop:"FOOBAR,allow_empty"`
}
If s is nil, nothing will be done and err will be errs.ErrNilPtr.
If s is not a pointer to a struct, nothing will be done and err will be errs.ErrBadType.
*/
func PopulateStruct[T any](s T) (err error) {
var structVal reflect.Value
var structType reflect.Type
var field reflect.StructField
var fieldVal reflect.Value
var tagVal string
var valSplit []string
var varNm string
var varVal string
var optsMap map[string]bool
var force bool
var allowEmpty bool
var defined bool
if reflect.TypeOf(s).Kind() != reflect.Ptr {
err = errs.ErrBadType
return
}
structVal = reflect.ValueOf(s)
if structVal.IsNil() || structVal.IsZero() || !structVal.IsValid() {
err = errs.ErrNilPtr
return
}
structVal = reflect.ValueOf(s).Elem()
structType = structVal.Type()
if structType.Kind() != reflect.Struct {
err = errs.ErrBadType
return
}
for i := 0; i < structVal.NumField(); i++ {
field = structType.Field(i)
fieldVal = structVal.Field(i)
// Skip explicitly skipped or non-tagged fields.
tagVal = field.Tag.Get(StructTagPopulate)
if tagVal == "" || strings.TrimSpace(tagVal) == "-" || strings.HasPrefix(tagVal, "-,") {
continue
}
fieldVal = structVal.Field(i)
if fieldVal.Kind() != reflect.Ptr && fieldVal.Kind() != reflect.String {
continue
}
optsMap = make(map[string]bool)
valSplit = strings.Split(tagVal, ",")
if valSplit == nil || len(valSplit) == 0 {
continue
}
varNm = valSplit[0]
if strings.TrimSpace(varNm) == "" {
continue
}
if len(valSplit) >= 2 {
for _, o := range valSplit[1:] {
optsMap[o] = true
}
}
force = optsMap["force"]
allowEmpty = optsMap["allow_empty"]
// if !force && (!fieldVal.IsNil() && !fieldVal.IsZero()) {
if !force && !fieldVal.IsZero() {
continue
}
if fieldVal.Kind() == reflect.Ptr {
if field.Type.Elem().Kind() != reflect.String {
continue
}
}
if !fieldVal.CanSet() {
continue
}
varVal, defined = os.LookupEnv(varNm)
if !defined && !allowEmpty {
continue
}
switch fieldVal.Kind() {
case reflect.Ptr:
fieldVal.Set(reflect.ValueOf(&varVal))
case reflect.String:
fieldVal.SetString(varVal)
}
}
return
}
// interpolateMap is used by Interpolate and interpolateReflect for maps. v should be a reflect.Value of a map.
func interpolateMap(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
// interpolateMap is used by Interpolate for maps. v should be a reflect.Value of a map.
func interpolateMap(v reflect.Value) (err error) {
var kVal reflect.Value
var vVal reflect.Value
var newMap reflect.Value
var wg sync.WaitGroup
var numJobs int
@@ -455,7 +306,6 @@ func interpolateMap(v reflect.Value, opts []optInterpolate, tagOpts []optInterpo
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
var t reflect.Type = v.Type()
var kind reflect.Kind = t.Kind()
var valOpts *interpolateOpts = new(interpolateOpts)
if kind != reflect.Map {
err = errs.ErrBadType
@@ -466,78 +316,54 @@ func interpolateMap(v reflect.Value, opts []optInterpolate, tagOpts []optInterpo
return
}
*valOpts = defaultInterpolateOpts
if opts != nil && len(opts) > 0 {
for _, opt := range opts {
if err = opt(valOpts); err != nil {
return
}
}
}
if tagOpts != nil && len(tagOpts) > 0 {
for _, opt := range tagOpts {
if err = opt(valOpts); err != nil {
return
}
}
}
if valOpts.noMapKey && valOpts.noMapVal {
return
}
numJobs = v.Len()
errChan = make(chan error, numJobs)
wg.Add(numJobs)
newMap = reflect.MakeMap(reflect.TypeOf(v.Interface()))
newMap = reflect.MakeMap(v.Type())
for _, e := range v.MapKeys() {
kVal = e
go func(mapK reflect.Value) {
for _, kVal = range v.MapKeys() {
vVal = v.MapIndex(kVal)
go func(key, val reflect.Value) {
var mapErr error
var newKey reflect.Value
var newVal reflect.Value
var vVal reflect.Value = v.MapIndex(mapK)
newKey = reflect.New(key.Type()).Elem()
newVal = reflect.New(val.Type()).Elem()
newKey.Set(key.Convert(newKey.Type()))
newVal.Set(val.Convert(newVal.Type()))
defer wg.Done()
if !valOpts.noMapKey {
newKey = reflect.New(reflect.TypeOf(mapK.Interface()))
newKey.Set(vVal)
if mapK.Kind() == reflect.String {
if mapErr = interpolateStringReflect(newKey, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = interpolateValue(newKey, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
// key
if key.Kind() == reflect.String {
if mapErr = interpolateStringReflect(newKey); mapErr != nil {
errChan <- mapErr
return
}
} else {
newKey = mapK
if mapErr = interpolateValue(newKey); mapErr != nil {
errChan <- mapErr
return
}
}
if !valOpts.noMapVal {
newVal = reflect.New(vVal.Type())
newVal.Set(vVal)
if vVal.Kind() == reflect.String {
if mapErr = interpolateStringReflect(newVal, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
} else {
if mapErr = interpolateValue(newVal, opts, nil); mapErr != nil {
errChan <- mapErr
return
}
// value
if val.Kind() == reflect.String {
if mapErr = interpolateStringReflect(newVal); mapErr != nil {
errChan <- mapErr
return
}
} else {
newVal = vVal
if mapErr = interpolateValue(newVal); mapErr != nil {
errChan <- mapErr
return
}
}
newMap.SetMapIndex(reflect.ValueOf(newKey), reflect.ValueOf(newVal))
}(kVal)
newMap.SetMapIndex(newKey.Convert(key.Type()), newVal.Convert(key.Type()))
}(kVal, vVal)
}
go func() {
@@ -560,13 +386,13 @@ func interpolateMap(v reflect.Value, opts []optInterpolate, tagOpts []optInterpo
return
}
v.Set(newMap)
v.Set(newMap.Convert(v.Type()))
return
}
// interpolateSlice is used by Interpolate and interpolateReflect for slices. v should be a reflect.Value of a slice.
func interpolateSlice(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
// interpolateSlice is used by Interpolate for slices and arrays. v should be a reflect.Value of a slice/array.
func interpolateSlice(v reflect.Value) (err error) {
var wg sync.WaitGroup
var errChan chan error
@@ -575,28 +401,21 @@ func interpolateSlice(v reflect.Value, opts []optInterpolate, tagOpts []optInter
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
var t reflect.Type = v.Type()
var kind reflect.Kind = t.Kind()
var valOpts *interpolateOpts = new(interpolateOpts)
return
if kind != reflect.Slice {
switch kind {
case reflect.Slice:
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
case reflect.Array:
if v.IsZero() || !v.IsValid() {
return
}
default:
err = errs.ErrBadType
return
}
if v.IsNil() || v.IsZero() || !v.IsValid() {
return
}
*valOpts = defaultInterpolateOpts
if opts != nil && len(opts) > 0 {
for _, opt := range opts {
if err = opt(valOpts); err != nil {
return
}
}
}
numJobs = v.Len()
errChan = make(chan error, numJobs)
wg.Add(numJobs)
@@ -604,24 +423,20 @@ func interpolateSlice(v reflect.Value, opts []optInterpolate, tagOpts []optInter
for i := 0; i < v.Len(); i++ {
go func(idx int) {
var sErr error
var newVal reflect.Value
defer wg.Done()
newVal = reflect.New(v.Index(idx).Type())
newVal.Set(v.Index(idx))
if v.Index(idx).Kind() == reflect.String {
if sErr = interpolateStringReflect(newVal, opts, tagOpts); sErr != nil {
if sErr = interpolateStringReflect(v.Index(idx)); sErr != nil {
errChan <- sErr
return
}
} else {
if sErr = interpolateValue(newVal, opts, tagOpts); sErr != nil {
if sErr = interpolateValue(v.Index(idx)); sErr != nil {
errChan <- sErr
return
}
}
v.Index(idx).Set(reflect.ValueOf(newVal))
}(i)
}
@@ -649,10 +464,15 @@ func interpolateSlice(v reflect.Value, opts []optInterpolate, tagOpts []optInter
}
// interpolateStringReflect is used for structs/nested strings using reflection.
func interpolateStringReflect(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
func interpolateStringReflect(v reflect.Value) (err error) {
var strVal string
if v.Kind() != reflect.String {
err = errs.ErrBadType
return
}
if strVal, err = interpolateString(v.String()); err != nil {
return
}
@@ -662,8 +482,8 @@ func interpolateStringReflect(v reflect.Value, opts []optInterpolate, tagOpts []
return
}
// interpolateStruct is used by Interpolate and interpolateReflect for structs. v should be a reflect.Value of a struct.
func interpolateStruct(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
// interpolateStruct is used by Interpolate for structs. v should be a reflect.Value of a struct.
func interpolateStruct(v reflect.Value) (err error) {
var field reflect.StructField
var fieldVal reflect.Value
@@ -693,7 +513,7 @@ func interpolateStruct(v reflect.Value, opts []optInterpolate, tagOpts []optInte
defer wg.Done()
if fErr = interpolateStructField(f, fv, opts, nil); fErr != nil {
if fErr = interpolateStructField(f, fv); fErr != nil {
errChan <- fErr
return
}
@@ -724,45 +544,31 @@ func interpolateStruct(v reflect.Value, opts []optInterpolate, tagOpts []optInte
}
// interpolateStructField interpolates a struct field.
func interpolateStructField(field reflect.StructField, v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
func interpolateStructField(field reflect.StructField, v reflect.Value) (err error) {
var tagVal string
// var ftKind reflect.Kind = field.Type.Kind()
var parsedTagOpts map[string]bool
var valOpts *interpolateOpts = new(interpolateOpts)
if !v.CanSet() {
return
}
*valOpts = defaultInterpolateOpts
// Skip if explicitly instructed to do so.
tagVal = field.Tag.Get(StructTagInterpolate)
parsedTagOpts = internal.StringToMapBool(tagVal)
parsedTagOpts = structutils.TagToBoolMap(field, StructTagInterpolate, structutils.TagMapTrim)
if parsedTagOpts["-"] {
return
}
if opts != nil && len(opts) > 0 {
for _, opt := range opts {
if err = opt(valOpts); err != nil {
return
}
}
}
if v.Kind() == reflect.Ptr {
err = interpolateStructField(field, v.Elem(), opts, tagOpts)
err = interpolateStructField(field, v.Elem())
} else {
err = interpolateValue(v, opts, tagOpts)
err = interpolateValue(v)
}
return
}
// interpolateValue is a dispatcher for a reflect value.
func interpolateValue(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
func interpolateValue(v reflect.Value) (err error) {
var kind reflect.Kind = v.Kind()
@@ -772,23 +578,23 @@ func interpolateValue(v reflect.Value, opts []optInterpolate, tagOpts []optInter
return
}
v = v.Elem()
if err = interpolateValue(v, opts, tagOpts); err != nil {
if err = interpolateValue(v); err != nil {
return
}
case reflect.String:
if err = interpolateStringReflect(v, opts, tagOpts); err != nil {
if err = interpolateStringReflect(v); err != nil {
return
}
return
case reflect.Slice:
if err = interpolateSlice(v, opts, tagOpts); err != nil {
case reflect.Slice, reflect.Array:
if err = interpolateSlice(v); err != nil {
}
case reflect.Map:
if err = interpolateMap(v, opts, tagOpts); err != nil {
if err = interpolateMap(v); err != nil {
return
}
case reflect.Struct:
if err = interpolateStruct(v, opts, tagOpts); err != nil {
if err = interpolateStruct(v); err != nil {
return
}
}