diff --git a/lib/chcol/json.go b/lib/chcol/json.go index a43dd23502..1203edd3e9 100644 --- a/lib/chcol/json.go +++ b/lib/chcol/json.go @@ -21,6 +21,7 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "slices" "strings" ) @@ -51,11 +52,22 @@ func (o *JSON) ValueAtPath(path string) (any, bool) { // NestedMap converts the flattened JSON data into a nested structure func (o *JSON) NestedMap() map[string]any { - nested := make(map[string]any) + result := make(map[string]any) - for key, value := range o.valuesByPath { - parts := strings.Split(key, ".") - current := nested + sortedPaths := make([]string, 0, len(o.valuesByPath)) + for path := range o.valuesByPath { + sortedPaths = append(sortedPaths, path) + } + slices.Sort(sortedPaths) + + for _, path := range sortedPaths { + value := o.valuesByPath[path] + if vt, ok := value.(Variant); ok && vt.Nil() { + continue + } + + parts := strings.Split(path, ".") + current := result for i := 0; i < len(parts)-1; i++ { part := parts[i] @@ -64,13 +76,14 @@ func (o *JSON) NestedMap() map[string]any { current[part] = make(map[string]any) } - current = current[part].(map[string]any) + if next, ok := current[part].(map[string]any); ok { + current = next + } } - current[parts[len(parts)-1]] = value } - return nested + return result } // MarshalJSON implements the json.Marshaler interface diff --git a/lib/chcol/json_test.go b/lib/chcol/json_test.go new file mode 100644 index 0000000000..a892f718bd --- /dev/null +++ b/lib/chcol/json_test.go @@ -0,0 +1,84 @@ +package chcol + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNestedMap(t *testing.T) { + cases := []struct { + name string + input *JSON + expected map[string]any + }{ + { + name: "nested object with values present", + input: &JSON{ + valuesByPath: map[string]any{ + "x": NewVariant(nil), + "x.a": NewVariant(42), + "x.b": NewVariant(64), + "x.b.c.d": NewVariant(96), + "a.b.c": NewVariant(128), + }, + }, + expected: map[string]any{ + "x": map[string]any{ + "a": NewVariant(42), + "b": NewVariant(64), + "c": map[string]any{ + "d": NewVariant(96), + }, + }, + "a": map[string]any{ + "b": map[string]any{ + "c": NewVariant(128), + }, + }, + }, + }, + { + name: "nested object with only top level path present", + input: &JSON{ + valuesByPath: map[string]any{ + "x": NewVariant(42), + "x.a": NewVariant(nil), + "x.b": NewVariant(nil), + "x.b.c.d": NewVariant(nil), + "a.b.c": NewVariant(nil), + }, + }, + expected: map[string]any{ + "x": NewVariant(42), + }, + }, + { + name: "nested object with typed paths", + input: &JSON{ + valuesByPath: map[string]any{ + "x": 42, + "a.b": "test value", + }, + }, + expected: map[string]any{ + "x": 42, + "a": map[string]any{ + "b": "test value", + }, + }, + }, + { + name: "empty object", + input: NewJSON(), + expected: map[string]any{}, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + actual := c.input.NestedMap() + require.Equal(t, c.expected, actual) + }) + } +}