package stringreplace import ( "encoding/json" "fmt" "reflect" "github.com/golang/glog" ) // VisitObjectStrings recursively visits all string fields in the object and calls the // visitor function on them. The visitor function can be used to modify the // value of the string fields. func VisitObjectStrings(obj interface{}, visitor func(string) (string, bool)) error { return visitValue(reflect.ValueOf(obj), visitor) } func visitValue(v reflect.Value, visitor func(string) (string, bool)) error { // you'll never be able to substitute on a nil. Check the kind first or you'll accidentally // end up panic-ing switch v.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice: if v.IsNil() { return nil } } switch v.Kind() { case reflect.Ptr, reflect.Interface: err := visitValue(v.Elem(), visitor) if err != nil { return err } case reflect.Slice, reflect.Array: vt := v.Type().Elem() for i := 0; i < v.Len(); i++ { val, err := visitUnsettableValues(vt, v.Index(i), visitor) if err != nil { return err } v.Index(i).Set(val) } case reflect.Struct: for i := 0; i < v.NumField(); i++ { err := visitValue(v.Field(i), visitor) if err != nil { return err } } case reflect.Map: vt := v.Type().Elem() for _, oldKey := range v.MapKeys() { newKey, err := visitUnsettableValues(oldKey.Type(), oldKey, visitor) if err != nil { return err } oldValue := v.MapIndex(oldKey) newValue, err := visitUnsettableValues(vt, oldValue, visitor) if err != nil { return err } v.SetMapIndex(oldKey, reflect.Value{}) v.SetMapIndex(newKey, newValue) } case reflect.String: if !v.CanSet() { return fmt.Errorf("unable to set String value '%v'", v) } s, asString := visitor(v.String()) if !asString { return fmt.Errorf("attempted to set String field to non-string value '%v'", s) } v.SetString(s) default: glog.V(5).Infof("Ignoring non-parameterizable field type '%s': %v", v.Kind(), v) return nil } return nil } // visitUnsettableValues creates a copy of the object you want to modify and returns the modified result func visitUnsettableValues(typeOf reflect.Type, original reflect.Value, visitor func(string) (string, bool)) (reflect.Value, error) { val := reflect.New(typeOf).Elem() existing := original // if the value type is interface, we must resolve it to a concrete value prior to setting it back. if existing.CanInterface() { existing = reflect.ValueOf(existing.Interface()) } switch existing.Kind() { case reflect.String: s, asString := visitor(existing.String()) if asString { val = reflect.ValueOf(s) } else { b := []byte(s) var data interface{} err := json.Unmarshal(b, &data) if err != nil { // the result of substitution may have been an unquoted string value, // which is an error when decoding in json(only "true", "false", and numeric // values can be unquoted), so try wrapping the value in quotes so it will be // properly converted to a string type during decoding. val = reflect.ValueOf(s) } else { val = reflect.ValueOf(data) } } default: if existing.IsValid() && existing.Kind() != reflect.Invalid { val.Set(existing) } visitValue(val, visitor) } return val, nil }