-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathundo.go
153 lines (129 loc) · 2.96 KB
/
undo.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package flatmap
import (
"fmt"
"strconv"
"strings"
)
/*
Undo takes a flattened map and nests it into a multi-level map. The flattened
map should follow the [JSONPath] standard. Please see the example to understand
how the nested output looks like.
[JSONPath]: https://datatracker.ietf.org/doc/html/rfc9535
*/
func Undo(flattened map[string]any) (map[string]any, error) {
// First, convert the flat map to a nested map. Then reshape the map into a
// slice where appropriate.
const magicSliceKey = "isSlice"
nested := make(map[string]any)
for key, value := range flattened {
p, err := pathFrom(key)
if err != nil {
return nil, err
}
current := nested
for i, k := range p {
key := k.Key()
if k.IsSlice() {
current[magicSliceKey] = true
}
isLast := i == len(p)-1
if isLast {
current[key] = value
break
}
if current[key] == nil {
current[key] = make(map[string]any)
}
current = current[key].(map[string]any)
}
}
// Convert maps to slices where appropriate using non recursive breadth
// first search.
queue := []map[string]any{nested}
for len(queue) > 0 {
current := queue[0]
queue = queue[1:]
for k, v := range current {
m, ok := v.(map[string]any)
if !ok {
// Not a map, we reached the end of the tree.
continue
}
if m[magicSliceKey] == nil {
// Just a normal map, enqueue.
queue = append(queue, m)
continue
}
// A map that needs to be converted to a slice.
delete(m, magicSliceKey)
slice, err := toSlice(m)
if err != nil {
return nil, err
}
for _, x := range slice {
if _, ok := x.(map[string]any); ok {
// Enqueue all maps in the slice.
queue = append(queue, x.(map[string]any))
}
}
current[k] = slice
}
}
return nested, nil
}
func toSlice(x map[string]any) ([]any, error) {
slice := make([]any, len(x))
for k, v := range x {
idx, err := strconv.Atoi(k)
if err != nil {
return nil, err
}
if idx >= len(slice) || idx < 0 {
return nil, fmt.Errorf("index %d out of bounds", idx)
}
slice[idx] = v
}
return slice, nil
}
type pathKey struct {
name string
index int
}
func (p pathKey) IsSlice() bool {
return p.index != -1
}
func (p pathKey) Key() string {
if p.IsSlice() {
return strconv.Itoa(p.index)
}
return p.name
}
type path []pathKey
func pathFrom(key string) (path, error) {
key = strings.TrimPrefix(key, "$")
split := strings.Split(key[1:], ".")
p := make(path, 0, len(split))
for _, s := range split {
stops, err := pathKeysFrom(s)
if err != nil {
return path{}, err
}
p = append(p, stops...)
}
return p, nil
}
func pathKeysFrom(key string) ([]pathKey, error) {
if strings.Contains(key, "[") {
start := strings.Index(key, "[")
end := strings.Index(key, "]")
index, err := strconv.Atoi(key[start+1 : end])
if err != nil {
return []pathKey{}, err
}
return []pathKey{
{name: key[:start], index: -1},
{index: index},
}, nil
}
return []pathKey{{name: key, index: -1}}, nil
}