Last active
May 28, 2024 05:38
-
-
Save shakahl/fb42ad206f3258ea72db421ee9795095 to your computer and use it in GitHub Desktop.
Go - Substitute deep nested value in a string from a nested map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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