Fix: expand dotted field paths in ItemBuilder.Set (#7302)

* fix: expand dotted field paths in ItemBuilder.Set

Signed-off-by: Krishnan K M <krishnankaruvattu@gmail.com>

* Fix: preserve dots in bracketed SetIf paths

Signed-off-by: Krishnan K M <krishnankaruvattu@gmail.com>

---------

Signed-off-by: Krishnan K M <krishnankaruvattu@gmail.com>
This commit is contained in:
Krishnan K M
2026-08-13 11:33:04 +01:00
committed by GitHub
parent f1da2f51dd
commit 334e41060f
3 changed files with 134 additions and 5 deletions
+14 -5
View File
@@ -2406,8 +2406,7 @@ func (g *CUEGenerator) arrayElementToCUEWithDepth(elem *ArrayElement, depth int)
if setIf, ok := op.(*SetIfOp); ok {
condStr := g.conditionToCUE(setIf.Cond())
valStr := indentMultilineValue(g.valueToCUE(setIf.Value()), innerIndent+"\t")
// Convert dot-separated path to CUE shorthand syntax: "a.b.c" -> "a: b: c"
cuePath := strings.ReplaceAll(setIf.Path(), ".", ": ")
cuePath := itemFieldLabel(setIf.Path())
sb.WriteString(fmt.Sprintf("%sif %s {\n", innerIndent, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", innerIndent, cuePath, valStr))
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
@@ -2487,7 +2486,7 @@ func (g *CUEGenerator) arrayBuilderToCUE(ab *ArrayBuilder, depth int) string {
if setIf, ok := op.(*SetIfOp); ok {
condStr := g.conditionToCUE(setIf.Cond())
valStr := g.valueToCUE(setIf.Value())
cuePath := strings.ReplaceAll(setIf.Path(), ".", ": ")
cuePath := itemFieldLabel(setIf.Path())
sb.WriteString(fmt.Sprintf("%sif %s {\n", extraIndent, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", extraIndent, cuePath, valStr))
sb.WriteString(fmt.Sprintf("%s}\n", extraIndent))
@@ -2516,6 +2515,16 @@ func (g *CUEGenerator) arrayBuilderToCUE(ab *ArrayBuilder, depth int) string {
return sb.String()
}
// itemFieldLabel converts a dot-separated field path into CUE nested-field
// shorthand. Dots inside brackets are preserved as part of the key.
func itemFieldLabel(field string) string {
parts := splitPath(field)
if len(parts) < 2 {
return field
}
return strings.Join(parts, ": ")
}
// writeItemBuilderOps writes the CUE for ItemBuilder operations.
func (g *CUEGenerator) writeItemBuilderOps(sb *strings.Builder, ops []itemOp, depth int) {
indent := strings.Repeat(g.indent, depth)
@@ -2524,7 +2533,7 @@ func (g *CUEGenerator) writeItemBuilderOps(sb *strings.Builder, ops []itemOp, de
switch o := op.(type) {
case setOp:
valStr := g.valueToCUE(o.value)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, o.field, valStr))
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, itemFieldLabel(o.field), valStr))
case ifBlockOp:
condStr := g.conditionToCUE(o.cond)
@@ -2538,7 +2547,7 @@ func (g *CUEGenerator) writeItemBuilderOps(sb *strings.Builder, ops []itemOp, de
case setDefaultOp:
defStr := g.valueToCUE(o.defValue)
sb.WriteString(fmt.Sprintf("%s%s: *%s | %s\n", indent, o.field, defStr, o.typeName))
sb.WriteString(fmt.Sprintf("%s%s: *%s | %s\n", indent, itemFieldLabel(o.field), defStr, o.typeName))
}
}
}
+111
View File
@@ -2749,6 +2749,117 @@ var _ = Describe("CUEGenerator", func() {
Expect(cue).To(ContainSubstring("name: e.name"))
Expect(cue).To(ContainSubstring("value: e.value"))
})
It("ItemBuilder.Set expands a dotted path into nested fields", func() {
maxRead := defkit.Int("maxReadRequestUnits").Optional()
indexes := defkit.Array("indexes").WithFields(
defkit.String("name"),
).Optional()
c := defkit.NewComponent("table").
Workload("v1", "ConfigMap").
Params(indexes, maxRead).
Template(func(tpl *defkit.Template) {
arr := defkit.NewArray().ForEachWith(indexes,
func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("name", v.Field("name"))
item.Set("onDemandThroughput.maxReadRequestUnits", maxRead)
})
tpl.Output(defkit.NewResource("v1", "ConfigMap").
Set("spec.indexes", arr))
})
cue := c.ToCue()
Expect(cue).To(ContainSubstring("onDemandThroughput: maxReadRequestUnits: parameter.maxReadRequestUnits"))
// The unexpanded label is not valid CUE and must not reappear.
Expect(cue).NotTo(ContainSubstring("onDemandThroughput.maxReadRequestUnits:"))
// A single-segment field is unaffected.
Expect(cue).To(ContainSubstring("name: v.name"))
Expect(parseCUE(cue)).To(Succeed())
})
It("ItemBuilder.Set expands a dotted path inside a conditional block", func() {
maxRead := defkit.Int("maxReadRequestUnits").Optional()
maxWrite := defkit.Int("maxWriteRequestUnits").Optional()
indexes := defkit.Array("indexes").WithFields(
defkit.String("name"),
).Optional()
c := defkit.NewComponent("table").
Workload("v1", "ConfigMap").
Params(indexes, maxRead, maxWrite).
Template(func(tpl *defkit.Template) {
arr := defkit.NewArray().ForEachWith(indexes,
func(item *defkit.ItemBuilder) {
item.If(maxRead.IsSet(), func() {
item.Set("onDemandThroughput.maxReadRequestUnits", maxRead)
})
item.If(maxWrite.IsSet(), func() {
item.Set("onDemandThroughput.maxWriteRequestUnits", maxWrite)
})
})
tpl.Output(defkit.NewResource("v1", "ConfigMap").
Set("spec.indexes", arr))
})
cue := c.ToCue()
// Sibling writes to the same parent unify into one struct.
Expect(cue).To(ContainSubstring("onDemandThroughput: maxReadRequestUnits: parameter.maxReadRequestUnits"))
Expect(cue).To(ContainSubstring("onDemandThroughput: maxWriteRequestUnits: parameter.maxWriteRequestUnits"))
Expect(parseCUE(cue)).To(Succeed())
})
It("ItemBuilder.Set does not split on a dot inside a bracketed key", func() {
items := defkit.Array("items").WithFields(
defkit.String("value"),
).Optional()
c := defkit.NewComponent("annotated").
Workload("v1", "ConfigMap").
Params(items).
Template(func(tpl *defkit.Template) {
arr := defkit.NewArray().ForEachWith(items,
func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set(`metadata.annotations["example.com/owner"]`, v.Field("value"))
})
tpl.Output(defkit.NewResource("v1", "ConfigMap").
Set("spec.items", arr))
})
cue := c.ToCue()
// The dot inside the brackets is part of the key, not a separator.
// Bracketed keys are not otherwise supported here; this only guards
// against one being split apart.
Expect(cue).To(ContainSubstring(`annotations["example.com/owner"]: v.value`))
Expect(cue).NotTo(ContainSubstring(`example: com/owner`))
})
It("ArrayElement.SetIf preserves dots inside bracketed keys", func() {
enabled := defkit.Bool("enabled").Optional()
items := defkit.Array("items").WithFields(
defkit.String("value"),
).Optional()
c := defkit.NewComponent("annotated").
Workload("v1", "ConfigMap").
Params(enabled, items).
Template(func(tpl *defkit.Template) {
const path = `labels["app.kubernetes.io/name"]`
arr := defkit.NewArray().
Item(defkit.NewArrayElement().
SetIf(enabled.IsSet(), path, defkit.Lit("static"))).
ForEach(items, defkit.NewArrayElement().
SetIf(enabled.IsSet(), path, defkit.Reference("m.value")))
tpl.Output(defkit.NewResource("v1", "ConfigMap").
Set("spec.items", arr))
})
cue := c.ToCue()
Expect(strings.Count(cue, `labels["app.kubernetes.io/name"]:`)).To(Equal(2))
Expect(cue).NotTo(ContainSubstring(`labels["app: kubernetes: io/name"]:`))
})
})
Describe("From().Filter().Map().Dedupe() pipeline", func() {
@@ -19,6 +19,7 @@ package defkit_test
import (
"testing"
"cuelang.org/go/cue/parser"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
@@ -27,3 +28,11 @@ func TestDefkit(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Defkit Suite")
}
// parseCUE parses generated CUE and returns any syntax error. Substring
// assertions cannot tell a well-formed definition from one that only breaks
// once a parser reaches it.
func parseCUE(src string) error {
_, err := parser.ParseFile("generated.cue", src)
return err
}