Skip to content

Commit 582c2df

Browse files
Track bundle tags during Walk and WalkType (#2978)
## Why This information is needed during automatic code generation for required and enum field validation. ## Tests New unit tests.
1 parent c9184a8 commit 582c2df

File tree

7 files changed

+127
-43
lines changed

7 files changed

+127
-43
lines changed

libs/structdiff/diff.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"sort"
88

99
"github.com/databricks/cli/libs/structdiff/structpath"
10-
"github.com/databricks/cli/libs/structdiff/structtag"
1110
)
1211

1312
type Change struct {
@@ -123,16 +122,15 @@ func diffStruct(path *structpath.PathNode, s1, s2 reflect.Value, changes *[]Chan
123122
continue
124123
}
125124

126-
tag := structtag.JSONTag(sf.Tag.Get("json"))
127-
node := structpath.NewStructField(path, tag, sf.Name)
125+
node := structpath.NewStructField(path, sf.Tag, sf.Name)
128126
v1Field := s1.Field(i)
129127
v2Field := s2.Field(i)
130128

131129
zero1 := v1Field.IsZero()
132130
zero2 := v2Field.IsZero()
133131

134132
if zero1 || zero2 {
135-
if tag.OmitEmpty() {
133+
if node.JSONTag().OmitEmpty() {
136134
if zero1 {
137135
if !slices.Contains(forced1, sf.Name) {
138136
v1Field = reflect.ValueOf(nil)

libs/structdiff/structpath/path.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package structpath
22

33
import (
44
"fmt"
5+
"reflect"
56
"strconv"
67

78
"github.com/databricks/cli/libs/structdiff/structtag"
@@ -18,9 +19,10 @@ const (
1819
// PathNode represents a node in a path for struct diffing.
1920
// It can represent struct fields, map keys, or array/slice indices.
2021
type PathNode struct {
21-
prev *PathNode
22-
jsonTag structtag.JSONTag // For lazy JSON key resolution
23-
key string // Computed key (JSON key for structs, string key for maps, or Go field name for fallback)
22+
prev *PathNode
23+
jsonTag structtag.JSONTag // For lazy JSON key resolution
24+
bundleTag structtag.BundleTag
25+
key string // Computed key (JSON key for structs, string key for maps, or Go field name for fallback)
2426
// If index >= 0, the node specifies a slice/array index in index.
2527
// If index < 0, this describes the type of node (see tagStruct and other consts above)
2628
index int
@@ -30,6 +32,10 @@ func (p *PathNode) JSONTag() structtag.JSONTag {
3032
return p.jsonTag
3133
}
3234

35+
func (p *PathNode) BundleTag() structtag.BundleTag {
36+
return p.bundleTag
37+
}
38+
3339
func (p *PathNode) IsRoot() bool {
3440
return p == nil
3541
}
@@ -113,12 +119,16 @@ func NewMapKey(prev *PathNode, key string) *PathNode {
113119

114120
// NewStructField creates a new PathNode for a struct field.
115121
// The jsonTag is used for lazy JSON key resolution, and fieldName is used as fallback.
116-
func NewStructField(prev *PathNode, jsonTag structtag.JSONTag, fieldName string) *PathNode {
122+
func NewStructField(prev *PathNode, tag reflect.StructTag, fieldName string) *PathNode {
123+
jsonTag := structtag.JSONTag(tag.Get("json"))
124+
bundleTag := structtag.BundleTag(tag.Get("bundle"))
125+
117126
return &PathNode{
118-
prev: prev,
119-
jsonTag: jsonTag,
120-
key: fieldName,
121-
index: tagUnresolvedStruct,
127+
prev: prev,
128+
jsonTag: jsonTag,
129+
bundleTag: bundleTag,
130+
key: fieldName,
131+
index: tagUnresolvedStruct,
122132
}
123133
}
124134

libs/structdiff/structpath/path_test.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package structpath
22

33
import (
4+
"reflect"
45
"testing"
56

6-
"github.com/databricks/cli/libs/structdiff/structtag"
77
"github.com/stretchr/testify/assert"
88
)
99

@@ -42,28 +42,28 @@ func TestPathNode(t *testing.T) {
4242
},
4343
{
4444
name: "struct field with JSON tag",
45-
node: NewStructField(nil, structtag.JSONTag("json_name"), "GoFieldName"),
45+
node: NewStructField(nil, reflect.StructTag(`json:"json_name"`), "GoFieldName"),
4646
String: ".json_name",
4747
DynPath: "json_name",
4848
Field: "json_name",
4949
},
5050
{
5151
name: "struct field without JSON tag (fallback to Go name)",
52-
node: NewStructField(nil, structtag.JSONTag(""), "GoFieldName"),
52+
node: NewStructField(nil, reflect.StructTag(""), "GoFieldName"),
5353
String: ".GoFieldName",
5454
DynPath: "GoFieldName",
5555
Field: "GoFieldName",
5656
},
5757
{
5858
name: "struct field with dash JSON tag",
59-
node: NewStructField(nil, structtag.JSONTag("-"), "GoFieldName"),
59+
node: NewStructField(nil, reflect.StructTag(`json:"-"`), "GoFieldName"),
6060
String: ".-",
6161
DynPath: "-",
6262
Field: "-",
6363
},
6464
{
6565
name: "struct field with JSON tag options",
66-
node: NewStructField(nil, structtag.JSONTag("lazy_field,omitempty"), "LazyField"),
66+
node: NewStructField(nil, reflect.StructTag(`json:"lazy_field,omitempty"`), "LazyField"),
6767
String: ".lazy_field",
6868
DynPath: "lazy_field",
6969
Field: "lazy_field",
@@ -85,21 +85,21 @@ func TestPathNode(t *testing.T) {
8585
// Two node tests
8686
{
8787
name: "struct field -> array index",
88-
node: NewIndex(NewStructField(nil, structtag.JSONTag("items"), "Items"), 3),
88+
node: NewIndex(NewStructField(nil, reflect.StructTag(`json:"items"`), "Items"), 3),
8989
String: ".items[3]",
9090
DynPath: "items[3]",
9191
Index: 3,
9292
},
9393
{
9494
name: "struct field -> map key",
95-
node: NewMapKey(NewStructField(nil, structtag.JSONTag("config"), "Config"), "database"),
95+
node: NewMapKey(NewStructField(nil, reflect.StructTag(`json:"config"`), "Config"), "database"),
9696
String: `.config["database"]`,
9797
DynPath: "config.database",
9898
MapKey: "database",
9999
},
100100
{
101101
name: "struct field -> struct field",
102-
node: NewStructField(NewStructField(nil, structtag.JSONTag("user"), "User"), structtag.JSONTag("name"), "Name"),
102+
node: NewStructField(NewStructField(nil, reflect.StructTag(`json:"user"`), "User"), reflect.StructTag(`json:"name"`), "Name"),
103103
String: ".user.name",
104104
DynPath: "user.name",
105105
Field: "name",
@@ -113,14 +113,14 @@ func TestPathNode(t *testing.T) {
113113
},
114114
{
115115
name: "map key -> struct field",
116-
node: NewStructField(NewMapKey(nil, "primary"), structtag.JSONTag("host"), "Host"),
116+
node: NewStructField(NewMapKey(nil, "primary"), reflect.StructTag(`json:"host"`), "Host"),
117117
String: `["primary"].host`,
118118
DynPath: `primary.host`,
119119
Field: "host",
120120
},
121121
{
122122
name: "array index -> struct field",
123-
node: NewStructField(NewIndex(nil, 2), structtag.JSONTag("id"), "ID"),
123+
node: NewStructField(NewIndex(nil, 2), reflect.StructTag(`json:"id"`), "ID"),
124124
String: "[2].id",
125125
Field: "id",
126126
},
@@ -133,21 +133,21 @@ func TestPathNode(t *testing.T) {
133133
},
134134
{
135135
name: "struct field without JSON tag -> struct field with JSON tag",
136-
node: NewStructField(NewStructField(nil, structtag.JSONTag(""), "Parent"), structtag.JSONTag("child_name"), "ChildName"),
136+
node: NewStructField(NewStructField(nil, reflect.StructTag(""), "Parent"), reflect.StructTag(`json:"child_name"`), "ChildName"),
137137
String: ".Parent.child_name",
138138
DynPath: "Parent.child_name",
139139
Field: "child_name",
140140
},
141141
{
142142
name: "any key",
143-
node: NewAnyKey(NewStructField(nil, structtag.JSONTag(""), "Parent")),
143+
node: NewAnyKey(NewStructField(nil, reflect.StructTag(""), "Parent")),
144144
String: ".Parent[*]",
145145
DynPath: "Parent.*",
146146
AnyKey: true,
147147
},
148148
{
149149
name: "any index",
150-
node: NewAnyIndex(NewStructField(nil, structtag.JSONTag(""), "Parent")),
150+
node: NewAnyIndex(NewStructField(nil, reflect.StructTag(""), "Parent")),
151151
String: ".Parent[*]",
152152
DynPath: "Parent[*]",
153153
AnyIndex: true,

libs/structwalk/walk.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"sort"
88

99
"github.com/databricks/cli/libs/structdiff/structpath"
10-
"github.com/databricks/cli/libs/structdiff/structtag"
1110
)
1211

1312
// VisitFunc is invoked for every scalar (int, uint, float, string, bool) field encountered while walking v.
@@ -113,16 +112,15 @@ func walkStruct(path *structpath.PathNode, s reflect.Value, visit VisitFunc) {
113112
if sf.Name == "ForceSendFields" {
114113
continue
115114
}
116-
tag := sf.Tag.Get("json")
117-
if tag == "-" {
115+
116+
node := structpath.NewStructField(path, sf.Tag, sf.Name)
117+
if node.JSONTag().Name() == "-" {
118118
continue // skip fields without json name
119119
}
120-
jsonTag := structtag.JSONTag(tag)
121-
fieldVal := s.Field(i)
122-
node := structpath.NewStructField(path, jsonTag, sf.Name)
123120

121+
fieldVal := s.Field(i)
124122
// Skip zero values with omitempty unless field is explicitly forced.
125-
if jsonTag.OmitEmpty() && fieldVal.IsZero() && !slices.Contains(forced, sf.Name) {
123+
if node.JSONTag().OmitEmpty() && fieldVal.IsZero() && !slices.Contains(forced, sf.Name) {
126124
continue
127125
}
128126

libs/structwalk/walk_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,31 @@ func TestValueJobSettings(t *testing.T) {
8383
".timeout_seconds": 3600,
8484
}, flatten(t, jobSettings))
8585
}
86+
87+
func TestValueBundleTag(t *testing.T) {
88+
type Foo struct {
89+
A string `bundle:"readonly"`
90+
B string `bundle:"internal"`
91+
C string
92+
D string `bundle:"internal,readonly"`
93+
}
94+
95+
var readonly, internal []string
96+
err := Walk(Foo{
97+
A: "a",
98+
B: "b",
99+
C: "c",
100+
D: "d",
101+
}, func(path *structpath.PathNode, value any) {
102+
if path.BundleTag().ReadOnly() {
103+
readonly = append(readonly, path.String())
104+
}
105+
if path.BundleTag().Internal() {
106+
internal = append(internal, path.String())
107+
}
108+
})
109+
require.NoError(t, err)
110+
111+
assert.Equal(t, []string{".A", ".D"}, readonly)
112+
assert.Equal(t, []string{".B", ".D"}, internal)
113+
}

libs/structwalk/walktype.go

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"reflect"
66

77
"github.com/databricks/cli/libs/structdiff/structpath"
8-
"github.com/databricks/cli/libs/structdiff/structtag"
98
)
109

1110
// VisitTypeFunc is invoked for fields encountered while walking typ. This includes both leaf nodes as well as any
@@ -98,25 +97,20 @@ func walkTypeStruct(path *structpath.PathNode, st reflect.Type, visit VisitTypeF
9897
if sf.PkgPath != "" {
9998
continue // unexported
10099
}
101-
tag := sf.Tag.Get("json")
100+
node := structpath.NewStructField(path, sf.Tag, sf.Name)
102101

103102
// Handle embedded structs (anonymous fields without json tags)
104-
if sf.Anonymous && tag == "" {
103+
if sf.Anonymous && node.JSONTag() == "" {
105104
// For embedded structs, walk the embedded type at the current path level
106105
// This flattens the embedded struct's fields into the parent struct
107106
walkTypeValue(path, sf.Type, visit, visitedCount)
108107
continue
109108
}
110109

111-
if tag == "-" {
112-
continue // skip fields without json name
113-
}
114-
jsonTag := structtag.JSONTag(tag)
115-
if jsonTag.Name() == "-" {
110+
if node.JSONTag().Name() == "-" {
116111
continue
117112
}
118-
fieldType := sf.Type
119-
node := structpath.NewStructField(path, jsonTag, sf.Name)
120-
walkTypeValue(node, fieldType, visit, visitedCount)
113+
114+
walkTypeValue(node, sf.Type, visit, visitedCount)
121115
}
122116
}

libs/structwalk/walktype_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,62 @@ func TestTypeRoot(t *testing.T) {
151151
)
152152
}
153153

154+
func getReadonlyFields(t *testing.T, typ reflect.Type) []string {
155+
var results []string
156+
err := WalkType(typ, func(path *structpath.PathNode, typ reflect.Type) {
157+
if path == nil {
158+
return
159+
}
160+
if path.BundleTag().ReadOnly() {
161+
results = append(results, path.DynPath())
162+
}
163+
})
164+
require.NoError(t, err)
165+
return results
166+
}
167+
168+
func TestTypeReadonlyFields(t *testing.T) {
169+
readonlyFields := getReadonlyFields(t, reflect.TypeOf(config.Root{}))
170+
171+
expected := []string{
172+
"bundle.mode",
173+
"bundle.target",
174+
"resources.jobs.*.id",
175+
"resources.pipelines.*.id",
176+
"workspace.current_user.short_name",
177+
}
178+
179+
for _, v := range expected {
180+
assert.Contains(t, readonlyFields, v)
181+
}
182+
}
183+
184+
func TestTypeBundleTag(t *testing.T) {
185+
type Foo struct {
186+
A string `bundle:"readonly"`
187+
B string `bundle:"internal"`
188+
C string
189+
D string `bundle:"internal,readonly"`
190+
}
191+
192+
var readonly, internal []string
193+
err := WalkType(reflect.TypeOf(Foo{}), func(path *structpath.PathNode, typ reflect.Type) {
194+
if path == nil {
195+
return
196+
}
197+
if path.BundleTag().ReadOnly() {
198+
readonly = append(readonly, path.String())
199+
}
200+
if path.BundleTag().Internal() {
201+
internal = append(internal, path.String())
202+
}
203+
})
204+
require.NoError(t, err)
205+
206+
assert.Equal(t, []string{".A", ".D"}, readonly)
207+
assert.Equal(t, []string{".B", ".D"}, internal)
208+
}
209+
154210
func TestWalkTypeVisited(t *testing.T) {
155211
type Inner struct {
156212
A int

0 commit comments

Comments
 (0)