Feat: Implement array builder and enhance CUE generation for X-Definitions (#7039)

* feat: implement array builder and enhance CUE generation with conditional outputs

Signed-off-by: Amit Singh <singhamitch@outlook.com>
Co-authored-by: Amit Singh <singhamitch@outlook.com>
Co-authored-by: Amit Singh <singhamitch@outlook.com>
Signed-off-by: Amit Singh <singhamitch@outlook.com>

* feat: enhance CUE generation for struct fields and add support for array element types

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Amit Singh <singhamitch@outlook.com>
Signed-off-by: Amit Singh <singhamitch@outlook.com>

* feat: refactor CUE generation logic for struct fields and streamline resource output handling

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

---------

Signed-off-by: Amit Singh <singhamitch@outlook.com>
Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Co-authored-by: Amit Singh <singhamitch@outlook.com>
This commit is contained in:
Ayush Kumar
2026-02-12 19:57:56 +05:30
committed by GitHub
parent 29a26d5650
commit 1396884a44
7 changed files with 827 additions and 78 deletions

View File

@@ -0,0 +1,283 @@
/*
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
// entryKind describes what kind of array entry this is.
type entryKind int
const (
entryStatic entryKind = iota // always-present item
entryConditional // conditional item (if cond { item })
entryForEach // iterated item (for v in source { item })
)
// arrayEntry is a single entry in an ArrayBuilder.
type arrayEntry struct {
kind entryKind
element *ArrayElement // the item fields (for static, conditional, forEach)
cond Condition // for conditional entries
source Value // for forEach entries (iteration source)
guard Condition // for forEach entries (optional guard: if source != _|_)
itemBuilder *ItemBuilder // for forEachWith entries (complex per-item logic)
}
// ArrayBuilder builds CUE arrays with static items, conditional items, and for-each items.
// This is the core type for building arrays where some items are always present,
// some are conditional, and some come from iteration.
//
// Example:
//
// NewArray().
// Item(cpuMetric).
// ItemIf(mem.IsSet(), memMetric).
// ForEachGuarded(podCustomMetrics.IsSet(), podCustomMetrics, customMetric)
type ArrayBuilder struct {
entries []arrayEntry
}
func (a *ArrayBuilder) value() {}
func (a *ArrayBuilder) expr() {}
// NewArray creates a new ArrayBuilder.
func NewArray() *ArrayBuilder {
return &ArrayBuilder{
entries: make([]arrayEntry, 0),
}
}
// Item adds an always-present item to the array.
func (a *ArrayBuilder) Item(elem *ArrayElement) *ArrayBuilder {
a.entries = append(a.entries, arrayEntry{
kind: entryStatic,
element: elem,
})
return a
}
// ItemIf adds a conditional item to the array.
// The item is only included when the condition is true.
func (a *ArrayBuilder) ItemIf(cond Condition, elem *ArrayElement) *ArrayBuilder {
a.entries = append(a.entries, arrayEntry{
kind: entryConditional,
element: elem,
cond: cond,
})
return a
}
// ForEach adds an iterated item to the array.
// For each element in source, an item is created using the element template.
// In the element template, use Reference("m.field") to reference iteration variable fields.
func (a *ArrayBuilder) ForEach(source Value, elem *ArrayElement) *ArrayBuilder {
a.entries = append(a.entries, arrayEntry{
kind: entryForEach,
element: elem,
source: source,
})
return a
}
// ForEachGuarded adds a guarded iterated item to the array.
// The guard condition (typically source.IsSet()) wraps the for loop.
// Generates: if guard for m in source { item }
func (a *ArrayBuilder) ForEachGuarded(guard Condition, source Value, elem *ArrayElement) *ArrayBuilder {
a.entries = append(a.entries, arrayEntry{
kind: entryForEach,
element: elem,
source: source,
guard: guard,
})
return a
}
// Entries returns all entries in the array builder.
func (a *ArrayBuilder) Entries() []arrayEntry { return a.entries }
// entryForEachWith indicates a complex iterated item using an ItemBuilder.
const entryForEachWith entryKind = 3
// ForEachWith adds a complex iterated item to the array, using an ItemBuilder
// callback for per-item operations like conditionals, let bindings, and defaults.
// Uses "v" as the default iteration variable name.
func (a *ArrayBuilder) ForEachWith(source Value, fn func(item *ItemBuilder)) *ArrayBuilder {
return a.ForEachWithVar("v", source, fn)
}
// ForEachWithVar is like ForEachWith but allows specifying the iteration variable name.
func (a *ArrayBuilder) ForEachWithVar(varName string, 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,
itemBuilder: ib,
})
return a
}
// --- ItemBuilder ---
// itemOp is a single operation recorded by the ItemBuilder.
type itemOp interface {
isItemOp()
}
// setOp records an unconditional field assignment.
type setOp struct {
field string
value Value
}
func (setOp) isItemOp() {}
// ifBlockOp records a conditional block of nested operations.
type ifBlockOp struct {
cond Condition
body []itemOp
}
func (ifBlockOp) isItemOp() {}
// letOp records a private field binding (_name: value).
type letOp struct {
name string
value Value
}
func (letOp) isItemOp() {}
// setDefaultOp records a CUE default value: field: *defValue | typeName.
type setDefaultOp struct {
field string
defValue Value
typeName string
}
func (setDefaultOp) isItemOp() {}
// ItemBuilder records per-item operations for complex ForEach iterations.
// It supports field assignment, conditionals, let bindings, and CUE default values.
type ItemBuilder struct {
varName string
ops []itemOp
}
// Var returns a reference builder for the iteration variable.
// Use v.Field("port") to reference v.port in CUE.
func (b *ItemBuilder) Var() *IterVarBuilder {
return &IterVarBuilder{varName: b.varName}
}
// Set records an unconditional field assignment.
func (b *ItemBuilder) Set(field string, value Value) {
b.ops = append(b.ops, setOp{field: field, value: value})
}
// If records a conditional block of operations.
func (b *ItemBuilder) If(cond Condition, fn func()) {
outer := b.ops
b.ops = make([]itemOp, 0)
fn()
inner := b.ops
b.ops = outer
b.ops = append(b.ops, ifBlockOp{cond: cond, body: inner})
}
// IfSet records a conditional block that executes when the iteration variable's field is set.
// Generates CUE: if v.field != _|_ { ... }
func (b *ItemBuilder) IfSet(field string, fn func()) {
cond := &IterFieldExistsCondition{varName: b.varName, field: field}
b.If(cond, fn)
}
// IfNotSet records a conditional block that executes when the iteration variable's field is NOT set.
// Generates CUE: if v.field == _|_ { ... }
func (b *ItemBuilder) IfNotSet(field string, fn func()) {
cond := &IterFieldExistsCondition{varName: b.varName, field: field, negate: true}
b.If(cond, fn)
}
// Let records a private field binding and returns a reference to it.
// Generates CUE: _name: value
func (b *ItemBuilder) Let(name string, value Value) Value {
b.ops = append(b.ops, letOp{name: name, value: value})
return &IterLetRef{name: name}
}
// SetDefault records a CUE default value assignment.
// Generates CUE: field: *defValue | typeName
func (b *ItemBuilder) SetDefault(field string, defValue Value, typeName string) {
b.ops = append(b.ops, setDefaultOp{field: field, defValue: defValue, typeName: typeName})
}
// FieldExists returns a Condition that checks if the iteration variable's field is set.
// Generates CUE: v.field != _|_
func (b *ItemBuilder) FieldExists(field string) Condition {
return &IterFieldExistsCondition{varName: b.varName, field: field}
}
// FieldNotExists returns a Condition that checks if the iteration variable's field is NOT set.
// Generates CUE: v.field == _|_
func (b *ItemBuilder) FieldNotExists(field string) Condition {
return &IterFieldExistsCondition{varName: b.varName, field: field, negate: true}
}
// Ops returns the recorded operations.
func (b *ItemBuilder) Ops() []itemOp { return b.ops }
// VarName returns the iteration variable name.
func (b *ItemBuilder) VarName() string { return b.varName }
// IterVarBuilder provides access to iteration variable fields.
type IterVarBuilder struct {
varName string
}
// Ref returns a Value referencing the iteration variable itself.
// Generates CUE: v (or whatever the variable name is).
// Useful when iterating over primitive arrays (e.g., [...int]).
func (v *IterVarBuilder) Ref() *IterVarRef {
return &IterVarRef{varName: v.varName}
}
// Field returns a Value referencing the iteration variable's field.
// v.Field("port") generates CUE: v.port
func (v *IterVarBuilder) Field(name string) *IterFieldRef {
return &IterFieldRef{varName: v.varName, field: name}
}
// ArrayConcatValue represents an array concatenation: left + right.
// Used for expressions like [items] + parameter.extraVolumeMounts.
type ArrayConcatValue struct {
left Value
right Value
}
func (a *ArrayConcatValue) value() {}
func (a *ArrayConcatValue) expr() {}
// Left returns the left operand.
func (a *ArrayConcatValue) Left() Value { return a.left }
// Right returns the right operand.
func (a *ArrayConcatValue) Right() Value { return a.right }
// ArrayConcat creates an array concatenation expression.
// Generates CUE: left + right
func ArrayConcat(left, right Value) *ArrayConcatValue {
return &ArrayConcatValue{left: left, right: right}
}

View File

@@ -28,6 +28,7 @@ import (
type CollectionOp struct {
source Value
ops []collectionOperation
guard Condition // optional guard: wraps entire comprehension in if cond
}
type collectionOperation interface {
@@ -60,6 +61,21 @@ func F(name string) FieldRef {
func (c *CollectionOp) expr() {}
func (c *CollectionOp) value() {}
// Guard adds a guard condition that wraps the entire comprehension.
// When the guard is set, the generated CUE is:
//
// [if guard for v in source if filter {v}]
//
// This is used when the source may not exist and needs a
// protective condition (e.g., if parameter.privileges != _|_).
func (c *CollectionOp) Guard(cond Condition) *CollectionOp {
c.guard = cond
return c
}
// GetGuard returns the guard condition, if any.
func (c *CollectionOp) GetGuard() Condition { return c.guard }
// Filter keeps only items matching the predicate.
// Usage: .Filter(Field("expose").Eq(true))
func (c *CollectionOp) Filter(pred Predicate) *CollectionOp {

View File

@@ -296,6 +296,9 @@ type Template struct {
concatHelpers []*ConcatHelper // list.Concat helpers
dedupeHelpers []*DedupeHelper // Deduplication helpers
// Output groups for grouped conditional outputs
outputGroups []*outputGroup
// Trait-specific fields
patch *PatchResource // Patch operations for traits
patchStrategy string // Patch strategy (e.g., "retainKeys", "jsonMergePatch")
@@ -416,6 +419,48 @@ func (t *Template) GetOutput() *Resource { return t.output }
// GetOutputs returns all auxiliary resources.
func (t *Template) GetOutputs() map[string]*Resource { return t.outputs }
// outputGroup represents a group of outputs that share a common condition.
type outputGroup struct {
cond Condition
outputs map[string]*Resource
}
// OutputGroup is a builder for adding outputs within a grouped condition.
type OutputGroup struct {
tpl *Template
cond Condition
outputs map[string]*Resource
}
// Add adds a named resource to the output group.
func (g *OutputGroup) Add(name string, r *Resource) *OutputGroup {
g.outputs[name] = r
return g
}
// OutputsGroupIf groups multiple outputs under a single condition.
// This generates one `if cond { ... }` block containing all grouped outputs.
func (t *Template) OutputsGroupIf(cond Condition, fn func(g *OutputGroup)) {
group := &OutputGroup{
tpl: t,
cond: cond,
outputs: make(map[string]*Resource),
}
fn(group)
if t.outputGroups == nil {
t.outputGroups = make([]*outputGroup, 0)
}
t.outputGroups = append(t.outputGroups, &outputGroup{
cond: cond,
outputs: group.outputs,
})
}
// GetOutputGroups returns all output groups.
func (t *Template) GetOutputGroups() []*outputGroup {
return t.outputGroups
}
// --- Patch methods for traits ---
// Patch returns the PatchResource builder for traits.

View File

@@ -21,6 +21,15 @@ import (
"strings"
)
// 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 {
if strings.ContainsAny(name, "-./") {
return fmt.Sprintf("%q", name)
}
return name
}
// CUEGenerator generates CUE definitions from Go component definitions.
type CUEGenerator struct {
indent string
@@ -247,12 +256,8 @@ func (g *CUEGenerator) GenerateFullDefinition(c *ComponentDefinition) string {
sb.WriteString(")\n\n")
}
// Write component header - quote names that contain special characters
name := c.GetName()
if strings.ContainsAny(name, "-./") {
name = fmt.Sprintf("%q", name)
}
sb.WriteString(fmt.Sprintf("%s: {\n", name))
// Write component header
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))
@@ -467,9 +472,31 @@ func (g *CUEGenerator) writeStructFieldForHelper(sb *strings.Builder, f *StructF
// Get CUE type for the field type
fieldType := g.cueTypeForParamType(f.FieldType())
if f.HasDefault() {
sb.WriteString(fmt.Sprintf("%s%s: *%v | %s\n", indent, name, formatCUEValue(f.GetDefault()), fieldType))
} else {
switch {
case f.HasDefault():
enumValues := f.GetEnumValues()
switch {
case len(enumValues) > 0:
// Enum with default: *"default" | "other1" | "other2"
defaultStr := fmt.Sprintf("%v", f.GetDefault())
var enumParts []string
enumParts = append(enumParts, fmt.Sprintf("*%s", formatCUEValue(f.GetDefault())))
for _, v := range enumValues {
if v != defaultStr {
enumParts = append(enumParts, fmt.Sprintf("%q", v))
}
}
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, name, strings.Join(enumParts, " | ")))
case f.FieldType() == ParamTypeArray && f.GetElementType() != "":
elemCUE := g.cueTypeForParamType(f.GetElementType())
sb.WriteString(fmt.Sprintf("%s%s: *%v | [...%s]\n", indent, name, formatCUEValue(f.GetDefault()), elemCUE))
default:
sb.WriteString(fmt.Sprintf("%s%s: *%v | %s\n", indent, name, formatCUEValue(f.GetDefault()), fieldType))
}
case f.FieldType() == ParamTypeArray && f.GetElementType() != "":
elemCUE := g.cueTypeForParamType(f.GetElementType())
sb.WriteString(fmt.Sprintf("%s%s%s: [...%s]\n", indent, name, optional, elemCUE))
default:
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, fieldType))
}
}
@@ -833,7 +860,8 @@ func (g *CUEGenerator) writeResourceOutput(sb *strings.Builder, name string, res
indent = strings.Repeat(g.indent, depth)
}
sb.WriteString(fmt.Sprintf("%s%s: {\n", indent, name))
quotedName := cueLabel(name)
sb.WriteString(fmt.Sprintf("%s%s: {\n", indent, quotedName))
innerIndent := strings.Repeat(g.indent, depth+1)
// Handle conditional apiVersion or static apiVersion
@@ -1229,22 +1257,8 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
case *HelperVar:
// Return reference to the helper by name
return val.Name()
case *StringParam:
return "parameter." + val.Name()
case *IntParam:
return "parameter." + val.Name()
case *BoolParam:
return "parameter." + val.Name()
case *FloatParam:
return "parameter." + val.Name()
case *ArrayParam:
return "parameter." + val.Name()
case *MapParam:
return "parameter." + val.Name()
case *StringKeyMapParam:
return "parameter." + val.Name()
case *EnumParam:
return "parameter." + val.Name()
case *StringParam, *IntParam, *BoolParam, *FloatParam, *ArrayParam, *MapParam, *StringKeyMapParam, *EnumParam:
return "parameter." + v.(Param).Name()
case *DynamicMapParam:
// Dynamic map parameters reference just "parameter"
return "parameter"
@@ -1273,6 +1287,10 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
case *LetRef:
// Return reference to a let binding variable
return val.Name()
case *ArrayBuilder:
return g.arrayBuilderToCUE(val, 1)
case *ArrayConcatValue:
return g.valueToCUE(val.Left()) + " + " + g.valueToCUE(val.Right())
case *ListComprehension:
// Return list comprehension CUE
return g.listComprehensionToCUE(val)
@@ -1288,6 +1306,20 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
case *ParamFieldRef:
// Reference to a field within a struct parameter: parameter.name.field.path
return fmt.Sprintf("parameter.%s.%s", val.ParamName(), val.FieldPath())
case *InterpolatedString:
return g.interpolatedStringToCUE(val)
case *PlusExpr:
parts := make([]string, len(val.Parts()))
for i, p := range val.Parts() {
parts[i] = g.valueToCUE(p)
}
return strings.Join(parts, " + ")
case *IterVarRef:
return val.VarName()
case *IterFieldRef:
return fmt.Sprintf("%s.%s", val.VarName(), val.FieldName())
case *IterLetRef:
return val.RefName()
default:
// Try to get name from Param interface
if p, ok := v.(Param); ok {
@@ -1306,6 +1338,41 @@ func (g *CUEGenerator) cueFuncToCUE(fn *CUEFunc) string {
return fmt.Sprintf("%s.%s(%s)", fn.Package(), fn.Function(), strings.Join(args, ", "))
}
// interpolatedStringToCUE converts an InterpolatedString to CUE string interpolation.
// Literal string values are inlined directly. All other values are wrapped in \(...).
// Example: Interpolation(vela.Namespace(), Lit(":"), name) → "\(context.namespace):\(parameter.name)"
func (g *CUEGenerator) interpolatedStringToCUE(is *InterpolatedString) string {
var sb strings.Builder
sb.WriteString(`"`)
for _, part := range is.Parts() {
if lit, ok := part.(*Literal); ok {
if s, ok := lit.Val().(string); ok {
sb.WriteString(s)
continue
}
}
sb.WriteString(`\(`)
sb.WriteString(g.valueToCUE(part))
sb.WriteString(`)`)
}
sb.WriteString(`"`)
return sb.String()
}
// valueToCUEAtDepth converts a Value to CUE syntax with depth-aware indentation.
// For types that support depth (ArrayBuilder, ArrayConcatValue), it uses the given depth.
// For all other types, it falls back to the standard valueToCUE.
func (g *CUEGenerator) valueToCUEAtDepth(v Value, depth int) string {
switch val := v.(type) {
case *ArrayBuilder:
return g.arrayBuilderToCUE(val, depth)
case *ArrayConcatValue:
return g.valueToCUEAtDepth(val.Left(), depth) + " + " + g.valueToCUE(val.Right())
default:
return g.valueToCUE(v)
}
}
// arrayElementToCUE converts an ArrayElement to CUE syntax.
// Uses default depth of 1 for backwards compatibility.
func (g *CUEGenerator) arrayElementToCUE(elem *ArrayElement) string {
@@ -1336,10 +1403,106 @@ func (g *CUEGenerator) arrayElementToCUEWithDepth(elem *ArrayElement, depth int)
sb.WriteString(fmt.Sprintf("%s}\n", innerIndent))
}
}
// Write patchKey-annotated fields (nested patchKey inside array elements)
for _, pkf := range elem.PatchKeyFields() {
sb.WriteString(fmt.Sprintf("%s// +patchKey=%s\n", innerIndent, pkf.key))
valStr := g.valueToCUEAtDepth(pkf.value, depth+1)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", innerIndent, pkf.field, valStr))
}
sb.WriteString(fmt.Sprintf("%s}", indent))
return sb.String()
}
// arrayBuilderToCUE converts an ArrayBuilder to CUE syntax.
// Generates: [{static}, if cond {{conditional}}, if guard for m in source {iterated}]
func (g *CUEGenerator) arrayBuilderToCUE(ab *ArrayBuilder, depth int) string {
var sb strings.Builder
indent := strings.Repeat(g.indent, depth)
innerIndent := strings.Repeat(g.indent, depth+1)
deepIndent := strings.Repeat(g.indent, depth+2)
sb.WriteString("[\n")
for _, entry := range ab.Entries() {
switch entry.kind {
case entryStatic:
sb.WriteString(innerIndent)
sb.WriteString(g.arrayElementToCUEWithDepth(entry.element, depth+1))
sb.WriteString(",\n")
case entryConditional:
condStr := g.conditionToCUE(entry.cond)
sb.WriteString(fmt.Sprintf("%sif %s {\n", innerIndent, condStr))
sb.WriteString(deepIndent)
sb.WriteString(g.arrayElementToCUEWithDepth(entry.element, depth+2))
sb.WriteString("\n")
sb.WriteString(fmt.Sprintf("%s},\n", innerIndent))
case entryForEach:
sourceStr := g.valueToCUE(entry.source)
if entry.guard != nil {
guardStr := g.conditionToCUE(entry.guard)
sb.WriteString(fmt.Sprintf("%sif %s for m in %s {\n", innerIndent, guardStr, sourceStr))
} else {
sb.WriteString(fmt.Sprintf("%sfor m in %s {\n", innerIndent, sourceStr))
}
// Write each field from the element template
for key, val := range entry.element.Fields() {
valStr := g.valueToCUE(val)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", deepIndent, key, valStr))
}
// Write conditional operations
for _, op := range entry.element.Ops() {
if setIf, ok := op.(*SetIfOp); ok {
condStr := g.conditionToCUE(setIf.Cond())
valStr := g.valueToCUE(setIf.Value())
cuePath := strings.ReplaceAll(setIf.Path(), ".", ": ")
sb.WriteString(fmt.Sprintf("%sif %s {\n", deepIndent, condStr))
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", deepIndent, cuePath, valStr))
sb.WriteString(fmt.Sprintf("%s}\n", deepIndent))
}
}
sb.WriteString(fmt.Sprintf("%s},\n", innerIndent))
case entryForEachWith:
sourceStr := g.valueToCUE(entry.source)
sb.WriteString(fmt.Sprintf("%sfor %s in %s {\n", innerIndent, entry.itemBuilder.VarName(), sourceStr))
g.writeItemBuilderOps(&sb, entry.itemBuilder.Ops(), depth+2)
sb.WriteString(fmt.Sprintf("%s},\n", innerIndent))
}
}
sb.WriteString(fmt.Sprintf("%s]", indent))
return sb.String()
}
// writeItemBuilderOps writes the CUE for ItemBuilder operations.
func (g *CUEGenerator) writeItemBuilderOps(sb *strings.Builder, ops []itemOp, depth int) {
indent := strings.Repeat(g.indent, depth)
for _, op := range ops {
switch o := op.(type) {
case setOp:
valStr := g.valueToCUE(o.value)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, o.field, valStr))
case ifBlockOp:
condStr := g.conditionToCUE(o.cond)
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condStr))
g.writeItemBuilderOps(sb, o.body, depth+1)
sb.WriteString(fmt.Sprintf("%s}\n", indent))
case letOp:
valStr := g.valueToCUE(o.value)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, o.name, valStr))
case setDefaultOp:
defStr := g.valueToCUE(o.defValue)
sb.WriteString(fmt.Sprintf("%s%s: *%s | %s\n", indent, o.field, defStr, o.typeName))
}
}
}
// collectionOpToCUE generates CUE for a collection operation.
func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
sourceStr := g.valueToCUE(col.Source())
@@ -1360,30 +1523,53 @@ func (g *CUEGenerator) collectionOpToCUE(col *CollectionOp) string {
}
}
// Build the list comprehension for Map operations
// Check if there's a Map operation
hasMap := false
for _, op := range ops {
if _, ok := op.(*mapOp); ok {
hasMap = true
break
}
}
// Build the list comprehension
var sb strings.Builder
sb.WriteString("[for v in ")
sb.WriteString("[")
// Add guard condition if present (wraps entire comprehension)
if guard := col.GetGuard(); guard != nil {
guardStr := g.conditionToCUE(guard)
sb.WriteString("if ")
sb.WriteString(guardStr)
sb.WriteString(" ")
}
sb.WriteString("for v in ")
sb.WriteString(sourceStr)
if filterCondition != "" {
sb.WriteString(" if ")
sb.WriteString(filterCondition)
}
sb.WriteString(" {\n")
sb.WriteString("\t\t\t\t{\n")
// Check operations for Map
for _, op := range ops {
if mOp, ok := op.(*mapOp); ok {
for fieldName, fieldVal := range mOp.mappings {
valStr := g.fieldValueToCUE(fieldVal)
sb.WriteString(fmt.Sprintf("\t\t\t\t\t%s: %s\n", fieldName, valStr))
if hasMap {
// Map operations: render mapped fields in a struct
sb.WriteString(" {\n")
sb.WriteString("\t\t\t\t{\n")
for _, op := range ops {
if mOp, ok := op.(*mapOp); ok {
for fieldName, fieldVal := range mOp.mappings {
valStr := g.fieldValueToCUE(fieldVal)
sb.WriteString(fmt.Sprintf("\t\t\t\t\t%s: %s\n", fieldName, valStr))
}
}
}
sb.WriteString("\t\t\t\t}\n")
sb.WriteString("\t\t\t}]")
} else {
// Filter-only: pass through the iteration variable
sb.WriteString(" {v}]")
}
sb.WriteString("\t\t\t\t}\n")
sb.WriteString("\t\t\t}]")
return sb.String()
}
@@ -1660,12 +1846,7 @@ func (g *CUEGenerator) conditionToCUE(cond Condition) string {
case *FalsyCondition:
return fmt.Sprintf("!parameter.%s", c.ParamName())
case *InCondition:
// Generate: parameter.name == val1 || parameter.name == val2 || ...
parts := make([]string, len(c.Values()))
for i, v := range c.Values() {
parts[i] = fmt.Sprintf("parameter.%s == %s", c.ParamName(), formatCUEValue(v))
}
return strings.Join(parts, " || ")
return g.inConditionToCUE(c)
case *StringContainsCondition:
return fmt.Sprintf(`strings.Contains(parameter.%s, %q)`, c.ParamName(), c.Substr())
case *StringMatchesCondition:
@@ -1722,6 +1903,16 @@ func (g *CUEGenerator) conditionToCUE(cond Condition) string {
// Check if len(source) != 0
sourceStr := g.valueToCUE(c.Source())
return fmt.Sprintf("len(%s) != 0", sourceStr)
case *LenValueCondition:
// Check len(source) op n for arbitrary values
sourceStr := g.valueToCUE(c.Source())
return fmt.Sprintf("len(%s) %s %d", sourceStr, c.Op(), c.Length())
case *IterFieldExistsCondition:
// Check if iteration variable field exists/not exists
if c.IsNegated() {
return fmt.Sprintf("%s.%s == _|_", c.VarName(), c.FieldName())
}
return fmt.Sprintf("%s.%s != _|_", c.VarName(), c.FieldName())
case *PathExistsCondition:
// Check if a path exists: path != _|_
return fmt.Sprintf("%s != _|_", c.Path())
@@ -1751,6 +1942,16 @@ func (g *CUEGenerator) conditionToCUE(cond Condition) string {
}
}
// inConditionToCUE converts an InCondition to CUE syntax.
// Generates: parameter.name == val1 || parameter.name == val2 || ...
func (g *CUEGenerator) inConditionToCUE(c *InCondition) string {
parts := make([]string, len(c.Values()))
for i, v := range c.Values() {
parts[i] = fmt.Sprintf("parameter.%s == %s", c.ParamName(), formatCUEValue(v))
}
return strings.Join(parts, " || ")
}
// exprToCUE converts an Expr to CUE syntax.
func (g *CUEGenerator) exprToCUE(e Expr) string {
if v, ok := e.(Value); ok {
@@ -2092,6 +2293,14 @@ func (g *CUEGenerator) writeMapParam(sb *strings.Builder, p *MapParam, indent, n
g.writeParam(sb, field, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else if valType := p.ValueType(); valType != "" {
// Typed map: [string]: type
cueType := g.cueTypeForParamType(valType)
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))
}
} else {
// Generic object
sb.WriteString(fmt.Sprintf("%s%s%s: {...}\n", indent, name, optional))
@@ -2133,15 +2342,38 @@ func (g *CUEGenerator) writeStructField(sb *strings.Builder, f *StructField, dep
fieldType := g.cueTypeForParamType(f.FieldType())
if nested := f.GetNested(); nested != nil {
nested := f.GetNested()
switch {
case nested != nil:
sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional))
for _, nestedField := range nested.GetFields() {
g.writeStructField(sb, nestedField, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else if f.HasDefault() {
sb.WriteString(fmt.Sprintf("%s%s: *%v | %s\n", indent, name, formatCUEValue(f.GetDefault()), fieldType))
} else {
case f.HasDefault():
enumValues := f.GetEnumValues()
switch {
case len(enumValues) > 0:
// Enum with default: *"default" | "other1" | "other2"
defaultStr := fmt.Sprintf("%v", f.GetDefault())
var enumParts []string
enumParts = append(enumParts, fmt.Sprintf("*%s", formatCUEValue(f.GetDefault())))
for _, v := range enumValues {
if v != defaultStr {
enumParts = append(enumParts, fmt.Sprintf("%q", v))
}
}
sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, name, strings.Join(enumParts, " | ")))
case f.FieldType() == ParamTypeArray && f.GetElementType() != "":
elemCUE := g.cueTypeForParamType(f.GetElementType())
sb.WriteString(fmt.Sprintf("%s%s: *%v | [...%s]\n", indent, name, formatCUEValue(f.GetDefault()), elemCUE))
default:
sb.WriteString(fmt.Sprintf("%s%s: *%v | %s\n", indent, name, formatCUEValue(f.GetDefault()), fieldType))
}
case f.FieldType() == ParamTypeArray && f.GetElementType() != "":
elemCUE := g.cueTypeForParamType(f.GetElementType())
sb.WriteString(fmt.Sprintf("%s%s%s: [...%s]\n", indent, name, optional, elemCUE))
default:
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, fieldType))
}
}

View File

@@ -459,11 +459,21 @@ func PathExists(path string) *PathExistsCondition {
// --- Array Element Struct ---
// patchKeyField represents a field within an ArrayElement that has a patchKey annotation.
// This is used for nested patchKey annotations inside array elements,
// e.g., volumeMounts inside a containers element.
type patchKeyField struct {
field string // field name (e.g., "volumeMounts")
key string // patchKey value (e.g., "name")
value Value // the array value
}
// ArrayElement represents a single element in an array patch.
// Used for building array values with struct elements.
type ArrayElement struct {
fields map[string]Value
ops []ResourceOp // nested operations for complex structs
fields map[string]Value
ops []ResourceOp // nested operations for complex structs
patchKeyFields []patchKeyField // nested patchKey-annotated array fields
}
func (a *ArrayElement) expr() {}
@@ -489,12 +499,28 @@ func (a *ArrayElement) SetIf(cond Condition, key string, value Value) *ArrayElem
return a
}
// PatchKeyField adds a patchKey-annotated array field to the array element.
// This generates within the element:
//
// // +patchKey=key
// field: value
//
// Used for nested patchKey annotations inside array elements, e.g.,
// volumeMounts with patchKey=name inside a containers element.
func (a *ArrayElement) PatchKeyField(field string, key string, value Value) *ArrayElement {
a.patchKeyFields = append(a.patchKeyFields, patchKeyField{field: field, key: key, value: value})
return a
}
// Fields returns all fields set on this element.
func (a *ArrayElement) Fields() map[string]Value { return a.fields }
// Ops returns any conditional operations.
func (a *ArrayElement) Ops() []ResourceOp { return a.ops }
// PatchKeyFields returns the patchKey-annotated fields.
func (a *ArrayElement) PatchKeyFields() []patchKeyField { return a.patchKeyFields }
// --- Reference Expressions ---
// Ref creates a raw reference expression.
@@ -582,6 +608,68 @@ func ForEachMap() *ForEachMapOp {
}
}
// --- CUE String Interpolation ---
// InterpolatedString represents a CUE string interpolation expression.
// Literal string parts are inlined, Value parts are wrapped in \(...).
//
// Example:
//
// Interpolation(vela.Namespace(), Lit(":"), name)
// // Generates: "\(context.namespace):\(parameter.name)"
type InterpolatedString struct {
parts []Value
}
func (i *InterpolatedString) value() {}
func (i *InterpolatedString) expr() {}
// Parts returns the interpolation parts.
func (i *InterpolatedString) Parts() []Value { return i.parts }
// Interpolation creates a CUE string interpolation expression.
// Literal string values are inlined directly. All other values are
// wrapped in \(...) interpolation syntax.
func Interpolation(parts ...Value) *InterpolatedString {
return &InterpolatedString{parts: parts}
}
// --- LenValueCondition ---
// LenValueCondition checks the length of an arbitrary Value (not just a parameter).
// This extends LenCondition to work with let variables and other expressions.
// Generates: len(source) op n
type LenValueCondition struct {
baseCondition
source Value
op string // ==, !=, <, <=, >, >=
length int
}
// Source returns the source value being measured.
func (c *LenValueCondition) Source() Value { return c.source }
// Op returns the comparison operator.
func (c *LenValueCondition) Op() string { return c.op }
// Length returns the length to compare against.
func (c *LenValueCondition) Length() int { return c.length }
// LenGt creates a condition: len(source) > n.
func LenGt(source Value, n int) *LenValueCondition {
return &LenValueCondition{source: source, op: ">", length: n}
}
// LenGe creates a condition: len(source) >= n.
func LenGe(source Value, n int) *LenValueCondition {
return &LenValueCondition{source: source, op: ">=", length: n}
}
// LenEq creates a condition: len(source) == n.
func LenEq(source Value, n int) *LenValueCondition {
return &LenValueCondition{source: source, op: "==", length: n}
}
// Over specifies the source to iterate over.
func (f *ForEachMapOp) Over(source string) *ForEachMapOp {
f.source = source
@@ -612,3 +700,80 @@ func (f *ForEachMapOp) WithBody(ops ...ResourceOp) *ForEachMapOp {
f.body = append(f.body, ops...)
return f
}
// PlusExpr represents a + operator between multiple values.
// Generates CUE: a + b + c
// Works for string concatenation, array concatenation, etc.
type PlusExpr struct {
parts []Value
}
func (p *PlusExpr) value() {}
func (p *PlusExpr) expr() {}
// Parts returns the operands.
func (p *PlusExpr) Parts() []Value { return p.parts }
// Plus creates a + expression between values.
// Generates CUE: parts[0] + parts[1] + ...
func Plus(parts ...Value) *PlusExpr {
return &PlusExpr{parts: parts}
}
// IterFieldRef references a field on the iteration variable.
// Generates CUE: v.fieldName (where v is the iteration variable).
type IterFieldRef struct {
varName string
field string
}
func (r *IterFieldRef) value() {}
func (r *IterFieldRef) expr() {}
// VarName returns the iteration variable name.
func (r *IterFieldRef) VarName() string { return r.varName }
// FieldName returns the field name.
func (r *IterFieldRef) FieldName() string { return r.field }
// IterVarRef references the iteration variable itself (not a field on it).
// Generates CUE: v (where v is the iteration variable).
type IterVarRef struct {
varName string
}
func (r *IterVarRef) value() {}
func (r *IterVarRef) expr() {}
// VarName returns the iteration variable name.
func (r *IterVarRef) VarName() string { return r.varName }
// IterLetRef references a let binding defined inside an iteration body.
// Generates CUE: _name (a private CUE identifier).
type IterLetRef struct {
name string
}
func (r *IterLetRef) value() {}
func (r *IterLetRef) expr() {}
// RefName returns the binding name.
func (r *IterLetRef) RefName() string { return r.name }
// IterFieldExistsCondition checks if an iteration variable field exists.
// Generates CUE: v.field != _|_ (or v.field == _|_ when negated).
type IterFieldExistsCondition struct {
baseCondition
varName string
field string
negate bool
}
// VarName returns the iteration variable name.
func (c *IterFieldExistsCondition) VarName() string { return c.varName }
// FieldName returns the field name.
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 }

View File

@@ -801,6 +801,7 @@ type StructField struct {
nested *StructParam // for nested structs
schemaRef string // reference to a helper definition (e.g., "HealthProbe")
enumValues []string // allowed enum values for string fields
elementType ParamType // for array fields: element type (e.g., ParamTypeString for [...string])
}
// Field creates a new struct field definition.
@@ -883,6 +884,15 @@ func (f *StructField) Enum(values ...string) *StructField {
// GetEnumValues returns the allowed enum values.
func (f *StructField) GetEnumValues() []string { return f.enumValues }
// ArrayOf sets the element type for array fields (e.g., ParamTypeString for [...string]).
func (f *StructField) ArrayOf(elemType ParamType) *StructField {
f.elementType = elemType
return f
}
// GetElementType returns the element type for array fields.
func (f *StructField) GetElementType() ParamType { return f.elementType }
// StructParam represents a structured parameter with named fields.
type StructParam struct {
baseParam

View File

@@ -407,12 +407,8 @@ func (g *TraitCUEGenerator) GenerateFullDefinition(t *TraitDefinition) string {
sb.WriteString(")\n\n")
}
// Write trait header - quote names with special characters
name := t.GetName()
if strings.ContainsAny(name, "-./") {
name = fmt.Sprintf("%q", name)
}
sb.WriteString(fmt.Sprintf("%s: {\n", name))
// Write trait header
sb.WriteString(fmt.Sprintf("%s: {\n", cueLabel(t.GetName())))
sb.WriteString(fmt.Sprintf("%stype: \"trait\"\n", g.indent))
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))
@@ -556,6 +552,12 @@ func (g *TraitCUEGenerator) writeUnifiedTemplate(sb *strings.Builder, t *TraitDe
return
}
// Render let bindings before patch/outputs
for _, lb := range tpl.GetLetBindings() {
exprStr := gen.valueToCUE(lb.Expr())
sb.WriteString(fmt.Sprintf("%slet %s = %s\n", indent, lb.Name(), exprStr))
}
// Generate patch block if present
if tpl.HasPatch() {
// Write patch strategy comment if set
@@ -572,7 +574,16 @@ func (g *TraitCUEGenerator) writeUnifiedTemplate(sb *strings.Builder, t *TraitDe
if outputs := tpl.GetOutputs(); len(outputs) > 0 {
sb.WriteString(fmt.Sprintf("%soutputs: {\n", indent))
for name, res := range outputs {
g.writeResourceOutput(sb, gen, name, res, depth+1)
g.writeTraitResourceOutput(sb, gen, name, res, depth+1)
}
// Render output groups (multiple outputs under one condition)
for _, group := range tpl.GetOutputGroups() {
condStr := gen.conditionToCUE(group.cond)
sb.WriteString(fmt.Sprintf("%s\tif %s {\n", indent, condStr))
for gName, gRes := range group.outputs {
g.writeTraitResourceOutput(sb, gen, gName, gRes, depth+2)
}
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
@@ -817,20 +828,11 @@ func (g *TraitCUEGenerator) writePatchKeyOp(sb *strings.Builder, gen *CUEGenerat
}
}
// writeResourceOutput writes a resource as CUE (reusing component generation logic).
func (g *TraitCUEGenerator) writeResourceOutput(sb *strings.Builder, gen *CUEGenerator, name string, res *Resource, depth int) {
indent := strings.Repeat(g.indent, depth)
innerIndent := strings.Repeat(g.indent, depth+1)
sb.WriteString(fmt.Sprintf("%s%s: {\n", indent, name))
sb.WriteString(fmt.Sprintf("%sapiVersion: %q\n", innerIndent, res.APIVersion()))
sb.WriteString(fmt.Sprintf("%skind: %q\n", innerIndent, res.Kind()))
// Build field tree and write it
tree := gen.buildFieldTree(res.Ops())
gen.writeFieldTree(sb, tree, depth+1)
sb.WriteString(fmt.Sprintf("%s}\n", indent))
// writeTraitResourceOutput writes a resource as CUE for trait outputs.
// This handles OutputsIf conditions and VersionConditionals, which the old
// writeResourceOutput method did not support.
func (g *TraitCUEGenerator) writeTraitResourceOutput(sb *strings.Builder, gen *CUEGenerator, name string, res *Resource, depth int) {
gen.writeResourceOutput(sb, name, res, res.outputCondition, depth)
}
// generateParameterBlock generates the parameter schema for the trait.
@@ -1225,12 +1227,8 @@ func (g *TraitCUEGenerator) GenerateDefinitionWithRawTemplate(t *TraitDefinition
sb.WriteString(")\n\n")
}
// Write trait header - quote names with special characters
name := t.GetName()
if strings.ContainsAny(name, "-./") {
name = fmt.Sprintf("%q", name)
}
sb.WriteString(fmt.Sprintf("%s: {\n", name))
// Write trait header
sb.WriteString(fmt.Sprintf("%s: {\n", cueLabel(t.GetName())))
sb.WriteString(fmt.Sprintf("%stype: \"trait\"\n", g.indent))
sb.WriteString(fmt.Sprintf("%sannotations: {}\n", g.indent))