ARM-software/golang-utils

View on GitHub
utils/environment/envvar.go

Summary

Maintainability
A
0 mins
Test Coverage
package environment

import (
    "fmt"
    "regexp"
    "sort"
    "strings"

    validation "github.com/go-ozzo/ozzo-validation/v4"
    "golang.org/x/exp/maps"

    "github.com/ARM-software/golang-utils/utils/commonerrors"
    "github.com/ARM-software/golang-utils/utils/platform"
)

var (
    envvarKeyRegex   = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") // See [IEEE Std 1003.1-2008 / IEEE POSIX P1003.2/ISO 9945.2](http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_10_02)
    errEnvvarInvalid = validation.NewError("validation_is_environment_variable", "must be a valid Posix environment variable")

    // IsEnvironmentVariableKey defines a validation rule for environment variable keys ([IEEE Std 1003.1-2008 / IEEE POSIX P1003.2/ISO 9945.2](http://www.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_10_02)) for use with github.com/go-ozzo/ozzo-validation
    // TODO use the built-in implementation in `is` package when https://github.com/go-ozzo/ozzo-validation/issues/186 is looked at.
    IsEnvironmentVariableKey = validation.NewStringRuleWithError(isEnvVarKey, errEnvvarInvalid)
)

type EnvVar struct {
    key             string
    value           string
    validationRules []validation.Rule
}

func (e *EnvVar) MarshalText() (text []byte, err error) {
    err = e.Validate()
    text = []byte(e.String())
    return
}

func (e *EnvVar) UnmarshalText(text []byte) error {
    env, err := ParseEnvironmentVariable(string(text))
    if err != nil {
        return err
    }
    e.key = env.GetKey()
    e.value = env.GetValue()
    return nil
}

func (e *EnvVar) Equal(v IEnvironmentVariable) bool {
    if v == nil {
        return e == nil
    }
    if e == nil {
        return false
    }
    return e.GetKey() == v.GetKey() && e.GetValue() == v.GetValue()
}

func (e *EnvVar) GetKey() string {
    return e.key
}

func (e *EnvVar) GetValue() string {
    return e.value
}

func (e *EnvVar) String() string {
    return fmt.Sprintf("%v=%v", e.GetKey(), e.GetValue())
}

func (e *EnvVar) Validate() (err error) {
    err = validation.Validate(e.GetKey(), validation.Required, IsEnvironmentVariableKey)
    if err != nil {
        err = fmt.Errorf("%w: environment variable name `%v` is not valid: %v", commonerrors.ErrInvalid, e.GetKey(), err.Error())
        return
    }
    if len(e.validationRules) > 0 {
        err = validation.Validate(e.GetValue(), e.validationRules...)
        if err != nil {
            err = fmt.Errorf("%w: environment variable `%v` value is not valid: %v", commonerrors.ErrInvalid, e.GetKey(), err.Error())
        }
    }
    return
}

func isEnvVarKey(value string) bool {
    // FIXME remove when supported by the validation tool see https://github.com/go-ozzo/ozzo-validation/issues/186
    return envvarKeyRegex.MatchString(value)
}

// ParseEnvironmentVariable parses an environment variable definition, in the form "key=value".
func ParseEnvironmentVariable(variable string) (IEnvironmentVariable, error) {
    elements := strings.Split(strings.TrimSpace(variable), "=")
    if len(elements) < 2 {
        return nil, fmt.Errorf("%w: invalid environment variable entry as not following key=value", commonerrors.ErrInvalid)
    }
    value := elements[1]
    if len(elements) > 2 {
        var valueElems []string
        for i := 1; i < len(elements); i++ {
            valueElems = append(valueElems, elements[i])
        }
        value = strings.Join(valueElems, "=")
    }
    envvar := NewEnvironmentVariable(elements[0], value)
    return envvar, envvar.Validate()
}

// NewEnvironmentVariable returns an environment variable defined by a key and a value.
func NewEnvironmentVariable(key, value string) IEnvironmentVariable {
    return NewEnvironmentVariableWithValidation(key, value)

}

// CloneEnvironmentVariable returns a clone of the environment variable.
func CloneEnvironmentVariable(envVar IEnvironmentVariable) IEnvironmentVariable {
    if envVar == nil {
        return nil
    }
    return NewEnvironmentVariable(envVar.GetKey(), envVar.GetValue())
}

// NewEnvironmentVariableWithValidation returns an environment variable defined by a key and a value but with the possibility to define value validation rules.
func NewEnvironmentVariableWithValidation(key, value string, rules ...validation.Rule) IEnvironmentVariable {
    return &EnvVar{
        key:             key,
        value:           value,
        validationRules: rules,
    }
}

// ValidateEnvironmentVariables validates that environment variables are correctly defined in regard to their schema.
func ValidateEnvironmentVariables(vars ...IEnvironmentVariable) error {
    for i := range vars {
        err := vars[i].Validate()
        if err != nil {
            return err
        }
    }
    return nil
}

