Skip to content

Instantly share code, notes, and snippets.

@shakahl
Last active May 28, 2024 05:38
Show Gist options
  • Save shakahl/fb42ad206f3258ea72db421ee9795095 to your computer and use it in GitHub Desktop.
Save shakahl/fb42ad206f3258ea72db421ee9795095 to your computer and use it in GitHub Desktop.
Go - Substitute deep nested value in a string from a nested map
package main
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
pkgerrors "github.com/pkg/errors"
)
// Define an error struct
type ErrGetNestedValueNilPointerDereferenceError struct {
msg string
}
// Create a function Error() string and associate it to the struct.
func (error *ErrGetNestedValueNilPointerDereferenceError) Error() string {
return error.msg
}
// Then create an error object using MyError struct.
func NewErrGetNestedValueNilPointerDereferenceError() error {
return &ErrGetNestedValueNilPointerDereferenceError{
msg: "nil pointer dereference",
}
}
func ErrGetNestedValueNilPointerDereference() error {
return pkgerrors.New("nil pointer dereference")
}
func ErrGetNestedValueNilPointerDereferenceWithContext(keys []string) error {
return pkgerrors.Wrap(NewErrGetNestedValueNilPointerDereferenceError(), CreateErrorContext(map[string]any{
"keys": keys,
"path": strings.Join(keys, "."),
}))
}
func GetNestedValue(value any, keys ...string) (any, error) {
if len(keys) == 0 {
return nil, errors.New("no keys provided")
}
for _, key := range keys {
v := reflect.ValueOf(value)
// Dereference pointers
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, ErrGetNestedValueNilPointerDereferenceWithContext(keys)
}
v = v.Elem()
}
switch v.Kind() {
case reflect.Map:
value = v.MapIndex(reflect.ValueOf(key)).Interface()
case reflect.Struct:
field := v.FieldByName(key)
if !field.IsValid() {
return nil, errors.New("field not found")
}
value = field.Interface()
default:
return nil, errors.New("key path not found")
}
}
return value, nil
}
func SubstituteDeepMapVariables(str string, input any) (string, error) {
re := regexp.MustCompile(`\{([^{}]+)\}`)
matches := re.FindAllStringSubmatch(str, -1)
for _, match := range matches {
fullMatch := match[0]
keys := strings.Split(match[1], ".")
value, err := GetNestedValue(input, keys...)
if err != nil {
return "", err
}
str = strings.Replace(str, fullMatch, fmt.Sprintf("%v", value), -1)
}
return str, nil
}
// ExtendErrorWithContext extends an error with context information.
func ExtendErrorWithContext(err error, context any) error {
if err == nil {
return nil
}
contextStr := CreateErrorContext(context)
return fmt.Errorf("%w %s", err, contextStr)
}
// extractContext extracts context information from map, struct, or pointers.
func CreateErrorContext(context any) string {
var sb strings.Builder
v := reflect.ValueOf(context)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
switch v.Kind() {
case reflect.Map:
for _, key := range v.MapKeys() {
val := v.MapIndex(key)
fmt.Fprintf(&sb, "%v=%v ", key, val)
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
val := v.Field(i)
fmt.Fprintf(&sb, "%s=%v ", field.Name, val)
}
default:
return ""
}
return strings.TrimSpace(sb.String())
}
// ParseErrorContext parses key-value pairs from an error string and returns them as a nested map.
func ParseErrorContext(e any) map[string]any {
errStr := fmt.Sprint(e)
re := regexp.MustCompile(`(\w+(\.\w+)*)=("[^"]*"|\S+)`)
matches := re.FindAllStringSubmatch(errStr, -1)
result := make(map[string]any)
for _, match := range matches {
keys := strings.Split(match[1], ".")
value := match[3]
if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
value = value[1 : len(value)-1]
}
// Build the nested map structure
currentMap := result
for i, key := range keys {
if i == len(keys)-1 {
currentMap[key] = value
} else {
if _, exists := currentMap[key]; !exists {
currentMap[key] = make(map[string]any)
}
currentMap = currentMap[key].(map[string]any)
}
}
}
return result
}
// ExtractErrorMessage extracts the original error message without context.
func ExtractErrorMessage(v any) string {
errStr := fmt.Sprint(v)
// Define the regular expression pattern to match key-value pairs.
re := regexp.MustCompile(`((\w+(\.\w+)*)\=.*)$`)
return strings.TrimSpace(re.ReplaceAllString(errStr, ""))
}
type InnerStruct struct {
Level3 string
}
type OuterStruct struct {
Inner *InnerStruct
Inner2 *InnerStruct
Code int
}
func main() {
// Example error
err := errors.New("original error")
// Example context as map
contextMap := map[string]any{
"level1": map[string]any{
"level2": "some_value",
},
}
// Example context as struct
contextStruct := OuterStruct{
Inner: &InnerStruct{
Level3: "struct_value",
},
Code: 500,
}
// Extend error with map context
errWithMapContext := ExtendErrorWithContext(err, contextMap)
fmt.Println(errWithMapContext)
// Outputs: original error level1=map[level2:some_value]
// Extend error with struct context
errWithStructContext := ExtendErrorWithContext(err, contextStruct)
fmt.Println(errWithStructContext)
// Outputs: original error Inner=&{Level3:struct_value} Code=500
// Example error string with key-value pairs.
errStr := `Error: something went wrong code=500 msg="Internal Server Error" detail="User not found"`
// Parse the error context.
context := ParseErrorContext(errStr)
// Print the parsed context.
for key, value := range context {
fmt.Printf("%s: %s\n", key, value)
}
// Example nested map with struct and pointers
nestedMap := map[string]any{
"level1": map[string]any{
"level2": &OuterStruct{
Inner: &InnerStruct{
Level3: "target_value",
},
},
},
}
// Example struct input
exampleStruct := OuterStruct{
Inner: &InnerStruct{
Level3: "struct_value",
},
}
// Testing with nested map
s := `
## Testing with nested map
level1.level2.Inner.Level3: {level1.level2.Inner.Level3}"
`
result, err := SubstituteDeepMapVariables(s, nestedMap)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println(result)
// outputs: Hello World! target_value
}
// Testing with struct
s2 := `
## Testing with struct
Inner.Level3: {Inner.Level3}
Inner2: {Inner2}
Inner2.vmi: {Inner2.vmi}
`
result2, err := SubstituteDeepMapVariables(s2, exampleStruct)
if err != nil {
fmt.Println(pkgerrors.Cause(err))
// fmt.Printf(format, a)
// switch err := errors.Cause(err).(type) {
// case *MyError:
// // handle specifically
// default:
// // unknown error
// }
// if err := ErrGetNestedValueNilPointerDereference(); err != nil {
// log.Printf("%+v", err)
// }
// switch err {
// case ErrGetNestedValueNilPointerDereference:
// ectx := ParseErrorContext(err)
// if path, ok := ectx["path"]; ok == true {
// err = errors.New("missing key specified")
// err = ExtendErrorWithContext(err, path)
// }
// }
// fmt.Println("Error:", pkgerrors.Wrap(err, "missing key specified"))
// fmt.Println()
// fmt.Println("Error message: ", ExtractErrorMessage(err))
} else {
fmt.Println(result2)
// outputs: Hello World! struct_value
}
}
package main
import (
"errors"
"fmt"
"reflect"
"regexp"
"strings"
)
var (
ErrNilPointerDereferenceError = errors.New("nil pointer dereference")
// ErrFieldNotFoundError = errors.New("field not found")
// ErrKeyPathNotFoundError = errors.New("key path not found")
)
func newGetNestedValueErrNilPointerDereferenceError(keys []string) error {
return fmt.Errorf("nested key path does not exists %v", strings.Join(keys, "."), ErrNilPointerDereferenceError)
}
func newGetNestedValueErrFieldNotFoundError(keys []string) error {
return &ErrFieldNotFoundError{
Keys: keys,
Err: ErrNilPointerDereferenceError,
}
// return fmt.Errorf("nested key path does not exists %v", strings.Join(keys, "."), ErrFieldNotFoundError)
}
func newGetNestedValueErrKeyPathNotFoundError(keys []string) error {
return &ErrKeyPathNotFoundError{
Keys: keys,
Err: ErrNilPointerDereferenceError,
}
//return &ErrKeyPathNotFoundError{keys} fmt.Errorf("nested key path does not exists %v", strings.Join(keys, "."), ErrKeyPathNotFoundError)
}
type ErrFieldNotFoundError struct {
Err error
Keys []string
}
func (e *ErrFieldNotFoundError) Path() string {
return strings.Join(e.Keys, ".")
}
func (e *ErrFieldNotFoundError) Error() string {
msg := "field not found"
if e.Err == nil {
return msg
} else {
return fmt.Sprintf("%q: %v", msg, e.Err.Error())
}
}
func (e *ErrFieldNotFoundError) Unwrap() error {
return e.Err
}
func (e *ErrFieldNotFoundError) Is(target error) bool {
t, ok := target.(*ErrFieldNotFoundError)
if !ok {
return false
}
if TestStringSlicesEqual((*t).Keys, (*e).Keys) {
return true
}
return false
}
type ErrKeyPathNotFoundError struct {
Err error
Keys []string
}
func (e *ErrKeyPathNotFoundError) Path() string {
return strings.Join(e.Keys, ".")
}
func (e *ErrKeyPathNotFoundError) Error() string {
msg := "key path not found"
if e.Err == nil {
return msg
} else {
return fmt.Sprintf("%q: %v", msg, e.Err.Error())
}
}
func (e *ErrKeyPathNotFoundError) Unwrap() error {
return e.Err
}
func (e *ErrKeyPathNotFoundError) Is(target error) bool {
t, ok := target.(*ErrKeyPathNotFoundError)
if !ok {
return false
}
if TestStringSlicesEqual(t.Keys, e.Keys) {
return true
}
return false
}
/*
type ErrNilPointerDereferenceError struct {
Err error
}
func (e *ErrGetNestedValueNilPointerDereferenceError) Error() string {
return "nil pointer dereference"
}
func (e *ErrGetNestedValueNilPointerDereferenceError) String() string {
return e.Error()
}
func (e *ErrGetNestedValueNilPointerDereferenceError) Unwrap() error {
return e.Err
}
type ErrGetNestedValueNilPointerDereferenceError struct {
err error
keys []string
}
func (e ErrGetNestedValueNilPointerDereferenceError) String() string {
return fmt.Sprint("nil pointer dereference")
}
func newErrGetNestedValueNilPointerDereferenceError(msg string, keys []string) {
return ErrGetNestedValueNilPointerDereferenceError{msg, keys}
}
*/
func TestStringSlicesEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
// CheckStringMapContainsValue checks if a string key exists in a map
func CheckStringMapContainsValue(m map[string]any, v any) bool {
for _, x := range m {
if x == v {
return true
}
}
return false
}
// GetNestedValue retrieves a deep nested value from input map
func GetNestedValue(m map[string]any, keys ...string) (any, error) {
if len(keys) == 0 {
return nil, errors.New("no keys provided")
}
var value any = m
for _, key := range keys {
v := reflect.ValueOf(value)
// Dereference pointers
if v.Kind() == reflect.Ptr {
if v.IsNil() {
return nil, newGetNestedValueErrNilPointerDereferenceError(keys)
//return nil, errors.New("nil pointer dereference at path=" + strings.Join(keys, "."))
}
v = v.Elem()
}
switch v.Kind() {
case reflect.Map:
value = v.MapIndex(reflect.ValueOf(key)).Interface()
case reflect.Struct:
field := v.FieldByName(key)
if !field.IsValid() {
return nil, newGetNestedValueErrFieldNotFoundError(keys)
//return nil, errors.New("field not found")
}
value = field.Interface()
default:
return nil, newGetNestedValueErrKeyPathNotFoundError(keys)
//return nil, errors.New("key path not found")
}
}
return value, nil
}
func ExpandNestedTemplateVars(str string, nestedMap map[string]any) (string, error) {
re := regexp.MustCompile(`\{([^{}]+)\}`)
matches := re.FindAllStringSubmatch(str, -1)
for _, match := range matches {
fullMatch := match[0]
keys := strings.Split(match[1], ".")
value, err := GetNestedValue(nestedMap, keys...)
if err != nil {
return "", err
}
str = strings.Replace(str, fullMatch, fmt.Sprintf("%v", value), -1)
}
return str, nil
}
// ExtendErrorWithContext extends an error with context information.
func ExtendErrorWithContext(err error, context any) error {
if err == nil {
return nil
}
contextStr := CreateErrorContext(context)
return fmt.Errorf("%w %s", err, contextStr)
}
// CreateErrorContext extracts context information from map, struct, or pointers.
func CreateErrorContext(context any) string {
var sb strings.Builder
v := reflect.ValueOf(context)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
switch v.Kind() {
case reflect.Map:
for _, key := range v.MapKeys() {
val := v.MapIndex(key)
fmt.Fprintf(&sb, "%v=%v ", key, val)
}
case reflect.Struct:
for i := 0; i < v.NumField(); i++ {
field := v.Type().Field(i)
val := v.Field(i)
fmt.Fprintf(&sb, "%s=%v ", field.Name, val)
}
default:
return ""
}
return strings.TrimSpace(sb.String())
}
// ParseErrorContext parses key-value pairs from an error string and returns them as a nested map.
func ParseErrorContext(errValue any) map[string]any {
errStr := fmt.Sprint(errValue)
re := regexp.MustCompile(`(\w+(\.\w+)*)=("[^"]*"|\S+)`)
matches := re.FindAllStringSubmatch(errStr, -1)
result := make(map[string]any)
for _, match := range matches {
keys := strings.Split(match[1], ".")
value := match[3]
if strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) {
value = value[1 : len(value)-1]
}
// Build the nested map structure
currentMap := result
for i, key := range keys {
if i == len(keys)-1 {
currentMap[key] = value
} else {
if _, exists := currentMap[key]; !exists {
currentMap[key] = make(map[string]any)
}
currentMap = currentMap[key].(map[string]any)
}
}
}
return result
}
// ExtractErrorMessage extracts the original error message without context.
func ExtractErrorMessage(errValue any) string {
// Resolve error input type
errStr := fmt.Sprint(errValue)
// Define the regular expression pattern to match key-value pairs.
re := regexp.MustCompile(`((\w+(\.\w+)*)\=.*)$`)
return strings.TrimSpace(re.ReplaceAllString(errStr, ""))
}
func main() {
fmt.Println(fmt.Errorf("decompress %v: %w", "wrapper_error", errors.New("base_error")))
type innerStruct struct {
Level3 string
}
type testStructValue2 struct {
Inner *innerStruct
ValueStr string
Code int
}
type testServiceArgs struct {
Name string `pulumi:"name"`
Tenant string `pulumi:"tenant"`
Role string `pulumi:"role"`
Host string `pulumi:"host"`
Port int `pulumi:"port"`
Scheme string `pulumi:"scheme"`
PassHostHeader bool `pulumi:"passHostHeader" yaml:"passHostHeader"`
}
// Example nested map with struct and pointers
nestedContextMap := map[string]any{
"this": map[string]any{
"environment": "testing",
"service": &testServiceArgs{
Name: "career",
Tenant: "cdp",
Role: "api",
Host: "192.168.124.23",
Port: 80,
Scheme: "https",
PassHostHeader: true,
},
"overrides": map[string]any{
"k8s": map[string]any{
"namespace": "",
"service": "",
},
},
},
"other_data_1": &testStructValue2{
Inner: &innerStruct{
Level3: "[Level3 value]",
},
Code: 43,
},
"other_data_2": "[other_data_2_value]",
}
tplStr := `
k8s-svc: {this.service.Name}-{this.environment}-{this.service.Role2}
`
resultStr, err := ExpandNestedTemplateVars(tplStr, nestedContextMap)
if err != nil {
var keyError *ErrFieldNotFoundError
var keyPathError *ErrKeyPathNotFoundError
if errors.As(err, &keyError) {
fmt.Printf("ERROR: Invalid key in template variable: {%v}\n", keyError.Path())
} else if errors.As(err, &keyPathError) {
fmt.Printf("ERROR: Non-existent key path in template variable: {%v}\n", keyError.Path())
} else {
fmt.Printf("ERROR: %v \n", err)
}
}
// if err != nil {
// errFieldNotFound := &ErrFieldNotFoundError{}
// if errors.As(err, errFieldNotFound.(*ErrFieldNotFoundError)) == true {
// fmt.Printf("ERROR: non existent key path: %v\n", err)
// } else {
// fmt.Printf("ERROR: %v\n", err)
// }
// }
fmt.Printf("template string: \n %v", tplStr)
fmt.Println()
fmt.Printf("expanded template string: \n %v \n", resultStr)
fmt.Println()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment