Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions bundle/direct/dresources/all_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/cli/bundle/deployplan"
"github.com/databricks/cli/libs/dyn"
"github.com/databricks/cli/libs/structs/dynpath"
"github.com/databricks/cli/libs/structs/structaccess"
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structwalk"
Expand Down Expand Up @@ -155,9 +156,10 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W
require.NotNil(t, remappedState)

require.NoError(t, structwalk.Walk(newState, func(path *structpath.PathNode, val any, field *reflect.StructField) {
remoteValue, err := structaccess.Get(remappedState, dyn.MustPathFromString(path.DynPath()))
dynPath := dynpath.ConvertPathNodeToDynPath(path, reflect.TypeOf(newState))
remoteValue, err := structaccess.Get(remappedState, dyn.MustPathFromString(dynPath))
if err != nil {
t.Errorf("Failed to read %s from remapped remote state %#v", path.DynPath(), remappedState)
t.Errorf("Failed to read %s from remapped remote state %#v", dynPath, remappedState)
}
if val == nil {
// t.Logf("Ignoring %s nil, remoteValue=%#v", path.String(), remoteValue)
Expand All @@ -172,7 +174,7 @@ func testCRUD(t *testing.T, group string, adapter *Adapter, client *databricks.W
// t.Logf("Testing %s v=%#v, remoteValue=%#v", path.String(), val, remoteValue)
// We expect fields set explicitly to be preserved by testserver, which is true for all resources as of today.
// If not true for your resource, add exception here:
assert.Equal(t, val, remoteValue, path.DynPath())
assert.Equal(t, val, remoteValue, dynPath)
}))

err = adapter.DoDelete(ctx, createdID)
Expand Down
3 changes: 2 additions & 1 deletion bundle/internal/validation/enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"text/template"

"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/libs/structs/dynpath"
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structtag"
"github.com/databricks/cli/libs/structs/structwalk"
Expand Down Expand Up @@ -132,7 +133,7 @@ func extractEnumFields(typ reflect.Type) ([]EnumPatternInfo, error) {
return true
}

fieldPath := path.DynPath()
fieldPath := dynpath.ConvertPathNodeToDynPath(path, typ)
fieldsByPattern[fieldPath] = enumValues
}
return true
Expand Down
3 changes: 2 additions & 1 deletion bundle/internal/validation/required.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"text/template"

"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/libs/structs/dynpath"
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/structs/structtag"
"github.com/databricks/cli/libs/structs/structwalk"
Expand Down Expand Up @@ -68,7 +69,7 @@ func extractRequiredFields(typ reflect.Type) ([]RequiredPatternInfo, error) {
return true
}

parentPath := path.Parent().DynPath()
parentPath := dynpath.ConvertPathNodeToDynPath(path.Parent(), typ)
fieldsByPattern[parentPath] = append(fieldsByPattern[parentPath], fieldName)
return true
})
Expand Down
104 changes: 104 additions & 0 deletions libs/structs/dynpath/dynpath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package dynpath

import (
"reflect"
"strconv"
"strings"

"github.com/databricks/cli/libs/structs/structaccess"
"github.com/databricks/cli/libs/structs/structpath"
)

// ConvertPathNodeToDynPath converts a PathNode to dyn path format string.
// Uses the provided root type to determine context-aware wildcard formatting:
// - BracketStar accessing maps renders as "parent.*"
// - BracketStar accessing arrays/slices renders as "parent[*]"
// - DotStar always renders as "parent.*"
func ConvertPathNodeToDynPath(path *structpath.PathNode, rootType reflect.Type) string {
segments := path.AsSlice()
var result strings.Builder
currentType := rootType

for _, segment := range segments {
// Dereference pointers
for currentType != nil && currentType.Kind() == reflect.Pointer {
currentType = currentType.Elem()
}

if index, ok := segment.Index(); ok {
// Array/slice index access
result.WriteString("[")
result.WriteString(strconv.Itoa(index))
result.WriteString("]")
if currentType != nil && (currentType.Kind() == reflect.Array || currentType.Kind() == reflect.Slice) {
currentType = currentType.Elem()
} else {
currentType = nil
}

} else if field, ok := segment.Field(); ok {
// Struct field access
if result.Len() > 0 {
result.WriteString(".")
}
result.WriteString(field)
if currentType != nil && currentType.Kind() == reflect.Struct {
if sf, _, ok := structaccess.FindStructFieldByKeyType(currentType, field); ok {
currentType = sf.Type
} else {
currentType = nil
}
} else {
currentType = nil
}

} else if key, ok := segment.MapKey(); ok {
// Map key access - always use dot notation in DynPath
if result.Len() > 0 {
result.WriteString(".")
}
result.WriteString(key)
if currentType != nil && currentType.Kind() == reflect.Map {
currentType = currentType.Elem()
} else {
currentType = nil
}

} else if segment.DotStar() {
// Field wildcard - always uses dot notation
if result.Len() > 0 {
result.WriteString(".")
}
result.WriteString("*")

// If it's a map, we can inspect the type; otherwise we cannot since we don't know what field to look into
if currentType != nil && currentType.Kind() == reflect.Map {
currentType = currentType.Elem()
} else {
currentType = nil
}

} else if segment.BracketStar() {
if currentType != nil && (currentType.Kind() == reflect.Array || currentType.Kind() == reflect.Slice) {
result.WriteString("[*]")
currentType = currentType.Elem()
} else {
// Map wildcard uses dot notation in DynPath
if result.Len() > 0 {
result.WriteString(".")
}
result.WriteString("*")

if currentType != nil && currentType.Kind() == reflect.Map {
currentType = currentType.Elem()
} else {
currentType = nil
}

// QQQ return error if we cannot disambiguate?
}
}
}

return result.String()
}
205 changes: 205 additions & 0 deletions libs/structs/dynpath/dynpath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package dynpath

