Skip to content

Commit 8f8dd81

Browse files
authored
Add libs/structwalk (#2957)
## Changes - New function to walk over values: structwalk.Walk() - New function to walk over types: structwalk.WalkType() - structpath is extended to support AnyKey / AnyIndex.
1 parent b07f304 commit 8f8dd81

8 files changed

Lines changed: 718 additions & 18 deletions

File tree

libs/structdiff/structpath/path.go

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@ import (
77
"github.com/databricks/cli/libs/structdiff/jsontag"
88
)
99

10+
const (
11+
tagStruct = -1
12+
tagMapKey = -2
13+
tagUnresolvedStruct = -3
14+
tagAnyKey = -4
15+
tagAnyIndex = -5
16+
)
17+
1018
// PathNode represents a node in a path for struct diffing.
1119
// It can represent struct fields, map keys, or array/slice indices.
1220
type PathNode struct {
1321
prev *PathNode
1422
jsonTag jsontag.JSONTag // For lazy JSON key resolution
1523
key string // Computed key (JSON key for structs, string key for maps, or Go field name for fallback)
1624
// If index >= 0, the node specifies a slice/array index in index.
17-
// If index == -1, the node specifies a struct attribute
18-
// If index == -2, the node specifies a map key in key
19-
// If index == -3, the node specifies an unresolved struct attribute
25+
// If index < 0, this describes the type of node (see tagStruct and other consts above)
2026
index int
2127
}
2228

@@ -42,21 +48,35 @@ func (p *PathNode) MapKey() (string, bool) {
4248
if p == nil {
4349
return "", false
4450
}
45-
if p.index == -2 {
51+
if p.index == tagMapKey {
4652
return p.key, true
4753
}
4854
return "", false
4955
}
5056

57+
func (p *PathNode) AnyKey() bool {
58+
if p == nil {
59+
return false
60+
}
61+
return p.index == tagAnyKey
62+
}
63+
64+
func (p *PathNode) AnyIndex() bool {
65+
if p == nil {
66+
return false
67+
}
68+
return p.index == tagAnyIndex
69+
}
70+
5171
func (p *PathNode) resolveField() {
52-
if p.index == -3 {
72+
if p.index == tagUnresolvedStruct {
5373
// Lazy resolve JSON key for struct fields
5474
jsonName := p.jsonTag.Name()
5575
if jsonName != "" {
5676
p.key = jsonName
5777
}
5878
// If jsonName is empty, key already contains the Go field name as fallback
59-
p.index = -1
79+
p.index = tagStruct
6080
}
6181
}
6282

@@ -65,14 +85,17 @@ func (p *PathNode) Field() (string, bool) {
6585
return "", false
6686
}
6787
p.resolveField()
68-
if p.index == -1 {
88+
if p.index == tagStruct {
6989
return p.key, true
7090
}
7191
return "", false
7292
}
7393

7494
// NewIndex creates a new PathNode for an array/slice index.
7595
func NewIndex(prev *PathNode, index int) *PathNode {
96+
if index < 0 {
97+
panic("index msut be non-negative")
98+
}
7699
return &PathNode{
77100
prev: prev,
78101
index: index,
@@ -84,7 +107,7 @@ func NewMapKey(prev *PathNode, key string) *PathNode {
84107
return &PathNode{
85108
prev: prev,
86109
key: key,
87-
index: -2,
110+
index: tagMapKey,
88111
}
89112
}
90113

@@ -95,7 +118,21 @@ func NewStructField(prev *PathNode, jsonTag jsontag.JSONTag, fieldName string) *
95118
prev: prev,
96119
jsonTag: jsonTag,
97120
key: fieldName,
98-
index: -3, // Unresolved struct attribute
121+
index: tagUnresolvedStruct,
122+
}
123+
}
124+
125+
func NewAnyKey(prev *PathNode) *PathNode {
126+
return &PathNode{
127+
prev: prev,
128+
index: tagAnyKey,
129+
}
130+
}
131+
132+
func NewAnyIndex(prev *PathNode) *PathNode {
133+
return &PathNode{
134+
prev: prev,
135+
index: tagAnyIndex,
99136
}
100137
}
101138

@@ -109,9 +146,13 @@ func (p *PathNode) String() string {
109146
return p.prev.String() + "[" + strconv.Itoa(p.index) + "]"
110147
}
111148

149+
if p.index == tagAnyKey || p.index == tagAnyIndex {
150+
return p.prev.String() + "[*]"
151+
}
152+
112153
p.resolveField()
113154

114-
if p.index == -1 {
155+
if p.index == tagStruct {
115156
return p.prev.String() + "." + p.key
116157
}
117158

@@ -128,6 +169,19 @@ func (p *PathNode) DynPath() string {
128169
return p.prev.DynPath() + "[" + strconv.Itoa(p.index) + "]"
129170
}
130171

172+
if p.index == tagAnyKey {
173+
prev := p.prev.DynPath()
174+
if prev == "" {
175+
return "*"
176+
} else {
177+
return prev + ".*"
178+
}
179+
}
180+
181+
if p.index == tagAnyIndex {
182+
return p.prev.DynPath() + "[*]"
183+
}
184+
131185
p.resolveField()
132186

133187
prev := p.prev.DynPath()

libs/structdiff/structpath/path_test.go

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ import (
99

1010
func TestPathNode(t *testing.T) {
1111
tests := []struct {
12-
name string
13-
node *PathNode
14-
String string
15-
DynPath string
16-
Index any
17-
MapKey any
18-
Field any
19-
Root any
12+
name string
13+
node *PathNode
14+
String string
15+
DynPath string
16+
Index any
17+
MapKey any
18+
Field any
19+
Root any
20+
AnyKey bool
21+
AnyIndex bool
2022
}{
2123
// Single node tests
2224
{
@@ -66,6 +68,19 @@ func TestPathNode(t *testing.T) {
6668
DynPath: "lazy_field",
6769
Field: "lazy_field",
6870
},
71+
{
72+
name: "any key",
73+
node: NewAnyKey(nil),
74+
String: "[*]",
75+
DynPath: "*",
76+
AnyKey: true,
77+
},
78+
{
79+
name: "any index",
80+
node: NewAnyIndex(nil),
81+
String: "[*]",
82+
AnyIndex: true,
83+
},
6984

7085
// Two node tests
7186
{
@@ -123,6 +138,20 @@ func TestPathNode(t *testing.T) {
123138
DynPath: "Parent.child_name",
124139
Field: "child_name",
125140
},
141+
{
142+
name: "any key",
143+
node: NewAnyKey(NewStructField(nil, jsontag.JSONTag(""), "Parent")),
144+
String: ".Parent[*]",
145+
DynPath: "Parent.*",
146+
AnyKey: true,
147+
},
148+
{
149+
name: "any index",
150+
node: NewAnyIndex(NewStructField(nil, jsontag.JSONTag(""), "Parent")),
151+
String: ".Parent[*]",
152+
DynPath: "Parent[*]",
153+
AnyIndex: true,
154+
},
126155
}
127156

128157
for _, tt := range tests {
@@ -172,12 +201,17 @@ func TestPathNode(t *testing.T) {
172201
assert.True(t, isMapKey)
173202
}
174203

204+
// IsRoot
175205
isRoot := tt.node.IsRoot()
176206
if tt.Root == nil {
177207
assert.False(t, isRoot)
178208
} else {
179209
assert.True(t, isRoot)
180210
}
211+
212+
// AnyKey, AnyIndex
213+
assert.Equal(t, tt.AnyKey, tt.node.AnyKey())
214+
assert.Equal(t, tt.AnyIndex, tt.node.AnyIndex())
181215
})
182216
}
183217
}

libs/structwalk/util_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package structwalk
2+
3+
type Simple struct {
4+
X int
5+
}
6+
7+
type Types struct {
8+
ValidField string `json:"valid_field"`
9+
ValidFieldNoTag string
10+
IgnoredField string `json:"-"`
11+
IgnoredFieldOdd string `json:"-,omitempty"`
12+
EmptyTagField string `json:""`
13+
unexportedField string `json:"unexported"` //nolint
14+
unexportedFieldNoTag string //nolint
15+
16+
IntField int
17+
BoolField bool `json:bool_field` // nolint (bad syntax)
18+
AnyField any
19+
20+
ValidFieldPtr *string `json:"valid_field_ptr"`
21+
ValidFieldPtrNoTag *string
22+
IgnoredFieldPtr *string `json:"-"`
23+
IgnoredFieldOddPtr *string `json:"-,omitempty"` //nolint
24+
EmptyTagFieldPtr *string `json:""`
25+
unexportedFieldPtr *string `json:"unexported"` //nolint
26+
unexportedFieldNoTagPtr *string //nolint
27+
28+
SliceString []string
29+
ArrayString [2]string
30+
31+
Nested Simple
32+
NestedPtr *Simple
33+
Slice []Simple
34+
Array [2]Simple
35+
Map map[string]Simple
36+
MapPtr map[string]*Simple
37+
MapIntKey map[int]*Simple
38+
39+
// Fields with omitempty to test ForceSendFields behaviour
40+
OmitStr string `json:"omit_str,omitempty"`
41+
OmitInt int `json:"omit_int,omitempty"`
42+
OmitBool bool `json:"omit_bool,omitempty"`
43+
44+
FuncField func() string `json:"-"`
45+
ChanField chan string `json:"-"`
46+
47+
// List of field names to be force-sent even if they hold zero values.
48+
ForceSendFields []string `json:"-"`
49+
}
50+
51+
type SelfIndirect struct {
52+
X *Self
53+
}
54+
55+
type Self struct {
56+
ValidField string `json:"valid_field"`
57+
58+
SelfReference *Self
59+
60+
SelfSlice []Self
61+
SelfSlicePtr []*Self
62+
63+
SelfArrayPtr [5]*Self
64+
65+
SelfMap map[string]Self
66+
SelfMapPtr map[string]*Self
67+
68+
SelfIndirect SelfIndirect
69+
SelfIndirectPtr *SelfIndirect
70+
71+
// List of field names to be force-sent even if they hold zero values.
72+
ForceSendFields []string `json:"-"`
73+
}

0 commit comments

Comments
 (0)