// ParseEnvironmentVariables parses a list of key=value entries such as os.Environ() and returns a list of the corresponding environment variables.
// Any entry failing parsing will be ignored.
func ParseEnvironmentVariables(variables ...string) (envVars []IEnvironmentVariable) {
    for i := range variables {
        envvar, err := ParseEnvironmentVariable(variables[i])
        if err != nil {
            continue
        }
        envVars = append(envVars, envvar)
    }
    return
}

// FindEnvironmentVariable looks for an environment variable in a list. if no environment variable matches, an error is returned
func FindEnvironmentVariable(envvar string, envvars ...IEnvironmentVariable) (IEnvironmentVariable, error) {
    for i := range envvars {
        if envvars[i].GetKey() == envvar {
            return envvars[i], nil
        }
    }
    return nil, fmt.Errorf("%w: environment variable '%v' not set", commonerrors.ErrNotFound, envvar)
}

// FindFoldEnvironmentVariable looks for an environment variable in a list similarly to FindEnvironmentVariable but without case-sensitivity.
func FindFoldEnvironmentVariable(envvar string, envvars ...IEnvironmentVariable) (IEnvironmentVariable, error) {
    for i := range envvars {
        if strings.EqualFold(envvars[i].GetKey(), envvar) {
            return envvars[i], nil
        }
    }
    return nil, fmt.Errorf("%w: environment variable '%v' not set", commonerrors.ErrNotFound, envvar)
}

// ExpandEnvironmentVariables returns a list of environment variables with their value being expanded.
// Expansion assumes that all the variables are present in the envvars list.
// If recursive is set to true, then expansion is performed recursively over the variable list.
func ExpandEnvironmentVariables(recursive bool, envvars ...IEnvironmentVariable) (expandedEnvVars []IEnvironmentVariable) {
    for i := range envvars {
        expandedEnvVars = append(expandedEnvVars, ExpandEnvironmentVariable(recursive, envvars[i], envvars...))
    }
    return
}

// ExpandEnvironmentVariable returns a clone of envVarToExpand but with an expanded value based on environment variables defined in envvars list.
// Expansion assumes that all the variables are present in the envvars list.
// If recursive is set to true, then expansion is performed recursively over the variable list.
func ExpandEnvironmentVariable(recursive bool, envVarToExpand IEnvironmentVariable, envvars ...IEnvironmentVariable) (expandedEnvVar IEnvironmentVariable) {
    if len(envvars) == 0 || envVarToExpand == nil {
        return envVarToExpand
    }
    mappingFunc := func(envvarKey string) (string, bool) {
        envVar, err := FindEnvironmentVariable(envvarKey, envvars...)
        if commonerrors.Any(err, commonerrors.ErrNotFound) {
            return "", false
        }
        return envVar.GetValue(), true
    }
    expandedEnvVar = NewEnvironmentVariable(envVarToExpand.GetKey(), platform.ExpandParameter(envVarToExpand.GetValue(), mappingFunc, recursive))
    return
}

// UniqueEnvironmentVariables returns a list of unique environment variables.
// caseSensitive states whether two same keys but with different case should be both considered unique.
func UniqueEnvironmentVariables(caseSensitive bool, envvars ...IEnvironmentVariable) (uniqueEnvVars []IEnvironmentVariable) {
    uniqueSet := map[string]IEnvironmentVariable{}
    recordUniqueEnvVar(caseSensitive, envvars, uniqueSet)
    uniqueEnvVars = maps.Values(uniqueSet)
    return
}

// SortEnvironmentVariables sorts a list of environment variable alphabetically no matter the case.
func SortEnvironmentVariables(envvars []IEnvironmentVariable) {
    if len(envvars) == 0 {
        return
    }
    sort.SliceStable(envvars, func(i, j int) bool {
        return strings.ToLower(envvars[i].GetKey()) < strings.ToLower(envvars[j].GetKey())
    })
}

// MergeEnvironmentVariableSets merges two sets of environment variables.
// If both sets have a same environment variable, its value in set 1 will take precedence.
// caseSensitive states whether two similar keys with different case should be considered as different
func MergeEnvironmentVariableSets(caseSensitive bool, envvarSet1 []IEnvironmentVariable, envvarSet2 ...IEnvironmentVariable) (mergedEnvVars []IEnvironmentVariable) {
    mergeSet := map[string]IEnvironmentVariable{}
    recordUniqueEnvVar(caseSensitive, envvarSet1, mergeSet)
    recordUniqueEnvVar(caseSensitive, envvarSet2, mergeSet)
    mergedEnvVars = maps.Values(mergeSet)
    return
}

func recordUniqueEnvVar(caseSensitive bool, envvarSet []IEnvironmentVariable, hashTable map[string]IEnvironmentVariable) {
    for i := range envvarSet {
        key := envvarSet[i].GetKey()
        if !caseSensitive {
            key = strings.ToLower(key)
        }
        if _, contains := hashTable[key]; !contains {
            hashTable[key] = envvarSet[i]
        }
    }
}