diff --git a/pkg/definition/defkit/array_builder.go b/pkg/definition/defkit/array_builder.go new file mode 100644 index 000000000..5c455c853 --- /dev/null +++ b/pkg/definition/defkit/array_builder.go @@ -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} +} diff --git a/pkg/definition/defkit/collections.go b/pkg/definition/defkit/collections.go index caea9f230..95d1fd47c 100644 --- a/pkg/definition/defkit/collections.go +++ b/pkg/definition/defkit/collections.go @@ -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 { diff --git a/pkg/definition/defkit/component.go b/pkg/definition/defkit/component.go index 8541f2cd4..d717c7593 100644 --- a/pkg/definition/defkit/component.go +++ b/pkg/definition/defkit/component.go @@ -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. diff --git a/pkg/definition/defkit/cuegen.go b/pkg/definition/defkit/cuegen.go index 27008d03f..34b4ff23c 100644 --- a/pkg/definition/defkit/cuegen.go +++ b/pkg/definition/defkit/cuegen.go @@ -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)) } } diff --git a/pkg/definition/defkit/expr.go b/pkg/definition/defkit/expr.go index d8f216949..59a250693 100644 --- a/pkg/definition/defkit/expr.go +++ b/pkg/definition/defkit/expr.go @@ -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 } diff --git a/pkg/definition/defkit/param.go b/pkg/definition/defkit/param.go index 8ff39f0be..0244d79da 100644 --- a/pkg/definition/defkit/param.go +++ b/pkg/definition/defkit/param.go @@ -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 diff --git a/pkg/definition/defkit/trait.go b/pkg/definition/defkit/trait.go index 8c0b1d57f..2e61bd707 100644 --- a/pkg/definition/defkit/trait.go +++ b/pkg/definition/defkit/trait.go @@ -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))