import (
"reflect"
"testing"

"github.com/databricks/cli/libs/structs/structpath"
"github.com/stretchr/testify/assert"
)

func TestConvertPathNodeToDynPath(t *testing.T) {
tests := []struct {
name string
structPath string // PathNode string representation to be parsed
dynPath string // Expected DynPath format
rootType reflect.Type // optional, for context-aware wildcard rendering
}{
// Basic node types
{
name: "nil path",
structPath: "",
dynPath: "",
},
{
name: "array index",
structPath: "[5]",
dynPath: "[5]",
},
{
name: "map key",
structPath: "['mykey']",
dynPath: "mykey",
},
{
name: "struct field",
structPath: "json_name",
dynPath: "json_name",
},
{
name: "dot star",
structPath: "*",
dynPath: "*",
},
{
name: "bracket star - array type",
structPath: "[*]",
rootType: reflect.TypeOf([]int{}),
dynPath: "[*]",
},
{
name: "bracket star - map type",
structPath: "[*]",
rootType: reflect.TypeOf(map[string]int{}),
dynPath: "*",
},

// Compound paths
{
name: "struct field -> array index",
structPath: "items[3]",
dynPath: "items[3]",
},
{
name: "struct field -> map key",
structPath: "config['database']",
dynPath: "config.database",
},
{
name: "struct field -> struct field",
structPath: "user.name",
dynPath: "user.name",
},
{
name: "map key -> array index",
structPath: "['servers'][0]",
dynPath: "servers[0]",
},
{
name: "map key -> struct field",
structPath: "['primary'].host",
dynPath: "primary.host",
},
{
name: "array index -> struct field",
structPath: "[2].id",
dynPath: "[2].id",
},
{
name: "array index -> map key",
structPath: "[1]['status']",
dynPath: "[1].status",
},

// Wildcard combinations
{
name: "dot star with parent",
structPath: "Parent.*",
dynPath: "Parent.*",
},
{
name: "bracket star with parent - array",
structPath: "Parent[*]",
rootType: reflect.TypeOf(struct {
Parent []int
}{}),
dynPath: "Parent[*]",
},

{
name: "bracket star with parent - array",
structPath: "parent[*]",
rootType: reflect.TypeOf(struct {
Parent []int `json:"parent"`
}{}),
dynPath: "parent[*]",
},

{
name: "bracket star with parent - map",
structPath: "Parent[*]",
rootType: reflect.TypeOf(struct {
Parent map[string]int
}{}),
dynPath: "Parent.*",
},

// Special characters and edge cases
{
name: "map key with single quote",
structPath: "['key''s']",
dynPath: "key's",
},
{
name: "map key with multiple single quotes",
structPath: "['''''']",
dynPath: "''",
},
{
name: "empty map key",
structPath: "['']",
dynPath: "",
},
{
name: "map key with reserved characters",
structPath: "['key\x00[],`']",
dynPath: "key\x00[],`",
},
{
name: "field with special characters",
structPath: "field@name:with#symbols!",
dynPath: "field@name:with#symbols!",
},
{
name: "field with spaces",
structPath: "field with spaces",
dynPath: "field with spaces",
},
{
name: "field with unicode",
structPath: "名前🙂",
dynPath: "名前🙂",
},

// Complex real-world example
{
name: "complex nested path",
structPath: "user.*.settings['theme'][0].color",
dynPath: "user.*.settings.theme[0].color",
},

// Mixed JSON tag and Go field name scenarios
//{
// name: "Go field -> JSON field",
// structPath: "Parent.child_name",
// dynPath: "Parent.child_name",
//},
//{
// name: "JSON field -> Go field",
// structPath: "parent.ChildName",
// dynPath: "parent.ChildName",
//},
//{
// name: "dash JSON tag field",
// structPath: "-",
// dynPath: "-",
//},
//{
// name: "JSON tag with options",
// structPath: "lazy_field",
// dynPath: "lazy_field",
//},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pathNode, err := structpath.Parse(tt.structPath)
t.Logf("%q node=%#v", tt.structPath, pathNode)
assert.NoError(t, err, "Failed to parse structPath: %s", tt.structPath)

// Convert to DynPath and verify result
result := ConvertPathNodeToDynPath(pathNode, tt.rootType)
assert.Equal(t, tt.dynPath, result, "ConvertPathNodeToDynPath conversion should match expected DynPath format")
})
}
}
Loading
Loading