Feat: defkit comp def discrepancies (#7048)

* Feat: add MapVariant operation and support for OneOf parameters with default variant

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance string parameter output to include optional prefix in CUE generation

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: add ConditionalOrFieldRef for fallback handling and support inline array values

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: update CUE generation to support inline arrays with conditional wrapping

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: update CUE generation to support inline arrays with conditional wrapping

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: add support for compound optional fields and enhance array builder with guarded filtering

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: add comprehensive tests for ArrayBuilder functionality

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: implement field grouping in StatusBuilder for consolidated CUE output

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance CUE decomposition to support condValues and improve filtering logic

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: add metadata labels to ComponentDefinition and update CUE generation

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: clean up comments and formatting in cuegen and param files

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance CUE generation, trait definitions, and PatchContainer logic

CUE Generation:
- Simplify condition decomposition logic and rename GetDirective method
- Add condition decomposition and lifting logic to improve generated CUE output
- Refactor cueTypeForParamType to use a standalone cueTypeStr function for reusability

Collections:
- Enhance MapVariant operation to merge variant mappings and preserve non-matching items

Trait Definitions:
- Enhance TraitDefinition and PatchContainerConfig with new attributes (MultiContainerCheckField, MultiContainerErrMsg)
- Update emission logic in TraitCUEGenerator for multi-container support

PatchContainer:
- Update error messages to use camelCase for consistency with KubeVela conventions

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance test descriptions for clarity and accuracy in array_builder, collections, and status tests

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance handling of optional fields in collections and improve test descriptions for clarity

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: improve ConditionalOrFieldRef tests for clarity and accuracy in handling primary and fallback fields

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: enhance test descriptions for clarity and accuracy in array_builder and expr tests

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

* Feat: improve formatting of test data in collections tests for better readability

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
This commit is contained in:
Ayush Kumar
2026-03-01 21:29:18 -08:00
committed by GitHub
parent 5369aa666a
commit 3f7ad2ba95
20 changed files with 3541 additions and 266 deletions
+22
View File
@@ -32,6 +32,7 @@ type arrayEntry struct {
cond Condition // for conditional entries
source Value // for forEach entries (iteration source)
guard Condition // for forEach entries (optional guard: if source != _|_)
filter Predicate // for forEach entries (optional filter: if v.field == value)
itemBuilder *ItemBuilder // for forEachWith entries (complex per-item logic)
}
@@ -129,6 +130,27 @@ func (a *ArrayBuilder) ForEachWithVar(varName string, source Value, fn func(item
return a
}
// ForEachWithGuardedFiltered adds a guarded and filtered complex iterated item to the array.
// The guard condition wraps the for loop, and the filter predicate filters iteration items.
// Generates: if guard for v in source if filter { ... }
func (a *ArrayBuilder) ForEachWithGuardedFiltered(guard Condition, filter Predicate, source Value, fn func(item *ItemBuilder)) *ArrayBuilder {
return a.ForEachWithGuardedFilteredVar("v", guard, filter, source, fn)
}
// ForEachWithGuardedFilteredVar is like ForEachWithGuardedFiltered but allows specifying the iteration variable name.
func (a *ArrayBuilder) ForEachWithGuardedFilteredVar(varName string, guard Condition, filter Predicate, source Value, fn func(item *ItemBuilder)) *ArrayBuilder {
ib := &ItemBuilder{varName: varName, ops: make([]itemOp, 0)}
fn(ib)
a.entries = append(a.entries, arrayEntry{
kind: entryForEachWith,
source: source,
guard: guard,
filter: filter,
itemBuilder: ib,
})
return a
}
// --- ItemBuilder ---
// itemOp is a single operation recorded by the ItemBuilder.
+744
View File
@@ -0,0 +1,744 @@
/*
Copyright 2025 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package defkit_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
var _ = Describe("ArrayBuilder", func() {
Describe("NewArray", func() {
It("should create an empty array builder", func() {
ab := defkit.NewArray()
Expect(ab).NotTo(BeNil())
Expect(ab.Entries()).To(BeEmpty())
})
It("should implement the Value interface and retain builder behavior", func() {
ab := defkit.NewArray()
var v defkit.Value = ab // compile-time interface check
Expect(v).NotTo(BeNil())
// Verify the builder still works through the interface
ab.Item(defkit.NewArrayElement().Set("name", defkit.Lit("test")))
Expect(ab.Entries()).To(HaveLen(1))
})
})
Describe("Item", func() {
It("should add a static entry", func() {
elem := defkit.NewArrayElement().Set("name", defkit.Lit("cpu"))
ab := defkit.NewArray().Item(elem)
Expect(ab.Entries()).To(HaveLen(1))
})
It("should chain multiple static items", func() {
elem1 := defkit.NewArrayElement().Set("name", defkit.Lit("cpu"))
elem2 := defkit.NewArrayElement().Set("name", defkit.Lit("memory"))
ab := defkit.NewArray().Item(elem1).Item(elem2)
Expect(ab.Entries()).To(HaveLen(2))
})
})
Describe("ItemIf", func() {
It("should add a conditional entry", func() {
mem := defkit.String("memory")
elem := defkit.NewArrayElement().Set("name", defkit.Lit("memory"))
ab := defkit.NewArray().ItemIf(mem.IsSet(), elem)
Expect(ab.Entries()).To(HaveLen(1))
})
})
Describe("ForEach", func() {
It("should add a forEach entry", func() {
ports := defkit.List("ports")
elem := defkit.NewArrayElement().Set("port", defkit.Reference("m.port"))
ab := defkit.NewArray().ForEach(ports, elem)
Expect(ab.Entries()).To(HaveLen(1))
})
})
Describe("ForEachGuarded", func() {
It("should add a guarded forEach entry", func() {
ports := defkit.List("ports")
elem := defkit.NewArrayElement().Set("port", defkit.Reference("m.port"))
ab := defkit.NewArray().ForEachGuarded(ports.IsSet(), ports, elem)
Expect(ab.Entries()).To(HaveLen(1))
})
})
Describe("ForEachWith", func() {
It("should add a forEachWith entry with ItemBuilder", func() {
ports := defkit.List("ports")
ab := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("port", v.Field("port"))
})
entries := ab.Entries()
Expect(entries).To(HaveLen(1))
})
It("should use 'v' as default variable name", func() {
ports := defkit.List("ports")
ab := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
Expect(item.VarName()).To(Equal("v"))
})
Expect(ab.Entries()).To(HaveLen(1))
})
})
Describe("ForEachWithVar", func() {
It("should use custom variable name", func() {
ports := defkit.List("ports")
ab := defkit.NewArray().ForEachWithVar("p", ports, func(item *defkit.ItemBuilder) {
Expect(item.VarName()).To(Equal("p"))
v := item.Var()
item.Set("port", v.Field("port"))
})
Expect(ab.Entries()).To(HaveLen(1))
})
})
Describe("ForEachWithGuardedFiltered", func() {
It("should add entry with guard and filter", func() {
ports := defkit.List("ports")
ab := defkit.NewArray().ForEachWithGuardedFiltered(
ports.IsSet(),
defkit.FieldEquals("expose", true),
ports,
func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("port", v.Field("port"))
},
)
Expect(ab.Entries()).To(HaveLen(1))
})
It("should use 'v' as default variable name", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWithGuardedFiltered(
ports.IsSet(),
defkit.FieldEquals("expose", true),
ports,
func(item *defkit.ItemBuilder) {
Expect(item.VarName()).To(Equal("v"))
},
)
})
})
Describe("ForEachWithGuardedFilteredVar", func() {
It("should use custom variable name with guard and filter", func() {
ports := defkit.List("ports")
ab := defkit.NewArray().ForEachWithGuardedFilteredVar(
"p",
ports.IsSet(),
defkit.FieldEquals("expose", true),
ports,
func(item *defkit.ItemBuilder) {
Expect(item.VarName()).To(Equal("p"))
v := item.Var()
item.Set("port", v.Field("port"))
},
)
Expect(ab.Entries()).To(HaveLen(1))
})
})
Describe("mixed entries", func() {
It("should support mixing static, conditional, forEach, and forEachWith entries", func() {
mem := defkit.String("memory")
ports := defkit.List("ports")
metrics := defkit.List("metrics")
staticElem := defkit.NewArrayElement().Set("name", defkit.Lit("cpu"))
condElem := defkit.NewArrayElement().Set("name", defkit.Lit("memory"))
forEachElem := defkit.NewArrayElement().Set("name", defkit.Reference("m.name"))
ab := defkit.NewArray().
Item(staticElem).
ItemIf(mem.IsSet(), condElem).
ForEach(metrics, forEachElem).
ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("port", v.Field("port"))
})
Expect(ab.Entries()).To(HaveLen(4))
})
})
Describe("fluent chaining", func() {
It("should return the same builder for all methods", func() {
ab := defkit.NewArray()
elem := defkit.NewArrayElement().Set("name", defkit.Lit("test"))
ports := defkit.List("ports")
result := ab.Item(elem)
Expect(result).To(BeIdenticalTo(ab))
result = ab.ItemIf(ports.IsSet(), elem)
Expect(result).To(BeIdenticalTo(ab))
result = ab.ForEach(ports, elem)
Expect(result).To(BeIdenticalTo(ab))
result = ab.ForEachGuarded(ports.IsSet(), ports, elem)
Expect(result).To(BeIdenticalTo(ab))
result = ab.ForEachWith(ports, func(item *defkit.ItemBuilder) {})
Expect(result).To(BeIdenticalTo(ab))
result = ab.ForEachWithGuardedFiltered(
ports.IsSet(), defkit.FieldEquals("expose", true), ports,
func(item *defkit.ItemBuilder) {},
)
Expect(result).To(BeIdenticalTo(ab))
})
})
})
var _ = Describe("ItemBuilder", func() {
Describe("Var", func() {
It("should return an IterVarBuilder with the correct variable name", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
Expect(v).NotTo(BeNil())
// v.Field returns an IterFieldRef which implements Value
fieldRef := v.Field("port")
Expect(fieldRef).NotTo(BeNil())
Expect(fieldRef.VarName()).To(Equal("v"))
Expect(fieldRef.FieldName()).To(Equal("port"))
// v.Ref returns an IterVarRef for the whole variable
varRef := v.Ref()
Expect(varRef).NotTo(BeNil())
Expect(varRef.VarName()).To(Equal("v"))
})
})
})
Describe("Set", func() {
It("should record a setOp", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("port", v.Field("port"))
item.Set("name", v.Field("name"))
Expect(item.Ops()).To(HaveLen(2))
})
})
})
Describe("If", func() {
It("should record an ifBlockOp with nested operations", func() {
ports := defkit.List("ports")
exposeType := defkit.String("exposeType")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.If(defkit.Eq(exposeType, defkit.Lit("NodePort")), func() {
item.Set("nodePort", v.Field("nodePort"))
})
Expect(item.Ops()).To(HaveLen(1))
})
})
})
Describe("IfSet", func() {
It("should record a conditional for field existence", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.IfSet("name", func() {
item.Set("name", v.Field("name"))
})
Expect(item.Ops()).To(HaveLen(1))
})
})
})
Describe("IfNotSet", func() {
It("should record a conditional for field non-existence", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
_ = item.Var()
item.IfNotSet("name", func() {
item.Set("name", defkit.Lit("default"))
})
Expect(item.Ops()).To(HaveLen(1))
})
})
})
Describe("IfSet and IfNotSet together", func() {
It("should form an if/else pattern", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.IfSet("containerPort", func() {
item.Set("containerPort", v.Field("containerPort"))
})
item.IfNotSet("containerPort", func() {
item.Set("containerPort", v.Field("port"))
})
Expect(item.Ops()).To(HaveLen(2))
})
})
})
Describe("Let", func() {
It("should record a letOp and return a reference with correct name", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
ref := item.Let("_name",
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
letRef, ok := ref.(*defkit.IterLetRef)
Expect(ok).To(BeTrue(), "expected *IterLetRef")
Expect(letRef.RefName()).To(Equal("_name"))
Expect(item.Ops()).To(HaveLen(1))
})
})
It("should allow using the let reference in subsequent operations", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
nameRef := item.Let("_name",
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
item.SetDefault("name", nameRef, "string")
Expect(item.Ops()).To(HaveLen(2))
})
})
})
Describe("SetDefault", func() {
It("should record a setDefaultOp", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
item.SetDefault("name", defkit.Lit("default"), "string")
Expect(item.Ops()).To(HaveLen(1))
})
})
})
Describe("FieldExists", func() {
It("should return a Condition for field existence with correct var and field name", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
cond := item.FieldExists("name")
Expect(cond).NotTo(BeNil())
iterCond, ok := cond.(*defkit.IterFieldExistsCondition)
Expect(ok).To(BeTrue(), "expected *IterFieldExistsCondition")
Expect(iterCond.VarName()).To(Equal("v"))
Expect(iterCond.FieldName()).To(Equal("name"))
Expect(iterCond.IsNegated()).To(BeFalse())
})
})
})
Describe("FieldNotExists", func() {
It("should return a negated Condition for field non-existence", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
cond := item.FieldNotExists("name")
Expect(cond).NotTo(BeNil())
iterCond, ok := cond.(*defkit.IterFieldExistsCondition)
Expect(ok).To(BeTrue(), "expected *IterFieldExistsCondition")
Expect(iterCond.VarName()).To(Equal("v"))
Expect(iterCond.FieldName()).To(Equal("name"))
Expect(iterCond.IsNegated()).To(BeTrue())
})
})
})
Describe("complex ItemBuilder pattern", func() {
It("should support nested conditionals with let bindings and defaults", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
// Unconditional field
item.Set("port", v.Field("port"))
// If/else for containerPort
item.IfSet("containerPort", func() {
item.Set("containerPort", v.Field("containerPort"))
})
item.IfNotSet("containerPort", func() {
item.Set("containerPort", v.Field("port"))
})
// Nested: name with let binding, default, and protocol suffix
item.IfSet("name", func() {
item.Set("name", v.Field("name"))
})
item.IfNotSet("name", func() {
nameRef := item.Let("_name",
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
item.SetDefault("name", nameRef, "string")
item.If(defkit.Ne(v.Field("protocol"), defkit.Lit("TCP")), func() {
item.Set("name", defkit.Plus(nameRef, defkit.Lit("-"), defkit.StringsToLower(v.Field("protocol"))))
})
})
// set=1, ifSet(containerPort)=1, ifNotSet(containerPort)=1, ifSet(name)=1, ifNotSet(name)=1
Expect(item.Ops()).To(HaveLen(5))
})
})
})
})
var _ = Describe("IterVarBuilder", func() {
It("should return field references with correct variable and field names", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
portRef := v.Field("port")
Expect(portRef.VarName()).To(Equal("v"))
Expect(portRef.FieldName()).To(Equal("port"))
nameRef := v.Field("name")
Expect(nameRef.VarName()).To(Equal("v"))
Expect(nameRef.FieldName()).To(Equal("name"))
})
})
It("should return a whole-variable reference with correct variable name", func() {
items := defkit.StringList("items")
defkit.NewArray().ForEachWith(items, func(item *defkit.ItemBuilder) {
v := item.Var()
ref := v.Ref()
Expect(ref.VarName()).To(Equal("v"))
})
})
It("should use custom variable name for field references", func() {
ports := defkit.List("ports")
defkit.NewArray().ForEachWithVar("p", ports, func(item *defkit.ItemBuilder) {
v := item.Var()
fieldRef := v.Field("port")
Expect(fieldRef.VarName()).To(Equal("p"))
Expect(fieldRef.FieldName()).To(Equal("port"))
ref := v.Ref()
Expect(ref.VarName()).To(Equal("p"))
})
})
})
var _ = Describe("ArrayConcat", func() {
It("should create an array concatenation value", func() {
left := defkit.NewArray()
right := defkit.List("extra")
concat := defkit.ArrayConcat(left, right)
Expect(concat).NotTo(BeNil())
Expect(concat.Left()).To(BeIdenticalTo(left))
Expect(concat.Right()).To(BeIdenticalTo(right))
})
It("should implement the Value interface and preserve operands", func() {
left := defkit.NewArray()
right := defkit.List("extra")
var v defkit.Value = defkit.ArrayConcat(left, right) // compile-time interface check
Expect(v).NotTo(BeNil())
concat := v.(*defkit.ArrayConcatValue)
Expect(concat.Left()).To(BeIdenticalTo(left))
Expect(concat.Right()).To(BeIdenticalTo(right))
})
})
var _ = Describe("ArrayBuilder CUE Generation", func() {
var gen *defkit.CUEGenerator
BeforeEach(func() {
gen = defkit.NewCUEGenerator()
})
It("should generate CUE for ForEachWith with simple field assignments", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.String("name"),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports).
Template(func(tpl *defkit.Template) {
containerPorts := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("containerPort", v.Field("port"))
item.Set("name", v.Field("name"))
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
SetIf(ports.IsSet(), "spec.template.spec.containers[0].ports", containerPorts),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring("for v in parameter.ports"))
Expect(cue).To(ContainSubstring("containerPort: v.port"))
Expect(cue).To(ContainSubstring("name: v.name"))
})
It("should generate CUE for ForEachWith with IfSet/IfNotSet conditionals", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.Int("containerPort"),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports).
Template(func(tpl *defkit.Template) {
containerPorts := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.IfSet("containerPort", func() {
item.Set("containerPort", v.Field("containerPort"))
})
item.IfNotSet("containerPort", func() {
item.Set("containerPort", v.Field("port"))
})
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
SetIf(ports.IsSet(), "spec.template.spec.containers[0].ports", containerPorts),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring("if v.containerPort != _|_"))
Expect(cue).To(ContainSubstring("containerPort: v.containerPort"))
Expect(cue).To(ContainSubstring("if v.containerPort == _|_"))
Expect(cue).To(ContainSubstring("containerPort: v.port"))
})
It("should generate CUE for ForEachWith with let bindings and defaults", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports).
Template(func(tpl *defkit.Template) {
containerPorts := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
nameRef := item.Let("_name",
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
item.SetDefault("name", nameRef, "string")
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
SetIf(ports.IsSet(), "spec.template.spec.containers[0].ports", containerPorts),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring(`_name: "port-" + strconv.FormatInt(v.port, 10)`))
Expect(cue).To(ContainSubstring("name: *_name | string"))
})
It("should generate CUE for ForEachWithVar with custom variable name", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports).
Template(func(tpl *defkit.Template) {
containerPorts := defkit.NewArray().ForEachWithVar("p", ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("containerPort", v.Field("port"))
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
SetIf(ports.IsSet(), "spec.template.spec.containers[0].ports", containerPorts),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring("for p in parameter.ports"))
Expect(cue).To(ContainSubstring("containerPort: p.port"))
})
It("should generate CUE for ForEachWithGuardedFiltered with guard and filter", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.Bool("expose").Default(false),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports).
Template(func(tpl *defkit.Template) {
exposePorts := defkit.NewArray().ForEachWithGuardedFiltered(
ports.IsSet(),
defkit.FieldEquals("expose", true),
ports,
func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("port", v.Field("port"))
},
)
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.ports", exposePorts),
)
})
cue := gen.GenerateFullDefinition(comp)
// Guard condition
Expect(cue).To(ContainSubstring(`parameter["ports"] != _|_`))
// Filter predicate
Expect(cue).To(ContainSubstring("v.expose == true"))
// Field assignment
Expect(cue).To(ContainSubstring("port: v.port"))
})
It("should generate CUE for ForEachWith with nested If conditions", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.String("protocol"),
)
exposeType := defkit.String("exposeType")
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports, exposeType).
Template(func(tpl *defkit.Template) {
result := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("port", v.Field("port"))
item.If(defkit.Ne(v.Field("protocol"), defkit.Lit("TCP")), func() {
item.Set("protocol", v.Field("protocol"))
})
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
SetIf(ports.IsSet(), "spec.ports", result),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring("port: v.port"))
Expect(cue).To(ContainSubstring(`v.protocol != "TCP"`))
Expect(cue).To(ContainSubstring("protocol: v.protocol"))
})
It("should auto-detect strconv import from ForEachWith ItemBuilder ops", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports).
Template(func(tpl *defkit.Template) {
result := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.Let("_name",
defkit.Plus(defkit.Lit("port-"), defkit.StrconvFormatInt(v.Field("port"), 10)))
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.ports", result),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring(`"strconv"`))
})
It("should auto-detect strings import from ForEachWith ItemBuilder ops", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.String("protocol"),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
WithImports().
Params(ports).
Template(func(tpl *defkit.Template) {
result := defkit.NewArray().ForEachWith(ports, func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("name", defkit.StringsToLower(v.Field("protocol")))
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.ports", result),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring(`"strings"`))
})
It("should generate CUE for helper backed by FromArray with ArrayBuilder", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.Bool("expose").Default(false),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports).
Template(func(tpl *defkit.Template) {
exposePortsArray := defkit.NewArray().ForEachWithGuardedFiltered(
ports.IsSet(),
defkit.FieldEquals("expose", true),
ports,
func(item *defkit.ItemBuilder) {
v := item.Var()
item.Set("port", v.Field("port"))
},
)
exposePorts := tpl.Helper("exposePorts").
FromArray(exposePortsArray).
AfterOutput().
Build()
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", defkit.VelaCtx().Name()),
)
tpl.OutputsIf(exposePorts.NotEmpty(), "svc",
defkit.NewResource("v1", "Service").
Set("spec.ports", exposePorts),
)
})
cue := gen.GenerateFullDefinition(comp)
// Helper should be named exposePorts
Expect(cue).To(ContainSubstring("exposePorts:"))
// Guard + filter + iteration
Expect(cue).To(ContainSubstring(`parameter["ports"] != _|_`))
Expect(cue).To(ContainSubstring("v.expose == true"))
Expect(cue).To(ContainSubstring("port: v.port"))
// Outputs should reference exposePorts
Expect(cue).To(ContainSubstring("len(exposePorts) != 0"))
})
})
+114 -1
View File
@@ -90,6 +90,19 @@ func (c *CollectionOp) Map(mappings FieldMap) *CollectionOp {
return c
}
// MapVariant adds a conditional field mapping that only applies when the iteration
// variable's discriminator field equals the given variant name.
// Generates: if v.discriminator == "variantName" { ...mappings... }
// Usage: .MapVariant("type", "pvc", FieldMap{"persistentVolumeClaim.claimName": FieldRef("claimName")})
func (c *CollectionOp) MapVariant(discriminator, variantName string, mappings FieldMap) *CollectionOp {
c.ops = append(c.ops, &mapVariantOp{
discriminator: discriminator,
variantName: variantName,
mappings: mappings,
})
return c
}
// Pick selects only the specified fields from each item.
// Usage: .Pick("name", "mountPath")
func (c *CollectionOp) Pick(fields ...string) *CollectionOp {
@@ -236,10 +249,36 @@ func (f FieldRef) resolve(item map[string]any) any {
}
// Or provides a fallback if the field is nil or empty.
// Generates CUE like: *v.field | fallback
func (f FieldRef) Or(fallback FieldValue) *OrFieldRef {
return &OrFieldRef{primary: f, fallback: fallback}
}
// OrConditional provides a fallback using if/else blocks instead of default syntax.
// Generates CUE like:
//
// if v.field != _|_ { name: v.field }
// if v.field == _|_ { name: fallbackExpr }
func (f FieldRef) OrConditional(fallback FieldValue) *ConditionalOrFieldRef {
return &ConditionalOrFieldRef{primary: f, fallback: fallback}
}
// ConditionalOrFieldRef represents a field reference with a conditional fallback.
// Instead of generating CUE default syntax (*v.field | fallback), it generates
// two if/else blocks for the field.
type ConditionalOrFieldRef struct {
primary FieldRef
fallback FieldValue
}
func (c *ConditionalOrFieldRef) resolve(item map[string]any) any {
val := item[string(c.primary)]
if val == nil || val == "" {
return c.fallback.resolve(item)
}
return val
}
// OrFieldRef represents a field reference with a fallback value.
type OrFieldRef struct {
primary FieldRef
@@ -368,7 +407,18 @@ func (m *mapOp) apply(items []any) []any {
if itemMap, ok := item.(map[string]any); ok {
newItem := make(map[string]any)
for newKey, fieldVal := range m.mappings {
newItem[newKey] = fieldVal.resolve(itemMap)
resolved := fieldVal.resolve(itemMap)
// Skip nil values from optional field types — they should be
// omitted from the output entirely, not included as nil.
if resolved == nil {
if _, isOpt := fieldVal.(*OptionalField); isOpt {
continue
}
if _, isCompOpt := fieldVal.(*CompoundOptionalField); isCompOpt {
continue
}
}
newItem[newKey] = resolved
}
result = append(result, newItem)
}
@@ -376,6 +426,47 @@ func (m *mapOp) apply(items []any) []any {
return result
}
// mapVariantOp represents a conditional map operation based on a discriminator field value.
// When the iteration variable's discriminator field equals the variant name,
// the variant's field mappings are included.
// Generates: if v.discriminator == "variantName" { ...mappings... }
type mapVariantOp struct {
discriminator string
variantName string
mappings FieldMap
}
func (m *mapVariantOp) apply(items []any) []any {
// Runtime apply: merge variant mappings into matching items, pass others through.
// This mirrors CUE semantics where each variant condition is evaluated for every
// item in the loop body — non-matching items are kept so later MapVariant ops
// can process them.
result := make([]any, 0, len(items))
for _, item := range items {
itemMap, ok := item.(map[string]any)
if !ok {
result = append(result, item)
continue
}
disc, exists := itemMap[m.discriminator]
if !exists || fmt.Sprintf("%v", disc) != m.variantName {
// Non-matching: pass through unchanged
result = append(result, item)
continue
}
// Matching: copy existing fields and merge variant mappings
newItem := make(map[string]any, len(itemMap)+len(m.mappings))
for k, v := range itemMap {
newItem[k] = v
}
for newKey, fieldVal := range m.mappings {
newItem[newKey] = fieldVal.resolve(itemMap)
}
result = append(result, newItem)
}
return result
}
type pickOp struct {
fields []string
}
@@ -698,6 +789,28 @@ func OptionalFieldRef(field string) *OptionalField {
return &OptionalField{field: field}
}
// CompoundOptionalField includes a field value only if the field exists AND an additional condition is met.
// Generates CUE: if v.field != _|_ if additionalCond { fieldName: v.field }
type CompoundOptionalField struct {
field string
additionalCond Condition
}
func (o *CompoundOptionalField) resolve(item map[string]any) any {
val, exists := item[o.field]
if !exists || val == nil {
return nil
}
return val
}
// OptionalFieldWithCond creates a field reference that includes the field only when
// both the field exists and the additional condition is satisfied.
// Generates CUE: if v.field != _|_ if cond { fieldName: v.field }
func OptionalFieldWithCond(field string, cond Condition) *CompoundOptionalField {
return &CompoundOptionalField{field: field, additionalCond: cond}
}
// NestedFieldMap creates a nested object from field mappings.
// This is an alias for Nested, providing a clearer name for struct array helpers.
// Usage: defkit.NestedFieldMap(defkit.FieldMap{"claimName": defkit.FieldRef("claimName")})
+244 -18
View File
@@ -81,6 +81,19 @@ var _ = Describe("Collections", func() {
Expect(col.Operations()).To(HaveLen(1))
})
It("should chain MapVariant operation", func() {
volumes := defkit.List("volumes")
col := defkit.Each(volumes).
Map(defkit.FieldMap{"name": defkit.FieldRef("name")}).
MapVariant("type", "pvc", defkit.FieldMap{
"persistentVolumeClaim.claimName": defkit.FieldRef("claimName"),
}).
MapVariant("type", "emptyDir", defkit.FieldMap{
"emptyDir.medium": defkit.FieldRef("medium"),
})
Expect(col.Operations()).To(HaveLen(3))
})
It("should chain Rename operation", func() {
ports := defkit.List("ports")
col := defkit.Each(ports).Rename("port", "containerPort")
@@ -101,42 +114,85 @@ var _ = Describe("Collections", func() {
})
Context("FieldRef", func() {
It("should resolve field from item", func() {
It("should store field name and resolve correctly from item", func() {
ref := defkit.FieldRef("port")
Expect(ref).NotTo(BeNil())
Expect(string(ref)).To(Equal("port"))
// Verify it resolves correctly through a collection
ports := defkit.List("ports")
col := defkit.Each(ports).Map(defkit.FieldMap{"p": ref})
items := []any{map[string]any{"port": 8080}}
results := col.Collect(items)
Expect(results).To(HaveLen(1))
Expect(results[0]["p"]).To(Equal(8080))
})
It("should support Or fallback", func() {
ref := defkit.FieldRef("name").Or(defkit.Format("port-%v", defkit.FieldRef("port")))
Expect(ref).NotTo(BeNil())
It("should support Or fallback and use fallback when primary is nil", func() {
ports := defkit.List("ports")
col := defkit.Each(ports).Map(defkit.FieldMap{
"display": defkit.FieldRef("name").Or(defkit.Format("port-%v", defkit.FieldRef("port"))),
})
// Primary field present — should use primary
items := []any{map[string]any{"port": 80, "name": "http"}}
results := col.Collect(items)
Expect(results[0]["display"]).To(Equal("http"))
// Primary field missing — should use fallback
items = []any{map[string]any{"port": 443}}
results = col.Collect(items)
Expect(results[0]["display"]).To(Equal("port-443"))
})
})
Context("FieldEquals", func() {
It("should create equality predicate", func() {
It("should filter items matching the field equality predicate", func() {
pred := defkit.FieldEquals("expose", true)
Expect(pred).NotTo(BeNil())
ports := defkit.List("ports")
col := defkit.Each(ports).Filter(pred)
items := []any{
map[string]any{"port": 80, "expose": true},
map[string]any{"port": 443, "expose": false},
}
results := col.Collect(items)
Expect(results).To(HaveLen(1))
Expect(results[0]["port"]).To(Equal(80))
})
})
Context("FieldExists", func() {
It("should create existence predicate", func() {
It("should filter items where field is present", func() {
pred := defkit.FieldExists("items")
Expect(pred).NotTo(BeNil())
ports := defkit.List("data")
col := defkit.Each(ports).Filter(pred)
items := []any{
map[string]any{"name": "a", "items": []string{"x"}},
map[string]any{"name": "b"},
}
results := col.Collect(items)
Expect(results).To(HaveLen(1))
Expect(results[0]["name"]).To(Equal("a"))
})
})
Context("Format", func() {
It("should create format field value", func() {
It("should create format field value with required imports", func() {
f := defkit.Format("port-%v", defkit.FieldRef("port"))
Expect(f).NotTo(BeNil())
Expect(f.RequiredImports()).To(ContainElement("strconv"))
})
})
Context("LitField", func() {
It("should create literal field value", func() {
It("should resolve literal field value from item", func() {
lit := defkit.LitField("TCP")
Expect(lit).NotTo(BeNil())
// LitField should resolve to the literal value regardless of item content
ports := defkit.List("ports")
col := defkit.Each(ports).Map(defkit.FieldMap{"protocol": lit})
items := []any{map[string]any{"port": 80}}
results := col.Collect(items)
Expect(results).To(HaveLen(1))
Expect(results[0]["protocol"]).To(Equal("TCP"))
})
})
@@ -181,32 +237,98 @@ var _ = Describe("Collections", func() {
})
Context("Nested", func() {
It("should create nested field mapping", func() {
It("should resolve nested field mapping from item", func() {
nested := defkit.Nested(defkit.FieldMap{
"claimName": defkit.FieldRef("claimName"),
})
Expect(nested).NotTo(BeNil())
// Verify nested resolves correctly through a collection
vols := defkit.List("volumes")
col := defkit.Each(vols).Map(defkit.FieldMap{
"pvc": nested,
})
items := []any{map[string]any{"claimName": "my-pvc"}}
results := col.Collect(items)
Expect(results).To(HaveLen(1))
Expect(results[0]["pvc"]).To(Equal(map[string]any{"claimName": "my-pvc"}))
})
})
Context("Optional and OptionalFieldRef", func() {
It("should create optional field reference", func() {
opt := defkit.Optional("items")
Expect(opt).NotTo(BeNil())
It("should include optional field when present and omit when absent", func() {
vols := defkit.List("volumes")
col := defkit.Each(vols).Map(defkit.FieldMap{
"name": defkit.FieldRef("name"),
"items": defkit.Optional("items"),
})
items := []any{
map[string]any{"name": "vol1", "items": []string{"a"}},
map[string]any{"name": "vol2"},
}
results := col.Collect(items)
Expect(results).To(HaveLen(2))
Expect(results[0]).To(HaveKey("items"))
Expect(results[1]).NotTo(HaveKey("items"))
})
It("should create optional field reference via OptionalFieldRef alias", func() {
opt := defkit.OptionalFieldRef("subPath")
Expect(opt).NotTo(BeNil())
vols := defkit.List("volumes")
col := defkit.Each(vols).Map(defkit.FieldMap{
"name": defkit.FieldRef("name"),
"subPath": defkit.OptionalFieldRef("subPath"),
})
items := []any{
map[string]any{"name": "vol1", "subPath": "/data"},
map[string]any{"name": "vol2"},
}
results := col.Collect(items)
Expect(results).To(HaveLen(2))
Expect(results[0]["subPath"]).To(Equal("/data"))
Expect(results[1]).NotTo(HaveKey("subPath"))
})
})
Context("CompoundOptionalField", func() {
It("should create compound optional field with OptionalFieldWithCond", func() {
cond := defkit.Eq(defkit.String("exposeType"), defkit.Lit("NodePort"))
compOpt := defkit.OptionalFieldWithCond("nodePort", cond)
Expect(compOpt).NotTo(BeNil())
// Verify it can be used in a FieldMap and the CUE generation picks it up
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.Int("nodePort"),
)
col := defkit.Each(ports).Map(defkit.FieldMap{
"port": defkit.FieldRef("port"),
"nodePort": compOpt,
})
Expect(col.Operations()).To(HaveLen(1))
})
It("should be usable as a FieldValue in FieldMap alongside regular fields", func() {
cond := defkit.Eq(defkit.String("exposeType"), defkit.Lit("NodePort"))
fm := defkit.FieldMap{
"port": defkit.FieldRef("port"),
"nodePort": defkit.OptionalFieldWithCond("nodePort", cond),
}
Expect(fm).To(HaveLen(2))
Expect(fm).To(HaveKey("port"))
Expect(fm).To(HaveKey("nodePort"))
})
})
Context("NestedFieldMap", func() {
It("should create nested field mapping (alias for Nested)", func() {
It("should resolve nested field mapping identically to Nested", func() {
nested := defkit.NestedFieldMap(defkit.FieldMap{
"claimName": defkit.FieldRef("claimName"),
})
Expect(nested).NotTo(BeNil())
vols := defkit.List("volumes")
col := defkit.Each(vols).Map(defkit.FieldMap{"pvc": nested})
items := []any{map[string]any{"claimName": "my-pvc"}}
results := col.Collect(items)
Expect(results).To(HaveLen(1))
Expect(results[0]["pvc"]).To(Equal(map[string]any{"claimName": "my-pvc"}))
})
})
@@ -817,6 +939,65 @@ var _ = Describe("Collections", func() {
})
})
Context("MapVariant chained operation behavior", func() {
It("should apply chained variant mappings to matching items and pass others through", func() {
volumes := defkit.List("volumes")
col := defkit.Each(volumes).
MapVariant("type", "pvc", defkit.FieldMap{
"pvcClaim": defkit.FieldRef("claimName"),
}).
MapVariant("type", "configMap", defkit.FieldMap{
"cmRef": defkit.FieldRef("cmName"),
}).
MapVariant("type", "emptyDir", defkit.FieldMap{
"medium": defkit.FieldRef("medium"),
})
items := []any{
map[string]any{"name": "data", "type": "pvc", "claimName": "data-pvc"},
map[string]any{"name": "config", "type": "configMap", "cmName": "app-config"},
map[string]any{"name": "cache", "type": "emptyDir", "medium": "Memory"},
}
results := col.Collect(items)
Expect(results).To(HaveLen(3))
// PVC item should have variant field merged, original fields preserved
Expect(results[0]["name"]).To(Equal("data"))
Expect(results[0]["pvcClaim"]).To(Equal("data-pvc"))
Expect(results[0]).NotTo(HaveKey("cmRef"))
// ConfigMap item should have variant field merged
Expect(results[1]["name"]).To(Equal("config"))
Expect(results[1]["cmRef"]).To(Equal("app-config"))
Expect(results[1]).NotTo(HaveKey("pvcClaim"))
// EmptyDir item should have variant field merged
Expect(results[2]["name"]).To(Equal("cache"))
Expect(results[2]["medium"]).To(Equal("Memory"))
})
It("should preserve items with no matching variant", func() {
volumes := defkit.List("volumes")
col := defkit.Each(volumes).
MapVariant("type", "pvc", defkit.FieldMap{
"pvcClaim": defkit.FieldRef("claimName"),
})
items := []any{
map[string]any{"name": "data", "type": "pvc", "claimName": "data-pvc"},
map[string]any{"name": "config", "type": "configMap", "cmName": "app-config"},
}
results := col.Collect(items)
Expect(results).To(HaveLen(2))
Expect(results[0]["pvcClaim"]).To(Equal("data-pvc"))
// Non-matching item passes through unchanged
Expect(results[1]["name"]).To(Equal("config"))
Expect(results[1]["type"]).To(Equal("configMap"))
})
})
Context("Flatten operation behavior", func() {
It("should flatten nested arrays", func() {
volumes := defkit.List("volumes")
@@ -877,6 +1058,51 @@ var _ = Describe("Collections", func() {
})
})
Context("OrConditional (ConditionalOrFieldRef)", func() {
It("should create ConditionalOrFieldRef that resolves primary field", func() {
ref := defkit.FieldRef("name").OrConditional(defkit.Format("port-%v", defkit.FieldRef("port")))
col := defkit.Each(defkit.List("items")).Map(defkit.FieldMap{"result": ref})
results := col.Collect([]any{map[string]any{"name": "web", "port": 80}})
Expect(results).To(HaveLen(1))
Expect(results[0]["result"]).To(Equal("web"))
})
It("should resolve primary vs fallback across multiple items with full structure", func() {
ports := defkit.List("ports")
col := defkit.Each(ports).Map(defkit.FieldMap{
"displayName": defkit.FieldRef("name").OrConditional(defkit.Format("port-%v", defkit.FieldRef("port"))),
"portNumber": defkit.FieldRef("port"),
"protocol": defkit.FieldRef("proto").OrConditional(defkit.LitField("TCP")),
})
items := []any{
map[string]any{"port": 80, "name": "http", "proto": "HTTP"}, // all primary fields present
map[string]any{"port": 443}, // name and proto nil → fallback
map[string]any{"port": 8080, "name": "", "proto": ""}, // name and proto empty → fallback
map[string]any{"port": 9090, "name": "grpc"}, // name present, proto nil → mixed
}
results := col.Collect(items)
Expect(results).To(HaveLen(4))
Expect(results[0]["displayName"]).To(Equal("http"))
Expect(results[0]["portNumber"]).To(Equal(80))
Expect(results[0]["protocol"]).To(Equal("HTTP"))
Expect(results[1]["displayName"]).To(Equal("port-443"))
Expect(results[1]["portNumber"]).To(Equal(443))
Expect(results[1]["protocol"]).To(Equal("TCP"))
Expect(results[2]["displayName"]).To(Equal("port-8080"))
Expect(results[2]["portNumber"]).To(Equal(8080))
Expect(results[2]["protocol"]).To(Equal("TCP"))
Expect(results[3]["displayName"]).To(Equal("grpc"))
Expect(results[3]["portNumber"]).To(Equal(9090))
Expect(results[3]["protocol"]).To(Equal("TCP"))
})
})
Context("FormatField RequiredImports", func() {
It("should require strconv for numeric formatting", func() {
f := defkit.Format("port-%v", defkit.FieldRef("port"))
+11
View File
@@ -29,6 +29,7 @@ import (
type ComponentDefinition struct {
baseDefinition // embedded common fields (name, description, params, template, etc.)
workload WorkloadType
labels map[string]string // metadata labels for the component definition
}
// WorkloadType represents the workload type for a component.
@@ -140,6 +141,16 @@ func (c *ComponentDefinition) Helper(name string, param Param) *ComponentDefinit
return c
}
// Labels sets metadata labels on the component definition.
// Usage: component.Labels(map[string]string{"ui-hidden": "true"})
func (c *ComponentDefinition) Labels(labels map[string]string) *ComponentDefinition {
c.labels = labels
return c
}
// GetLabels returns the component's metadata labels.
func (c *ComponentDefinition) GetLabels() map[string]string { return c.labels }
// RawCUE sets raw CUE for complex component definitions that don't fit the builder pattern.
// When set, this bypasses all other template settings and outputs the raw CUE directly.
func (c *ComponentDefinition) RawCUE(cue string) *ComponentDefinition {
+497 -64
View File
@@ -18,9 +18,13 @@ package defkit
import (
"fmt"
"sort"
"strings"
)
// cueOpenStruct is the CUE literal for an open struct type.
const cueOpenStruct = "{...}"
// cueLabel quotes a CUE field label if it contains characters that are not
// valid in a CUE identifier (letters, digits, underscore, $).
func cueLabel(name string) string {
@@ -172,6 +176,16 @@ func (g *CUEGenerator) collectImportsFromValue(v interface{}) {
g.collectImportsFromFieldValue(fv)
}
}
case *ArrayBuilder:
for _, entry := range val.Entries() {
if entry.itemBuilder != nil {
g.collectImportsFromItemOps(entry.itemBuilder.Ops())
}
}
case *PlusExpr:
for _, part := range val.Parts() {
g.collectImportsFromValue(part)
}
}
}
@@ -192,6 +206,8 @@ func (g *CUEGenerator) collectImportsFromFieldValue(fv FieldValue) {
switch val := fv.(type) {
case *OrFieldRef:
g.collectImportsFromFieldValue(val.fallback)
case *ConditionalOrFieldRef:
g.collectImportsFromFieldValue(val.fallback)
case *NestedField:
for _, nested := range val.mapping {
g.collectImportsFromFieldValue(nested)
@@ -199,6 +215,22 @@ func (g *CUEGenerator) collectImportsFromFieldValue(fv FieldValue) {
}
}
// collectImportsFromItemOps recursively scans ItemBuilder operations for import requirements.
func (g *CUEGenerator) collectImportsFromItemOps(ops []itemOp) {
for _, op := range ops {
switch o := op.(type) {
case setOp:
g.collectImportsFromValue(o.value)
case letOp:
g.collectImportsFromValue(o.value)
case setDefaultOp:
g.collectImportsFromValue(o.defValue)
case ifBlockOp:
g.collectImportsFromItemOps(o.body)
}
}
}
// collectImportsFromResource checks all operations in a resource for import requirements.
func (g *CUEGenerator) collectImportsFromResource(res *Resource) {
if res == nil {
@@ -260,7 +292,15 @@ func (g *CUEGenerator) GenerateFullDefinition(c *ComponentDefinition) string {
sb.WriteString(fmt.Sprintf("%s: {\n", cueLabel(c.GetName())))
sb.WriteString(fmt.Sprintf("%stype: \"component\"\n", g.indent))
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
if len(c.GetLabels()) > 0 {
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
for k, v := range c.GetLabels() {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
} else {
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
}
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, c.GetDescription()))
// Write attributes
@@ -674,6 +714,8 @@ func (g *CUEGenerator) writeHelper(sb *strings.Builder, helper *HelperVar, depth
g.writeMultiSourceHelper(sb, col, depth)
case *CollectionOp:
g.writeCollectionOpHelper(sb, col, depth, guard)
case *ArrayBuilder:
sb.WriteString(g.arrayBuilderToCUE(col, depth))
default:
sb.WriteString("[]")
}
@@ -787,8 +829,29 @@ func (g *CUEGenerator) writeCollectionOpHelper(sb *strings.Builder, col *Collect
}
for fieldName, fieldVal := range mOp.mappings {
valStr := g.fieldValueToCUE(fieldVal)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", fieldIndent, fieldName, valStr))
if condRef, isConditional := fieldVal.(*ConditionalOrFieldRef); isConditional {
// Emit if/else pattern for conditional field reference
primaryField := string(condRef.primary)
fallbackStr := g.fieldValueToCUE(condRef.fallback)
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ {\n", fieldIndent, primaryField))
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", fieldIndent, fieldName, primaryField))
sb.WriteString(fmt.Sprintf("%s}\n", fieldIndent))
sb.WriteString(fmt.Sprintf("%sif v.%s == _|_ {\n", fieldIndent, primaryField))
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", fieldIndent, fieldName, fallbackStr))
sb.WriteString(fmt.Sprintf("%s}\n", fieldIndent))
} else if optField, isOptional := fieldVal.(*OptionalField); isOptional {
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ {\n", fieldIndent, optField.field))
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", fieldIndent, fieldName, optField.field))
sb.WriteString(fmt.Sprintf("%s}\n", fieldIndent))
} else if compOpt, isCompound := fieldVal.(*CompoundOptionalField); isCompound {
condStr := g.conditionToCUE(compOpt.additionalCond)
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ if %s {\n", fieldIndent, compOpt.field, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", fieldIndent, fieldName, compOpt.field))
sb.WriteString(fmt.Sprintf("%s}\n", fieldIndent))
} else {
valStr := g.fieldValueToCUE(fieldVal)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", fieldIndent, fieldName, valStr))
}
}
sb.WriteString(fmt.Sprintf("%s},\n%s]", innerIndent, strings.Repeat(g.indent, depth)))
@@ -837,6 +900,11 @@ func (g *CUEGenerator) writeFieldMapAsHelper(sb *strings.Builder, mapping FieldM
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ {\n", indent, fieldVal.(*OptionalField).field))
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", indent, fieldName, fieldVal.(*OptionalField).field))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else if compOpt, isCompound := fieldVal.(*CompoundOptionalField); isCompound {
condStr := g.conditionToCUE(compOpt.additionalCond)
sb.WriteString(fmt.Sprintf("%sif v.%s != _|_ if %s {\n", indent, compOpt.field, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: v.%s\n", indent, fieldName, compOpt.field))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, fieldName, valStr))
}
@@ -919,6 +987,15 @@ func (g *CUEGenerator) writeResourceOutput(sb *strings.Builder, name string, res
}
}
// condValueEntry represents an additional conditional value at a field node.
// When the same path is written with multiple different conditions (e.g.,
// volumeMounts set under both "volumeMounts is set" and "volumes is set"),
// the first write uses value/cond and additional writes append here.
type condValueEntry struct {
value Value
cond Condition
}
// fieldNode represents a node in the field tree being built.
type fieldNode struct {
value Value // Direct value (if leaf)
@@ -927,11 +1004,13 @@ type fieldNode struct {
childOrder []string // Track insertion order
isArray bool
arrayIndex int
spreads []spreadEntry // Spread operations at this node level
forEach *ForEachOp // ForEach operation (for trait patches)
patchKey *PatchKeyOp // PatchKey operation (for array patches with merge key)
spreadAll *SpreadAllOp // SpreadAll operation (for array constraint patches)
patchStrategy string // e.g. "retainKeys" → generates // +patchStrategy=retainKeys
spreads []spreadEntry // Spread operations at this node level
forEach *ForEachOp // ForEach operation (for trait patches)
patchKey *PatchKeyOp // PatchKey operation (for array patches with merge key)
spreadAll *SpreadAllOp // SpreadAll operation (for array constraint patches)
patchStrategy string // e.g. "retainKeys" → generates // +patchStrategy=retainKeys
directives []string // e.g. ["patchKey=ip"] → generates // +patchKey=ip
condValues []condValueEntry // additional conditional values at same path
}
// spreadEntry represents a conditional spread operation.
@@ -970,6 +1049,8 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode {
g.insertSpreadAllIntoTree(root, o, nil)
case *PatchStrategyAnnotationOp:
g.insertAnnotationIntoTree(root, o.Path(), o.Strategy())
case *DirectiveOp:
g.insertDirectiveIntoTree(root, o.Path(), o.GetDirective())
case *IfBlock:
// For if blocks, process inner ops with the block's condition
for _, innerOp := range o.Ops() {
@@ -995,6 +1076,8 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode {
g.insertSpreadAllIntoTree(root, inner, o.Cond())
case *PatchStrategyAnnotationOp:
g.insertAnnotationIntoTree(root, inner.Path(), inner.Strategy())
case *DirectiveOp:
g.insertDirectiveIntoTree(root, inner.Path(), inner.GetDirective())
}
}
}
@@ -1003,6 +1086,20 @@ func (g *CUEGenerator) buildFieldTree(ops []ResourceOp) *fieldNode {
return root
}
// insertDirectiveIntoTree navigates to a node by path and adds a directive annotation.
func (g *CUEGenerator) insertDirectiveIntoTree(root *fieldNode, path string, directive string) {
parts := splitPath(path)
current := root
for _, part := range parts {
if _, exists := current.children[part]; !exists {
current.children[part] = newFieldNode()
current.childOrder = append(current.childOrder, part)
}
current = current.children[part]
}
current.directives = append(current.directives, directive)
}
// insertAnnotationIntoTree navigates to a node by path and sets its patchStrategy annotation.
func (g *CUEGenerator) insertAnnotationIntoTree(root *fieldNode, path string, strategy string) {
parts := splitPath(path)
@@ -1058,8 +1155,25 @@ func (g *CUEGenerator) insertIntoTree(root *fieldNode, path string, value Value,
// If this is the last part, set the value
if i == len(parts)-1 {
current.value = value
current.cond = cond
if current.value != nil && cond != nil {
// This path already has a value. If the existing value also
// has a condition, append this as an additional conditional
// value instead of overwriting.
if current.cond != nil {
current.condValues = append(current.condValues, condValueEntry{
value: value,
cond: cond,
})
} else {
// Existing value is unconditional; the new conditional
// value takes precedence (shouldn't normally happen).
current.value = value
current.cond = cond
}
} else {
current.value = value
current.cond = cond
}
}
}
}
@@ -1267,10 +1381,15 @@ func (g *CUEGenerator) writeFieldTree(sb *strings.Builder, node *fieldNode, dept
for _, name := range node.childOrder {
child := node.children[name]
if child.cond != nil {
switch {
case len(child.condValues) > 0:
// Multi-conditional nodes render their own if blocks internally,
// so they must be treated as unconditional at the parent level.
unconditional = append(unconditional, name)
case child.cond != nil:
condStr := g.conditionToCUE(child.cond)
conditional[condStr] = append(conditional[condStr], name)
} else {
default:
unconditional = append(unconditional, name)
}
}
@@ -1325,6 +1444,12 @@ func (g *CUEGenerator) liftChildConditions(node *fieldNode) {
canLift = false
break
}
// Don't lift if any grandchild has multiple conditional values,
// since those nodes manage their own condition rendering.
if len(grand.condValues) > 0 {
canLift = false
break
}
condStr := g.conditionToCUE(grand.cond)
if sharedCondStr == "" {
sharedCondStr = condStr
@@ -1346,6 +1471,78 @@ func (g *CUEGenerator) liftChildConditions(node *fieldNode) {
}
}
// tryDecomposeOrLift attempts to decompose a struct node into per-condition
// blocks or lift a shared child condition to the parent. Returns true if the
// node was handled, false if normal rendering should proceed.
func (g *CUEGenerator) tryDecomposeOrLift(sb *strings.Builder, name string, node *fieldNode, indent string, depth int) bool {
if node.value != nil || len(node.children) == 0 || len(node.spreads) > 0 || node.forEach != nil || node.patchKey != nil {
return false
}
// Decompose a struct node into per-condition blocks when every child
// subtree shares the same uniform set of leaf conditions.
condGroups := g.canDecomposeByCondition(node)
if condGroups != nil {
condStrs := make([]string, 0, len(condGroups))
for cs := range condGroups {
condStrs = append(condStrs, cs)
}
sort.Strings(condStrs)
for _, condStr := range condStrs {
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: {\n", indent, name))
for _, childName := range node.childOrder {
child := node.children[childName]
filteredChild := g.filterNodeByCondition(child, condStr)
if filteredChild != nil {
g.writeFieldNode(sb, childName, filteredChild, depth+2)
}
}
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
return true
}
// If all children share the same condition, lift it to avoid empty parent structs.
condStr := ""
canLift := true
for _, childName := range node.childOrder {
child := node.children[childName]
if child.cond == nil {
canLift = false
break
}
childCondStr := g.conditionToCUE(child.cond)
if condStr == "" {
condStr = childCondStr
} else if condStr != childCondStr {
canLift = false
break
}
}
if canLift && condStr != "" {
clone := &fieldNode{
children: make(map[string]*fieldNode, len(node.children)),
childOrder: append([]string(nil), node.childOrder...),
}
for childName, child := range node.children {
childCopy := *child
childCopy.cond = nil
clone.children[childName] = &childCopy
}
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: {\n", indent, name))
g.writeFieldTree(sb, clone, depth+2)
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
return true
}
return false
}
// writeFieldNode writes a single field node as CUE.
func (g *CUEGenerator) writeFieldNode(sb *strings.Builder, name string, node *fieldNode, depth int) {
indent := strings.Repeat(g.indent, depth)
@@ -1376,47 +1573,40 @@ func (g *CUEGenerator) writeFieldNode(sb *strings.Builder, name string, node *fi
return
}
// If all children share the same condition and there are no unconditional parts,
// lift the condition to avoid emitting empty parent structs.
if node.value == nil && len(node.children) > 0 && len(node.spreads) == 0 && node.forEach == nil && node.patchKey == nil {
condStr := ""
canLift := true
for _, childName := range node.childOrder {
child := node.children[childName]
if child.cond == nil {
canLift = false
break
}
childCondStr := g.conditionToCUE(child.cond)
if condStr == "" {
condStr = childCondStr
} else if condStr != childCondStr {
canLift = false
break
}
}
if canLift && condStr != "" {
// Clone node with cleared child conditions for rendering.
clone := &fieldNode{
children: make(map[string]*fieldNode, len(node.children)),
childOrder: append([]string(nil), node.childOrder...),
}
for childName, child := range node.children {
childCopy := *child
childCopy.cond = nil
clone.children[childName] = &childCopy
}
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: {\n", indent, name))
g.writeFieldTree(sb, clone, depth+2)
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
return
}
// Try to decompose or lift conditions for cleaner CUE output.
if g.tryDecomposeOrLift(sb, name, node, indent, depth) {
return
}
// Emit directive annotations before the field
for _, directive := range node.directives {
sb.WriteString(fmt.Sprintf("%s// +%s\n", indent, directive))
}
// Regular field
if node.value != nil && len(node.children) == 0 {
if len(node.condValues) > 0 {
// Multiple conditional values at the same path — render each
// inside its own if block.
if node.cond != nil {
condStr := g.conditionToCUE(node.cond)
valStr := g.valueToCUE(node.value)
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", indent, name, valStr))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
valStr := g.valueToCUE(node.value)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, name, valStr))
}
for _, cv := range node.condValues {
condStr := g.conditionToCUE(cv.cond)
valStr := g.valueToCUE(cv.value)
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", indent, name, valStr))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
return
}
// Leaf node with value
valStr := g.valueToCUE(node.value)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, name, valStr))
@@ -1428,6 +1618,134 @@ func (g *CUEGenerator) writeFieldNode(sb *strings.Builder, name string, node *fi
}
}
// canDecomposeByCondition checks if a struct node's children can be split into
// separate conditional blocks. This is possible when every child subtree has
// the same uniform set of leaf conditions (e.g., every leaf is guarded by either
// condition A or condition B). Returns a map of conditionString -> childNames,
// or nil if decomposition is not possible.
func (g *CUEGenerator) canDecomposeByCondition(node *fieldNode) map[string][]string {
if node.value != nil || len(node.spreads) > 0 || node.forEach != nil || node.patchKey != nil {
return nil
}
if len(node.children) == 0 {
return nil
}
// Collect the set of leaf conditions from each child subtree
childCondSets := make(map[string]map[string]bool)
for _, childName := range node.childOrder {
child := node.children[childName]
condSet := make(map[string]bool)
g.collectLeafConditions(child, condSet)
if len(condSet) == 0 {
return nil
}
// If any leaf is unconditional (empty string), can't decompose
if condSet[""] {
return nil
}
childCondSets[childName] = condSet
}
// All children must have the same condition set
var referenceSet map[string]bool
for _, condSet := range childCondSets {
if referenceSet == nil {
referenceSet = condSet
} else {
if len(condSet) != len(referenceSet) {
return nil
}
for k := range referenceSet {
if !condSet[k] {
return nil
}
}
}
}
// Need at least 2 different conditions to warrant decomposition
if len(referenceSet) < 2 {
return nil
}
result := make(map[string][]string)
for condStr := range referenceSet {
result[condStr] = append(result[condStr], node.childOrder...)
}
return result
}
// collectLeafConditions traverses a subtree and collects the CUE string
// representations of all leaf conditions. An unconditional leaf adds "".
func (g *CUEGenerator) collectLeafConditions(node *fieldNode, condSet map[string]bool) {
if node.cond != nil && node.value != nil {
condSet[g.conditionToCUE(node.cond)] = true
// Also collect conditions from condValues (additional conditional
// values at the same path).
for _, cv := range node.condValues {
condSet[g.conditionToCUE(cv.cond)] = true
}
return
}
if node.cond != nil && len(node.children) > 0 {
// Intermediate node with a condition (already lifted)
condSet[g.conditionToCUE(node.cond)] = true
return
}
if node.value != nil && node.cond == nil {
condSet[""] = true
return
}
for _, childName := range node.childOrder {
child := node.children[childName]
g.collectLeafConditions(child, condSet)
}
}
// filterNodeByCondition returns a copy of the subtree containing only leaves
// that match the given condition string. Intermediate nodes have their
// conditions cleared since the caller wraps the result in the condition block.
func (g *CUEGenerator) filterNodeByCondition(node *fieldNode, condStr string) *fieldNode {
if node.cond != nil {
if g.conditionToCUE(node.cond) == condStr {
nodeCopy := *node
nodeCopy.cond = nil
nodeCopy.condValues = nil // Strip condValues; other conditions handled by their own block
return &nodeCopy
}
// Check if the target condition is in condValues instead of the primary
for _, cv := range node.condValues {
if g.conditionToCUE(cv.cond) == condStr {
nodeCopy := *node
nodeCopy.value = cv.value
nodeCopy.cond = nil
nodeCopy.condValues = nil
return &nodeCopy
}
}
return nil
}
filtered := &fieldNode{
children: make(map[string]*fieldNode),
childOrder: make([]string, 0),
value: node.value,
}
for _, childName := range node.childOrder {
child := node.children[childName]
filteredChild := g.filterNodeByCondition(child, condStr)
if filteredChild != nil {
filtered.children[childName] = filteredChild
filtered.childOrder = append(filtered.childOrder, childName)
}
}
if len(filtered.children) == 0 && filtered.value == nil {
return nil
}
return filtered
}
// valueToCUE converts a Value to CUE syntax.
func (g *CUEGenerator) valueToCUE(v Value) string {
switch val := v.(type) {
@@ -1442,7 +1760,7 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
case *HelperVar:
// Return reference to the helper by name
return val.Name()
case *StringParam, *IntParam, *BoolParam, *FloatParam, *ArrayParam, *MapParam, *StringKeyMapParam, *EnumParam:
case *StringParam, *IntParam, *BoolParam, *FloatParam, *ArrayParam, *MapParam, *StringKeyMapParam, *EnumParam, *OneOfParam:
return "parameter." + v.(Param).Name()
case *DynamicMapParam:
// Dynamic map parameters reference just "parameter"
@@ -1454,6 +1772,8 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
return g.collectionOpToCUE(val)
case *MultiSource:
return g.multiSourceToCUE(val)
case *InlineArrayValue:
return g.inlineArrayToCUE(val)
case *ConcatExprValue:
return g.concatExprToCUE(val)
case *CUEFunc:
@@ -1679,7 +1999,15 @@ func (g *CUEGenerator) arrayBuilderToCUE(ab *ArrayBuilder, depth int) string {
case entryForEachWith:
sourceStr := g.valueToCUE(entry.source)
sb.WriteString(fmt.Sprintf("%sfor %s in %s {\n", innerIndent, entry.itemBuilder.VarName(), sourceStr))
guardPrefix := ""
if entry.guard != nil {
guardPrefix = "if " + g.conditionToCUE(entry.guard) + " "
}
filterSuffix := ""
if entry.filter != nil {
filterSuffix = " if " + g.predicateToCUE(entry.filter)
}
sb.WriteString(fmt.Sprintf("%s%sfor %s in %s%s {\n", innerIndent, guardPrefix, entry.itemBuilder.VarName(), sourceStr, filterSuffix))
g.writeItemBuilderOps(&sb, entry.itemBuilder.Ops(), depth+2)
sb.WriteString(fmt.Sprintf("%s},\n", innerIndent))
}
@@ -1736,12 +2064,15 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
}
}
// Check if there's a Map operation
// Check if there's a Map or MapVariant operation
hasMap := false
hasVariant := false
for _, op := range ops {
if _, ok := op.(*mapOp); ok {
hasMap = true
break
}
if _, ok := op.(*mapVariantOp); ok {
hasVariant = true
}
}
@@ -1764,7 +2095,7 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
sb.WriteString(filterCondition)
}
if hasMap {
if hasMap || hasVariant {
// Map operations: render mapped fields in a struct
sb.WriteString(" {\n")
sb.WriteString("\t\t\t\t{\n")
@@ -1775,6 +2106,21 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s != _|_ {\n", optField.field))
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: v.%s\n", fieldName, optField.field))
sb.WriteString("\t\t\t\t\t}\n")
} else if compOpt, isCompound := fieldVal.(*CompoundOptionalField); isCompound {
condStr := g.conditionToCUE(compOpt.additionalCond)
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s != _|_ if %s {\n", compOpt.field, condStr))
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: v.%s\n", fieldName, compOpt.field))
sb.WriteString("\t\t\t\t\t}\n")
} else if condRef, isConditional := fieldVal.(*ConditionalOrFieldRef); isConditional {
// Emit if/else pattern for conditional field reference
primaryField := string(condRef.primary)
fallbackStr := g.fieldValueToCUE(condRef.fallback)
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s != _|_ {\n", primaryField))
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: v.%s\n", fieldName, primaryField))
sb.WriteString("\t\t\t\t\t}\n")
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s == _|_ {\n", primaryField))
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: %s\n", fieldName, fallbackStr))
sb.WriteString("\t\t\t\t\t}\n")
} else {
valStr := g.fieldValueToCUE(fieldVal)
sb.WriteString(fmt.Sprintf("\t\t\t\t\t%s: %s\n", fieldName, valStr))
@@ -1782,6 +2128,23 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
}
}
}
// MapVariant operations: render conditional field blocks
for _, op := range ops {
if mvOp, ok := op.(*mapVariantOp); ok {
sb.WriteString(fmt.Sprintf("\t\t\t\t\tif v.%s == %q {\n", mvOp.discriminator, mvOp.variantName))
for fieldName, fieldVal := range mvOp.mappings {
if optField, isOptional := fieldVal.(*OptionalField); isOptional {
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\tif v.%s != _|_ {\n", optField.field))
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t\t%s: v.%s\n", fieldName, optField.field))
sb.WriteString("\t\t\t\t\t\t}\n")
} else {
valStr := g.fieldValueToCUE(fieldVal)
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t%s: %s\n", fieldName, valStr))
}
}
sb.WriteString("\t\t\t\t\t}\n")
}
}
sb.WriteString("\t\t\t\t}\n")
sb.WriteString("\t\t\t}]")
} else {
@@ -2052,6 +2415,19 @@ func (g *CUEGenerator) concatExprToCUE(ce *ConcatExprValue) string {
return strings.Join(parts, " + ")
}
// inlineArrayToCUE converts an InlineArrayValue to CUE syntax.
// Generates: [{field1: value1, field2: value2}]
func (g *CUEGenerator) inlineArrayToCUE(arr *InlineArrayValue) string {
var sb strings.Builder
sb.WriteString("[{\n")
for fieldName, fieldVal := range arr.Fields() {
valStr := g.valueToCUE(fieldVal)
sb.WriteString(fmt.Sprintf("\t\t\t\t\t\t\t%s: %s\n", fieldName, valStr))
}
sb.WriteString("\t\t\t\t\t\t}]")
return sb.String()
}
// conditionToCUE converts a Condition to CUE syntax.
func (g *CUEGenerator) conditionToCUE(cond Condition) string {
switch c := cond.(type) {
@@ -2262,11 +2638,21 @@ func (g *CUEGenerator) writeStatus(sb *strings.Builder, c *ComponentDefinition,
func (g *CUEGenerator) writeParam(sb *strings.Builder, param Param, depth int) {
indent := strings.Repeat(g.indent, depth)
// Write // +ignore directive if set (before +usage)
if ip, ok := param.(interface{ IsIgnore() bool }); ok && ip.IsIgnore() {
sb.WriteString(fmt.Sprintf("%s// +ignore\n", indent))
}
// Write description as comment if present
if desc := param.GetDescription(); desc != "" {
sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", indent, desc))
}
// Write // +short=X directive if set (after +usage)
if sp, ok := param.(interface{ GetShort() string }); ok && sp.GetShort() != "" {
sb.WriteString(fmt.Sprintf("%s// +short=%s\n", indent, sp.GetShort()))
}
name := param.Name()
optional := "?"
forceOptional := false
@@ -2299,6 +2685,8 @@ func (g *CUEGenerator) writeParam(sb *strings.Builder, param Param, depth int) {
g.writeStructParam(sb, p, indent, name, optional, depth)
case *EnumParam:
g.writeEnumParam(sb, p, indent, name, optional)
case *OneOfParam:
g.writeOneOfParam(sb, p, indent, name, optional, depth)
default:
// Generic fallback
sb.WriteString(fmt.Sprintf("%s%s%s: _\n", indent, name, optional))
@@ -2356,9 +2744,9 @@ func (g *CUEGenerator) writeStringParam(sb *strings.Builder, p *StringParam, ind
if p.HasDefault() {
if len(constraints) > 0 {
sb.WriteString(fmt.Sprintf("%s%s: *%q | string & %s\n", indent, name, p.GetDefault(), strings.Join(constraints, " & ")))
sb.WriteString(fmt.Sprintf("%s%s%s: *%q | string & %s\n", indent, name, optional, p.GetDefault(), strings.Join(constraints, " & ")))
} else {
sb.WriteString(fmt.Sprintf("%s%s: *%q | string\n", indent, name, p.GetDefault()))
sb.WriteString(fmt.Sprintf("%s%s%s: *%q | string\n", indent, name, optional, p.GetDefault()))
}
} else {
if len(constraints) > 0 {
@@ -2521,11 +2909,11 @@ func (g *CUEGenerator) writeMapParam(sb *strings.Builder, p *MapParam, indent, n
if cueType != "" {
sb.WriteString(fmt.Sprintf("%s%s%s: [string]: %s\n", indent, name, optional, cueType))
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: {...}\n", indent, name, optional))
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, cueOpenStruct))
}
} else {
// Generic object
sb.WriteString(fmt.Sprintf("%s%s%s: {...}\n", indent, name, optional))
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, cueOpenStruct))
}
}
@@ -2635,8 +3023,50 @@ func (g *CUEGenerator) writeEnumParam(sb *strings.Builder, p *EnumParam, indent,
}
}
// cueTypeForParamType converts a ParamType to its CUE type string.
func (g *CUEGenerator) cueTypeForParamType(pt ParamType) string {
// writeOneOfParam writes a discriminated union parameter.
// The param name is used as the discriminator field name (e.g., OneOf("type")).
// Generates:
//
// type: *"default" | "variant1" | "variant2"
// if type == "variant1" { field1: string }
// if type == "variant2" { field2: int }
func (g *CUEGenerator) writeOneOfParam(sb *strings.Builder, p *OneOfParam, indent, name, optional string, depth int) {
variants := p.GetVariants()
// Build discriminator field: type: *"default" | "variant1" | "variant2"
var enumParts []string
if p.HasDefault() {
defaultStr := fmt.Sprintf("%v", p.GetDefault())
enumParts = append(enumParts, fmt.Sprintf("*%s", formatCUEValue(p.GetDefault())))
for _, v := range variants {
if v.Name() != defaultStr {
enumParts = append(enumParts, fmt.Sprintf("%q", v.Name()))
}
}
} else {
for _, v := range variants {
enumParts = append(enumParts, fmt.Sprintf("%q", v.Name()))
}
}
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, strings.Join(enumParts, " | ")))
// Write conditional blocks for each variant
for _, variant := range variants {
fields := variant.GetFields()
if len(fields) == 0 {
continue
}
sb.WriteString(fmt.Sprintf("%sif %s == %q {\n", indent, name, variant.Name()))
for _, field := range fields {
g.writeStructField(sb, field, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
}
// cueTypeStr converts a ParamType to its CUE type string.
func cueTypeStr(pt ParamType) string {
switch pt {
case ParamTypeString:
return string(ParamTypeString)
@@ -2648,15 +3078,18 @@ func (g *CUEGenerator) cueTypeForParamType(pt ParamType) string {
return "float"
case ParamTypeArray:
return "[...]"
case ParamTypeMap:
return "{...}"
case ParamTypeStruct:
return "{...}"
case ParamTypeMap, ParamTypeStruct, ParamTypeOneOf:
return cueOpenStruct
default:
return "_"
}
}
// cueTypeForParamType converts a ParamType to its CUE type string.
func (g *CUEGenerator) cueTypeForParamType(pt ParamType) string {
return cueTypeStr(pt)
}
// formatCUEValue formats a Go value as a CUE literal.
func formatCUEValue(v any) string {
switch val := v.(type) {
+636
View File
@@ -17,6 +17,8 @@ limitations under the License.
package defkit_test
import (
"strings"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
@@ -137,6 +139,48 @@ var _ = Describe("CUEGenerator", func() {
// ForceOptional with default: has ? (field can be absent, defaults when present)
Expect(cue).To(ContainSubstring(`optionalDefault?: *"Honor" | "Ignore"`))
})
It("should generate // +ignore directive for ignored parameters", func() {
comp := defkit.NewComponent("test").
Params(
defkit.String("visible").Description("A visible field"),
defkit.Enum("hidden").Values("A", "B").Default("A").Ignore().Description("An ignored field"),
)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).NotTo(ContainSubstring("// +ignore\n\t// +usage=A visible field"))
Expect(cue).To(ContainSubstring("// +ignore\n\t// +usage=An ignored field"))
})
It("should generate // +short directive for params with short flags", func() {
comp := defkit.NewComponent("test").
Params(
defkit.String("image").Required().Description("Container image").Short("i"),
)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("// +usage=Container image"))
Expect(cue).To(ContainSubstring("// +short=i"))
})
It("should generate both // +ignore and // +short directives in correct order", func() {
comp := defkit.NewComponent("test").
Params(
defkit.Int("port").Ignore().Description("Deprecated field, please use ports instead").Short("p"),
)
cue := gen.GenerateParameterSchema(comp)
// Order should be: // +ignore, // +usage=..., // +short=p
ignoreIdx := strings.Index(cue, "// +ignore")
usageIdx := strings.Index(cue, "// +usage=Deprecated field")
shortIdx := strings.Index(cue, "// +short=p")
Expect(ignoreIdx).To(BeNumerically(">", 0))
Expect(usageIdx).To(BeNumerically(">", ignoreIdx))
Expect(shortIdx).To(BeNumerically(">", usageIdx))
})
})
Describe("GenerateParameterSchema with complex types", func() {
@@ -220,6 +264,412 @@ var _ = Describe("CUEGenerator", func() {
})
})
Describe("GenerateParameterSchema with OneOf parameters", func() {
It("should generate discriminator field and conditional variant blocks", func() {
comp := defkit.NewComponent("test").
Params(
defkit.OneOf("type").
Default("emptyDir").
Description("Volume type").
Variants(
defkit.Variant("pvc").Fields(
defkit.Field("claimName", defkit.ParamTypeString).Required(),
),
defkit.Variant("emptyDir").Fields(
defkit.Field("medium", defkit.ParamTypeString).Default("").Enum("", "Memory"),
),
),
)
cue := gen.GenerateParameterSchema(comp)
// Discriminator field with default
Expect(cue).To(ContainSubstring(`*"emptyDir"`))
Expect(cue).To(ContainSubstring(`"pvc"`))
// Conditional blocks
Expect(cue).To(ContainSubstring(`if type == "pvc"`))
Expect(cue).To(ContainSubstring("claimName: string"))
Expect(cue).To(ContainSubstring(`if type == "emptyDir"`))
Expect(cue).To(ContainSubstring(`medium:`))
})
It("should generate OneOf inside array with shared fields", func() {
comp := defkit.NewComponent("test").
Params(
defkit.List("volumes").WithFields(
defkit.String("name").Required(),
defkit.OneOf("type").Default("emptyDir").Variants(
defkit.Variant("pvc").Fields(
defkit.Field("claimName", defkit.ParamTypeString).Required(),
),
defkit.Variant("emptyDir"),
),
),
)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("volumes?:"))
Expect(cue).To(ContainSubstring("[...{"))
Expect(cue).To(ContainSubstring("name: string"))
Expect(cue).To(ContainSubstring(`type: *"emptyDir" | "pvc"`))
Expect(cue).To(ContainSubstring(`if type == "pvc"`))
Expect(cue).To(ContainSubstring("claimName: string"))
})
It("should omit conditional block for empty variants", func() {
comp := defkit.NewComponent("test").
Params(
defkit.OneOf("kind").
Variants(
defkit.Variant("simple"), // no fields
defkit.Variant("complex").Fields(
defkit.Field("config", defkit.ParamTypeString).Required(),
),
),
)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).NotTo(ContainSubstring(`if kind == "simple"`))
Expect(cue).To(ContainSubstring(`if kind == "complex"`))
Expect(cue).To(ContainSubstring("config: string"))
})
})
Describe("GenerateFullDefinition with MapVariant", func() {
It("should generate conditional field blocks in comprehension", func() {
volumes := defkit.List("volumes")
comp := defkit.NewComponent("test").
Workload("batch/v1", "Job").
Params(volumes).
Template(func(tpl *defkit.Template) {
tpl.Output(
defkit.NewResource("batch/v1", "Job").
Set("spec.volumes",
defkit.Each(volumes).
Map(defkit.FieldMap{"name": defkit.FieldRef("name")}).
MapVariant("type", "pvc", defkit.FieldMap{
"persistentVolumeClaim.claimName": defkit.FieldRef("claimName"),
}).
MapVariant("type", "emptyDir", defkit.FieldMap{
"emptyDir.medium": defkit.FieldRef("medium"),
}),
),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring("for v in parameter.volumes"))
Expect(cue).To(ContainSubstring("name: v.name"))
Expect(cue).To(ContainSubstring(`if v.type == "pvc"`))
Expect(cue).To(ContainSubstring("persistentVolumeClaim.claimName: v.claimName"))
Expect(cue).To(ContainSubstring(`if v.type == "emptyDir"`))
Expect(cue).To(ContainSubstring("emptyDir.medium: v.medium"))
})
It("should generate optional fields inside variant blocks", func() {
volumes := defkit.List("volumes")
comp := defkit.NewComponent("test").
Workload("batch/v1", "Job").
Params(volumes).
Template(func(tpl *defkit.Template) {
tpl.Output(
defkit.NewResource("batch/v1", "Job").
Set("spec.volumes",
defkit.Each(volumes).
Map(defkit.FieldMap{"name": defkit.FieldRef("name")}).
MapVariant("type", "configMap", defkit.FieldMap{
"configMap.name": defkit.FieldRef("cmName"),
"configMap.items": defkit.OptionalFieldRef("items"),
}),
),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring(`if v.type == "configMap"`))
Expect(cue).To(ContainSubstring("configMap.name: v.cmName"))
Expect(cue).To(ContainSubstring("if v.items != _|_"))
Expect(cue).To(ContainSubstring("configMap.items: v.items"))
})
})
Describe("Multi-conditional leaf values (condValues)", func() {
It("should render both conditional values when same path is set with different conditions", func() {
gen := defkit.NewCUEGenerator()
volMounts := defkit.String("volumeMounts")
volumes := defkit.String("volumes")
comp := defkit.NewComponent("test-multi-cond").
Description("Test multi-conditional values").
AutodetectWorkload().
Params(volMounts, volumes).
Template(func(tpl *defkit.Template) {
res := defkit.NewResource("batch/v1", "Job")
res.
If(volMounts.IsSet()).
Set("spec.containers[0].volumeMounts", defkit.Lit("mountsArray")).
EndIf().
If(volumes.IsSet()).
Set("spec.containers[0].volumeMounts", defkit.Lit("volumesArray")).
EndIf()
tpl.Output(res)
})
cue := gen.GenerateFullDefinition(comp)
// Both conditions should appear for the same field
Expect(cue).To(ContainSubstring(`if parameter["volumeMounts"] != _|_`))
Expect(cue).To(ContainSubstring(`if parameter["volumes"] != _|_`))
// The field should appear twice, each in its own if block
Expect(strings.Count(cue, "volumeMounts:")).To(BeNumerically(">=", 2))
})
It("should not activate condValues when path is set unconditionally then conditionally", func() {
gen := defkit.NewCUEGenerator()
optional := defkit.String("opt")
comp := defkit.NewComponent("test-uncond-then-cond").
Description("Test unconditional then conditional").
AutodetectWorkload().
Params(optional).
Template(func(tpl *defkit.Template) {
res := defkit.NewResource("v1", "Pod")
res.
Set("spec.field", defkit.Lit("default")).
SetIf(optional.IsSet(), "spec.field", optional)
tpl.Output(res)
})
cue := gen.GenerateFullDefinition(comp)
// Conditional should win (overwrites unconditional)
Expect(cue).To(ContainSubstring("field: parameter.opt"))
})
})
Describe("Decomposition with condValues (condValues + canDecomposeByCondition)", func() {
It("should decompose struct when leaf nodes have condValues from SetIf with different And conditions", func() {
// This is the webservice CPU/memory limit branching pattern:
// Two SetIf calls with different conditions target the same leaf path,
// creating condValues. The parent struct must still decompose correctly.
gen := defkit.NewCUEGenerator()
cpu := defkit.String("cpu")
limit := defkit.Object("limit")
comp := defkit.NewComponent("test-condval-decompose").
Description("Test condValues decomposition").
AutodetectWorkload().
Params(cpu, limit).
Template(func(tpl *defkit.Template) {
res := defkit.NewResource("v1", "Pod")
// When cpu is set and limit.cpu exists: use limit.cpu for limits, cpu for requests
res.SetIf(defkit.And(cpu.IsSet(), defkit.PathExists("parameter.limit.cpu")),
"spec.resources.requests.cpu", cpu)
res.SetIf(defkit.And(cpu.IsSet(), defkit.PathExists("parameter.limit.cpu")),
"spec.resources.limits.cpu", defkit.Reference("parameter.limit.cpu"))
// When cpu is set but limit.cpu does NOT exist: use cpu for both
res.SetIf(defkit.And(cpu.IsSet(), defkit.Not(defkit.PathExists("parameter.limit.cpu"))),
"spec.resources.limits.cpu", cpu)
res.SetIf(defkit.And(cpu.IsSet(), defkit.Not(defkit.PathExists("parameter.limit.cpu"))),
"spec.resources.requests.cpu", cpu)
tpl.Output(res)
})
cue := gen.GenerateFullDefinition(comp)
// Both compound conditions should appear as separate blocks
Expect(cue).To(ContainSubstring(`parameter["cpu"] != _|_`))
Expect(cue).To(ContainSubstring("parameter.limit.cpu"))
// resources should be decomposed into per-condition blocks
// Each block should contain both limits and requests
Expect(strings.Count(cue, "resources:")).To(BeNumerically(">=", 2))
Expect(strings.Count(cue, "limits:")).To(BeNumerically(">=", 2))
Expect(strings.Count(cue, "requests:")).To(BeNumerically(">=", 2))
// The block where limit.cpu exists should use parameter.limit.cpu for limits
Expect(cue).To(ContainSubstring("cpu: parameter.limit.cpu"))
// The block where limit.cpu does NOT exist should use parameter.cpu for limits
Expect(strings.Count(cue, "cpu: parameter.cpu")).To(BeNumerically(">=", 2))
})
It("should decompose when two sibling leaves at the same path have different condValues", func() {
// Pattern: limits.cpu set under condition A (primary) and condition B (condValue),
// requests.cpu set under condition A (primary) and condition B (condValue).
// collectLeafConditions must see both A and B from condValues.
gen := defkit.NewCUEGenerator()
flagA := defkit.Bool("flagA")
flagB := defkit.Bool("flagB")
comp := defkit.NewComponent("test-condval-siblings").
Description("Test condValues sibling decomposition").
AutodetectWorkload().
Params(flagA, flagB).
Template(func(tpl *defkit.Template) {
res := defkit.NewResource("v1", "Pod")
res.SetIf(flagA.IsSet(), "spec.nested.child1", defkit.Lit("A1"))
res.SetIf(flagB.IsSet(), "spec.nested.child1", defkit.Lit("B1"))
res.SetIf(flagA.IsSet(), "spec.nested.child2", defkit.Lit("A2"))
res.SetIf(flagB.IsSet(), "spec.nested.child2", defkit.Lit("B2"))
tpl.Output(res)
})
cue := gen.GenerateFullDefinition(comp)
// Both conditions should appear
Expect(cue).To(ContainSubstring(`parameter["flagA"]`))
Expect(cue).To(ContainSubstring(`parameter["flagB"]`))
// nested should be decomposed into per-condition blocks
Expect(strings.Count(cue, "nested:")).To(BeNumerically(">=", 2))
// In the flagA block: child1 = "A1", child2 = "A2"
flagAIdx := strings.Index(cue, `parameter["flagA"]`)
Expect(flagAIdx).To(BeNumerically(">", 0))
endA := flagAIdx + 200
if endA > len(cue) {
endA = len(cue)
}
afterFlagA := cue[flagAIdx:endA]
Expect(afterFlagA).To(ContainSubstring(`child1: "A1"`))
Expect(afterFlagA).To(ContainSubstring(`child2: "A2"`))
// In the flagB block: child1 = "B1", child2 = "B2"
flagBIdx := strings.Index(cue, `parameter["flagB"]`)
Expect(flagBIdx).To(BeNumerically(">", 0))
endB := flagBIdx + 200
if endB > len(cue) {
endB = len(cue)
}
afterFlagB := cue[flagBIdx:endB]
Expect(afterFlagB).To(ContainSubstring(`child1: "B1"`))
Expect(afterFlagB).To(ContainSubstring(`child2: "B2"`))
})
It("should filter condValues correctly: primary condition returns primary value, condValue condition returns condValue value", func() {
// Verifies filterNodeByCondition returns the correct value
// when the target condition matches a condValue rather than the primary.
gen := defkit.NewCUEGenerator()
modeA := defkit.String("modeA")
modeB := defkit.String("modeB")
comp := defkit.NewComponent("test-filter-condval").
Description("Test filterNodeByCondition with condValues").
AutodetectWorkload().
Params(modeA, modeB).
Template(func(tpl *defkit.Template) {
res := defkit.NewResource("v1", "Pod")
// Same leaf path, two different conditions, two different values
res.SetIf(modeA.IsSet(), "spec.wrapper.target", defkit.Lit("value-from-A"))
res.SetIf(modeB.IsSet(), "spec.wrapper.target", defkit.Lit("value-from-B"))
// Second leaf to make decomposition viable
res.SetIf(modeA.IsSet(), "spec.wrapper.other", defkit.Lit("other-A"))
res.SetIf(modeB.IsSet(), "spec.wrapper.other", defkit.Lit("other-B"))
tpl.Output(res)
})
cue := gen.GenerateFullDefinition(comp)
// Find the modeA block and verify it has value-from-A
modeAIdx := strings.Index(cue, `parameter["modeA"]`)
Expect(modeAIdx).To(BeNumerically(">", 0))
endA := modeAIdx + 250
if endA > len(cue) {
endA = len(cue)
}
afterA := cue[modeAIdx:endA]
Expect(afterA).To(ContainSubstring(`target: "value-from-A"`))
Expect(afterA).To(ContainSubstring(`other: "other-A"`))
// Find the modeB block and verify it has value-from-B
modeBIdx := strings.Index(cue, `parameter["modeB"]`)
Expect(modeBIdx).To(BeNumerically(">", 0))
endB := modeBIdx + 250
if endB > len(cue) {
endB = len(cue)
}
afterB := cue[modeBIdx:endB]
Expect(afterB).To(ContainSubstring(`target: "value-from-B"`))
Expect(afterB).To(ContainSubstring(`other: "other-B"`))
})
})
Describe("Intermediate node decomposition (canDecomposeByCondition)", func() {
It("should decompose struct into per-condition blocks when all leaves share the same condition set", func() {
gen := defkit.NewCUEGenerator()
cpu := defkit.String("cpu")
memory := defkit.String("memory")
comp := defkit.NewComponent("test-decompose").
Description("Test condition decomposition").
AutodetectWorkload().
Params(cpu, memory).
Template(func(tpl *defkit.Template) {
res := defkit.NewResource("v1", "Pod")
res.
If(cpu.IsSet()).
Set("spec.resources.limits.cpu", cpu).
Set("spec.resources.requests.cpu", cpu).
EndIf().
If(memory.IsSet()).
Set("spec.resources.limits.memory", memory).
Set("spec.resources.requests.memory", memory).
EndIf()
tpl.Output(res)
})
cue := gen.GenerateFullDefinition(comp)
// resources should NOT appear unconditionally
// It should appear inside if blocks
Expect(cue).To(ContainSubstring(`if parameter["cpu"] != _|_ {`))
Expect(cue).To(ContainSubstring(`if parameter["memory"] != _|_ {`))
// Each condition block should contain resources with limits and requests
// Find the cpu block
cpuIdx := strings.Index(cue, `if parameter["cpu"] != _|_ {`)
Expect(cpuIdx).To(BeNumerically(">", 0))
// After the cpu condition, resources should appear
afterCpu := cue[cpuIdx:]
Expect(afterCpu).To(ContainSubstring("resources:"))
Expect(afterCpu).To(ContainSubstring("limits:"))
Expect(afterCpu).To(ContainSubstring("requests:"))
})
It("should not decompose when children have unconditional values", func() {
gen := defkit.NewCUEGenerator()
cpu := defkit.String("cpu")
comp := defkit.NewComponent("test-no-decompose").
Description("Test no decomposition").
AutodetectWorkload().
Params(cpu).
Template(func(tpl *defkit.Template) {
res := defkit.NewResource("v1", "Pod")
res.
If(cpu.IsSet()).
Set("spec.resources.limits.cpu", cpu).
EndIf().
Set("spec.resources.limits.memory", defkit.Lit("128Mi"))
tpl.Output(res)
})
cue := gen.GenerateFullDefinition(comp)
// resources should appear as a regular struct (not decomposed)
// because it has a mix of conditional and unconditional children
Expect(cue).To(ContainSubstring("resources:"))
Expect(cue).To(ContainSubstring("limits:"))
Expect(cue).To(ContainSubstring(`memory: "128Mi"`))
})
})
Describe("GenerateFullDefinition", func() {
It("should generate complete CUE definition", func() {
comp := defkit.NewComponent("webservice").
@@ -399,4 +849,190 @@ var _ = Describe("CUEGenerator", func() {
Expect(cue).To(ContainSubstring("strings"))
})
})
Describe("GenerateFullDefinition with ConditionalOrFieldRef", func() {
It("should generate if/else pattern for conditional field reference", func() {
gen := defkit.NewCUEGenerator()
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.String("name"),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(ports).
Template(func(tpl *defkit.Template) {
containerPorts := defkit.Each(ports).Map(defkit.FieldMap{
"containerPort": defkit.FieldRef("port"),
"name": defkit.FieldRef("name").OrConditional(defkit.Format("port-%v", defkit.FieldRef("port"))),
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.template.spec.containers[0].ports", containerPorts),
)
})
cue := gen.GenerateFullDefinition(comp)
// Should generate if/else blocks, NOT default syntax
Expect(cue).To(ContainSubstring("if v.name != _|_"))
Expect(cue).To(ContainSubstring("name: v.name"))
Expect(cue).To(ContainSubstring("if v.name == _|_"))
Expect(cue).To(ContainSubstring("strconv.FormatInt(v.port, 10)"))
})
})
Describe("GenerateFullDefinition with Directive", func() {
It("should render // +patchKey directive before field value", func() {
gen := defkit.NewCUEGenerator()
hostAliases := defkit.Object("hostAliases")
comp := defkit.NewComponent("test").
Workload("apps/v1", "DaemonSet").
Params(hostAliases).
Template(func(tpl *defkit.Template) {
tpl.Output(
defkit.NewResource("apps/v1", "DaemonSet").
SetIf(hostAliases.IsSet(), "spec.template.spec.hostAliases", hostAliases).
Directive("spec.template.spec.hostAliases", "patchKey=ip"),
)
})
cue := gen.GenerateFullDefinition(comp)
// Directive should appear before the field value
Expect(cue).To(ContainSubstring("// +patchKey=ip"))
Expect(cue).To(ContainSubstring("hostAliases: parameter.hostAliases"))
// Verify ordering: directive before field
patchIdx := strings.Index(cue, "// +patchKey=ip")
hostIdx := strings.Index(cue, "hostAliases: parameter.hostAliases")
Expect(patchIdx).To(BeNumerically("<", hostIdx))
})
})
Describe("GenerateFullDefinition with CompoundOptionalField", func() {
It("should generate compound conditional for OptionalFieldWithCond in collection Map", func() {
gen := defkit.NewCUEGenerator()
exposeType := defkit.Enum("exposeType").
Values("ClusterIP", "NodePort", "LoadBalancer").
Default("ClusterIP")
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.String("name"),
defkit.Int("nodePort"),
defkit.Bool("expose").Default(false),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(exposeType, ports).
Template(func(tpl *defkit.Template) {
exposePorts := defkit.Each(ports).
Filter(defkit.FieldEquals("expose", true)).
Map(defkit.FieldMap{
"port": defkit.FieldRef("port"),
"nodePort": defkit.OptionalFieldWithCond("nodePort", defkit.Eq(exposeType, defkit.Lit("NodePort"))),
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.ports", exposePorts),
)
})
cue := gen.GenerateFullDefinition(comp)
// Should generate compound conditional: if v.nodePort != _|_ if <cond> { nodePort: v.nodePort }
Expect(cue).To(ContainSubstring("v.nodePort != _|_"))
Expect(cue).To(ContainSubstring(`parameter.exposeType == "NodePort"`))
Expect(cue).To(ContainSubstring("nodePort: v.nodePort"))
})
It("should generate simple optional conditional alongside compound conditional", func() {
gen := defkit.NewCUEGenerator()
exposeType := defkit.Enum("exposeType").
Values("ClusterIP", "NodePort", "LoadBalancer").
Default("ClusterIP")
ports := defkit.List("ports").WithFields(
defkit.Int("port").Required(),
defkit.Int("nodePort"),
defkit.String("protocol"),
defkit.Bool("expose").Default(false),
)
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Params(exposeType, ports).
Template(func(tpl *defkit.Template) {
exposePorts := defkit.Each(ports).
Filter(defkit.FieldEquals("expose", true)).
Map(defkit.FieldMap{
"port": defkit.FieldRef("port"),
"nodePort": defkit.OptionalFieldWithCond("nodePort", defkit.Eq(exposeType, defkit.Lit("NodePort"))),
"protocol": defkit.OptionalFieldRef("protocol"),
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.ports", exposePorts),
)
})
cue := gen.GenerateFullDefinition(comp)
// Compound conditional for nodePort
Expect(cue).To(ContainSubstring("v.nodePort != _|_"))
Expect(cue).To(ContainSubstring(`parameter.exposeType == "NodePort"`))
Expect(cue).To(ContainSubstring("nodePort: v.nodePort"))
// Simple optional conditional for protocol
Expect(cue).To(ContainSubstring("v.protocol != _|_"))
Expect(cue).To(ContainSubstring("protocol: v.protocol"))
})
})
Describe("GenerateFullDefinition with InlineArray", func() {
It("should render inline array value as [{field: value}]", func() {
gen := defkit.NewCUEGenerator()
port := defkit.Int("port")
comp := defkit.NewComponent("test").
Workload("apps/v1", "DaemonSet").
Params(port).
Template(func(tpl *defkit.Template) {
tpl.Output(
defkit.NewResource("apps/v1", "DaemonSet").
Set("spec.template.spec.containers[0].ports", defkit.InlineArray(map[string]defkit.Value{
"containerPort": port,
})),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring("containerPort: parameter.port"))
})
It("should render inline array with multiple fields", func() {
gen := defkit.NewCUEGenerator()
port := defkit.Int("port")
comp := defkit.NewComponent("test").
Workload("apps/v1", "DaemonSet").
Params(port).
Template(func(tpl *defkit.Template) {
tpl.Output(
defkit.NewResource("apps/v1", "DaemonSet").
Set("spec.template.spec.containers[0].ports", defkit.InlineArray(map[string]defkit.Value{
"containerPort": port,
"protocol": defkit.Lit("TCP"),
})),
)
})
cue := gen.GenerateFullDefinition(comp)
Expect(cue).To(ContainSubstring("containerPort: parameter.port"))
Expect(cue).To(ContainSubstring(`protocol: "TCP"`))
})
})
})
+28
View File
@@ -355,6 +355,14 @@ type CUEFunc struct {
func (c *CUEFunc) expr() {}
func (c *CUEFunc) value() {}
// RequiredImports returns the CUE imports required by this function call.
func (c *CUEFunc) RequiredImports() []string {
if c.pkg != "" {
return []string{c.pkg}
}
return nil
}
// Package returns the CUE package name.
func (c *CUEFunc) Package() string { return c.pkg }
@@ -798,3 +806,23 @@ func (c *IterFieldExistsCondition) FieldName() string { return c.field }
// IsNegated returns true if this is a "not exists" check.
func (c *IterFieldExistsCondition) IsNegated() bool { return c.negate }
// InlineArrayValue represents an inline array literal containing struct elements.
// This generates CUE like: [{field1: value1, field2: value2}]
// Used for deprecated parameter fallbacks that create a single-element array.
type InlineArrayValue struct {
fields map[string]Value
}
func (a *InlineArrayValue) expr() {}
func (a *InlineArrayValue) value() {}
// Fields returns the field mappings.
func (a *InlineArrayValue) Fields() map[string]Value { return a.fields }
// InlineArray creates an inline array value with a single struct element.
// Example: InlineArray(map[string]Value{"containerPort": port})
// Generates: [{containerPort: parameter.port}]
func InlineArray(fields map[string]Value) *InlineArrayValue {
return &InlineArrayValue{fields: fields}
}
+63 -16
View File
@@ -194,59 +194,63 @@ var _ = Describe("Expressions", func() {
})
Context("CueFunc expressions", func() {
It("should create StrconvFormatInt function", func() {
It("should create StrconvFormatInt function with correct args", func() {
num := defkit.Int("port")
fn := defkit.StrconvFormatInt(num, 10)
Expect(fn).NotTo(BeNil())
Expect(fn.Package()).To(Equal("strconv"))
Expect(fn.Function()).To(Equal("FormatInt"))
Expect(fn.Args()).To(HaveLen(2))
Expect(fn.Args()[0]).To(Equal(num))
Expect(fn.Args()[1]).To(Equal(defkit.Lit(10)))
})
It("should create StringsToLower function", func() {
It("should create StringsToLower function with correct arg", func() {
str := defkit.String("name")
fn := defkit.StringsToLower(str)
Expect(fn).NotTo(BeNil())
Expect(fn.Package()).To(Equal("strings"))
Expect(fn.Function()).To(Equal("ToLower"))
Expect(fn.Args()).To(HaveLen(1))
Expect(fn.Args()[0]).To(Equal(str))
})
It("should create StringsToUpper function", func() {
It("should create StringsToUpper function with correct arg", func() {
str := defkit.String("name")
fn := defkit.StringsToUpper(str)
Expect(fn).NotTo(BeNil())
Expect(fn.Package()).To(Equal("strings"))
Expect(fn.Function()).To(Equal("ToUpper"))
Expect(fn.Args()).To(HaveLen(1))
Expect(fn.Args()[0]).To(Equal(str))
})
It("should create StringsHasPrefix function", func() {
It("should create StringsHasPrefix function with correct args", func() {
str := defkit.String("path")
fn := defkit.StringsHasPrefix(str, "/api")
Expect(fn).NotTo(BeNil())
Expect(fn.Package()).To(Equal("strings"))
Expect(fn.Function()).To(Equal("HasPrefix"))
Expect(fn.Args()).To(HaveLen(2))
Expect(fn.Args()[0]).To(Equal(str))
Expect(fn.Args()[1]).To(Equal(defkit.Lit("/api")))
})
It("should create StringsHasSuffix function", func() {
It("should create StringsHasSuffix function with correct args", func() {
str := defkit.String("file")
fn := defkit.StringsHasSuffix(str, ".yaml")
Expect(fn).NotTo(BeNil())
Expect(fn.Package()).To(Equal("strings"))
Expect(fn.Function()).To(Equal("HasSuffix"))
Expect(fn.Args()).To(HaveLen(2))
Expect(fn.Args()[0]).To(Equal(str))
Expect(fn.Args()[1]).To(Equal(defkit.Lit(".yaml")))
})
It("should create ListConcat function", func() {
It("should create ListConcat function with correct args", func() {
list1 := defkit.List("list1")
list2 := defkit.List("list2")
fn := defkit.ListConcat(list1, list2)
Expect(fn).NotTo(BeNil())
Expect(fn.Package()).To(Equal("list"))
Expect(fn.Function()).To(Equal("Concat"))
Expect(fn.Args()).To(HaveLen(2))
Expect(fn.Args()[0]).To(Equal(list1))
Expect(fn.Args()[1]).To(Equal(list2))
})
})
@@ -259,12 +263,14 @@ var _ = Describe("Expressions", func() {
})
It("should set fields on array element", func() {
nameLit := defkit.Lit("test")
portLit := defkit.Lit(8080)
elem := defkit.NewArrayElement().
Set("name", defkit.Lit("test")).
Set("port", defkit.Lit(8080))
Set("name", nameLit).
Set("port", portLit)
Expect(elem.Fields()).To(HaveLen(2))
Expect(elem.Fields()["name"]).NotTo(BeNil())
Expect(elem.Fields()["port"]).NotTo(BeNil())
Expect(elem.Fields()["name"]).To(Equal(nameLit))
Expect(elem.Fields()["port"]).To(Equal(portLit))
})
It("should set conditional fields on array element", func() {
@@ -286,6 +292,11 @@ var _ = Describe("Expressions", func() {
It("should create ForEachMap expression", func() {
forEach := defkit.ForEachMap()
Expect(forEach).NotTo(BeNil())
Expect(forEach.Source()).To(Equal("parameter"))
Expect(forEach.KeyVar()).To(Equal("k"))
Expect(forEach.ValVar()).To(Equal("v"))
Expect(forEach.KeyExpr()).To(BeEmpty())
Expect(forEach.ValExpr()).To(BeEmpty())
})
It("should set source and variable names", func() {
@@ -316,11 +327,47 @@ var _ = Describe("Expressions", func() {
It("should create parameter reference", func() {
ref := defkit.ParamRef("image")
Expect(ref).NotTo(BeNil())
Expect(ref.Path()).To(Equal("parameter.image"))
})
It("should reference nested parameter", func() {
ref := defkit.ParamRef("config.port")
Expect(ref).NotTo(BeNil())
Expect(ref.Path()).To(Equal("parameter.config.port"))
})
})
Context("InlineArrayValue", func() {
It("should create inline array with fields and store correct values", func() {
port := defkit.Int("port")
arr := defkit.InlineArray(map[string]defkit.Value{
"containerPort": port,
})
Expect(arr.Fields()).To(HaveLen(1))
Expect(arr.Fields()).To(HaveKey("containerPort"))
Expect(arr.Fields()["containerPort"]).To(Equal(port))
})
It("should create inline array with multiple fields and store correct values", func() {
port := defkit.Int("port")
protocol := defkit.Lit("TCP")
arr := defkit.InlineArray(map[string]defkit.Value{
"containerPort": port,
"protocol": protocol,
})
Expect(arr.Fields()).To(HaveLen(2))
Expect(arr.Fields()["containerPort"]).To(Equal(port))
Expect(arr.Fields()["protocol"]).To(Equal(protocol))
})
It("should implement Value interface", func() {
port := defkit.Int("port")
arr := defkit.InlineArray(map[string]defkit.Value{
"containerPort": port,
})
var v defkit.Value = arr // compile-time check
Expect(v).NotTo(BeNil())
Expect(arr.Fields()["containerPort"]).To(Equal(port))
})
})
})
+18
View File
@@ -252,6 +252,14 @@ func (hb *HelperBuilder) FromFields(source Value, fields ...string) *HelperBuild
return hb
}
// FromArray uses a pre-built ArrayBuilder as the helper source.
// This enables complex iteration patterns (ForEachWithGuardedFiltered)
// that can't be expressed through the standard From/Filter/Map pipeline.
func (hb *HelperBuilder) FromArray(ab *ArrayBuilder) *HelperBuilder {
hb.source = &arrayBuilderSource{builder: ab}
return hb
}
// FromHelper references another helper as the source.
// This enables helper chaining for patterns like deduplication.
//
@@ -273,6 +281,13 @@ type helperRefSource struct {
func (h *helperRefSource) isHelperSource() {}
// arrayBuilderSource wraps an ArrayBuilder as a helper source.
type arrayBuilderSource struct {
builder *ArrayBuilder
}
func (a *arrayBuilderSource) isHelperSource() {}
// Each applies a transformation function to each element.
// The function receives a Value representing the current item and returns
// a transformed Value.
@@ -474,6 +489,9 @@ func (hb *HelperBuilder) buildCollection() Value {
hb.applyOpsToCollection(col)
return col
case *arrayBuilderSource:
return src.builder
default:
// Default: empty collection
return Each(Lit([]any{}))
+70 -1
View File
@@ -23,7 +23,9 @@ type baseParam struct {
required bool
defaultValue any
description string
forceOptional bool // when true, field stays optional even with a default value
forceOptional bool // when true, field stays optional even with a default value
short string // short flag alias (e.g. "i" → // +short=i)
ignore bool // when true, emits // +ignore directive
}
func (p *baseParam) expr() {}
@@ -37,6 +39,8 @@ func (p *baseParam) HasDefault() bool { return p.defaultValue != nil }
func (p *baseParam) GetDefault() any { return p.defaultValue }
func (p *baseParam) GetDescription() string { return p.description }
func (p *baseParam) IsForceOptional() bool { return p.forceOptional }
func (p *baseParam) GetShort() string { return p.short }
func (p *baseParam) IsIgnore() bool { return p.ignore }
// IsSet returns a condition that checks if the parameter has a value.
// This is used with SetIf for conditional field assignment.
@@ -142,6 +146,20 @@ func (p *StringParam) ForceOptional() *StringParam {
return p
}
// Short sets a short flag alias for the parameter.
// This generates a // +short=X directive in the CUE output.
func (p *StringParam) Short(s string) *StringParam {
p.short = s
return p
}
// Ignore marks the parameter as ignored by the UI.
// This generates a // +ignore directive in the CUE output.
func (p *StringParam) Ignore() *StringParam {
p.ignore = true
return p
}
// Default sets a default value for the parameter.
func (p *StringParam) Default(value string) *StringParam {
p.defaultValue = value
@@ -320,6 +338,20 @@ func (p *IntParam) Description(desc string) *IntParam {
return p
}
// Short sets a short flag alias for the parameter.
// This generates a // +short=X directive in the CUE output.
func (p *IntParam) Short(s string) *IntParam {
p.short = s
return p
}
// Ignore marks the parameter as ignored by the UI.
// This generates a // +ignore directive in the CUE output.
func (p *IntParam) Ignore() *IntParam {
p.ignore = true
return p
}
// Min sets the minimum value constraint for the parameter.
// This generates CUE like: int & >=n
func (p *IntParam) Min(n int) *IntParam {
@@ -417,6 +449,20 @@ func (p *BoolParam) Description(desc string) *BoolParam {
return p
}
// Short sets a short flag alias for the parameter.
// This generates a // +short=X directive in the CUE output.
func (p *BoolParam) Short(s string) *BoolParam {
p.short = s
return p
}
// Ignore marks the parameter as ignored by the UI.
// This generates a // +ignore directive in the CUE output.
func (p *BoolParam) Ignore() *BoolParam {
p.ignore = true
return p
}
// IsTrue returns a condition that checks if the bool parameter is truthy.
// In CUE, this generates `if parameter.name` instead of `if parameter.name == true`.
func (p *BoolParam) IsTrue() Condition {
@@ -1033,6 +1079,20 @@ func (p *EnumParam) Description(desc string) *EnumParam {
return p
}
// Short sets a short flag alias for the parameter.
// This generates a // +short=X directive in the CUE output.
func (p *EnumParam) Short(s string) *EnumParam {
p.short = s
return p
}
// Ignore marks the parameter as ignored by the UI.
// This generates a // +ignore directive in the CUE output.
func (p *EnumParam) Ignore() *EnumParam {
p.ignore = true
return p
}
// GetValues returns the allowed enum values.
func (p *EnumParam) GetValues() []string {
return p.values
@@ -1095,6 +1155,12 @@ func (p *OneOfParam) Variants(variants ...*OneOfVariant) *OneOfParam {
return p
}
// Default sets the default variant name for the discriminator.
func (p *OneOfParam) Default(value string) *OneOfParam {
p.defaultValue = value
return p
}
// Required marks the parameter as required.
func (p *OneOfParam) Required() *OneOfParam {
p.required = true
@@ -1197,6 +1263,9 @@ func (p *StringKeyMapParam) Description(desc string) *StringKeyMapParam {
return p
}
// GetType returns the parameter type.
func (p *StringKeyMapParam) GetType() ParamType { return p.paramType }
// DynamicMapParam represents a parameter where the parameter itself is a dynamic map.
// In CUE: parameter: [string]: T (where T is the value type)
// This is used for traits like labels where all user values become map keys.
+197 -57
View File
@@ -362,42 +362,51 @@ var _ = Describe("Parameters", func() {
It("should create Eq condition from IntParam", func() {
replicas := defkit.Int("replicas")
cond := replicas.Eq(3)
Expect(cond).NotTo(BeNil())
// Check it implements Condition interface
_, ok := cond.(defkit.Condition) //lint:ignore S1040 intentional interface check for documentation
Expect(ok).To(BeTrue())
pcc, ok := cond.(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue(), "expected *ParamCompareCondition")
Expect(pcc.ParamName()).To(Equal("replicas"))
Expect(pcc.Op()).To(Equal("=="))
Expect(pcc.CompareValue()).To(Equal(3))
})
It("should create comparison conditions from StringParam", func() {
status := defkit.String("status")
// Eq
eqCond := status.Eq("running")
Expect(eqCond).NotTo(BeNil())
eqCond, ok := status.Eq("running").(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue())
Expect(eqCond.ParamName()).To(Equal("status"))
Expect(eqCond.Op()).To(Equal("=="))
Expect(eqCond.CompareValue()).To(Equal("running"))
// Ne
neCond := status.Ne("error")
Expect(neCond).NotTo(BeNil())
neCond, ok := status.Ne("error").(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue())
Expect(neCond.ParamName()).To(Equal("status"))
Expect(neCond.Op()).To(Equal("!="))
Expect(neCond.CompareValue()).To(Equal("error"))
})
It("should create numeric comparison conditions", func() {
replicas := defkit.Int("replicas")
// Gt
gtCond := replicas.Gt(1)
Expect(gtCond).NotTo(BeNil())
gtCond, ok := replicas.Gt(1).(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue())
Expect(gtCond.Op()).To(Equal(">"))
Expect(gtCond.CompareValue()).To(Equal(1))
// Gte
gteCond := replicas.Gte(1)
Expect(gteCond).NotTo(BeNil())
gteCond, ok := replicas.Gte(1).(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue())
Expect(gteCond.Op()).To(Equal(">="))
Expect(gteCond.CompareValue()).To(Equal(1))
// Lt
ltCond := replicas.Lt(10)
Expect(ltCond).NotTo(BeNil())
ltCond, ok := replicas.Lt(10).(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue())
Expect(ltCond.Op()).To(Equal("<"))
Expect(ltCond.CompareValue()).To(Equal(10))
// Lte
lteCond := replicas.Lte(10)
Expect(lteCond).NotTo(BeNil())
lteCond, ok := replicas.Lte(10).(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue())
Expect(lteCond.Op()).To(Equal("<="))
Expect(lteCond.CompareValue()).To(Equal(10))
})
})
@@ -405,30 +414,35 @@ var _ = Describe("Parameters", func() {
It("should create Add expression from IntParam", func() {
replicas := defkit.Int("replicas")
expr := replicas.Add(1)
Expect(expr).NotTo(BeNil())
// Check it implements Value interface
_, ok := expr.(defkit.Value) //lint:ignore S1040 intentional interface check for documentation
Expect(ok).To(BeTrue())
arith, ok := expr.(*defkit.ParamArithExpr)
Expect(ok).To(BeTrue(), "expected *ParamArithExpr")
Expect(arith.ParamName()).To(Equal("replicas"))
Expect(arith.Op()).To(Equal("+"))
Expect(arith.ArithValue()).To(Equal(1))
})
It("should create arithmetic expressions from IntParam", func() {
replicas := defkit.Int("replicas")
// Add
addExpr := replicas.Add(1)
Expect(addExpr).NotTo(BeNil())
addExpr, ok := replicas.Add(1).(*defkit.ParamArithExpr)
Expect(ok).To(BeTrue())
Expect(addExpr.Op()).To(Equal("+"))
Expect(addExpr.ArithValue()).To(Equal(1))
// Sub
subExpr := replicas.Sub(1)
Expect(subExpr).NotTo(BeNil())
subExpr, ok := replicas.Sub(1).(*defkit.ParamArithExpr)
Expect(ok).To(BeTrue())
Expect(subExpr.Op()).To(Equal("-"))
Expect(subExpr.ArithValue()).To(Equal(1))
// Mul
mulExpr := replicas.Mul(2)
Expect(mulExpr).NotTo(BeNil())
mulExpr, ok := replicas.Mul(2).(*defkit.ParamArithExpr)
Expect(ok).To(BeTrue())
Expect(mulExpr.Op()).To(Equal("*"))
Expect(mulExpr.ArithValue()).To(Equal(2))
// Div
divExpr := replicas.Div(2)
Expect(divExpr).NotTo(BeNil())
divExpr, ok := replicas.Div(2).(*defkit.ParamArithExpr)
Expect(ok).To(BeTrue())
Expect(divExpr.Op()).To(Equal("/"))
Expect(divExpr.ArithValue()).To(Equal(2))
})
})
@@ -436,19 +450,21 @@ var _ = Describe("Parameters", func() {
It("should create Concat expression from StringParam", func() {
name := defkit.String("name")
expr := name.Concat("-suffix")
Expect(expr).NotTo(BeNil())
// Check it implements Value interface
_, ok := expr.(defkit.Value) //lint:ignore S1040 intentional interface check for documentation
Expect(ok).To(BeTrue())
concat, ok := expr.(*defkit.ParamConcatExpr)
Expect(ok).To(BeTrue(), "expected *ParamConcatExpr")
Expect(concat.ParamName()).To(Equal("name"))
Expect(concat.Suffix()).To(Equal("-suffix"))
Expect(concat.Prefix()).To(BeEmpty())
})
It("should create Prepend expression from StringParam", func() {
name := defkit.String("name")
expr := name.Prepend("prefix-")
Expect(expr).NotTo(BeNil())
// Check it implements Value interface
_, ok := expr.(defkit.Value) //lint:ignore S1040 intentional interface check for documentation
Expect(ok).To(BeTrue())
concat, ok := expr.(*defkit.ParamConcatExpr)
Expect(ok).To(BeTrue(), "expected *ParamConcatExpr")
Expect(concat.ParamName()).To(Equal("name"))
Expect(concat.Prefix()).To(Equal("prefix-"))
Expect(concat.Suffix()).To(BeEmpty())
})
})
@@ -476,14 +492,20 @@ var _ = Describe("Parameters", func() {
config := defkit.Struct("config")
fieldRef := config.Field("port")
cond := fieldRef.IsSet()
Expect(cond).NotTo(BeNil())
isSet, ok := cond.(*defkit.ParamPathIsSetCondition)
Expect(ok).To(BeTrue(), "expected *ParamPathIsSetCondition")
Expect(isSet.Path()).To(Equal("config.port"))
})
It("should create Eq condition from field ref", func() {
config := defkit.Struct("config")
fieldRef := config.Field("enabled")
cond := fieldRef.Eq(true)
Expect(cond).NotTo(BeNil())
pcc, ok := cond.(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue(), "expected *ParamCompareCondition")
Expect(pcc.ParamName()).To(Equal("config.enabled"))
Expect(pcc.Op()).To(Equal("=="))
Expect(pcc.CompareValue()).To(Equal(true))
})
})
})
@@ -492,26 +514,33 @@ var _ = Describe("Parameters", func() {
It("should create StringList parameter", func() {
p := defkit.StringList("tags")
Expect(p.Name()).To(Equal("tags"))
Expect(p.ElementType()).To(Equal(defkit.ParamTypeString))
Expect(p.IsRequired()).To(BeFalse())
})
It("should create IntList parameter", func() {
p := defkit.IntList("ports")
Expect(p.Name()).To(Equal("ports"))
Expect(p.ElementType()).To(Equal(defkit.ParamTypeInt))
Expect(p.IsRequired()).To(BeFalse())
})
It("should create StringKeyMap parameter", func() {
p := defkit.StringKeyMap("labels")
Expect(p.Name()).To(Equal("labels"))
Expect(p.GetType()).To(Equal(defkit.ParamTypeMap))
})
It("should create List parameter", func() {
p := defkit.List("items")
Expect(p.Name()).To(Equal("items"))
Expect(p.IsRequired()).To(BeFalse())
})
It("should create Object parameter", func() {
p := defkit.Object("config")
Expect(p.Name()).To(Equal("config"))
Expect(p.IsRequired()).To(BeFalse())
})
})
@@ -519,13 +548,19 @@ var _ = Describe("Parameters", func() {
It("should create IsSet condition from param", func() {
replicas := defkit.Int("replicas")
cond := replicas.IsSet()
Expect(cond).NotTo(BeNil())
isSet, ok := cond.(*defkit.IsSetCondition)
Expect(ok).To(BeTrue(), "expected *IsSetCondition")
Expect(isSet.ParamName()).To(Equal("replicas"))
})
It("should create NotSet condition from param", func() {
replicas := defkit.Int("replicas")
cond := replicas.NotSet()
Expect(cond).NotTo(BeNil())
notCond, ok := cond.(*defkit.NotCondition)
Expect(ok).To(BeTrue(), "expected *NotCondition")
inner, ok := notCond.Inner().(*defkit.IsSetCondition)
Expect(ok).To(BeTrue(), "expected inner *IsSetCondition")
Expect(inner.ParamName()).To(Equal("replicas"))
})
})
@@ -533,13 +568,17 @@ var _ = Describe("Parameters", func() {
It("should create IsTrue condition", func() {
enabled := defkit.Bool("enabled")
cond := enabled.IsTrue()
Expect(cond).NotTo(BeNil())
truthy, ok := cond.(*defkit.TruthyCondition)
Expect(ok).To(BeTrue(), "expected *TruthyCondition")
Expect(truthy.ParamName()).To(Equal("enabled"))
})
It("should create IsFalse condition", func() {
enabled := defkit.Bool("enabled")
cond := enabled.IsFalse()
Expect(cond).NotTo(BeNil())
falsy, ok := cond.(*defkit.FalsyCondition)
Expect(ok).To(BeTrue(), "expected *FalsyCondition")
Expect(falsy.ParamName()).To(Equal("enabled"))
})
})
@@ -556,7 +595,11 @@ var _ = Describe("Parameters", func() {
It("should create length constraint conditions", func() {
arr := defkit.Array("items")
gteCond := arr.LenGte(1)
Expect(gteCond).NotTo(BeNil())
lenCond, ok := gteCond.(*defkit.LenCondition)
Expect(ok).To(BeTrue(), "expected *LenCondition")
Expect(lenCond.ParamName()).To(Equal("items"))
Expect(lenCond.Op()).To(Equal(">="))
Expect(lenCond.Length()).To(Equal(1))
})
It("should set WithFields for array items", func() {
@@ -603,6 +646,30 @@ var _ = Describe("Parameters", func() {
})
})
Context("OneOfParam Default method", func() {
It("should set default variant name", func() {
p := defkit.OneOf("type").Default("emptyDir")
Expect(p.HasDefault()).To(BeTrue())
Expect(p.GetDefault()).To(Equal("emptyDir"))
})
It("should support fluent chaining with Default", func() {
p := defkit.OneOf("type").
Default("emptyDir").
Description("Volume type").
Variants(
defkit.Variant("pvc").Fields(
defkit.Field("claimName", defkit.ParamTypeString).Required(),
),
defkit.Variant("emptyDir"),
)
Expect(p.HasDefault()).To(BeTrue())
Expect(p.GetDefault()).To(Equal("emptyDir"))
Expect(p.GetDescription()).To(Equal("Volume type"))
Expect(p.GetVariants()).To(HaveLen(2))
})
})
Context("IntParam Optional method", func() {
It("should set int as optional", func() {
p := defkit.Int("replicas").Optional()
@@ -628,13 +695,21 @@ var _ = Describe("Parameters", func() {
It("should create LenLt condition", func() {
arr := defkit.Array("items")
cond := arr.LenLt(10)
Expect(cond).NotTo(BeNil())
lenCond, ok := cond.(*defkit.LenCondition)
Expect(ok).To(BeTrue(), "expected *LenCondition")
Expect(lenCond.ParamName()).To(Equal("items"))
Expect(lenCond.Op()).To(Equal("<"))
Expect(lenCond.Length()).To(Equal(10))
})
It("should create LenLte condition", func() {
arr := defkit.Array("items")
cond := arr.LenLte(10)
Expect(cond).NotTo(BeNil())
lenCond, ok := cond.(*defkit.LenCondition)
Expect(ok).To(BeTrue(), "expected *LenCondition")
Expect(lenCond.ParamName()).To(Equal("items"))
Expect(lenCond.Op()).To(Equal("<="))
Expect(lenCond.Length()).To(Equal(10))
})
})
@@ -756,7 +831,9 @@ var _ = Describe("Parameters", func() {
It("should create IsSet condition", func() {
path := defkit.ParamPath("config.port")
cond := path.IsSet()
Expect(cond).NotTo(BeNil())
isSet, ok := cond.(*defkit.ParamPathIsSetCondition)
Expect(ok).To(BeTrue(), "expected *ParamPathIsSetCondition")
Expect(isSet.Path()).To(Equal("config.port"))
})
})
@@ -789,7 +866,70 @@ var _ = Describe("Parameters", func() {
It("should create Ne condition", func() {
p := defkit.Int("count")
cond := p.Ne(0)
Expect(cond).NotTo(BeNil())
pcc, ok := cond.(*defkit.ParamCompareCondition)
Expect(ok).To(BeTrue(), "expected *ParamCompareCondition")
Expect(pcc.ParamName()).To(Equal("count"))
Expect(pcc.Op()).To(Equal("!="))
Expect(pcc.CompareValue()).To(Equal(0))
})
})
Context("Short method", func() {
It("should set short flag on StringParam", func() {
p := defkit.String("image").Short("i")
Expect(p.GetShort()).To(Equal("i"))
})
It("should set short flag on IntParam", func() {
p := defkit.Int("port").Short("p")
Expect(p.GetShort()).To(Equal("p"))
})
It("should set short flag on BoolParam", func() {
p := defkit.Bool("debug").Short("d")
Expect(p.GetShort()).To(Equal("d"))
})
It("should set short flag on EnumParam", func() {
p := defkit.Enum("protocol").Values("TCP", "UDP").Short("p")
Expect(p.GetShort()).To(Equal("p"))
})
It("should return empty string when not set", func() {
p := defkit.String("image")
Expect(p.GetShort()).To(BeEmpty())
})
It("should support fluent chaining with other methods", func() {
p := defkit.String("image").Required().Description("Container image").Short("i")
Expect(p.Name()).To(Equal("image"))
Expect(p.IsRequired()).To(BeTrue())
Expect(p.GetDescription()).To(Equal("Container image"))
Expect(p.GetShort()).To(Equal("i"))
})
})
Context("Ignore method", func() {
It("should mark StringParam as ignored", func() {
p := defkit.String("port").Ignore()
Expect(p.IsIgnore()).To(BeTrue())
})
It("should mark IntParam as ignored", func() {
p := defkit.Int("port").Ignore()
Expect(p.IsIgnore()).To(BeTrue())
})
It("should mark BoolParam as ignored", func() {
p := defkit.Bool("debug").Ignore()
Expect(p.IsIgnore()).To(BeTrue())
})
It("should mark EnumParam as ignored", func() {
p := defkit.Enum("type").Values("A", "B").Ignore()
Expect(p.IsIgnore()).To(BeTrue())
})
It("should not be ignored by default", func() {
p := defkit.String("image")
Expect(p.IsIgnore()).To(BeFalse())
})
It("should support fluent chaining with Short and other methods", func() {
p := defkit.Int("port").Ignore().Description("Deprecated field").Short("p")
Expect(p.IsIgnore()).To(BeTrue())
Expect(p.GetShort()).To(Equal("p"))
Expect(p.GetDescription()).To(Equal("Deprecated field"))
})
})
})
+4
View File
@@ -223,10 +223,14 @@ type PatchContainerConfig struct {
ContainersDescription string // +usage description for the containers param (auto-generated if empty)
CustomParamsBlock string // custom CUE block for #PatchParams (for complex types)
MultiContainerParam string // alternate name for multi-container param (default: "probes" for probes, "containers" for others)
MultiContainerCheckField string // field name for multi-container error check (default: "containerName")
MultiContainerErrMsg string // custom error message for multi-container mode (default: "container name must be set for %s")
CustomPatchContainerBlock string // custom CUE block for PatchContainer body (for complex merge logic)
CustomPatchBlock string // custom CUE block for the patch: spec: template: spec: { ... } body
CustomParameterBlock string // custom CUE block for the parameter definition
PatchStrategy string // if set, emitted as // +patchStrategy=<value> before the patch block (e.g., "open")
ParamsTypeName string // custom name for the #PatchParams helper (default: "PatchParams")
NoDefaultDisjunction bool // if true, omit the * default marker on parameter disjunction
}
// --- Let Binding Support ---
@@ -628,6 +628,202 @@ var _ = Describe("PatchContainer", func() {
})
})
Context("MultiContainerCheckField and MultiContainerErrMsg", func() {
It("should use default check field 'containerName' and default error message with camelCase", func() {
trait := defkit.NewTrait("default-multi-err-test").
Description("Test default multi-container error").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
PatchFields: []defkit.PatchContainerField{
{ParamName: "image", TargetField: "image"},
},
})
})
cue := trait.ToCue()
// Default check field is "containerName"
Expect(cue).To(ContainSubstring(`if c.containerName == ""`))
Expect(cue).To(ContainSubstring(`if c.containerName != ""`))
// Default error message uses "containerName" (camelCase, matching the field name)
Expect(cue).To(ContainSubstring(`err: "containerName must be set for containers"`))
})
It("should use custom MultiContainerCheckField when set", func() {
trait := defkit.NewTrait("custom-check-field-test").
Description("Test custom check field").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
MultiContainerParam: "probes",
MultiContainerCheckField: "name",
PatchFields: []defkit.PatchContainerField{
{ParamName: "image", TargetField: "image"},
},
})
})
cue := trait.ToCue()
// Custom check field "name" instead of default "containerName"
Expect(cue).To(ContainSubstring(`if c.name == ""`))
Expect(cue).To(ContainSubstring(`if c.name != ""`))
Expect(cue).NotTo(ContainSubstring(`c.containerName == ""`))
})
It("should use custom MultiContainerErrMsg when set", func() {
trait := defkit.NewTrait("custom-err-msg-test").
Description("Test custom error message").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
MultiContainerParam: "probes",
MultiContainerCheckField: "name",
MultiContainerErrMsg: "containerName must be set when specifying startup probe for multiple containers",
PatchFields: []defkit.PatchContainerField{
{ParamName: "image", TargetField: "image"},
},
})
})
cue := trait.ToCue()
// Custom error message overrides the default
Expect(cue).To(ContainSubstring(`err: "containerName must be set when specifying startup probe for multiple containers"`))
// Default error message should NOT appear
Expect(cue).NotTo(ContainSubstring(`err: "containerName must be set for probes"`))
})
It("should use default error with MultiContainerParam name", func() {
trait := defkit.NewTrait("multi-param-err-test").
Description("Test error message includes multi param name").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
MultiContainerParam: "probes",
PatchFields: []defkit.PatchContainerField{
{ParamName: "image", TargetField: "image"},
},
})
})
cue := trait.ToCue()
// Default error message should include the multi-container param name
Expect(cue).To(ContainSubstring(`err: "containerName must be set for probes"`))
})
})
Context("writePatchParamMapping typed scalar fields", func() {
It("should map typed scalar fields unconditionally in _params block even with IsSet and no default", func() {
trait := defkit.NewTrait("typed-scalar-unconditional-test").
Description("Test typed scalar fields pass through unconditionally").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
Groups: []defkit.PatchContainerGroup{
{
TargetField: "startupProbe",
Fields: defkit.PatchFields(
defkit.PatchField("terminationGracePeriodSeconds").Int().IsSet(),
defkit.PatchField("exec").IsSet(),
),
},
},
})
})
cue := trait.ToCue()
// Int().IsSet() with no default: typed scalar should be unconditional in _params block
Expect(cue).To(ContainSubstring("terminationGracePeriodSeconds: parameter.terminationGracePeriodSeconds"))
// But untyped IsSet() with no default (e.g., exec) should remain conditional in _params block
Expect(cue).To(MatchRegexp(`if parameter\.exec != _\|_\s*\{[^}]*exec:\s*parameter\.exec`))
// Both should still be conditional in the PatchContainer body
Expect(cue).To(ContainSubstring("if _params.terminationGracePeriodSeconds != _|_"))
Expect(cue).To(ContainSubstring("if _params.exec != _|_"))
})
It("should keep untyped IsSet fields conditional in _params block", func() {
trait := defkit.NewTrait("untyped-conditional-test").
Description("Test untyped fields remain conditional").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
Groups: []defkit.PatchContainerGroup{
{
TargetField: "startupProbe",
Fields: defkit.PatchFields(
defkit.PatchField("exec").IsSet(),
defkit.PatchField("httpGet").IsSet(),
),
},
},
})
})
cue := trait.ToCue()
// Untyped IsSet() fields should still be conditional in _params block
Expect(cue).To(MatchRegexp(`if parameter\.exec != _\|_\s*\{[^}]*exec:\s*parameter\.exec`))
Expect(cue).To(MatchRegexp(`if parameter\.httpGet != _\|_\s*\{[^}]*httpGet:\s*parameter\.httpGet`))
})
It("should map typed scalar fields with default unconditionally regardless of IsSet", func() {
trait := defkit.NewTrait("typed-with-default-test").
Description("Test typed fields with default are unconditional").
AppliesTo("deployments.apps").
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
Groups: []defkit.PatchContainerGroup{
{
TargetField: "startupProbe",
Fields: defkit.PatchFields(
defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0"),
defkit.PatchField("periodSeconds").Int().IsSet().Default("10"),
),
},
},
})
})
cue := trait.ToCue()
// Typed fields with defaults are always unconditional in _params block
Expect(cue).To(ContainSubstring("initialDelaySeconds: parameter.initialDelaySeconds"))
Expect(cue).To(ContainSubstring("periodSeconds: parameter.periodSeconds"))
// Should NOT be wrapped in conditionals in _params block
Expect(cue).NotTo(MatchRegexp(`if parameter\.initialDelaySeconds[^{]*\{[^}]*initialDelaySeconds:\s*parameter\.initialDelaySeconds`))
})
})
Context("Template Let Bindings", func() {
It("should accumulate bindings in order for CUE generation", func() {
tpl := defkit.NewTemplate()
+27
View File
@@ -226,3 +226,30 @@ func (r *Resource) EndIf() *Resource {
}
return r
}
// Directive records a CUE directive annotation on a field path.
// The directive string should be like "patchKey=ip" and will be rendered as // +patchKey=ip.
func (r *Resource) Directive(path string, directive string) *Resource {
op := &DirectiveOp{path: path, directive: directive}
if r.currentIf != nil {
r.currentIf.ops = append(r.currentIf.ops, op)
} else {
r.ops = append(r.ops, op)
}
return r
}
// DirectiveOp records a CUE directive annotation on a field path.
// The directive string (e.g. "patchKey=ip") is rendered as // +patchKey=ip.
type DirectiveOp struct {
path string
directive string
}
func (d *DirectiveOp) resourceOp() {}
// Path returns the field path.
func (d *DirectiveOp) Path() string { return d.path }
// GetDirective returns the directive string.
func (d *DirectiveOp) GetDirective() string { return d.directive }
+39
View File
@@ -202,4 +202,43 @@ var _ = Describe("Resource", func() {
Expect(isSpreadIf).To(BeTrue())
})
})
Context("Directive", func() {
It("should record a Directive operation", func() {
r := defkit.NewResource("apps/v1", "DaemonSet").
Directive("spec.template.spec.hostAliases", "patchKey=ip")
Expect(r.Ops()).To(HaveLen(1))
dirOp, ok := r.Ops()[0].(*defkit.DirectiveOp)
Expect(ok).To(BeTrue())
Expect(dirOp.Path()).To(Equal("spec.template.spec.hostAliases"))
Expect(dirOp.GetDirective()).To(Equal("patchKey=ip"))
})
It("should record Directive within If block", func() {
hostAliases := defkit.Object("hostAliases")
r := defkit.NewResource("apps/v1", "DaemonSet").
If(hostAliases.IsSet()).
Set("spec.template.spec.hostAliases", hostAliases).
Directive("spec.template.spec.hostAliases", "patchKey=ip").
EndIf()
Expect(r.Ops()).To(HaveLen(1))
ifBlock, ok := r.Ops()[0].(*defkit.IfBlock)
Expect(ok).To(BeTrue())
Expect(ifBlock.Ops()).To(HaveLen(2))
_, isDirOp := ifBlock.Ops()[1].(*defkit.DirectiveOp)
Expect(isDirOp).To(BeTrue())
})
It("should combine with Set operations", func() {
hostAliases := defkit.Object("hostAliases")
r := defkit.NewResource("apps/v1", "DaemonSet").
SetIf(hostAliases.IsSet(), "spec.template.spec.hostAliases", hostAliases).
Directive("spec.template.spec.hostAliases", "patchKey=ip")
Expect(r.Ops()).To(HaveLen(2))
_, isSetIf := r.Ops()[0].(*defkit.SetIfOp)
_, isDirOp := r.Ops()[1].(*defkit.DirectiveOp)
Expect(isSetIf).To(BeTrue())
Expect(isDirOp).To(BeTrue())
})
})
})
+122 -7
View File
@@ -98,9 +98,7 @@ func (s *StatusBuilder) Build() string {
var parts []string
for _, f := range s.fields {
parts = append(parts, s.buildField(f))
}
parts = append(parts, s.buildGroupedFields()...)
if s.message != "" {
// Use simple quotes around message - don't use %q which escapes backslashes,
@@ -111,6 +109,79 @@ func (s *StatusBuilder) Build() string {
return strings.Join(parts, "\n")
}
// buildGroupedFields groups fields by parent prefix and generates consolidated CUE blocks.
// Fields with the same parent (e.g., "status.active", "status.failed") are consolidated
// into a single block with column-aligned defaults.
func (s *StatusBuilder) buildGroupedFields() []string {
type fieldGroup struct {
parent string
fields []*StatusField
}
groups := make([]fieldGroup, 0)
groupIndex := make(map[string]int)
var simpleFields []*StatusField
for _, f := range s.fields {
parts := strings.Split(f.name, ".")
if len(parts) == 2 {
parent := parts[0]
if idx, ok := groupIndex[parent]; ok {
groups[idx].fields = append(groups[idx].fields, f)
} else {
groupIndex[parent] = len(groups)
groups = append(groups, fieldGroup{parent: parent, fields: []*StatusField{f}})
}
} else {
simpleFields = append(simpleFields, f)
}
}
var result []string
for _, g := range groups {
result = append(result, s.buildConsolidatedGroup(g.parent, g.fields))
}
for _, f := range simpleFields {
result = append(result, s.buildField(f))
}
return result
}
// buildConsolidatedGroup generates a single CUE block for fields sharing a parent prefix.
// Field values are column-aligned within the block.
func (s *StatusBuilder) buildConsolidatedGroup(parent string, fields []*StatusField) string {
var defaults []string
var conditionals []string
// Calculate max child field name length for column alignment
maxLen := 0
for _, f := range fields {
parts := strings.Split(f.name, ".")
childName := parts[1]
if len(childName) > maxLen {
maxLen = len(childName)
}
}
for _, f := range fields {
parts := strings.Split(f.name, ".")
childName := parts[1]
padding := strings.Repeat(" ", maxLen-len(childName))
defaults = append(defaults, fmt.Sprintf("\t%s:%s %s", childName, padding, f.defaultExpr()))
conditionals = append(conditionals, fmt.Sprintf("\tif context.output.%s != _|_ {\n\t\t%s: context.output.%s\n\t}", f.sourcePath, childName, f.sourcePath))
}
if len(conditionals) == 0 {
return fmt.Sprintf("%s: {\n%s\n}", parent, strings.Join(defaults, "\n"))
}
return fmt.Sprintf("%s: {\n%s\n} & {\n%s\n}", parent, strings.Join(defaults, "\n"), strings.Join(conditionals, "\n"))
}
// RawCUE sets raw CUE for complex status expressions that don't fit the builder pattern.
func (s *StatusBuilder) RawCUE(cue string) *StatusBuilder {
s.rawCUE = cue
@@ -236,7 +307,18 @@ func (h *HealthBuilder) Build() string {
parts = append(parts, h.buildGroupedFields()...)
if len(h.conditions) > 0 {
healthExpr := strings.Join(h.conditions, " && ")
// When multiple conditions are joined with &&, auto-parenthesize each
// for correct precedence and readability. Skip already-parenthesized
// conditions (e.g. from StatusOr). Single conditions are left as-is.
conditions := h.conditions
if len(conditions) > 1 {
wrapped := make([]string, len(conditions))
for i, c := range conditions {
wrapped[i] = parenthesizeCondition(c)
}
conditions = wrapped
}
healthExpr := strings.Join(conditions, " && ")
if h.useDefault {
parts = append(parts, fmt.Sprintf("_isHealth: %s", healthExpr), "isHealth: *_isHealth | bool")
} else {
@@ -251,6 +333,39 @@ func (h *HealthBuilder) Build() string {
return strings.Join(parts, "\n")
}
// parenthesizeCondition wraps a condition string in parentheses if it isn't already
// fully enclosed in a matching pair. This prevents double-wrapping conditions that
// are already parenthesized (e.g. from StatusOr).
func parenthesizeCondition(s string) string {
s = strings.TrimSpace(s)
if isFullyParenthesized(s) {
return s
}
return "(" + s + ")"
}
// isFullyParenthesized checks whether the string is enclosed by a single matching
// pair of parentheses. For example "(a == b)" and "(a || b)" return true, but
// "(a) && (b)" returns false because the first closing paren appears before the end.
func isFullyParenthesized(s string) bool {
if len(s) < 2 || s[0] != '(' || s[len(s)-1] != ')' {
return false
}
depth := 0
for i, ch := range s {
if ch == '(' {
depth++
} else if ch == ')' {
depth--
}
// If depth reaches 0 before the last character, the outer parens don't enclose everything
if depth == 0 && i < len(s)-1 {
return false
}
}
return depth == 0
}
// buildGroupedFields groups fields by parent prefix and generates consolidated CUE blocks.
// Fields with the same parent (e.g., "ready.replicas", "ready.updatedReplicas") are
// consolidated into a single block with column-aligned defaults.
@@ -402,9 +517,9 @@ func DeploymentHealth() *HealthBuilder {
IntField("ready.replicas", "status.replicas", 0).
IntField("ready.observedGeneration", "status.observedGeneration", 0).
HealthyWhen(
"("+StatusEq("context.output.spec.replicas", "ready.readyReplicas")+")",
"("+StatusEq("context.output.spec.replicas", "ready.updatedReplicas")+")",
"("+StatusEq("context.output.spec.replicas", "ready.replicas")+")",
StatusEq("context.output.spec.replicas", "ready.readyReplicas"),
StatusEq("context.output.spec.replicas", "ready.updatedReplicas"),
StatusEq("context.output.spec.replicas", "ready.replicas"),
StatusOr(StatusEq("ready.observedGeneration", "context.output.metadata.generation"), "ready.observedGeneration > context.output.metadata.generation"),
).
WithDefault().
+188 -43
View File
@@ -28,9 +28,11 @@ import (
var _ = Describe("Status", func() {
Context("StatusBuilder", func() {
It("should create a status builder", func() {
It("should create a status builder that produces empty CUE without fields", func() {
s := defkit.Status()
Expect(s).NotTo(BeNil())
cue := s.Build()
Expect(cue).To(BeEmpty())
})
It("should add IntField to status", func() {
@@ -71,9 +73,11 @@ var _ = Describe("Status", func() {
})
Context("HealthBuilder", func() {
It("should create a health builder", func() {
It("should create a health builder that produces empty CUE without conditions", func() {
h := defkit.Health()
Expect(h).NotTo(BeNil())
cue := h.Build()
Expect(cue).To(BeEmpty())
})
It("should add IntField through HealthBuilder", func() {
@@ -110,11 +114,11 @@ var _ = Describe("Status", func() {
Expect(cue).To(ContainSubstring("isHealth: ready.replicas == desired.replicas"))
})
It("should add multiple health conditions", func() {
It("should add multiple health conditions with auto-parenthesization", func() {
h := defkit.Health().
HealthyWhen("condition1", "condition2", "condition3")
cue := h.Build()
Expect(cue).To(ContainSubstring("isHealth: condition1 && condition2 && condition3"))
Expect(cue).To(ContainSubstring("isHealth: (condition1) && (condition2) && (condition3)"))
})
It("should support RawCUE override on health builder", func() {
@@ -229,6 +233,49 @@ var _ = Describe("Status", func() {
})
})
Context("StatusBuilder field grouping", func() {
It("should consolidate multiple fields with same parent into one block", func() {
s := defkit.Status().
IntField("status.active", "status.active", 0).
IntField("status.failed", "status.failed", 0).
IntField("status.succeeded", "status.succeeded", 0).
Message(`Active/Failed/Succeeded:\(status.active)/\(status.failed)/\(status.succeeded)`)
cue := s.Build()
Expect(strings.Count(cue, "status: {")).To(Equal(1))
Expect(cue).To(ContainSubstring("active:"))
Expect(cue).To(ContainSubstring("failed:"))
Expect(cue).To(ContainSubstring("succeeded:"))
})
It("should keep different parent prefixes as separate blocks", func() {
s := defkit.Status().
IntField("ready.replicas", "status.numberReady", 0).
IntField("desired.replicas", "status.desiredNumberScheduled", 0).
Message(`Ready:\(ready.replicas)/\(desired.replicas)`)
cue := s.Build()
Expect(strings.Count(cue, "ready: {")).To(Equal(1))
Expect(strings.Count(cue, "desired: {")).To(Equal(1))
})
It("should column-align fields within consolidated block", func() {
s := defkit.Status().
IntField("status.active", "status.active", 0).
IntField("status.succeeded", "status.succeeded", 0)
cue := s.Build()
// "active" is shorter than "succeeded", so it should be padded
Expect(cue).To(ContainSubstring("active: *0 | int"))
Expect(cue).To(ContainSubstring("succeeded: *0 | int"))
})
It("should handle simple (non-nested) fields without grouping", func() {
s := defkit.Status().
IntField("succeeded", "status.succeeded", 0)
cue := s.Build()
Expect(cue).To(ContainSubstring("succeeded: *0 | int"))
Expect(cue).To(ContainSubstring("context.output.status.succeeded"))
})
})
Context("HealthBuilder field grouping", func() {
It("should consolidate multiple fields with same parent into one block", func() {
h := defkit.Health().
@@ -286,104 +333,202 @@ var _ = Describe("Status", func() {
})
})
Context("HealthBuilder auto-parenthesization", func() {
It("should NOT parenthesize a single condition", func() {
h := defkit.Health().
HealthyWhen("ready.replicas == desired.replicas")
cue := h.Build()
Expect(cue).To(ContainSubstring("isHealth: ready.replicas == desired.replicas"))
})
It("should auto-parenthesize multiple conditions", func() {
h := defkit.Health().
HealthyWhen("a == b", "c == d")
cue := h.Build()
Expect(cue).To(ContainSubstring("isHealth: (a == b) && (c == d)"))
})
It("should NOT double-wrap already-parenthesized conditions", func() {
h := defkit.Health().
HealthyWhen("a == b", defkit.StatusOr("x == y", "x > y"))
cue := h.Build()
// StatusOr returns "(x == y || x > y)" which is already fully parenthesized
Expect(cue).To(ContainSubstring("(a == b) && (x == y || x > y)"))
})
It("should NOT treat partially-parenthesized strings as fully wrapped", func() {
// "(a) && (b)" starts with ( and ends with ) but is not fully enclosed
h := defkit.Health().
HealthyWhen("first", "(a) && (b)")
cue := h.Build()
Expect(cue).To(ContainSubstring("(first) && ((a) && (b))"))
})
It("should work with StatusEq without manual parens", func() {
h := defkit.Health().
HealthyWhen(
defkit.StatusEq("spec.replicas", "ready.replicas"),
defkit.StatusEq("spec.replicas", "ready.updated"),
)
cue := h.Build()
Expect(cue).To(ContainSubstring("(spec.replicas == ready.replicas) && (spec.replicas == ready.updated)"))
})
It("should work with WithDefault and StatusEq", func() {
h := defkit.Health().
HealthyWhen(
defkit.StatusEq("a", "b"),
defkit.StatusEq("c", "d"),
).
WithDefault()
cue := h.Build()
Expect(cue).To(ContainSubstring("_isHealth: (a == b) && (c == d)"))
Expect(cue).To(ContainSubstring("isHealth: *_isHealth | bool"))
})
It("should produce correct CUE for DaemonSetHealth with auto-parens", func() {
h := defkit.DaemonSetHealth()
cue := h.Build()
// Each equality condition should be parenthesized
Expect(cue).To(ContainSubstring("(desired.replicas == ready.replicas)"))
Expect(cue).To(ContainSubstring("(desired.replicas == updated.replicas)"))
Expect(cue).To(ContainSubstring("(desired.replicas == current.replicas)"))
// StatusOr is already wrapped, should not be double-wrapped
Expect(cue).To(ContainSubstring("(generation.observed == generation.metadata || generation.observed > generation.metadata)"))
})
It("should produce correct CUE for DeploymentHealth with auto-parens", func() {
h := defkit.DeploymentHealth()
cue := h.Build()
Expect(cue).To(ContainSubstring("(context.output.spec.replicas == ready.readyReplicas)"))
Expect(cue).To(ContainSubstring("(context.output.spec.replicas == ready.updatedReplicas)"))
Expect(cue).To(ContainSubstring("(context.output.spec.replicas == ready.replicas)"))
Expect(cue).To(ContainSubstring("_isHealth:"))
})
})
Context("HealthBuilder Expressions", func() {
It("should create Condition expression", func() {
It("should generate Condition expression that checks condition status", func() {
h := defkit.Health()
cond := h.Condition("Ready")
Expect(cond).NotTo(BeNil())
expr := h.Condition("Ready").IsTrue()
policy := h.Policy(expr)
Expect(policy).To(ContainSubstring("Ready"))
Expect(policy).To(ContainSubstring("isHealth:"))
Expect(policy).To(ContainSubstring(`"True"`))
})
It("should create Field expression", func() {
h := defkit.Health()
field := h.Field("status.replicas")
Expect(field).NotTo(BeNil())
})
It("should create FieldRef expression", func() {
h := defkit.Health()
ref := h.FieldRef("spec.replicas")
Expect(ref).NotTo(BeNil())
})
It("should create Phase expression", func() {
It("should generate Phase expression that checks status.phase", func() {
h := defkit.Health()
expr := h.Phase("Running", "Succeeded")
Expect(expr).NotTo(BeNil())
policy := h.Policy(expr)
Expect(policy).To(ContainSubstring("isHealth:"))
Expect(policy).To(ContainSubstring("Running"))
Expect(policy).To(ContainSubstring("Succeeded"))
Expect(policy).To(ContainSubstring("context.output.status.phase"))
})
It("should create PhaseField expression", func() {
It("should generate PhaseField expression with custom field path", func() {
h := defkit.Health()
expr := h.PhaseField("status.currentPhase", "Active", "Ready")
Expect(expr).NotTo(BeNil())
policy := h.Policy(expr)
Expect(policy).To(ContainSubstring("isHealth:"))
Expect(policy).To(ContainSubstring("context.output.status.currentPhase"))
Expect(policy).To(ContainSubstring("Active"))
Expect(policy).To(ContainSubstring("Ready"))
})
It("should create Exists expression", func() {
It("should generate Exists expression that checks field != _|_", func() {
h := defkit.Health()
expr := h.Exists("status.loadBalancer.ingress")
Expect(expr).NotTo(BeNil())
policy := h.Policy(expr)
Expect(policy).To(ContainSubstring("isHealth:"))
Expect(policy).To(ContainSubstring("status.loadBalancer.ingress"))
Expect(policy).To(ContainSubstring("!= _|_"))
})
It("should create NotExists expression", func() {
It("should generate NotExists expression that checks field == _|_", func() {
h := defkit.Health()
expr := h.NotExists("status.error")
Expect(expr).NotTo(BeNil())
policy := h.Policy(expr)
Expect(policy).To(ContainSubstring("isHealth:"))
Expect(policy).To(ContainSubstring("status.error"))
Expect(policy).To(ContainSubstring("== _|_"))
})
It("should create And expression", func() {
It("should generate And expression combining multiple conditions", func() {
h := defkit.Health()
expr1 := h.Condition("Ready").IsTrue()
expr2 := h.Condition("Synced").IsTrue()
and := h.And(expr1, expr2)
Expect(and).NotTo(BeNil())
policy := h.Policy(and)
Expect(policy).To(ContainSubstring("Ready"))
Expect(policy).To(ContainSubstring("Synced"))
Expect(policy).To(ContainSubstring("&&"))
})
It("should create Or expression", func() {
It("should generate Or expression combining multiple conditions", func() {
h := defkit.Health()
expr1 := h.Phase("Running")
expr2 := h.Phase("Succeeded")
or := h.Or(expr1, expr2)
Expect(or).NotTo(BeNil())
policy := h.Policy(or)
Expect(policy).To(ContainSubstring("Running"))
Expect(policy).To(ContainSubstring("Succeeded"))
Expect(policy).To(ContainSubstring("||"))
})
It("should create Not expression", func() {
It("should generate Not expression negating a condition", func() {
h := defkit.Health()
expr := h.Condition("Stalled").IsTrue()
not := h.Not(expr)
Expect(not).NotTo(BeNil())
policy := h.Policy(not)
Expect(policy).To(ContainSubstring("Stalled"))
Expect(policy).To(ContainSubstring("!"))
})
It("should create Always expression", func() {
It("should generate Always expression as isHealth: true", func() {
h := defkit.Health()
expr := h.Always()
Expect(expr).NotTo(BeNil())
policy := h.Policy(expr)
Expect(policy).To(ContainSubstring("isHealth: true"))
})
It("should create AllTrue expression", func() {
It("should generate AllTrue expression checking multiple conditions are True", func() {
h := defkit.Health()
expr := h.AllTrue("Ready", "Synced", "Available")
Expect(expr).NotTo(BeNil())
policy := h.Policy(expr)
Expect(policy).To(ContainSubstring("Ready"))
Expect(policy).To(ContainSubstring("Synced"))
Expect(policy).To(ContainSubstring("Available"))
Expect(policy).To(ContainSubstring("&&"))
})
It("should create AnyTrue expression", func() {
It("should generate AnyTrue expression checking any condition is True", func() {
h := defkit.Health()
expr := h.AnyTrue("Ready", "Available")
Expect(expr).NotTo(BeNil())
policy := h.Policy(expr)
Expect(policy).To(ContainSubstring("Ready"))
Expect(policy).To(ContainSubstring("Available"))
Expect(policy).To(ContainSubstring("||"))
})
It("should set health condition with HealthyWhenExpr", func() {
It("should set health condition with HealthyWhenExpr and generate correct CUE", func() {
h := defkit.Health()
expr := h.Condition("Ready").IsTrue()
h.HealthyWhenExpr(expr)
cue := h.Build()
Expect(cue).NotTo(BeEmpty())
Expect(cue).To(ContainSubstring("isHealth:"))
Expect(cue).To(ContainSubstring("Ready"))
Expect(cue).To(ContainSubstring(`"True"`))
})
It("should generate policy from expression", func() {
It("should generate policy with correct isHealth expression from Condition", func() {
h := defkit.Health()
expr := h.Condition("Ready").IsTrue()
policy := h.Policy(expr)
Expect(policy).NotTo(BeEmpty())
Expect(policy).To(ContainSubstring("isHealth:"))
Expect(policy).To(ContainSubstring("Ready"))
Expect(policy).To(ContainSubstring(`"True"`))
})
})
})
+76 -57
View File
@@ -42,10 +42,12 @@ type TraitDefinition struct {
baseDefinition // embedded common fields (name, description, params, template, etc.)
appliesToWorkloads []string // e.g., ["deployments.apps", "statefulsets.apps"]
conflictsWith []string // traits that conflict with this one
conflictsWithSet bool // true when ConflictsWith() was explicitly called
podDisruptive bool // whether applying this trait causes pod restart
stage string // "PreDispatch" or "PostDispatch" (default: "")
templateBlock string // raw CUE for template: block only (uses fluent API for header)
labels map[string]string // metadata labels for the trait definition
workloadRefPath *string // workloadRefPath attribute (nil = omit, pointer to distinguish from empty)
}
// NewTrait creates a new TraitDefinition builder.
@@ -75,6 +77,7 @@ func (t *TraitDefinition) AppliesTo(workloads ...string) *TraitDefinition {
// ConflictsWith specifies traits that cannot be used together with this trait.
func (t *TraitDefinition) ConflictsWith(traits ...string) *TraitDefinition {
t.conflictsWithSet = true
t.conflictsWith = append(t.conflictsWith, traits...)
return t
}
@@ -85,6 +88,12 @@ func (t *TraitDefinition) PodDisruptive(disruptive bool) *TraitDefinition {
return t
}
// WorkloadRefPath sets the workloadRefPath attribute for the trait.
func (t *TraitDefinition) WorkloadRefPath(path string) *TraitDefinition {
t.workloadRefPath = &path
return t
}
// Stage sets the trait application stage.
// Use "PreDispatch" for traits that must run before dispatch,
// "PostDispatch" for traits that run after (e.g., creating Services).
@@ -412,13 +421,17 @@ func (g *TraitCUEGenerator) GenerateFullDefinition(t *TraitDefinition) string {
sb.WriteString(fmt.Sprintf("%stype: \"trait\"\n", g.indent))
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
// Write labels block only if there are labels (omit empty labels to match original CUE format)
if len(t.GetLabels()) > 0 {
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
for k, v := range t.GetLabels() {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
// Write labels block when Labels() was explicitly called (nil check distinguishes unset from empty)
if t.labels != nil {
if len(t.labels) > 0 {
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
for k, v := range t.labels {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
} else {
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
}
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, t.GetDescription()))
@@ -439,10 +452,8 @@ func (g *TraitCUEGenerator) GenerateFullDefinition(t *TraitDefinition) string {
func (g *TraitCUEGenerator) writeAttributes(sb *strings.Builder, t *TraitDefinition, depth int) {
indent := strings.Repeat(g.indent, depth)
// podDisruptive (only emit when true, false is the default)
if t.IsPodDisruptive() {
sb.WriteString(fmt.Sprintf("%spodDisruptive: %v\n", indent, t.IsPodDisruptive()))
}
// podDisruptive (always emit — vela CUE always includes this attribute)
sb.WriteString(fmt.Sprintf("%spodDisruptive: %v\n", indent, t.IsPodDisruptive()))
// stage (if set)
if t.GetStage() != "" {
@@ -458,13 +469,17 @@ func (g *TraitCUEGenerator) writeAttributes(sb *strings.Builder, t *TraitDefinit
sb.WriteString(fmt.Sprintf("%sappliesToWorkloads: [%s]\n", indent, strings.Join(workloads, ", ")))
}
// conflictsWith (if set)
if len(t.GetConflictsWith()) > 0 {
conflicts := make([]string, len(t.GetConflictsWith()))
for i, c := range t.GetConflictsWith() {
conflicts[i] = fmt.Sprintf("%q", c)
// conflictsWith (emit when explicitly set, even if empty)
if t.conflictsWithSet {
if len(t.GetConflictsWith()) > 0 {
conflicts := make([]string, len(t.GetConflictsWith()))
for i, c := range t.GetConflictsWith() {
conflicts[i] = fmt.Sprintf("%q", c)
}
sb.WriteString(fmt.Sprintf("%sconflictsWith: [%s]\n", indent, strings.Join(conflicts, ", ")))
} else {
sb.WriteString(fmt.Sprintf("%sconflictsWith: []\n", indent))
}
sb.WriteString(fmt.Sprintf("%sconflictsWith: [%s]\n", indent, strings.Join(conflicts, ", ")))
}
// status (customStatus and healthPolicy)
@@ -490,6 +505,11 @@ func (g *TraitCUEGenerator) writeAttributes(sb *strings.Builder, t *TraitDefinit
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
// workloadRefPath (if explicitly set)
if t.workloadRefPath != nil {
sb.WriteString(fmt.Sprintf("%sworkloadRefPath: %q\n", indent, *t.workloadRefPath))
}
}
// GenerateTemplate generates the template block for a trait.
@@ -1098,7 +1118,7 @@ func (g *TraitCUEGenerator) generateParameterBlock(t *TraitDefinition, depth int
if dynMap.GetValueTypeUnion() != "" {
typeStr = dynMap.GetValueTypeUnion()
} else {
typeStr = cueTypeForParamType(dynMap.GetValueType())
typeStr = cueTypeStr(dynMap.GetValueType())
}
sb.WriteString(fmt.Sprintf("%sparameter: [string]: %s\n", indent, typeStr))
return sb.String()
@@ -1123,28 +1143,6 @@ func (g *TraitCUEGenerator) generateParameterBlock(t *TraitDefinition, depth int
return sb.String()
}
// cueTypeForParamType converts a ParamType to its CUE type string (standalone function).
func cueTypeForParamType(pt ParamType) string {
switch pt {
case ParamTypeString:
return string(ParamTypeString)
case ParamTypeInt:
return "int"
case ParamTypeBool:
return "bool"
case ParamTypeFloat:
return "float"
case ParamTypeArray:
return "[...]"
case ParamTypeMap:
return "{...}"
case ParamTypeStruct:
return "{...}"
default:
return "_"
}
}
// writePatchContainerPattern generates the complete PatchContainer pattern in CUE.
// This generates the #PatchParams helper, PatchContainer definition, patch block,
// parameter schema, and errs aggregation.
@@ -1153,10 +1151,16 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
innerIndent := strings.Repeat(g.indent, depth+1)
deepIndent := strings.Repeat(g.indent, depth+2)
// Generate #PatchParams helper definition
// Determine the helper type name (default: "PatchParams")
paramsTypeName := config.ParamsTypeName
if paramsTypeName == "" {
paramsTypeName = "PatchParams"
}
// Generate helper definition
if config.CustomParamsBlock != "" {
// Use custom params block (for complex schemas like startup-probe)
sb.WriteString(fmt.Sprintf("%s#PatchParams: {\n", indent))
sb.WriteString(fmt.Sprintf("%s#%s: {\n", indent, paramsTypeName))
sb.WriteString(fmt.Sprintf("%s// +usage=Specify the name of the target container, if not set, use the component name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%scontainerName: *\"\" | string\n", innerIndent))
// Write each line of custom params block with proper indentation
@@ -1165,7 +1169,7 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
sb.WriteString(fmt.Sprintf("%s#PatchParams: {\n", indent))
sb.WriteString(fmt.Sprintf("%s#%s: {\n", indent, paramsTypeName))
sb.WriteString(fmt.Sprintf("%s// +usage=Specify the name of the target container, if not set, use the component name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%scontainerName: *\"\" | string\n", innerIndent))
@@ -1192,11 +1196,10 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
sb.WriteString(fmt.Sprintf("%sPatchContainer: {\n", indent))
sb.WriteString(fmt.Sprintf("%s_params: #PatchParams\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s_params: #%s\n", innerIndent, paramsTypeName))
sb.WriteString(fmt.Sprintf("%sname: _params.containerName\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s_baseContainers: context.output.spec.template.spec.containers\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s_matchContainers_: [for _container_ in _baseContainers if _container_.name == name {_container_}]\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s_baseContainer: *_|_ | {...}\n", innerIndent))
// Container not found error
sb.WriteString(fmt.Sprintf("%sif len(_matchContainers_) == 0 {\n", innerIndent))
@@ -1276,10 +1279,18 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
sb.WriteString(fmt.Sprintf("%sif parameter.%s != _|_ {\n", innerIndent, multiParam))
sb.WriteString(fmt.Sprintf("%s\t// +patchKey=name\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\tcontainers: [for c in parameter.%s {\n", innerIndent, multiParam))
sb.WriteString(fmt.Sprintf("%s\t\tif c.containerName == \"\" {\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\t\terr: \"containerName must be set for %s\"\n", innerIndent, multiParam))
checkField := "containerName"
if config.MultiContainerCheckField != "" {
checkField = config.MultiContainerCheckField
}
errMsg := fmt.Sprintf("containerName must be set for %s", multiParam)
if config.MultiContainerErrMsg != "" {
errMsg = config.MultiContainerErrMsg
}
sb.WriteString(fmt.Sprintf("%s\t\tif c.%s == \"\" {\n", innerIndent, checkField))
sb.WriteString(fmt.Sprintf("%s\t\t\terr: \"%s\"\n", innerIndent, errMsg))
sb.WriteString(fmt.Sprintf("%s\t\t}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\tif c.containerName != \"\" {\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\tif c.%s != \"\" {\n", innerIndent, checkField))
sb.WriteString(fmt.Sprintf("%s\t\t\tPatchContainer & {_params: c}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t\t}\n", innerIndent))
sb.WriteString(fmt.Sprintf("%s\t}]\n", innerIndent))
@@ -1336,16 +1347,20 @@ func (g *TraitCUEGenerator) writePatchContainerPattern(sb *strings.Builder, conf
}
}
case config.AllowMultiple && multiParam != "":
sb.WriteString(fmt.Sprintf("%sparameter: *#PatchParams | close({\n", indent))
defaultMarker := "*"
if config.NoDefaultDisjunction {
defaultMarker = ""
}
sb.WriteString(fmt.Sprintf("%sparameter: %s#%s | close({\n", indent, defaultMarker, paramsTypeName))
containersDesc := config.ContainersDescription
if containersDesc == "" {
containersDesc = "Specify the settings for multiple containers"
}
sb.WriteString(fmt.Sprintf("%s// +usage=%s\n", innerIndent, containersDesc))
sb.WriteString(fmt.Sprintf("%s%s: [...#PatchParams]\n", innerIndent, multiParam))
sb.WriteString(fmt.Sprintf("%s%s: [...#%s]\n", innerIndent, multiParam, paramsTypeName))
sb.WriteString(fmt.Sprintf("%s})\n", indent))
default:
sb.WriteString(fmt.Sprintf("%sparameter: #PatchParams\n", indent))
sb.WriteString(fmt.Sprintf("%sparameter: #%s\n", indent, paramsTypeName))
}
// Generate errs aggregation
@@ -1457,7 +1472,7 @@ func (g *TraitCUEGenerator) writePatchContainerGroup(sb *strings.Builder, group
// propagating _|_ when the parameter is unset. Fields with defaults or value-based
// conditions (like '!= ""') always have a value and can be mapped unconditionally.
func (g *TraitCUEGenerator) writePatchParamMapping(sb *strings.Builder, field PatchContainerField, indent string, prefix string) {
if field.Condition == "!= _|_" && field.ParamDefault == "" {
if field.Condition == "!= _|_" && field.ParamDefault == "" && field.ParamType == "" {
sb.WriteString(fmt.Sprintf("%sif %s%s %s {\n", indent, prefix, field.ParamName, field.Condition))
sb.WriteString(fmt.Sprintf("%s\t%s: %s%s\n", indent, field.ParamName, prefix, field.ParamName))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
@@ -1497,13 +1512,17 @@ func (g *TraitCUEGenerator) GenerateDefinitionWithRawTemplate(t *TraitDefinition
sb.WriteString(fmt.Sprintf("%stype: \"trait\"\n", g.indent))
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
// Write labels block only if there are labels (omit empty labels to match original CUE format)
if len(t.GetLabels()) > 0 {
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
for k, v := range t.GetLabels() {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
// Write labels block when Labels() was explicitly called (nil check distinguishes unset from empty)
if t.labels != nil {
if len(t.labels) > 0 {
sb.WriteString(fmt.Sprintf("%slabels: {\n", g.indent))
for k, v := range t.labels {
sb.WriteString(fmt.Sprintf("%s\t%q: %q\n", g.indent, k, v))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
} else {
sb.WriteString(fmt.Sprintf("%slabels: {}\n", g.indent))
}
sb.WriteString(fmt.Sprintf("%s}\n", g.indent))
}
sb.WriteString(fmt.Sprintf("%sdescription: %q\n", g.indent, t.GetDescription()))
+245 -2
View File
@@ -187,8 +187,8 @@ parameter: #PatchParams
Expect(cue).To(ContainSubstring(`scaler: {`))
Expect(cue).To(ContainSubstring(`type: "trait"`))
Expect(cue).To(ContainSubstring(`description: "Scale workloads"`))
// podDisruptive: false is not emitted (it's the default)
Expect(cue).NotTo(ContainSubstring(`podDisruptive: false`))
// podDisruptive is always emitted
Expect(cue).To(ContainSubstring(`podDisruptive: false`))
Expect(cue).To(ContainSubstring(`appliesToWorkloads: ["deployments.apps"]`))
})
@@ -1375,4 +1375,247 @@ template: {
Expect(cue).To(ContainSubstring("host?: string"))
})
})
Context("podDisruptive always emitted", func() {
It("should emit podDisruptive: true when set to true", func() {
trait := defkit.NewTrait("disruptive").
Description("Disruptive trait").
AppliesTo("deployments.apps").
PodDisruptive(true)
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("podDisruptive: true"))
})
It("should emit podDisruptive: false when set to false", func() {
trait := defkit.NewTrait("nondisruptive").
Description("Non-disruptive trait").
AppliesTo("deployments.apps").
PodDisruptive(false)
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("podDisruptive: false"))
})
It("should emit podDisruptive: false when not explicitly set", func() {
trait := defkit.NewTrait("default-disruptive").
Description("Default trait").
AppliesTo("deployments.apps")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("podDisruptive: false"))
})
})
Context("ConflictsWith empty list emission", func() {
It("should emit conflictsWith with values when set", func() {
trait := defkit.NewTrait("conflicts-with-values").
Description("Trait with conflicts").
AppliesTo("deployments.apps").
ConflictsWith("scaler", "hpa")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`conflictsWith: ["scaler", "hpa"]`))
})
It("should emit conflictsWith: [] when explicitly set with no values", func() {
trait := defkit.NewTrait("conflicts-empty").
Description("Trait with empty conflicts").
AppliesTo("deployments.apps").
ConflictsWith()
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("conflictsWith: []"))
})
It("should not emit conflictsWith when never called", func() {
trait := defkit.NewTrait("no-conflicts").
Description("Trait without conflicts").
AppliesTo("deployments.apps")
cue := trait.ToCue()
Expect(cue).NotTo(ContainSubstring("conflictsWith"))
})
})
Context("Labels nil vs empty emission", func() {
It("should emit labels with values when set", func() {
trait := defkit.NewTrait("labels-values").
Description("Trait with labels").
AppliesTo("deployments.apps").
Labels(map[string]string{"ui-hidden": "true"})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("labels:"))
Expect(cue).To(ContainSubstring(`"ui-hidden": "true"`))
})
It("should emit labels: {} when explicitly set to empty map", func() {
trait := defkit.NewTrait("labels-empty").
Description("Trait with empty labels").
AppliesTo("deployments.apps").
Labels(map[string]string{})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("labels: {}"))
})
It("should not emit labels when never called", func() {
trait := defkit.NewTrait("no-labels").
Description("Trait without labels").
AppliesTo("deployments.apps")
cue := trait.ToCue()
Expect(cue).NotTo(ContainSubstring("labels:"))
})
})
Context("WorkloadRefPath attribute", func() {
It("should emit workloadRefPath when set to empty string", func() {
trait := defkit.NewTrait("wlref-empty").
Description("Trait with empty workloadRefPath").
AppliesTo("deployments.apps").
WorkloadRefPath("")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`workloadRefPath: ""`))
})
It("should emit workloadRefPath when set to a path", func() {
trait := defkit.NewTrait("wlref-path").
Description("Trait with workloadRefPath").
AppliesTo("deployments.apps").
WorkloadRefPath("spec.workloadRef")
cue := trait.ToCue()
Expect(cue).To(ContainSubstring(`workloadRefPath: "spec.workloadRef"`))
})
It("should not emit workloadRefPath when never called", func() {
trait := defkit.NewTrait("no-wlref").
Description("Trait without wlref path").
AppliesTo("deployments.apps")
cue := trait.ToCue()
Expect(cue).NotTo(ContainSubstring("workloadRefPath"))
})
})
Context("PatchContainer ParamsTypeName", func() {
It("should use default PatchParams when ParamsTypeName is empty", func() {
trait := defkit.NewTrait("default-params-name").
Description("Test default params type name").
AppliesTo("deployments.apps").
PodDisruptive(true).
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
PatchFields: defkit.PatchFields(
defkit.PatchField("image").Strategy("retainKeys"),
),
})
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("#PatchParams: {"))
Expect(cue).To(ContainSubstring("_params: #PatchParams"))
Expect(cue).NotTo(ContainSubstring("#StartupProbeParams"))
})
It("should use custom ParamsTypeName when set", func() {
trait := defkit.NewTrait("custom-params-name").
Description("Test custom params type name").
AppliesTo("deployments.apps").
PodDisruptive(true).
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
MultiContainerParam: "probes",
ParamsTypeName: "StartupProbeParams",
CustomParamsBlock: "initialDelaySeconds: *0 | int",
PatchFields: defkit.PatchFields(
defkit.PatchField("initialDelaySeconds").Int().IsSet().Default("0"),
),
})
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("#StartupProbeParams: {"))
Expect(cue).To(ContainSubstring("_params: #StartupProbeParams"))
Expect(cue).To(ContainSubstring("parameter: *#StartupProbeParams | close({"))
Expect(cue).To(ContainSubstring("probes: [...#StartupProbeParams]"))
Expect(cue).NotTo(ContainSubstring("#PatchParams"))
})
})
Context("PatchContainer NoDefaultDisjunction", func() {
It("should include * default marker by default", func() {
trait := defkit.NewTrait("default-disjunction").
Description("Test default disjunction").
AppliesTo("deployments.apps").
PodDisruptive(true).
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
PatchFields: defkit.PatchFields(
defkit.PatchField("image").Strategy("retainKeys"),
),
})
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("parameter: *#PatchParams | close({"))
})
It("should omit * default marker when NoDefaultDisjunction is true", func() {
trait := defkit.NewTrait("no-default-disjunction").
Description("Test no default disjunction").
AppliesTo("deployments.apps").
PodDisruptive(true).
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
AllowMultiple: true,
ContainersParam: "containers",
NoDefaultDisjunction: true,
PatchFields: defkit.PatchFields(
defkit.PatchField("image").Strategy("retainKeys"),
),
})
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("parameter: #PatchParams | close({"))
Expect(cue).NotTo(ContainSubstring("parameter: *#PatchParams"))
})
})
Context("PatchContainer no _baseContainer singular", func() {
It("should use _baseContainers (plural) not _baseContainer (singular)", func() {
trait := defkit.NewTrait("base-containers-test").
Description("Test base containers plural").
AppliesTo("deployments.apps").
PodDisruptive(true).
Template(func(tpl *defkit.Template) {
tpl.UsePatchContainer(defkit.PatchContainerConfig{
ContainerNameParam: "containerName",
DefaultToContextName: true,
PatchFields: defkit.PatchFields(
defkit.PatchField("image"),
),
})
})
cue := trait.ToCue()
Expect(cue).To(ContainSubstring("_baseContainers: context.output.spec.template.spec.containers"))
Expect(cue).NotTo(ContainSubstring("_baseContainer:"))
})
})
})