diff --git a/go/cmd/vtctldclient/cli/json.go b/go/cmd/vtctldclient/cli/json.go index 469b8bb999e..5aa9f50e48c 100644 --- a/go/cmd/vtctldclient/cli/json.go +++ b/go/cmd/vtctldclient/cli/json.go @@ -82,7 +82,8 @@ func MarshalJSONPretty(obj any) ([]byte, error) { } // ConvertToSnakeCase converts a string to snake_case or the keys of a -// map to snake_case. +// map to snake_case. This is useful when converting generic JSON data +// marshalled to a map[string]interface{} for printing. func ConvertToSnakeCase(val any) (any, error) { switch val := val.(type) { case string: @@ -101,8 +102,28 @@ func ConvertToSnakeCase(val any) (any, error) { val[sk] = sv } return val, nil + case map[any]interface{}: + for k, v := range val { + // We need to recurse into the key to support more complex + // key types. + sk, err := ConvertToSnakeCase(k) + if err != nil { + return nil, err + } + // We need to recurse into the map to convert nested maps + // to snake_case. + sv, err := ConvertToSnakeCase(v) + if err != nil { + return nil, err + } + delete(val, k) + val[sk] = sv + } + return val, nil case []interface{}: for i, v := range val { + // We need to recurse into the slice to convert nested maps + // to snake_case. sv, err := ConvertToSnakeCase(v) if err != nil { return nil, err @@ -110,7 +131,16 @@ func ConvertToSnakeCase(val any) (any, error) { val[i] = sv } return val, nil + case []string: + for i, v := range val { + // We need to recurse into the slice to convert nested maps + // to snake_case. + sk := stats.GetSnakeName(v) + val[i] = sk + } + return val, nil default: - return nil, fmt.Errorf("unsupported type %T", val) + // No need to do any conversion for things like bool. + return val, nil } } diff --git a/go/cmd/vtctldclient/cli/json_test.go b/go/cmd/vtctldclient/cli/json_test.go new file mode 100644 index 00000000000..183b599c44f --- /dev/null +++ b/go/cmd/vtctldclient/cli/json_test.go @@ -0,0 +1,128 @@ +/* +Copyright 2024 The Vitess Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cli + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestConvertToSnakeCase(t *testing.T) { + tests := []struct { + name string + val any + want any + wantErr bool + }{ + { + name: "string", + val: "MyValIsNotCool", + want: "my_val_is_not_cool", + }, + { + name: "string slice", + val: []string{ + "MyValIsNotCool", + "NeitherIsYours", + }, + want: []string{ + "my_val_is_not_cool", + "neither_is_yours", + }, + }, + { + name: "string map", + val: map[string]any{ + "MyValIsNotCool": "val1", + "NeitherIsYours": "val2", + }, + want: map[string]any{ + "my_val_is_not_cool": "val1", + "neither_is_yours": "val2", + }, + }, + { + name: "string map of slices", + val: map[string]any{ + "MyValIsNotCool": []string{"val1", "val2"}, + "NeitherIsYours": []string{"val3", "val4"}, + }, + want: map[string]any{ + "my_val_is_not_cool": []string{"val1", "val2"}, + "neither_is_yours": []string{"val3", "val4"}, + }, + }, + { + name: "string map of slices of string maps", + val: map[any]any{ + "MyValIsNotCool": []any{ + 0: map[any]any{ + "SubKey1": "val1", + "SubKey2": "val2", + }, + 1: map[any]any{ + "SubKey3": "val3", + "SubKey4": "val4", + }, + }, + "NeitherIsYours": []any{ + 0: map[any]any{ + "SubKey5": "val5", + "SubKey6": "val6", + }, + 1: map[any]any{ + "SubKey7": "val7", + "SubKey8": "val8", + }, + }, + }, + want: map[any]any{ + "my_val_is_not_cool": []any{ + 0: map[any]any{ + "sub_key1": "val1", + "sub_key2": "val2", + }, + 1: map[any]any{ + "sub_key3": "val3", + "sub_key4": "val4", + }, + }, + "neither_is_yours": []any{ + 0: map[any]any{ + "sub_key5": "val5", + "sub_key6": "val6", + }, + 1: map[any]any{ + "sub_key7": "val7", + "sub_key8": "val8", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ConvertToSnakeCase(tt.val) + if (err != nil) != tt.wantErr { + require.Fail(t, "unexpted error value", "ConvertToSnakeCase() error = %v, wantErr %v", err, tt.wantErr) + return + } + require.EqualValues(t, tt.want, got, "ConvertToSnakeCase() = %v, want %v", got, tt.want) + }) + } +}