adding envs tagging/interpolation
This commit is contained in:
@@ -12,22 +12,4 @@ var (
|
||||
|
||||
var (
|
||||
StructTagInterpolate string = "envsub"
|
||||
StructTagPopulate string = "envpop"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultInterpolateOpts interpolateOpts = interpolateOpts{
|
||||
noMapKey: false,
|
||||
noMapVal: false,
|
||||
}
|
||||
// InterpolateOptNoMapKey is the equivalent of the struct tag `no_map_key` for Interpolate.
|
||||
InterpolateOptNoMapKey optInterpolate = func(o *interpolateOpts) (err error) {
|
||||
o.noMapKey = true
|
||||
return
|
||||
}
|
||||
// InterpolateOptNoMapValue is the equivalent of the struct tag `no_map_value` for Interpolate.
|
||||
InterpolateOptNoMapValue optInterpolate = func(o *interpolateOpts) (err error) {
|
||||
o.noMapVal = true
|
||||
return
|
||||
}
|
||||
)
|
||||
|
||||
358
envs/funcs.go
358
envs/funcs.go
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,825 +0,0 @@
|
||||
package envs
|
||||
|
||||
import (
|
||||
`bytes`
|
||||
`errors`
|
||||
`fmt`
|
||||
`io/ioutil`
|
||||
`os`
|
||||
`reflect`
|
||||
`strings`
|
||||
`sync`
|
||||
|
||||
`r00t2.io/goutils/multierr`
|
||||
`r00t2.io/sysutils/errs`
|
||||
`r00t2.io/sysutils/internal`
|
||||
`r00t2.io/sysutils/paths`
|
||||
)
|
||||
|
||||
// GetEnvMap returns a map of all environment variables. All values are strings.
|
||||
func GetEnvMap() (envVars map[string]string) {
|
||||
|
||||
var envList []string = os.Environ()
|
||||
|
||||
envVars = envListToMap(envList)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetEnvMapNative returns a map of all environment variables, but attempts to "nativize" them.
|
||||
All values are interfaces. It is up to the caller to typeswitch them to proper types.
|
||||
|
||||
Note that the PATH/Path environment variable (for *Nix and Windows, respectively) will be
|
||||
a []string (as per GetPathEnv). No other env vars, even if they contain os.PathListSeparator,
|
||||
will be transformed to a slice or the like.
|
||||
If an error occurs during parsing the path env var, it will be rendered as a string.
|
||||
|
||||
All number types will attempt to be their 64-bit version (i.e. int64, uint64, float64, etc.).
|
||||
|
||||
If a type cannot be determined for a value, its string form will be used
|
||||
(as it would be found in GetEnvMap).
|
||||
*/
|
||||
func GetEnvMapNative() (envMap map[string]interface{}) {
|
||||
|
||||
var stringMap map[string]string = GetEnvMap()
|
||||
|
||||
envMap = nativizeEnvMap(stringMap)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetFirst gets the first instance if populated/set occurrence of varNames.
|
||||
|
||||
For example, if you have three potential env vars, FOO, FOOBAR, FOOBARBAZ,
|
||||
and want to follow the logic flow of:
|
||||
|
||||
1.) Check if FOO is set. If not,
|
||||
2.) Check if FOOBAR is set. If not,
|
||||
3.) Check if FOOBARBAZ is set.
|
||||
|
||||
Then this would be specified as:
|
||||
|
||||
GetFirst([]string{"FOO", "FOOBAR", "FOOBARBAZ"})
|
||||
|
||||
If val is "" and ok is true, this means that one of the specified variable names IS
|
||||
set but is set to an empty value. If ok is false, none of the specified variables
|
||||
are set.
|
||||
|
||||
It is a thin wrapper around GetFirstWithRef.
|
||||
*/
|
||||
func GetFirst(varNames []string) (val string, ok bool) {
|
||||
|
||||
val, ok, _ = GetFirstWithRef(varNames)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetFirstWithRef behaves exactly like GetFirst, but with an additional returned value, idx,
|
||||
which specifies the index in varNames in which a set variable was found. e.g. if:
|
||||
|
||||
GetFirstWithRef([]string{"FOO", "FOOBAR", "FOOBAZ"})
|
||||
|
||||
is called and FOO is not set but FOOBAR is, idx will be 1.
|
||||
|
||||
If ok is false, idx will always be -1 and should be ignored.
|
||||
*/
|
||||
func GetFirstWithRef(varNames []string) (val string, ok bool, idx int) {
|
||||
|
||||
idx = -1
|
||||
|
||||
for i, vn := range varNames {
|
||||
if HasEnv(vn) {
|
||||
ok = true
|
||||
idx = i
|
||||
val = os.Getenv(vn)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// GetPathEnv returns a slice of the PATH variable's items.
|
||||
func GetPathEnv() (pathList []string, err error) {
|
||||
|
||||
var pathVar string = internal.GetPathEnvName()
|
||||
|
||||
pathList = make([]string, 0)
|
||||
|
||||
for _, p := range strings.Split(os.Getenv(pathVar), string(os.PathListSeparator)) {
|
||||
if err = paths.RealPath(&p); err != nil {
|
||||
return
|
||||
}
|
||||
pathList = append(pathList, p)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetPidEnvMap will only work on *NIX-like systems with procfs.
|
||||
It gets the environment variables of a given process' PID.
|
||||
*/
|
||||
func GetPidEnvMap(pid uint32) (envMap map[string]string, err error) {
|
||||
|
||||
var envBytes []byte
|
||||
var envList []string
|
||||
var envArr [][]byte
|
||||
var procPath string
|
||||
var exists bool
|
||||
|
||||
envMap = make(map[string]string, 0)
|
||||
|
||||
procPath = fmt.Sprintf("/proc/%v/environ", pid)
|
||||
|
||||
if exists, err = paths.RealPathExists(&procPath); err != nil {
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
err = errors.New(fmt.Sprintf("information for pid %v does not exist", pid))
|
||||
return
|
||||
}
|
||||
|
||||
if envBytes, err = ioutil.ReadFile(procPath); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
envArr = bytes.Split(envBytes, []byte{0x0})
|
||||
envList = make([]string, len(envArr))
|
||||
for idx, b := range envArr {
|
||||
envList[idx] = string(b)
|
||||
}
|
||||
|
||||
envMap = envListToMap(envList)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
GetPidEnvMapNative, like GetEnvMapNative, returns a map of all environment variables, but attempts to "nativize" them.
|
||||
All values are interfaces. It is up to the caller to typeswitch them to proper types.
|
||||
|
||||
See the documentation for GetEnvMapNative for details.
|
||||
*/
|
||||
func GetPidEnvMapNative(pid uint32) (envMap map[string]interface{}, err error) {
|
||||
|
||||
var stringMap map[string]string
|
||||
|
||||
if stringMap, err = GetPidEnvMap(pid); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
envMap = nativizeEnvMap(stringMap)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
HasEnv is much like os.LookupEnv, but only returns a boolean for
|
||||
if the environment variable key exists or not.
|
||||
|
||||
This is useful anywhere you may need to set a boolean in a func call
|
||||
depending on the *presence* of an env var or not.
|
||||
*/
|
||||
func HasEnv(key string) (envIsSet bool) {
|
||||
|
||||
_, envIsSet = os.LookupEnv(key)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
Interpolate takes one of:
|
||||
|
||||
- a string (pointer only)
|
||||
- a struct (pointer only)
|
||||
- a map (comprised of the same requirements)
|
||||
- a slice (comprised of the same requirements)
|
||||
|
||||
and performs variable substitution on strings from environment variables.
|
||||
|
||||
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
|
||||
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
|
||||
|
||||
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 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) {
|
||||
|
||||
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
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
|
||||
return
|
||||
}
|
||||
ptrVal = sVal.Elem()
|
||||
ptrType = ptrVal.Type()
|
||||
ptrKind = ptrType.Kind()
|
||||
if ptrKind == reflect.String {
|
||||
err = interpolateStringReflect(ptrVal, opts, nil)
|
||||
} else {
|
||||
// Otherwise, it should be a struct ptr.
|
||||
if ptrKind != reflect.Struct {
|
||||
return
|
||||
}
|
||||
err = interpolateStruct(ptrVal, opts, nil)
|
||||
}
|
||||
case reflect.Map:
|
||||
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
|
||||
return
|
||||
}
|
||||
err = interpolateMap(sVal, opts, nil)
|
||||
case reflect.Slice:
|
||||
if sVal.IsNil() || sVal.IsZero() || !sVal.IsValid() {
|
||||
return
|
||||
}
|
||||
err = interpolateSlice(sVal, opts, nil)
|
||||
/*
|
||||
case reflect.Struct:
|
||||
if sVal.IsZero() || !sVal.IsValid() {
|
||||
return
|
||||
}
|
||||
err = interpolateStruct(sVal, opts, nil)
|
||||
|
||||
*/
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
InterpolateString takes (a pointer to) a struct or string and performs variable substitution on it
|
||||
from environment variables.
|
||||
|
||||
It supports both UNIX/Linux/POSIX syntax formats (e.g. $VARNAME, ${VARNAME}) and,
|
||||
if on Windows, it *additionally* supports the EXPAND_SZ format (e.g. %VARNAME%).
|
||||
|
||||
If s is nil, nothing will be done and err will be ErrNilPtr.
|
||||
|
||||
This is a standalone function that is much more performant than Interpolate
|
||||
at the cost of rigidity.
|
||||
*/
|
||||
func InterpolateString(s *string) (err error) {
|
||||
|
||||
var newStr string
|
||||
|
||||
if s == nil {
|
||||
err = errs.ErrNilPtr
|
||||
return
|
||||
}
|
||||
|
||||
if newStr, err = interpolateString(*s); err != nil {
|
||||
return
|
||||
}
|
||||
*s = newStr
|
||||
|
||||
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) {
|
||||
|
||||
var kVal reflect.Value
|
||||
var vVal reflect.Value
|
||||
var mIter *reflect.MapIter
|
||||
var newMap reflect.Value
|
||||
var wg sync.WaitGroup
|
||||
var numJobs int
|
||||
var errChan chan error
|
||||
var doneChan chan bool = make(chan bool, 1)
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
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()))
|
||||
|
||||
mIter = v.MapRange()
|
||||
for mIter.Next() {
|
||||
kVal = mIter.Key()
|
||||
vVal = mIter.Value()
|
||||
go func(mapK, mapV reflect.Value) {
|
||||
var mapErr error
|
||||
var newKey reflect.Value
|
||||
var newVal reflect.Value
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
if !valOpts.noMapKey {
|
||||
/*
|
||||
if mapK.Kind() == reflect.String {
|
||||
if mapErr = interpolateStringReflect(mapK, opts, nil); mapErr != nil {
|
||||
errChan <- mapErr
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if mapErr = interpolateValue(mapK, opts, nil); mapErr != nil {
|
||||
errChan <- mapErr
|
||||
return
|
||||
}
|
||||
}
|
||||
*/
|
||||
newKey = reflect.New(mapK.Type())
|
||||
newKey.Set(mapK)
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newKey = mapK
|
||||
}
|
||||
if !valOpts.noMapVal {
|
||||
/*
|
||||
if mapV.Kind() == reflect.String {
|
||||
if mapErr = interpolateStringReflect(mapV, opts, nil); mapErr != nil {
|
||||
errChan <- mapErr
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if mapErr = interpolateValue(mapV, opts, nil); mapErr != nil {
|
||||
errChan <- mapErr
|
||||
return
|
||||
}
|
||||
}
|
||||
*/
|
||||
newVal = reflect.New(mapV.Type())
|
||||
newVal.Set(mapV)
|
||||
if mapV.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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newVal = mapV
|
||||
}
|
||||
newMap.SetMapIndex(reflect.ValueOf(newKey), reflect.ValueOf(newVal))
|
||||
}(kVal, vVal)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
doneChan <- true
|
||||
}()
|
||||
|
||||
<-doneChan
|
||||
|
||||
for i := 0; i < numJobs; i++ {
|
||||
if err = <-errChan; err != nil {
|
||||
mErr.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if !mErr.IsEmpty() {
|
||||
err = mErr
|
||||
return
|
||||
}
|
||||
|
||||
v.Set(newMap)
|
||||
|
||||
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) {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var errChan chan error
|
||||
var numJobs int
|
||||
var doneChan chan bool = make(chan bool, 1)
|
||||
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 {
|
||||
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)
|
||||
|
||||
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 {
|
||||
errChan <- sErr
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if sErr = interpolateValue(newVal, opts, tagOpts); sErr != nil {
|
||||
errChan <- sErr
|
||||
return
|
||||
}
|
||||
}
|
||||
v.Index(idx).Set(reflect.ValueOf(newVal))
|
||||
}(i)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
doneChan <- true
|
||||
}()
|
||||
|
||||
<-doneChan
|
||||
|
||||
for i := 0; i < numJobs; i++ {
|
||||
if err = <-errChan; err != nil {
|
||||
mErr.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if !mErr.IsEmpty() {
|
||||
err = mErr
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// interpolateStringReflect is used for structs/nested strings using reflection.
|
||||
func interpolateStringReflect(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
|
||||
|
||||
var strVal string
|
||||
|
||||
if strVal, err = interpolateString(v.String()); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
v.Set(reflect.ValueOf(strVal).Convert(v.Type()))
|
||||
|
||||
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) {
|
||||
|
||||
var field reflect.StructField
|
||||
var fieldVal reflect.Value
|
||||
var wg sync.WaitGroup
|
||||
var errChan chan error
|
||||
var numJobs int
|
||||
var doneChan chan bool = make(chan bool, 1)
|
||||
var mErr *multierr.MultiError = multierr.NewMultiError(nil)
|
||||
var t reflect.Type = v.Type()
|
||||
var kind reflect.Kind = t.Kind()
|
||||
|
||||
if kind != reflect.Struct {
|
||||
err = errs.ErrBadType
|
||||
return
|
||||
}
|
||||
|
||||
numJobs = v.NumField()
|
||||
wg.Add(numJobs)
|
||||
errChan = make(chan error, numJobs)
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field = t.Field(i)
|
||||
fieldVal = v.Field(i)
|
||||
|
||||
go func(f reflect.StructField, fv reflect.Value) {
|
||||
var fErr error
|
||||
|
||||
defer wg.Done()
|
||||
|
||||
if fErr = interpolateStructField(f, fv, opts, nil); fErr != nil {
|
||||
errChan <- fErr
|
||||
return
|
||||
}
|
||||
}(field, fieldVal)
|
||||
}
|
||||
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(errChan)
|
||||
doneChan <- true
|
||||
}()
|
||||
|
||||
<-doneChan
|
||||
|
||||
for i := 0; i < numJobs; i++ {
|
||||
if err = <-errChan; err != nil {
|
||||
mErr.AddError(err)
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
|
||||
if !mErr.IsEmpty() {
|
||||
err = mErr
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// interpolateStructField interpolates a struct field.
|
||||
func interpolateStructField(field reflect.StructField, v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (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)
|
||||
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)
|
||||
} else {
|
||||
err = interpolateValue(v, opts, tagOpts)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// interpolateValue is a dispatcher for a reflect value.
|
||||
func interpolateValue(v reflect.Value, opts []optInterpolate, tagOpts []optInterpolate) (err error) {
|
||||
|
||||
var kind reflect.Kind = v.Kind()
|
||||
|
||||
switch kind {
|
||||
case reflect.Ptr:
|
||||
if v.IsNil() || v.IsZero() || !v.IsValid() {
|
||||
return
|
||||
}
|
||||
v = v.Elem()
|
||||
if err = interpolateValue(v, opts, tagOpts); err != nil {
|
||||
return
|
||||
}
|
||||
case reflect.String:
|
||||
if err = interpolateStringReflect(v, opts, tagOpts); err != nil {
|
||||
return
|
||||
}
|
||||
return
|
||||
case reflect.Slice:
|
||||
if err = interpolateSlice(v, opts, tagOpts); err != nil {
|
||||
}
|
||||
case reflect.Map:
|
||||
if err = interpolateMap(v, opts, tagOpts); err != nil {
|
||||
return
|
||||
}
|
||||
case reflect.Struct:
|
||||
if err = interpolateStruct(v, opts, tagOpts); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -1,33 +1,32 @@
|
||||
package envs
|
||||
|
||||
import (
|
||||
`errors`
|
||||
`os`
|
||||
`testing`
|
||||
`time`
|
||||
|
||||
`github.com/davecgh/go-spew/spew`
|
||||
`r00t2.io/sysutils/errs`
|
||||
)
|
||||
|
||||
type (
|
||||
testCustom string
|
||||
testStruct struct {
|
||||
Hello string `envpop:"USER"`
|
||||
HelloPtr *string `envpop:"USER"`
|
||||
HelloForce string `envpop:"USER,force"`
|
||||
HelloPtrForce *string `envpop:"USER,force"`
|
||||
Hello string
|
||||
HelloPtr *string
|
||||
HelloForce string
|
||||
HelloPtrForce *string
|
||||
HelloNo string `envsub:"-" envpop:"-"`
|
||||
HelloNoPtr *string `envsub:"-" envpop:"-"`
|
||||
BadType int
|
||||
NilField *string `envpop:"NONEXISTENTBADVAR,allow_empty"`
|
||||
NilField *string
|
||||
NilField2 *string
|
||||
PtrInt *int
|
||||
Custom testCustom
|
||||
MapStr map[string]string
|
||||
SliceStr []string
|
||||
SliceSlice [][]string
|
||||
SliceMap []map[string]string
|
||||
SliceStruct []testStruct
|
||||
SliceStruct []*testStruct
|
||||
}
|
||||
)
|
||||
|
||||
@@ -55,41 +54,6 @@ func TestInterpolateStruct(t *testing.T) {
|
||||
for _, i := range []interface{}{
|
||||
"i am ${USER}, it is ${CURDATETIME}",
|
||||
new(string),
|
||||
/*
|
||||
testStruct{
|
||||
Hello: "i am ${USER}, it is ${CURDATETIME}",
|
||||
HelloPtr: new(string),
|
||||
HelloForce: "i am ${USER}, it is ${CURDATETIME}",
|
||||
HelloPtrForce: new(string),
|
||||
HelloNo: "i am ${USER}, it is ${CURDATETIME}",
|
||||
HelloNoPtr: new(string),
|
||||
BadType: 4,
|
||||
NilField: nil,
|
||||
PtrInt: new(int),
|
||||
Custom: testCustom("i am ${USER}, it is ${CURDATETIME}"),
|
||||
SliceStr: []string{"i am ${USER}, it is ${CURDATETIME}"},
|
||||
SliceSlice: [][]string{[]string{"i am ${USER}, it is ${CURDATETIME}"}},
|
||||
SliceMap: []map[string]string{map[string]string{"i am ${USER} key": "i am ${USER} value, it is ${CURDATETIME}"}},
|
||||
SliceStruct: []testStruct{
|
||||
{
|
||||
Hello: "i am nested ${USER}, it is ${CURDATETIME}",
|
||||
HelloPtr: nil,
|
||||
HelloForce: "i am nested ${USER}, it is ${CURDATETIME}",
|
||||
HelloPtrForce: nil,
|
||||
HelloNo: "i am nested ${USER}, it is ${CURDATETIME}",
|
||||
HelloNoPtr: nil,
|
||||
BadType: 0,
|
||||
NilField: nil,
|
||||
PtrInt: nil,
|
||||
Custom: testCustom("i am nested ${USER}, it is ${CURDATETIME}"),
|
||||
SliceStr: []string{"i am nested ${USER}, it is ${CURDATETIME}"},
|
||||
SliceSlice: [][]string{[]string{"i am nested ${USER}, it is ${CURDATETIME}"}},
|
||||
SliceMap: []map[string]string{map[string]string{"i am nested ${USER} key": "i am ${USER} value, it is ${CURDATETIME}"}},
|
||||
SliceStruct: nil,
|
||||
},
|
||||
},
|
||||
},
|
||||
*/
|
||||
&testStruct{
|
||||
Hello: "i am ${USER}, it is ${CURDATETIME}",
|
||||
HelloPtr: new(string),
|
||||
@@ -105,8 +69,8 @@ func TestInterpolateStruct(t *testing.T) {
|
||||
SliceStr: []string{"i am ${USER}, it is ${CURDATETIME}"},
|
||||
SliceSlice: [][]string{[]string{"i am ${USER}, it is ${CURDATETIME}"}},
|
||||
SliceMap: []map[string]string{map[string]string{"i am ${USER} key": "i am ${USER} value, it is ${CURDATETIME}"}},
|
||||
SliceStruct: []testStruct{
|
||||
{
|
||||
SliceStruct: []*testStruct{
|
||||
&testStruct{
|
||||
Hello: "i am nested ${USER}, it is ${CURDATETIME}",
|
||||
HelloPtr: nil,
|
||||
HelloForce: "i am nested ${USER}, it is ${CURDATETIME}",
|
||||
@@ -149,34 +113,3 @@ func TestInterpolateStruct(t *testing.T) {
|
||||
t.Logf("After (%T):\n%v\n", i, spew.Sdump(i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestPopulateStruct(t *testing.T) {
|
||||
|
||||
var err error
|
||||
var greet string = "My username is ${USER}; hello!"
|
||||
var num int = 4
|
||||
var sp *testStruct = &testStruct{
|
||||
Hello: greet,
|
||||
HelloPtr: &greet,
|
||||
HelloForce: greet,
|
||||
HelloPtrForce: &greet,
|
||||
HelloNo: greet,
|
||||
HelloNoPtr: &greet,
|
||||
BadType: 4,
|
||||
PtrInt: &num,
|
||||
}
|
||||
|
||||
if err = PopulateStruct(sp); err != nil {
|
||||
if errors.Is(err, errs.ErrNilPtr) {
|
||||
err = nil
|
||||
t.Logf("Detected nil.")
|
||||
} else if errors.Is(err, errs.ErrBadType) {
|
||||
err = nil
|
||||
t.Log("Detected bad type.")
|
||||
} else {
|
||||
t.Fatalf("Failed interpolation: %v", err)
|
||||
}
|
||||
}
|
||||
t.Logf("Evaluated:\n%v", spew.Sdump(sp))
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package envs
|
||||
|
||||
type (
|
||||
interpolateOpts struct {
|
||||
noMapKey bool
|
||||
noMapVal bool
|
||||
isTagged bool
|
||||
}
|
||||
optInterpolate func(o *interpolateOpts) (err error)
|
||||
)
|
||||
Reference in New Issue
Block a user