Feat: extend fluent builder API validator patterns (#7092)

* Feat: Add NotEmpty and NegativePattern constraints to StringParam; implement Closed for MapParam

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

* feat: add validation support for array and map parameters

- Introduced validators for ArrayParam and MapParam, allowing for cross-field validation within structured parameters.
- Added NonEmpty validation for ArrayParam to ensure arrays are not empty.
- Implemented ConditionalStructOp for conditional struct generation based on specified conditions.
- Created a new Validator type for defining validation rules with optional guard conditions.
- Added tests for various validation scenarios, including mutual exclusion and conditional parameters.
- Enhanced the CUE generation logic to incorporate new validation features and conditional struct handling.

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

* feat: extend fluent API with new scoped field conditions and improve validation checks

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

* feat: enhance ArrayParam with NotEmpty constraint and update ScopedField documentation

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

* feat: rename ScopedField to LocalField for improved clarity in condition building

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

* feat: refactor local field conditions to use RegexMatch and streamline condition building

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

* feat: simplify condition handling by removing unused comparison types and refactoring NotCondition usage

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

* refactor: remove unused raw CUE block handling from baseDefinition and ComponentDefinition

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

* test: update condition handling in parameter tests to use NotExpr and Cond methods

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

* refactor: remove negative pattern handling from StringParam and related tests

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

* feat: add support for emitting raw header blocks in template generation

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

* refactor: remove non-empty check from ArrayParam and update related tests

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

* refactor: convert parameter constraint tests to use Ginkgo and Gomega for improved readability and maintainability

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

* feat: extend fluent APIs for OAM with new CUE generation tests and condition evaluations

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

* refactor: clean up whitespace in component, cuegen, expr, param, and resource files

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

* feat: enhance CUE generation by adding support for new expression types and iterator references

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

* refactor: remove unnecessary whitespace in cuegen.go

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

* refactor: rename LenOf to LenOfExpr for clarity in comparison methods

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

* feat: enhance CUE generation and validation for string arrays in ArrayParam

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

* ci: retrigger checks

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

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
Signed-off-by: Ayush Kumar <aykumar@guidewire.com>
Co-authored-by: Ayush Kumar <aykumar@guidewire.com>
This commit is contained in:
Ayush Kumar
2026-04-09 14:25:06 +01:00
committed by GitHub
co-authored by Ayush Kumar
parent 54197b4721
commit 012a134829
20 changed files with 2828 additions and 689 deletions
+24
View File
@@ -47,6 +47,10 @@ type baseDefinition struct {
helperDefinitions []HelperDefinition
rawCUE string
imports []string
// validators holds top-level parameter validators
validators []*Validator
// conditionalParamBlocks holds conditional parameter blocks
conditionalParamBlocks []*ConditionalParamBlock
// Placement constraints for cluster-aware definition deployment
runOn []placement.Condition
notRunOn []placement.Condition
@@ -64,6 +68,26 @@ func (b *baseDefinition) addParams(params ...Param) {
b.params = append(b.params, params...)
}
// addValidators appends validators to the definition.
func (b *baseDefinition) addValidators(validators ...*Validator) {
b.validators = append(b.validators, validators...)
}
// GetValidators returns the top-level validators.
func (b *baseDefinition) GetValidators() []*Validator {
return b.validators
}
// addConditionalParamBlock appends a conditional parameter block.
func (b *baseDefinition) addConditionalParamBlock(block *ConditionalParamBlock) {
b.conditionalParamBlocks = append(b.conditionalParamBlocks, block)
}
// GetConditionalParamBlocks returns the conditional parameter blocks.
func (b *baseDefinition) GetConditionalParamBlocks() []*ConditionalParamBlock {
return b.conditionalParamBlocks
}
// setTemplate sets the template function.
func (b *baseDefinition) setTemplate(fn func(tpl *Template)) {
b.template = fn
+27
View File
@@ -30,6 +30,7 @@ import (
type ComponentDefinition struct {
baseDefinition // embedded common fields (name, description, params, template, etc.)
workload WorkloadType
omitWorkloadType bool // when true, suppresses the auto-generated workload.type field in CUE
labels map[string]string // metadata labels for the component definition
childResourceKinds []common.ChildResourceKind
podSpecPath string
@@ -64,6 +65,18 @@ func (c *ComponentDefinition) Workload(apiVersion, kind string) *ComponentDefini
return c
}
// OmitWorkloadType suppresses the auto-generated workload.type field in the CUE output.
// Use this when the vela source CUE does not include a workload type field.
func (c *ComponentDefinition) OmitWorkloadType() *ComponentDefinition {
c.omitWorkloadType = true
return c
}
// IsOmitWorkloadType returns whether workload type should be suppressed in CUE output.
func (c *ComponentDefinition) IsOmitWorkloadType() bool {
return c.omitWorkloadType
}
// AutodetectWorkload sets the workload type to "autodetects.core.oam.dev".
// This is used for components where the workload type is auto-detected at runtime
// rather than being statically defined.
@@ -197,6 +210,20 @@ func (c *ComponentDefinition) PodSpecPath(path string) *ComponentDefinition {
// GetPodSpecPath returns the pod spec path.
func (c *ComponentDefinition) GetPodSpecPath() string { return c.podSpecPath }
// Validators adds top-level parameter validators to the component.
// These validators are emitted inside the parameter: { ... } block as _validate* variables.
func (c *ComponentDefinition) Validators(validators ...*Validator) *ComponentDefinition {
c.addValidators(validators...)
return c
}
// ConditionalParams adds a conditional parameter block to the component.
// This allows parameters to change shape based on a discriminator value.
func (c *ComponentDefinition) ConditionalParams(block *ConditionalParamBlock) *ComponentDefinition {
c.addConditionalParamBlock(block)
return c
}
// 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 {
+94
View File
@@ -866,6 +866,100 @@ var _ = Describe("ComponentDefinition", func() {
})
})
Context("OmitWorkloadType", func() {
It("should default to false", func() {
c := defkit.NewComponent("test").Workload("apps/v1", "Deployment")
Expect(c.IsOmitWorkloadType()).To(BeFalse())
})
It("should set omit workload type flag", func() {
c := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
OmitWorkloadType()
Expect(c.IsOmitWorkloadType()).To(BeTrue())
})
It("should suppress workload type in CUE output", func() {
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
OmitWorkloadType().
Template(func(tpl *defkit.Template) {
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", defkit.Lit("test")))
})
cue := comp.ToCue()
Expect(cue).To(ContainSubstring(`kind: "Deployment"`))
Expect(cue).NotTo(ContainSubstring(`type: "deployments.apps"`))
Expect(cue).NotTo(ContainSubstring(`type: "autodetects.core.oam.dev"`))
})
It("should include workload type when not omitted", func() {
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Template(func(tpl *defkit.Template) {
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", defkit.Lit("test")))
})
cue := comp.ToCue()
Expect(cue).To(ContainSubstring(`type: "deployments.apps"`))
})
})
Context("Validators on Component", func() {
It("should store validators via Validators method", func() {
v1 := defkit.Validate("a").WithName("_a")
v2 := defkit.Validate("b").WithName("_b")
c := defkit.NewComponent("test").Validators(v1, v2)
Expect(c.GetValidators()).To(HaveLen(2))
Expect(c.GetValidators()[0]).To(Equal(v1))
Expect(c.GetValidators()[1]).To(Equal(v2))
})
It("should accumulate validators across multiple calls", func() {
c := defkit.NewComponent("test").
Validators(defkit.Validate("a").WithName("_a")).
Validators(defkit.Validate("b").WithName("_b"))
Expect(c.GetValidators()).To(HaveLen(2))
})
It("should return empty when no validators set", func() {
c := defkit.NewComponent("test")
Expect(c.GetValidators()).To(BeEmpty())
})
})
Context("ConditionalParams on Component", func() {
It("should store conditional param blocks", func() {
block := defkit.ConditionalParams(
defkit.WhenParam(defkit.Bool("x").Eq(true)).Params(defkit.String("a")),
)
c := defkit.NewComponent("test").ConditionalParams(block)
Expect(c.GetConditionalParamBlocks()).To(HaveLen(1))
Expect(c.GetConditionalParamBlocks()[0]).To(Equal(block))
})
It("should accumulate blocks across multiple calls", func() {
b1 := defkit.ConditionalParams(
defkit.WhenParam(defkit.Bool("x").Eq(true)).Params(defkit.String("a")),
)
b2 := defkit.ConditionalParams(
defkit.WhenParam(defkit.Bool("y").Eq(true)).Params(defkit.String("b")),
)
c := defkit.NewComponent("test").
ConditionalParams(b1).
ConditionalParams(b2)
Expect(c.GetConditionalParamBlocks()).To(HaveLen(2))
})
It("should return empty when no blocks set", func() {
c := defkit.NewComponent("test")
Expect(c.GetConditionalParamBlocks()).To(BeEmpty())
})
})
Context("Repeated generation stability", func() {
It("should produce identical CUE across 20 runs", func() {
build := func() string {
@@ -0,0 +1,87 @@
/*
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
// ConditionalParamBlock represents a set of conditional parameter branches.
// Each branch has a guard condition and emits different parameters when active.
//
// Example:
//
// ConditionalParams(
// WhenParam(existingResources.Eq(false)).Params(
// defkit.Bool("forceDestroy").Default(false),
// ),
// WhenParam(existingResources.Eq(true)).Params(
// defkit.Bool("forceDestroy").Optional(),
// ),
// )
//
// Emits:
//
// if existingResources == false {
// forceDestroy: *false | bool
// }
// if existingResources == true {
// forceDestroy?: bool
// }
type ConditionalParamBlock struct {
branches []*ConditionalBranch
}
// ConditionalParams creates a new conditional parameter block from branches.
func ConditionalParams(branches ...*ConditionalBranch) *ConditionalParamBlock {
return &ConditionalParamBlock{branches: branches}
}
// Branches returns all conditional branches.
func (b *ConditionalParamBlock) Branches() []*ConditionalBranch {
return b.branches
}
// ConditionalBranch represents a single branch in a conditional parameter block.
// It contains a guard condition, parameters to emit when active, and optional validators.
type ConditionalBranch struct {
condition Condition
params []Param
validators []*Validator
}
// WhenParam creates a new conditional branch with the given guard condition.
func WhenParam(cond Condition) *ConditionalBranch {
return &ConditionalBranch{condition: cond}
}
// Params sets the parameters emitted when this branch's condition is true.
func (b *ConditionalBranch) Params(params ...Param) *ConditionalBranch {
b.params = append(b.params, params...)
return b
}
// Validators adds validators emitted inside this branch's conditional block.
func (b *ConditionalBranch) Validators(validators ...*Validator) *ConditionalBranch {
b.validators = append(b.validators, validators...)
return b
}
// Condition returns the branch's guard condition.
func (b *ConditionalBranch) Condition() Condition { return b.condition }
// GetParams returns the branch's parameters.
func (b *ConditionalBranch) GetParams() []Param { return b.params }
// GetValidators returns the branch's validators.
func (b *ConditionalBranch) GetValidators() []*Validator { return b.validators }
@@ -0,0 +1,111 @@
/*
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("ConditionalParam", func() {
Context("ConditionalParamBlock", func() {
It("should create a block with branches", func() {
b1 := defkit.WhenParam(defkit.Bool("x").Eq(true)).Params(defkit.String("a"))
b2 := defkit.WhenParam(defkit.Bool("x").Eq(false)).Params(defkit.String("b"))
block := defkit.ConditionalParams(b1, b2)
Expect(block.Branches()).To(HaveLen(2))
Expect(block.Branches()[0]).To(Equal(b1))
Expect(block.Branches()[1]).To(Equal(b2))
})
It("should create a block with a single branch", func() {
b1 := defkit.WhenParam(defkit.Bool("x").Eq(true)).Params(defkit.String("a"))
block := defkit.ConditionalParams(b1)
Expect(block.Branches()).To(HaveLen(1))
})
It("should create an empty block with no branches", func() {
block := defkit.ConditionalParams()
Expect(block.Branches()).To(BeEmpty())
})
})
Context("WhenParam / ConditionalBranch", func() {
It("should create a branch with condition", func() {
cond := defkit.Bool("flag").Eq(true)
branch := defkit.WhenParam(cond)
Expect(branch.Condition()).To(Equal(cond))
Expect(branch.GetParams()).To(BeEmpty())
Expect(branch.GetValidators()).To(BeEmpty())
})
It("should chain Params and return them via GetParams", func() {
p1 := defkit.String("name")
p2 := defkit.Int("count")
branch := defkit.WhenParam(defkit.Bool("x").Eq(true)).
Params(p1, p2)
Expect(branch.GetParams()).To(HaveLen(2))
Expect(branch.GetParams()[0]).To(Equal(p1))
Expect(branch.GetParams()[1]).To(Equal(p2))
})
It("should accumulate params across multiple Params calls", func() {
branch := defkit.WhenParam(defkit.Bool("x").Eq(true)).
Params(defkit.String("a")).
Params(defkit.String("b"))
Expect(branch.GetParams()).To(HaveLen(2))
})
It("should chain Validators and return them via GetValidators", func() {
v1 := defkit.Validate("check1").WithName("_v1")
v2 := defkit.Validate("check2").WithName("_v2")
branch := defkit.WhenParam(defkit.Bool("x").Eq(true)).
Params(defkit.String("name")).
Validators(v1, v2)
Expect(branch.GetValidators()).To(HaveLen(2))
Expect(branch.GetValidators()[0]).To(Equal(v1))
Expect(branch.GetValidators()[1]).To(Equal(v2))
})
It("should accumulate validators across multiple Validators calls", func() {
branch := defkit.WhenParam(defkit.Bool("x").Eq(true)).
Validators(defkit.Validate("a").WithName("_a")).
Validators(defkit.Validate("b").WithName("_b"))
Expect(branch.GetValidators()).To(HaveLen(2))
})
It("should preserve condition through full chain", func() {
cond := defkit.Bool("enabled").Eq(false)
branch := defkit.WhenParam(cond).
Params(defkit.String("name"), defkit.Int("count")).
Validators(defkit.Validate("check").WithName("_v"))
Expect(branch.Condition()).To(Equal(cond))
Expect(branch.GetParams()).To(HaveLen(2))
Expect(branch.GetValidators()).To(HaveLen(1))
})
})
})
+318 -64
View File
@@ -285,6 +285,16 @@ func (g *CUEGenerator) GenerateParameterSchema(c *ComponentDefinition) string {
g.writeParam(&sb, param, 1)
}
// Write top-level validators
for _, v := range c.GetValidators() {
g.writeValidator(&sb, v, 1)
}
// Write conditional parameter blocks
for _, block := range c.GetConditionalParamBlocks() {
g.writeConditionalParamBlock(&sb, block, 1)
}
sb.WriteString("}\n")
return sb.String()
}
@@ -394,6 +404,15 @@ func (g *CUEGenerator) GenerateTemplate(c *ComponentDefinition) string {
g.writeHelper(&sb, helper, 1)
}
// Emit raw header block (let bindings, helpers like _claimName)
if rawHeader := tpl.GetRawHeaderBlock(); rawHeader != "" {
for _, line := range strings.Split(strings.TrimSpace(rawHeader), "\n") {
sb.WriteString(g.indent)
sb.WriteString(line)
sb.WriteString("\n")
}
}
// Generate output block
if output := tpl.GetOutput(); output != nil {
g.writeResourceOutput(&sb, "output", output, nil, 1)
@@ -443,10 +462,150 @@ func (g *CUEGenerator) generateParameterBlock(c *ComponentDefinition, depth int)
g.writeParam(&sb, param, depth+1)
}
// Write top-level validators
for _, v := range c.GetValidators() {
g.writeValidator(&sb, v, depth+1)
}
// Write conditional parameter blocks
for _, block := range c.GetConditionalParamBlocks() {
g.writeConditionalParamBlock(&sb, block, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
return sb.String()
}
// writeValidator writes a CUE _validate* block.
// Example output:
//
// _validateTenantName: {
// "tenantName must not end with a hyphen": true
// if tenantName =~ ".*-$" {
// "tenantName must not end with a hyphen": false
// }
// }
func (g *CUEGenerator) writeValidator(sb *strings.Builder, v *Validator, depth int) {
indent := strings.Repeat(g.indent, depth)
inner := strings.Repeat(g.indent, depth+1)
inner2 := strings.Repeat(g.indent, depth+2)
// Determine the CUE variable name
name := v.CUEName()
if name == "" {
// Auto-generate from message — use a simple underscore-prefixed name
name = "_validate"
}
if v.GuardCondition() != nil {
// Guarded validator: wrap in if guard { ... }
guardCUE := g.conditionToCUE(v.GuardCondition())
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, guardCUE))
sb.WriteString(fmt.Sprintf("%s%s: {\n", inner, name))
sb.WriteString(fmt.Sprintf("%s%q: true\n", inner2, v.Message()))
if v.FailCondition() != nil {
failCUE := g.conditionToCUE(v.FailCondition())
sb.WriteString(fmt.Sprintf("%sif %s {\n", inner2, failCUE))
sb.WriteString(fmt.Sprintf("%s\t%q: false\n", inner2, v.Message()))
sb.WriteString(fmt.Sprintf("%s}\n", inner2))
}
sb.WriteString(fmt.Sprintf("%s}\n", inner))
sb.WriteString(fmt.Sprintf("%s}\n", indent))
} else {
// Unguarded validator
sb.WriteString(fmt.Sprintf("%s%s: {\n", indent, name))
sb.WriteString(fmt.Sprintf("%s%q: true\n", inner, v.Message()))
if v.FailCondition() != nil {
failCUE := g.conditionToCUE(v.FailCondition())
sb.WriteString(fmt.Sprintf("%sif %s {\n", inner, failCUE))
sb.WriteString(fmt.Sprintf("%s\t%q: false\n", inner, v.Message()))
sb.WriteString(fmt.Sprintf("%s}\n", inner))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
}
// writeConditionalParamBlock writes conditional parameter branches.
// Example output:
//
// if existingResources == false {
// forceDestroy: *false | bool
// }
// if existingResources == true {
// forceDestroy?: bool
// }
func (g *CUEGenerator) writeConditionalParamBlock(sb *strings.Builder, block *ConditionalParamBlock, depth int) {
indent := strings.Repeat(g.indent, depth)
for _, branch := range block.Branches() {
condCUE := g.conditionToCUE(branch.Condition())
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condCUE))
// Write params inside the conditional block
for _, param := range branch.GetParams() {
g.writeParam(sb, param, depth+1)
}
// Write validators inside the conditional block
for _, v := range branch.GetValidators() {
g.writeValidator(sb, v, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
}
// writeConditionalStructOp writes a conditional struct block in the output.
// Example output:
//
// if parameter["replicationConfiguration"] != _|_ {
// replicationConfiguration: {
// role: parameter.replicationConfiguration.role
// }
// }
func (g *CUEGenerator) writeConditionalStructOp(sb *strings.Builder, cs *ConditionalStructOp, depth int) {
indent := strings.Repeat(g.indent, depth)
// Build the struct
builder := &OutputStructBuilder{}
cs.Builder()(builder)
condCUE := g.conditionToCUE(cs.Cond())
sb.WriteString(fmt.Sprintf("%sif %s {\n", indent, condCUE))
// Split the path into segments and open nested structs
parts := splitPath(cs.Path())
currentIndent := indent + g.indent
for _, part := range parts {
sb.WriteString(fmt.Sprintf("%s%s: {\n", currentIndent, cueLabel(part)))
currentIndent += g.indent
}
// Write builder operations
for _, op := range builder.Ops() {
switch o := op.(type) {
case *structSetOp:
valCUE := g.valueToCUE(o.value)
sb.WriteString(fmt.Sprintf("%s%s: %s\n", currentIndent, cueLabel(o.field), valCUE))
case *structSetIfOp:
condStr := g.conditionToCUE(o.cond)
sb.WriteString(fmt.Sprintf("%sif %s {\n", currentIndent, condStr))
valCUE := g.valueToCUE(o.value)
sb.WriteString(fmt.Sprintf("%s\t%s: %s\n", currentIndent, cueLabel(o.field), valCUE))
sb.WriteString(fmt.Sprintf("%s}\n", currentIndent))
}
}
// Close nested structs
for i := len(parts) - 1; i >= 0; i-- {
currentIndent = currentIndent[:len(currentIndent)-len(g.indent)]
sb.WriteString(fmt.Sprintf("%s}\n", currentIndent))
}
// Close the if block
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
// WriteHelperDefinition writes a CUE helper type definition like #HealthProbe.
// This method is exported so it can be used by policy and workflow step generators.
func (g *CUEGenerator) WriteHelperDefinition(sb *strings.Builder, def HelperDefinition, depth int) {
@@ -1084,12 +1243,28 @@ func (g *CUEGenerator) writeResourceOutput(sb *strings.Builder, name string, res
// Write kind
sb.WriteString(fmt.Sprintf("%skind: %q\n", innerIndent, res.Kind()))
// Build a tree structure from the operations
tree := g.buildFieldTree(res.Ops())
// Separate ConditionalStructOps from regular ops
var regularOps []ResourceOp
var conditionalStructs []*ConditionalStructOp
for _, op := range res.Ops() {
if cs, ok := op.(*ConditionalStructOp); ok {
conditionalStructs = append(conditionalStructs, cs)
} else {
regularOps = append(regularOps, op)
}
}
// Build a tree structure from the regular operations
tree := g.buildFieldTree(regularOps)
// Write the tree as CUE
g.writeFieldTree(sb, tree, depth+1)
// Write conditional struct blocks
for _, cs := range conditionalStructs {
g.writeConditionalStructOp(sb, cs, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
// Close conditional block
@@ -1887,6 +2062,12 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
return val.Path()
case *Ref:
return val.Path()
case *LocalFieldRef:
// Local field reference — emits the bare field name (no parameter. prefix)
return val.Name()
case *TimeParseExpr:
// time.Parse(layout, field) call
return fmt.Sprintf(`time.Parse(%q, %s)`, val.Layout(), val.FieldName())
case *HelperVar:
// Return reference to the helper by name
return val.Name()
@@ -1929,17 +2110,32 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
case *ListComprehension:
// Return list comprehension CUE
return g.listComprehensionToCUE(val)
case *ParamArithExpr, *ParamConcatExpr, *ParamFieldRef, *InterpolatedString, *PlusExpr:
return g.exprValueToCUE(v)
case *IterVarRef, *IterFieldRef, *IterLetRef:
return g.iterRefToCUE(v)
case *ForEachMapOp:
return g.forEachMapOpToCUE(val)
default:
// Try to get name from Param interface
if p, ok := v.(Param); ok {
return "parameter." + p.Name()
}
return "_"
}
}
// exprValueToCUE handles expression-type values (arithmetic, concatenation, interpolation, plus).
func (g *CUEGenerator) exprValueToCUE(v Value) string {
switch val := v.(type) {
case *ParamArithExpr:
// Arithmetic expression on a parameter: parameter.name op value
return fmt.Sprintf("parameter.%s %s %s", val.ParamName(), val.Op(), formatCUEValue(val.ArithValue()))
case *ParamConcatExpr:
// String concatenation on a parameter
if val.Prefix() != "" {
return fmt.Sprintf("%s + parameter.%s", formatCUEValue(val.Prefix()), val.ParamName())
}
return fmt.Sprintf("parameter.%s + %s", val.ParamName(), formatCUEValue(val.Suffix()))
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)
@@ -1949,19 +2145,21 @@ func (g *CUEGenerator) valueToCUE(v Value) string {
parts[i] = g.valueToCUE(p)
}
return strings.Join(parts, " + ")
default:
return "_"
}
}
// iterRefToCUE handles iterator reference values.
func (g *CUEGenerator) iterRefToCUE(v Value) string {
switch val := v.(type) {
case *IterVarRef:
return val.VarName()
case *IterFieldRef:
return fmt.Sprintf("%s.%s", val.VarName(), val.FieldName())
case *IterLetRef:
return val.RefName()
case *ForEachMapOp:
return g.forEachMapOpToCUE(val)
default:
// Try to get name from Param interface
if p, ok := v.(Param); ok {
return "parameter." + p.Name()
}
return "_"
}
}
@@ -2675,8 +2873,6 @@ func (g *CUEGenerator) conditionToCUE(cond Condition) string {
return g.inConditionToCUE(c)
case *StringContainsCondition:
return fmt.Sprintf(`strings.Contains(parameter.%s, %q)`, c.ParamName(), c.Substr())
case *StringMatchesCondition:
return fmt.Sprintf(`parameter.%s =~ %q`, c.ParamName(), c.Pattern())
case *StringStartsWithCondition:
return fmt.Sprintf(`strings.HasPrefix(parameter.%s, %q)`, c.ParamName(), c.Prefix())
case *StringEndsWithCondition:
@@ -2694,25 +2890,10 @@ func (g *CUEGenerator) conditionToCUE(cond Condition) string {
left := g.exprToCUE(c.Left())
right := g.exprToCUE(c.Right())
return fmt.Sprintf("%s %s %s", left, c.Op(), right)
case *CompareCondition:
left := g.anyToCUE(c.Left())
right := g.anyToCUE(c.Right())
return fmt.Sprintf("%s %s %s", left, c.Operator(), right)
case *AndCondition:
left := g.conditionToCUE(c.left)
right := g.conditionToCUE(c.right)
return fmt.Sprintf("(%s) && (%s)", left, right)
case *OrCondition:
left := g.conditionToCUE(c.left)
right := g.conditionToCUE(c.right)
return fmt.Sprintf("(%s) || (%s)", left, right)
case *NotCondition:
// Special case: Not(IsSet("x")) -> parameter["x"] == _|_ (cleaner than !(parameter["x"] != _|_))
if isSet, ok := c.Inner().(*IsSetCondition); ok {
return fmt.Sprintf("parameter[%q] == _|_", isSet.ParamName())
}
inner := g.conditionToCUE(c.Inner())
return fmt.Sprintf("!(%s)", inner)
case *LogicalExpr:
parts := make([]string, len(c.Conditions()))
for i, sub := range c.Conditions() {
@@ -2724,6 +2905,14 @@ func (g *CUEGenerator) conditionToCUE(cond Condition) string {
}
return strings.Join(parts, op)
case *NotExpr:
// Special case: Not(IsSet("x")) -> parameter["x"] == _|_
if isSet, ok := c.Cond().(*IsSetCondition); ok {
return fmt.Sprintf("parameter[%q] == _|_", isSet.ParamName())
}
// Special case: Not(PathExists("x")) -> x == _|_
if pe, ok := c.Cond().(*PathExistsCondition); ok {
return fmt.Sprintf("%s == _|_", pe.Path())
}
return fmt.Sprintf("!(%s)", g.conditionToCUE(c.Cond()))
case *HasExposedPortsCondition:
// Check if any port has expose=true
@@ -2761,6 +2950,12 @@ func (g *CUEGenerator) conditionToCUE(cond Condition) string {
// For CUE, we generate: cond1 && cond2 && cond3
// which will be used in a single if statement
return strings.Join(parts, " && ")
case *RegexMatchCondition:
// General-purpose regex match: <value> =~ "pattern"
return fmt.Sprintf(`%s =~ %q`, g.valueToCUE(c.Source()), c.Pattern())
case *RawCUECondition:
// Raw CUE expression — emit verbatim
return c.Expr()
default:
return cueBoolTrue
}
@@ -2784,14 +2979,6 @@ func (g *CUEGenerator) exprToCUE(e Expr) string {
return "_"
}
// anyToCUE converts any value to CUE syntax.
func (g *CUEGenerator) anyToCUE(v any) string {
if val, ok := v.(Value); ok {
return g.valueToCUE(val)
}
return formatCUEValue(v)
}
// writeWorkload writes the workload definition.
func (g *CUEGenerator) writeWorkload(sb *strings.Builder, c *ComponentDefinition, depth int) {
indent := strings.Repeat(g.indent, depth)
@@ -2809,9 +2996,11 @@ func (g *CUEGenerator) writeWorkload(sb *strings.Builder, c *ComponentDefinition
sb.WriteString(fmt.Sprintf("%s%s%skind: %q\n", indent, g.indent, g.indent, workload.Kind()))
sb.WriteString(fmt.Sprintf("%s%s}\n", indent, g.indent))
// Write workload type
workloadType := g.inferWorkloadType(workload)
sb.WriteString(fmt.Sprintf("%s%stype: %q\n", indent, g.indent, workloadType))
// Write workload type (unless suppressed)
if !c.IsOmitWorkloadType() {
workloadType := g.inferWorkloadType(workload)
sb.WriteString(fmt.Sprintf("%s%stype: %q\n", indent, g.indent, workloadType))
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
@@ -2968,6 +3157,11 @@ func (g *CUEGenerator) writeStringParam(sb *strings.Builder, p *StringParam, ind
// Build constraint parts
var constraints []string
// NotEmpty constraint: !=""
if p.GetNotEmpty() {
constraints = append(constraints, `!=""`)
}
// Pattern constraint: =~"pattern"
if pattern := p.GetPattern(); pattern != "" {
constraints = append(constraints, fmt.Sprintf(`=~%q`, pattern))
@@ -3093,32 +3287,64 @@ func (g *CUEGenerator) writeArrayParam(sb *strings.Builder, p *ArrayParam, inden
// Reference to a helper definition like #HealthProbe
// For arrays, output [...#SchemaRef] to indicate an array of the helper type
sb.WriteString(fmt.Sprintf("%s%s%s: %s[...#%s]\n", indent, name, optional, constraintPrefix, schemaRef))
return
}
if schema := p.GetSchema(); schema != "" {
// Raw CUE schema - output directly
if constraintPrefix != "" {
sb.WriteString(fmt.Sprintf("%s%s%s: %s%s\n", indent, name, optional, constraintPrefix, schema))
} else if schema := p.GetSchema(); schema != "" {
// Raw CUE schema - output directly, with optional default
if p.HasDefault() {
defaultJSON := g.formatArrayDefault(p.GetDefault())
if constraintPrefix != "" {
sb.WriteString(fmt.Sprintf("%s%s%s: %s%s | *%s\n", indent, name, optional, constraintPrefix, schema, defaultJSON))
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: %s | *%s\n", indent, name, optional, schema, defaultJSON))
}
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, schema))
if constraintPrefix != "" {
sb.WriteString(fmt.Sprintf("%s%s%s: %s%s\n", indent, name, optional, constraintPrefix, schema))
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, schema))
}
}
} else {
elemType := g.cueTypeForParamType(p.ElementType())
// Check if array has structured fields
if fields := p.GetFields(); len(fields) > 0 {
sb.WriteString(fmt.Sprintf("%s%s%s: %s[...{\n", indent, name, optional, constraintPrefix))
for _, field := range fields {
g.writeParam(sb, field, depth+1)
}
// Write validators inside each array element struct
for _, v := range p.GetValidators() {
g.writeValidator(sb, v, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}]\n", indent))
} else if elemType != "" {
if p.HasNotEmpty() && elemType == "string" {
sb.WriteString(fmt.Sprintf("%s%s%s: %s[...(%s & !=\"\")]\n", indent, name, optional, constraintPrefix, elemType))
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: %s[...%s]\n", indent, name, optional, constraintPrefix, elemType))
}
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: %s[...]\n", indent, name, optional, constraintPrefix))
}
return
}
elemType := g.cueTypeForParamType(p.ElementType())
}
// Check if array has structured fields
if fields := p.GetFields(); len(fields) > 0 {
sb.WriteString(fmt.Sprintf("%s%s%s: %s[...{\n", indent, name, optional, constraintPrefix))
for _, field := range fields {
g.writeParam(sb, field, depth+1)
// formatArrayDefault formats an array default value as a CUE literal.
// Converts []any{"*"} to `["*"]`, []any{"Observe"} to `["Observe"]`, etc.
func (g *CUEGenerator) formatArrayDefault(val any) string {
if val == nil {
return "[]"
}
switch v := val.(type) {
case []any:
parts := make([]string, 0, len(v))
for _, elem := range v {
parts = append(parts, fmt.Sprintf("%q", elem))
}
sb.WriteString(fmt.Sprintf("%s}]\n", indent))
} else if elemType != "" {
sb.WriteString(fmt.Sprintf("%s%s%s: %s[...%s]\n", indent, name, optional, constraintPrefix, elemType))
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: %s[...]\n", indent, name, optional, constraintPrefix))
return "[" + strings.Join(parts, ", ") + "]"
default:
return fmt.Sprintf("%v", val)
}
}
@@ -3137,13 +3363,41 @@ func (g *CUEGenerator) writeMapParam(sb *strings.Builder, p *MapParam, indent, n
return
}
// Check if map has structured fields
if fields := p.GetFields(); len(fields) > 0 {
sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional))
for _, field := range fields {
// Check if map has structured fields, validators, or conditional fields
hasFields := len(p.GetFields()) > 0
hasValidators := len(p.GetValidators()) > 0
hasConditionalFields := len(p.GetConditionalFields()) > 0
if hasFields || hasValidators || hasConditionalFields {
if p.IsClosed() {
sb.WriteString(fmt.Sprintf("%s%s%s: close({\n", indent, name, optional))
} else {
sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional))
}
for _, field := range p.GetFields() {
g.writeParam(sb, field, depth+1)
}
sb.WriteString(fmt.Sprintf("%s}\n", indent))
// Write validators inside the struct
for _, v := range p.GetValidators() {
g.writeValidator(sb, v, depth+1)
}
// Write conditional fields inside the struct
for _, branch := range p.GetConditionalFields() {
condCUE := g.conditionToCUE(branch.Condition())
sb.WriteString(fmt.Sprintf("%s\tif %s {\n", indent, condCUE))
for _, param := range branch.GetParams() {
g.writeParam(sb, param, depth+2)
}
for _, v := range branch.GetValidators() {
g.writeValidator(sb, v, depth+2)
}
sb.WriteString(fmt.Sprintf("%s\t}\n", indent))
}
if p.IsClosed() {
sb.WriteString(fmt.Sprintf("%s})\n", indent))
} else {
sb.WriteString(fmt.Sprintf("%s}\n", indent))
}
} else if valType := p.ValueType(); valType != "" {
// Typed map: [string]: type
cueType := g.cueTypeForParamType(valType)
+386
View File
@@ -1463,6 +1463,392 @@ var _ = Describe("CUEGenerator", func() {
})
})
Describe("Fluent Builder API Validator Patterns CUE Generation", func() {
var gen *defkit.CUEGenerator
BeforeEach(func() {
gen = defkit.NewCUEGenerator()
})
Context("OmitWorkloadType CUE Generation", func() {
It("should suppress workload type field when omitted", func() {
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
OmitWorkloadType().
Template(func(tpl *defkit.Template) {
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", defkit.Lit("test")))
})
cue := comp.ToCue()
Expect(cue).To(ContainSubstring(`kind: "Deployment"`))
Expect(cue).To(ContainSubstring("workload:"))
Expect(cue).NotTo(MatchRegexp(`type:\s+"deployments\.apps"`))
})
It("should include workload type when not omitted", func() {
comp := defkit.NewComponent("test").
Workload("apps/v1", "Deployment").
Template(func(tpl *defkit.Template) {
tpl.Output(defkit.NewResource("apps/v1", "Deployment").
Set("metadata.name", defkit.Lit("test")))
})
cue := comp.ToCue()
Expect(cue).To(ContainSubstring(`type: "deployments.apps"`))
})
})
Context("RawHeaderBlock in Component Template", func() {
It("should emit raw header block before output", func() {
comp := defkit.NewComponent("test").
Workload("v1", "PersistentVolumeClaim").
Template(func(tpl *defkit.Template) {
tpl.SetRawHeaderBlock(`let _claimName = parameter.claimName + "-pvc"`)
tpl.Output(defkit.NewResource("v1", "PersistentVolumeClaim").
Set("metadata.name", defkit.Reference("_claimName")))
})
cue := gen.GenerateTemplate(comp)
Expect(cue).To(ContainSubstring(`let _claimName = parameter.claimName + "-pvc"`))
Expect(cue).To(ContainSubstring("_claimName"))
})
It("should emit multiline raw header block", func() {
comp := defkit.NewComponent("test").
Workload("v1", "ConfigMap").
Template(func(tpl *defkit.Template) {
tpl.SetRawHeaderBlock("let _a = parameter.a\nlet _b = parameter.b")
tpl.Output(defkit.NewResource("v1", "ConfigMap").
Set("metadata.name", defkit.Lit("test")))
})
cue := gen.GenerateTemplate(comp)
Expect(cue).To(ContainSubstring("let _a = parameter.a"))
Expect(cue).To(ContainSubstring("let _b = parameter.b"))
})
It("should not emit header block when empty", func() {
comp := defkit.NewComponent("test").
Workload("v1", "ConfigMap").
Template(func(tpl *defkit.Template) {
tpl.Output(defkit.NewResource("v1", "ConfigMap").
Set("metadata.name", defkit.Lit("test")))
})
cue := gen.GenerateTemplate(comp)
Expect(cue).NotTo(ContainSubstring("let _"))
})
})
Context("ArrayParam NotEmpty Elements CUE Generation", func() {
It("should generate [...(string & !=\"\")] for NotEmpty string arrays", func() {
p := defkit.StringList("tags").NotEmpty()
comp := defkit.NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`[...(string & !="")]`))
})
It("should generate normal [...string] without NotEmpty", func() {
p := defkit.StringList("tags")
comp := defkit.NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`[...string]`))
Expect(cue).NotTo(ContainSubstring(`!=""`))
})
})
Context("ArrayParam with Schema and Default CUE Generation", func() {
It("should generate array with OfEnum and default value", func() {
p := defkit.Array("methods").
OfEnum("GET", "POST", "DELETE").
Default([]any{"GET"})
comp := defkit.NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`["GET"]`))
Expect(cue).To(ContainSubstring(`"GET" | "POST" | "DELETE"`))
})
It("should generate array with OfEnum and nil default as empty array", func() {
p := defkit.Array("methods").
OfEnum("GET", "POST").
Default(nil)
comp := defkit.NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("*[]"))
})
It("should generate array with OfEnum and multi-value default", func() {
p := defkit.Array("methods").
OfEnum("GET", "POST", "DELETE").
Default([]any{"GET", "POST"})
comp := defkit.NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`["GET", "POST"]`))
})
})
Context("MapParam Closed CUE Generation", func() {
It("should emit close({...}) for closed struct", func() {
p := defkit.Object("governance").Closed().WithFields(
defkit.String("tenantName"),
defkit.String("departmentCode"),
)
comp := defkit.NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("close({"))
Expect(cue).To(ContainSubstring("})"))
Expect(cue).To(ContainSubstring("tenantName: string"))
})
It("should not emit close({}) for non-closed struct", func() {
p := defkit.Object("config").WithFields(
defkit.String("name"),
)
comp := defkit.NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).NotTo(ContainSubstring("close({"))
Expect(cue).To(ContainSubstring("config: {"))
})
})
Context("MapParam with Validators CUE Generation", func() {
It("should emit validators inside struct", func() {
v := defkit.Validate("name required").
WithName("_validateName").
FailWhen(defkit.LocalField("name").Eq(""))
p := defkit.Object("governance").WithFields(
defkit.String("name"),
).Validators(v)
comp := defkit.NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("governance: {"))
Expect(cue).To(ContainSubstring("_validateName:"))
Expect(cue).To(ContainSubstring(`"name required": true`))
})
})
Context("MapParam with ConditionalFields CUE Generation", func() {
It("should emit conditional fields inside struct", func() {
flag := defkit.Bool("flag").Default(false)
p := defkit.Object("config").Optional().ConditionalFields(
defkit.WhenParam(flag.Eq(true)).Params(
defkit.String("secret").Required(),
),
defkit.WhenParam(flag.Eq(false)).Params(
defkit.String("secret").Optional(),
),
)
comp := defkit.NewComponent("test").Params(flag, p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("config?: {"))
Expect(cue).To(ContainSubstring("if parameter.flag == true"))
Expect(cue).To(ContainSubstring("if parameter.flag == false"))
})
It("should emit validators inside conditional field branches", func() {
flag := defkit.Bool("flag").Default(false)
v := defkit.Validate("check").WithName("_v").
FailWhen(defkit.LocalField("secret").Eq(""))
p := defkit.Object("config").Optional().ConditionalFields(
defkit.WhenParam(flag.Eq(true)).
Params(defkit.String("secret").Required()).
Validators(v),
)
comp := defkit.NewComponent("test").Params(flag, p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("_v:"))
})
})
Context("Closed MapParam with Validators and ConditionalFields Combined", func() {
It("should combine all features", func() {
flag := defkit.Bool("flag").Default(false)
v := defkit.Validate("name required").
WithName("_validateName").
FailWhen(defkit.LocalField("name").Eq(""))
p := defkit.Object("governance").Closed().
WithFields(
defkit.String("name").NotEmpty(),
).
Validators(v).
ConditionalFields(
defkit.WhenParam(flag.Eq(true)).Params(
defkit.String("extra").Required(),
),
)
comp := defkit.NewComponent("test").Params(flag, p)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("close({"))
Expect(cue).To(ContainSubstring(`!=""`))
Expect(cue).To(ContainSubstring("_validateName:"))
Expect(cue).To(ContainSubstring("if parameter.flag == true"))
Expect(cue).To(ContainSubstring("})"))
})
})
Context("RegexMatch CUE Generation", func() {
It("should generate regex match for StringParam.Matches", func() {
p := defkit.String("name")
comp := defkit.NewComponent("test").
Params(p).
Workload("v1", "ConfigMap").
Template(func(tpl *defkit.Template) {
tpl.Output(defkit.NewResource("v1", "ConfigMap").
SetIf(p.Matches("^prod-"), "data.env", defkit.Lit("production")))
})
cue := gen.GenerateTemplate(comp)
Expect(cue).To(ContainSubstring(`parameter.name =~ "^prod-"`))
})
It("should generate regex match for LocalFieldRef.Matches in validator", func() {
v := defkit.Validate("bad").
WithName("_v").
FailWhen(defkit.LocalField("host").Matches(`\.internal$`))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`host =~ "\\.internal$"`))
})
})
Context("LocalFieldRef NotSet CUE Generation", func() {
It("should generate == _|_ for LocalFieldRef.NotSet", func() {
v := defkit.Validate("role required").
WithName("_v").
FailWhen(defkit.LocalField("role").NotSet())
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("role == _|_"))
})
})
Context("LocalFieldRef LenGt CUE Generation", func() {
It("should generate len(field) > n", func() {
v := defkit.Validate("too many").
WithName("_v").
FailWhen(defkit.LocalField("items").LenGt(10))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(items) > 10"))
})
})
Context("LenOfExpr CUE Generation", func() {
It("should generate len(expr) > n for Gt", func() {
v := defkit.Validate("name too long").
WithName("_v").
FailWhen(defkit.LenOf(defkit.LocalField("name")).Gt(63))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(name) > 63"))
})
It("should generate len(expr) >= n for Gte", func() {
v := defkit.Validate("check").
WithName("_v").
FailWhen(defkit.LenOf(defkit.LocalField("data")).Gte(100))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(data) >= 100"))
})
It("should generate len(expr) == n for Eq", func() {
v := defkit.Validate("check").
WithName("_v").
FailWhen(defkit.LenOf(defkit.LocalField("code")).Eq(3))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(code) == 3"))
})
})
Context("TimeParse CUE Generation", func() {
It("should generate time.Parse expressions", func() {
v := defkit.Validate("start before end").
WithName("_v").
FailWhen(defkit.TimeParse("2006-01-02T15:04:05Z", defkit.LocalField("start")).
Gte(defkit.TimeParse("2006-01-02T15:04:05Z", defkit.LocalField("end"))))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`time.Parse("2006-01-02T15:04:05Z", start)`))
Expect(cue).To(ContainSubstring(`time.Parse("2006-01-02T15:04:05Z", end)`))
})
})
Context("RawCUECondition CUE Generation", func() {
It("should emit raw expression verbatim", func() {
v := defkit.Validate("check").
WithName("_v").
FailWhen(defkit.CUEExpr(`len("prefix-"+parameter.name) > 63`))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`len("prefix-"+parameter.name) > 63`))
})
})
Context("ConditionalStructOp CUE Generation", func() {
It("should generate conditional struct in template output", func() {
replConfig := defkit.Object("replicationConfiguration").Optional()
comp := defkit.NewComponent("test").
Params(replConfig).
Workload("apps/v1", "Deployment").
Template(func(tpl *defkit.Template) {
tpl.Output(defkit.NewResource("v1", "ConfigMap").
Set("metadata.name", defkit.Lit("test")).
ConditionalStruct(replConfig.IsSet(), "spec.replication", func(b *defkit.OutputStructBuilder) {
b.Set("role", defkit.Reference("parameter.replicationConfiguration.role"))
b.SetIf(replConfig.IsSet(), "enabled", defkit.Lit(true))
}))
})
cue := gen.GenerateTemplate(comp)
Expect(cue).To(ContainSubstring(`if parameter["replicationConfiguration"] != _|_`))
Expect(cue).To(ContainSubstring("replication:"))
Expect(cue).To(ContainSubstring("role: parameter.replicationConfiguration.role"))
})
It("should generate nested path correctly", func() {
config := defkit.Object("config").Optional()
comp := defkit.NewComponent("test").
Params(config).
Workload("v1", "ConfigMap").
Template(func(tpl *defkit.Template) {
tpl.Output(defkit.NewResource("v1", "ConfigMap").
Set("metadata.name", defkit.Lit("test")).
ConditionalStruct(config.IsSet(), "spec.deep.nested.path", func(b *defkit.OutputStructBuilder) {
b.Set("key", defkit.Reference("parameter.config.key"))
}))
})
cue := gen.GenerateTemplate(comp)
Expect(cue).To(ContainSubstring("deep:"))
Expect(cue).To(ContainSubstring("nested:"))
Expect(cue).To(ContainSubstring("path:"))
})
})
})
Describe("Builtin WithDirectFields", func() {
It("should render fields directly without $params wrapper", func() {
ws := defkit.NewWorkflowStep("test").
+15 -45
View File
@@ -169,23 +169,6 @@ type TruthyCondition struct {
// ParamName returns the parameter name being checked.
func (t *TruthyCondition) ParamName() string { return t.paramName }
// CompareCondition represents a comparison between two values.
type CompareCondition struct {
baseCondition
left any
right any
op string
}
// Left returns the left operand.
func (c *CompareCondition) Left() any { return c.left }
// Right returns the right operand.
func (c *CompareCondition) Right() any { return c.right }
// Operator returns the comparison operator.
func (c *CompareCondition) Operator() string { return c.op }
// AndCondition represents a binary logical AND of two conditions.
// This is an internal IR type used by cuegen to combine conditions during code generation.
// For the user-facing API, use And() which accepts variadic conditions via LogicalExpr.
@@ -195,25 +178,6 @@ type AndCondition struct {
right Condition
}
// OrCondition represents a binary logical OR of two conditions.
// This is an internal IR type used by cuegen to combine conditions during code generation.
// For the user-facing API, use Or() which accepts variadic conditions via LogicalExpr.
type OrCondition struct {
baseCondition
left Condition
right Condition
}
// NotCondition represents a logical NOT of a condition.
// Used both internally (e.g., by ParamNotSet) and as part of the user-facing Not() API.
type NotCondition struct {
baseCondition
inner Condition
}
// Inner returns the negated condition.
func (n *NotCondition) Inner() Condition { return n.inner }
// --- Parameter Runtime Condition Types ---
// FalsyCondition represents a falsy check on a boolean parameter.
@@ -254,19 +218,25 @@ func (c *StringContainsCondition) ParamName() string { return c.paramName }
// Substr returns the substring to check for.
func (c *StringContainsCondition) Substr() string { return c.substr }
// StringMatchesCondition checks if a string parameter matches a regex pattern.
// Generates: parameter.name =~ "pattern"
type StringMatchesCondition struct {
// RegexMatchCondition checks if any Value matches a regex pattern.
// Generates: <value> =~ "pattern"
// Used by both LocalFieldRef.Matches() and StringParam.Matches().
type RegexMatchCondition struct {
baseCondition
paramName string
pattern string
source Value
pattern string
}
// ParamName returns the parameter name being checked.
func (c *StringMatchesCondition) ParamName() string { return c.paramName }
// Source returns the value being matched.
func (c *RegexMatchCondition) Source() Value { return c.source }
// Pattern returns the regex pattern to match against.
func (c *StringMatchesCondition) Pattern() string { return c.pattern }
// Pattern returns the regex pattern.
func (c *RegexMatchCondition) Pattern() string { return c.pattern }
// RegexMatch creates a condition that checks if a value matches a regex pattern.
func RegexMatch(source Value, pattern string) *RegexMatchCondition {
return &RegexMatchCondition{source: source, pattern: pattern}
}
// StringStartsWithCondition checks if a string parameter starts with a prefix.
// Generates: strings.HasPrefix(parameter.name, "prefix")
+61
View File
@@ -354,6 +354,67 @@ var _ = Describe("Expressions", func() {
})
})
Context("RegexMatch", func() {
It("should create a RegexMatchCondition with source and pattern", func() {
ref := defkit.LocalField("name")
rm := defkit.RegexMatch(ref, "^test-")
Expect(rm.Pattern()).To(Equal("^test-"))
Expect(rm.Source()).To(Equal(ref))
})
It("should work with StringParam as source", func() {
p := defkit.String("host")
rm := defkit.RegexMatch(p, `^prod-.*$`)
Expect(rm.Pattern()).To(Equal(`^prod-.*$`))
Expect(rm.Source()).To(Equal(p))
})
It("should be produced by StringParam.Matches", func() {
p := defkit.String("name")
cond := p.Matches("^prod-")
rm, ok := cond.(*defkit.RegexMatchCondition)
Expect(ok).To(BeTrue())
Expect(rm.Pattern()).To(Equal("^prod-"))
Expect(rm.Source()).To(Equal(p))
})
It("should be produced by LocalFieldRef.Matches", func() {
ref := defkit.LocalField("tenantName")
cond := ref.Matches(".*-$")
rm, ok := cond.(*defkit.RegexMatchCondition)
Expect(ok).To(BeTrue())
Expect(rm.Pattern()).To(Equal(".*-$"))
Expect(rm.Source()).To(Equal(ref))
})
})
Context("NotExpr replaces NotCondition", func() {
It("should be returned by baseParam.NotSet", func() {
p := defkit.Int("replicas")
cond := p.NotSet()
notExpr, ok := cond.(*defkit.NotExpr)
Expect(ok).To(BeTrue(), "expected *NotExpr, got %T", cond)
inner, ok := notExpr.Cond().(*defkit.IsSetCondition)
Expect(ok).To(BeTrue())
Expect(inner.ParamName()).To(Equal("replicas"))
})
It("should be returned by ParamNotSet", func() {
ne := defkit.ParamNotSet("defaults")
inner, ok := ne.Cond().(*defkit.IsSetCondition)
Expect(ok).To(BeTrue())
Expect(inner.ParamName()).To(Equal("defaults"))
})
It("should be returned by LocalFieldRef.NotSet", func() {
cond := defkit.LocalField("role").NotSet()
notExpr, ok := cond.(*defkit.NotExpr)
Expect(ok).To(BeTrue())
_, ok = notExpr.Cond().(*defkit.PathExistsCondition)
Expect(ok).To(BeTrue())
})
})
Context("InlineArrayValue", func() {
It("should create inline array with fields and store correct values", func() {
port := defkit.Int("port")
+111 -13
View File
@@ -16,6 +16,11 @@ limitations under the License.
package defkit
import (
"fmt"
"strings"
)
// baseParam provides common parameter functionality.
type baseParam struct {
name string
@@ -50,7 +55,7 @@ func (p *baseParam) IsSet() Condition {
// NotSet returns a condition that checks if the parameter is not set.
// This generates `if parameter["name"] == _|_` in CUE.
func (p *baseParam) NotSet() Condition {
return &NotCondition{inner: &IsSetCondition{paramName: p.name}}
return Not(&IsSetCondition{paramName: p.name})
}
// Eq creates a condition that compares this parameter to a literal value.
@@ -112,6 +117,7 @@ type StringParam struct {
enumValues []string // allowed enum values
openEnum bool // when true, appends | string to enum disjunction (open enum)
pattern string // regex pattern constraint
notEmpty bool // when true, emits !="" constraint
minLen *int // minimum length constraint
maxLen *int // maximum length constraint
}
@@ -227,6 +233,18 @@ func (p *StringParam) GetMaxLen() *int {
return p.maxLen
}
// NotEmpty adds a non-empty string constraint.
// This generates CUE like: string & !=""
func (p *StringParam) NotEmpty() *StringParam {
p.notEmpty = true
return p
}
// GetNotEmpty returns whether the non-empty constraint is set.
func (p *StringParam) GetNotEmpty() bool {
return p.notEmpty
}
// Concat creates a string concatenation expression.
// Example: name.Concat("-suffix") generates: parameter.name + "-suffix"
func (p *StringParam) Concat(suffix string) Value {
@@ -250,7 +268,7 @@ func (p *StringParam) Contains(substr string) Condition {
// Matches creates a condition that checks if this string parameter matches a regex pattern.
// Example: name.Matches("^prod-") generates: parameter.name =~ "^prod-"
func (p *StringParam) Matches(pattern string) Condition {
return &StringMatchesCondition{paramName: p.name, pattern: pattern}
return RegexMatch(p, pattern)
}
// StartsWith creates a condition that checks if this string parameter starts with a prefix.
@@ -581,12 +599,14 @@ func (p *FloatParam) In(values ...float64) Condition {
// ArrayParam represents an array/list parameter.
type ArrayParam struct {
baseParam
elementType ParamType
fields []Param // fields for structured array elements
schema string // raw CUE schema for the array elements
schemaRef string // reference to a helper definition (e.g., "HealthProbe")
minItems *int // minimum number of items
maxItems *int // maximum number of items
elementType ParamType
fields []Param // fields for structured array elements
schema string // raw CUE schema for the array elements
schemaRef string // reference to a helper definition (e.g., "HealthProbe")
minItems *int // minimum number of items
maxItems *int // maximum number of items
validators []*Validator // validators emitted inside each array element struct
notEmptyElements bool // when true, adds !="" constraint to string elements: [...(string & !="")]
}
// Array creates a new array parameter with the given name.
@@ -672,6 +692,45 @@ func (p *ArrayParam) GetSchemaRef() string {
return p.schemaRef
}
// OfEnum is a convenience method for arrays of enum values.
// It sets the schema to [...("val1" | "val2" | ...)].
// Example: Array("methods").OfEnum("GET", "POST") emits: [...("GET" | "POST")]
func (p *ArrayParam) OfEnum(values ...string) *ArrayParam {
quoted := make([]string, len(values))
for i, v := range values {
quoted[i] = fmt.Sprintf("%q", v)
}
p.schema = "[...(" + strings.Join(quoted, " | ") + ")]"
return p
}
// Validators adds validation rules to this array parameter.
// Validators are emitted inside each array element struct as _validate* blocks.
func (p *ArrayParam) Validators(validators ...*Validator) *ArrayParam {
p.validators = append(p.validators, validators...)
return p
}
// GetValidators returns the validators for this array parameter.
func (p *ArrayParam) GetValidators() []*Validator {
return p.validators
}
// NotEmpty adds a !="" constraint to each string element of the array.
// Changes [...string] to [...(string & !="")].
// Only applies to string-typed arrays; silently ignored for other element types.
// Consistent with StringParam.NotEmpty().
// Example: StringList("x").NotEmpty() produces [...(string & !="")]
func (p *ArrayParam) NotEmpty() *ArrayParam {
p.notEmptyElements = true
return p
}
// HasNotEmpty returns true if elements must be non-empty.
func (p *ArrayParam) HasNotEmpty() bool {
return p.notEmptyElements
}
// MinItems sets the minimum number of items constraint for the array.
// This generates CUE like: list.MinItems(n)
func (p *ArrayParam) MinItems(n int) *ArrayParam {
@@ -749,11 +808,14 @@ func (p *ArrayParam) IsNotEmpty() Condition {
// MapParam represents a map/dictionary parameter.
type MapParam struct {
baseParam
keyType ParamType
valueType ParamType
fields []Param // fields for structured map values
schema string // raw CUE schema for the map structure
schemaRef string // reference to a helper definition (e.g., "HealthProbe")
keyType ParamType
valueType ParamType
fields []Param // fields for structured map values
schema string // raw CUE schema for the map structure
schemaRef string // reference to a helper definition (e.g., "HealthProbe")
closed bool // when true, wraps struct output in close({...})
validators []*Validator // validators emitted inside this struct
conditionalFields []*ConditionalBranch // conditional field branches inside this struct
}
// Map creates a new map parameter with the given name.
@@ -840,6 +902,42 @@ func (p *MapParam) GetSchemaRef() string {
return p.schemaRef
}
// Closed marks the map as a closed struct, preventing extra fields.
// This generates CUE like: close({...})
func (p *MapParam) Closed() *MapParam {
p.closed = true
return p
}
// IsClosed returns whether the map is a closed struct.
func (p *MapParam) IsClosed() bool {
return p.closed
}
// Validators adds validation rules to this map parameter.
// Validators are emitted inside the struct as _validate* blocks.
func (p *MapParam) Validators(validators ...*Validator) *MapParam {
p.validators = append(p.validators, validators...)
return p
}
// GetValidators returns the validators for this map parameter.
func (p *MapParam) GetValidators() []*Validator {
return p.validators
}
// ConditionalFields adds conditional field branches inside this struct.
// Fields within each branch are emitted conditionally based on the guard.
func (p *MapParam) ConditionalFields(branches ...*ConditionalBranch) *MapParam {
p.conditionalFields = append(p.conditionalFields, branches...)
return p
}
// GetConditionalFields returns the conditional field branches.
func (p *MapParam) GetConditionalFields() []*ConditionalBranch {
return p.conditionalFields
}
// Field returns a reference to a nested field within this map parameter.
// This allows map parameters to be used as variables with field access.
// Example: requests := Map("requests").WithSchema(...); requests.Field("cpu") => parameter.requests.cpu
+327 -550
View File
@@ -17,592 +17,369 @@ limitations under the License.
package defkit
import (
"strings"
"testing"
"github.com/onsi/ginkgo/v2"
"github.com/onsi/gomega"
)
// --- Schema Constraint Tests ---
var _ = ginkgo.Describe("Parameter Constraints", func() {
var gen *CUEGenerator
func TestStringParamPattern(t *testing.T) {
p := String("name").Pattern("^[a-z][a-z0-9-]*$")
ginkgo.BeforeEach(func() {
gen = NewCUEGenerator()
})
if p.GetPattern() != "^[a-z][a-z0-9-]*$" {
t.Errorf("GetPattern() = %q, want %q", p.GetPattern(), "^[a-z][a-z0-9-]*$")
}
// --- Schema Constraint Tests ---
// Test CUE generation
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
ginkgo.Context("Schema Constraints", func() {
ginkgo.It("should set and generate string pattern constraint", func() {
p := String("name").Pattern("^[a-z][a-z0-9-]*$")
gomega.Expect(p.GetPattern()).To(gomega.Equal("^[a-z][a-z0-9-]*$"))
if !strings.Contains(cue, `=~"^[a-z][a-z0-9-]*$"`) {
t.Errorf("Generated CUE should contain pattern constraint, got:\n%s", cue)
}
}
func TestStringParamMinMaxLen(t *testing.T) {
p := String("name").MinLen(3).MaxLen(63)
minLen := p.GetMinLen()
maxLen := p.GetMaxLen()
if minLen == nil || *minLen != 3 {
t.Errorf("GetMinLen() = %v, want 3", minLen)
}
if maxLen == nil || *maxLen != 63 {
t.Errorf("GetMaxLen() = %v, want 63", maxLen)
}
// Test CUE generation
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
if !strings.Contains(cue, "strings.MinRunes(3)") {
t.Errorf("Generated CUE should contain MinRunes, got:\n%s", cue)
}
if !strings.Contains(cue, "strings.MaxRunes(63)") {
t.Errorf("Generated CUE should contain MaxRunes, got:\n%s", cue)
}
}
func TestIntParamMinMax(t *testing.T) {
p := Int("replicas").Min(1).Max(100)
minVal := p.GetMin()
maxVal := p.GetMax()
if minVal == nil || *minVal != 1 {
t.Errorf("GetMin() = %v, want 1", minVal)
}
if maxVal == nil || *maxVal != 100 {
t.Errorf("GetMax() = %v, want 100", maxVal)
}
// Test CUE generation
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
if !strings.Contains(cue, ">=1") {
t.Errorf("Generated CUE should contain >=1, got:\n%s", cue)
}
if !strings.Contains(cue, "<=100") {
t.Errorf("Generated CUE should contain <=100, got:\n%s", cue)
}
}
func TestFloatParamMinMax(t *testing.T) {
p := Float("ratio").Min(0.0).Max(1.0)
minVal := p.GetMin()
maxVal := p.GetMax()
if minVal == nil || *minVal != 0.0 {
t.Errorf("GetMin() = %v, want 0.0", minVal)
}
if maxVal == nil || *maxVal != 1.0 {
t.Errorf("GetMax() = %v, want 1.0", maxVal)
}
// Test CUE generation
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
if !strings.Contains(cue, ">=0") {
t.Errorf("Generated CUE should contain >=0, got:\n%s", cue)
}
if !strings.Contains(cue, "<=1") {
t.Errorf("Generated CUE should contain <=1, got:\n%s", cue)
}
}
func TestArrayParamMinMaxItems(t *testing.T) {
p := Array("tags").Of(ParamTypeString).MinItems(1).MaxItems(10)
minItems := p.GetMinItems()
maxItems := p.GetMaxItems()
if minItems == nil || *minItems != 1 {
t.Errorf("GetMinItems() = %v, want 1", minItems)
}
if maxItems == nil || *maxItems != 10 {
t.Errorf("GetMaxItems() = %v, want 10", maxItems)
}
// Test CUE generation
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
if !strings.Contains(cue, "list.MinItems(1)") {
t.Errorf("Generated CUE should contain MinItems, got:\n%s", cue)
}
if !strings.Contains(cue, "list.MaxItems(10)") {
t.Errorf("Generated CUE should contain MaxItems, got:\n%s", cue)
}
}
// --- Runtime Condition Tests ---
func TestStringParamContains(t *testing.T) {
p := String("name")
cond := p.Contains("prod")
gen := NewCUEGenerator()
cueStr := gen.conditionToCUE(cond)
expected := `strings.Contains(parameter.name, "prod")`
if cueStr != expected {
t.Errorf("conditionToCUE() = %q, want %q", cueStr, expected)
}
}
func TestStringParamMatches(t *testing.T) {
p := String("name")
cond := p.Matches("^prod-")
gen := NewCUEGenerator()
cueStr := gen.conditionToCUE(cond)
expected := `parameter.name =~ "^prod-"`
if cueStr != expected {
t.Errorf("conditionToCUE() = %q, want %q", cueStr, expected)
}
}
func TestStringParamStartsWith(t *testing.T) {
p := String("name")
cond := p.StartsWith("prod-")
gen := NewCUEGenerator()
cueStr := gen.conditionToCUE(cond)
expected := `strings.HasPrefix(parameter.name, "prod-")`
if cueStr != expected {
t.Errorf("conditionToCUE() = %q, want %q", cueStr, expected)
}
}
func TestStringParamEndsWith(t *testing.T) {
p := String("name")
cond := p.EndsWith("-prod")
gen := NewCUEGenerator()
cueStr := gen.conditionToCUE(cond)
expected := `strings.HasSuffix(parameter.name, "-prod")`
if cueStr != expected {
t.Errorf("conditionToCUE() = %q, want %q", cueStr, expected)
}
}
func TestStringParamIn(t *testing.T) {
p := String("name")
cond := p.In("api", "web", "worker")
gen := NewCUEGenerator()
cueStr := gen.conditionToCUE(cond)
// Should contain all values
if !strings.Contains(cueStr, `parameter.name == "api"`) {
t.Errorf("conditionToCUE() should contain 'api', got: %s", cueStr)
}
if !strings.Contains(cueStr, `parameter.name == "web"`) {
t.Errorf("conditionToCUE() should contain 'web', got: %s", cueStr)
}
if !strings.Contains(cueStr, " || ") {
t.Errorf("conditionToCUE() should contain '||', got: %s", cueStr)
}
}
func TestIntParamIn(t *testing.T) {
p := Int("port")
cond := p.In(80, 443, 8080)
gen := NewCUEGenerator()
cueStr := gen.conditionToCUE(cond)
if !strings.Contains(cueStr, "parameter.port == 80") {
t.Errorf("conditionToCUE() should contain '80', got: %s", cueStr)
}
if !strings.Contains(cueStr, "parameter.port == 443") {
t.Errorf("conditionToCUE() should contain '443', got: %s", cueStr)
}
}
func TestStringParamLenConditions(t *testing.T) {
p := String("name")
tests := []struct {
name string
cond Condition
expected string
}{
{"LenEq", p.LenEq(5), "len(parameter.name) == 5"},
{"LenGt", p.LenGt(5), "len(parameter.name) > 5"},
{"LenGte", p.LenGte(5), "len(parameter.name) >= 5"},
{"LenLt", p.LenLt(5), "len(parameter.name) < 5"},
{"LenLte", p.LenLte(5), "len(parameter.name) <= 5"},
}
gen := NewCUEGenerator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cueStr := gen.conditionToCUE(tt.cond)
if cueStr != tt.expected {
t.Errorf("conditionToCUE() = %q, want %q", cueStr, tt.expected)
}
})
}
}
func TestArrayParamConditions(t *testing.T) {
p := Array("tags").Of(ParamTypeString)
tests := []struct {
name string
cond Condition
expected string
}{
{"LenEq", p.LenEq(5), "len(parameter.tags) == 5"},
{"LenGt", p.LenGt(0), "len(parameter.tags) > 0"},
{"IsEmpty", p.IsEmpty(), "len(parameter.tags) == 0"},
{"IsNotEmpty", p.IsNotEmpty(), "len(parameter.tags) > 0"},
{"Contains", p.Contains("gpu"), `list.Contains(parameter.tags, "gpu")`},
}
gen := NewCUEGenerator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cueStr := gen.conditionToCUE(tt.cond)
if cueStr != tt.expected {
t.Errorf("conditionToCUE() = %q, want %q", cueStr, tt.expected)
}
})
}
}
func TestMapParamConditions(t *testing.T) {
p := Map("config")
tests := []struct {
name string
cond Condition
expected string
}{
{"HasKey", p.HasKey("debug"), "parameter.config.debug != _|_"},
{"LenEq", p.LenEq(5), "len(parameter.config) == 5"},
{"LenGt", p.LenGt(0), "len(parameter.config) > 0"},
{"IsEmpty", p.IsEmpty(), "len(parameter.config) == 0"},
{"IsNotEmpty", p.IsNotEmpty(), "len(parameter.config) > 0"},
}
gen := NewCUEGenerator()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cueStr := gen.conditionToCUE(tt.cond)
if cueStr != tt.expected {
t.Errorf("conditionToCUE() = %q, want %q", cueStr, tt.expected)
}
})
}
}
func TestBoolParamIsFalse(t *testing.T) {
p := Bool("enabled")
cond := p.IsFalse()
gen := NewCUEGenerator()
cueStr := gen.conditionToCUE(cond)
expected := "!parameter.enabled"
if cueStr != expected {
t.Errorf("conditionToCUE() = %q, want %q", cueStr, expected)
}
}
// --- Chaining Tests ---
func TestSchemaConstraintChaining(t *testing.T) {
// Test that schema constraint methods can be chained
strP := String("name").
Pattern("^[a-z]+$").
MinLen(3).
MaxLen(63).
Description("The name")
if strP.GetPattern() != "^[a-z]+$" {
t.Error("Pattern not set correctly after chaining")
}
if strP.GetMinLen() == nil || *strP.GetMinLen() != 3 {
t.Error("MinLen not set correctly after chaining")
}
if strP.GetMaxLen() == nil || *strP.GetMaxLen() != 63 {
t.Error("MaxLen not set correctly after chaining")
}
if strP.GetDescription() != "The name" {
t.Error("Description not set correctly after chaining")
}
intP := Int("replicas").
Min(1).
Max(100).
Default(3)
if intP.GetMin() == nil || *intP.GetMin() != 1 {
t.Error("Min not set correctly after chaining")
}
if intP.GetMax() == nil || *intP.GetMax() != 100 {
t.Error("Max not set correctly after chaining")
}
if !intP.HasDefault() || intP.GetDefault() != 3 {
t.Error("Default not set correctly after chaining")
}
}
// --- Combined Schema + Runtime Test ---
func TestCombinedSchemaAndRuntimeConditions(t *testing.T) {
// Schema constraints define WHAT values are valid
replicas := Int("replicas").Min(1).Max(100).Default(3)
// Runtime conditions control WHAT resources are generated
cond := replicas.Gt(5)
gen := NewCUEGenerator()
comp := NewComponent("test").Params(replicas)
// Check schema generation
schema := gen.GenerateParameterSchema(comp)
if !strings.Contains(schema, ">=1") {
t.Errorf("Schema should contain >=1, got:\n%s", schema)
}
if !strings.Contains(schema, "<=100") {
t.Errorf("Schema should contain <=100, got:\n%s", schema)
}
if !strings.Contains(schema, "*3") {
t.Errorf("Schema should contain default *3, got:\n%s", schema)
}
// Check runtime condition
condStr := gen.conditionToCUE(cond)
expected := "parameter.replicas > 5"
if condStr != expected {
t.Errorf("Runtime condition = %q, want %q", condStr, expected)
}
}
// --- Integration Tests with SetIf ---
func TestSetIfWithNewConditions(t *testing.T) {
name := String("name")
replicas := Int("replicas").Min(1).Max(100)
tags := Array("tags").Of(ParamTypeString)
comp := NewComponent("test-app").
Params(name, replicas, tags).
Template(func(t *Template) {
deployment := NewResource("apps/v1", "Deployment").
Set("metadata.name", name).
Set("spec.replicas", replicas).
// Test various conditions with SetIf
SetIf(name.StartsWith("prod-"), "metadata.labels.env", Lit("production")).
SetIf(name.Contains("canary"), "metadata.labels.deployment", Lit("canary")).
SetIf(replicas.Gt(5), "spec.strategy.type", Lit("RollingUpdate")).
SetIf(tags.IsNotEmpty(), "metadata.annotations.has-tags", Lit("true")).
SetIf(tags.Contains("gpu"), "spec.template.spec.nodeSelector.accelerator", Lit("nvidia"))
t.Output(deployment)
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(`=~"^[a-z][a-z0-9-]*$"`))
})
cue := comp.ToCue()
ginkgo.It("should set and generate string min/max length constraints", func() {
p := String("name").MinLen(3).MaxLen(63)
gomega.Expect(p.GetMinLen()).ToNot(gomega.BeNil())
gomega.Expect(*p.GetMinLen()).To(gomega.Equal(3))
gomega.Expect(p.GetMaxLen()).ToNot(gomega.BeNil())
gomega.Expect(*p.GetMaxLen()).To(gomega.Equal(63))
// Verify conditions are in the output
expectedConditions := []string{
`strings.HasPrefix(parameter.name, "prod-")`,
`strings.Contains(parameter.name, "canary")`,
`parameter.replicas > 5`,
`len(parameter.tags) > 0`,
`list.Contains(parameter.tags, "gpu")`,
}
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring("strings.MinRunes(3)"))
gomega.Expect(cue).To(gomega.ContainSubstring("strings.MaxRunes(63)"))
})
for _, expected := range expectedConditions {
if !strings.Contains(cue, expected) {
t.Errorf("Generated CUE should contain %q, got:\n%s", expected, cue)
}
}
}
ginkgo.It("should set and generate int min/max constraints", func() {
p := Int("replicas").Min(1).Max(100)
gomega.Expect(p.GetMin()).ToNot(gomega.BeNil())
gomega.Expect(*p.GetMin()).To(gomega.Equal(1))
gomega.Expect(p.GetMax()).ToNot(gomega.BeNil())
gomega.Expect(*p.GetMax()).To(gomega.Equal(100))
// --- Additional Edge Case Tests ---
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(">=1"))
gomega.Expect(cue).To(gomega.ContainSubstring("<=100"))
})
func TestFloatParamIn(t *testing.T) {
p := Float("ratio")
cond := p.In(0.5, 1.0, 2.0)
ginkgo.It("should set and generate float min/max constraints", func() {
p := Float("ratio").Min(0.0).Max(1.0)
gomega.Expect(p.GetMin()).ToNot(gomega.BeNil())
gomega.Expect(*p.GetMin()).To(gomega.Equal(0.0))
gomega.Expect(p.GetMax()).ToNot(gomega.BeNil())
gomega.Expect(*p.GetMax()).To(gomega.Equal(1.0))
gen := NewCUEGenerator()
cueStr := gen.conditionToCUE(cond)
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(">=0"))
gomega.Expect(cue).To(gomega.ContainSubstring("<=1"))
})
if !strings.Contains(cueStr, "parameter.ratio == 0.5") {
t.Errorf("conditionToCUE() should contain '0.5', got: %s", cueStr)
}
if !strings.Contains(cueStr, "parameter.ratio == 1") {
t.Errorf("conditionToCUE() should contain '1', got: %s", cueStr)
}
if !strings.Contains(cueStr, " || ") {
t.Errorf("conditionToCUE() should contain '||', got: %s", cueStr)
}
}
ginkgo.It("should set and generate array min/max items constraints", func() {
p := Array("tags").Of(ParamTypeString).MinItems(1).MaxItems(10)
gomega.Expect(p.GetMinItems()).ToNot(gomega.BeNil())
gomega.Expect(*p.GetMinItems()).To(gomega.Equal(1))
gomega.Expect(p.GetMaxItems()).ToNot(gomega.BeNil())
gomega.Expect(*p.GetMaxItems()).To(gomega.Equal(10))
func TestArrayContainsWithDifferentTypes(t *testing.T) {
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring("list.MinItems(1)"))
gomega.Expect(cue).To(gomega.ContainSubstring("list.MaxItems(10)"))
})
// String array with string value
strArray := Array("tags").Of(ParamTypeString)
strCond := strArray.Contains("value")
strResult := gen.conditionToCUE(strCond)
if strResult != `list.Contains(parameter.tags, "value")` {
t.Errorf("String contains = %q", strResult)
}
ginkgo.It("should generate string NotEmpty constraint", func() {
p := String("name").NotEmpty()
gomega.Expect(p.GetNotEmpty()).To(gomega.BeTrue())
// Int array with int value
intArray := Array("ports").Of(ParamTypeInt)
intCond := intArray.Contains(8080)
intResult := gen.conditionToCUE(intCond)
if intResult != `list.Contains(parameter.ports, 8080)` {
t.Errorf("Int contains = %q", intResult)
}
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(`!=""`))
})
// Bool value
boolArray := Array("flags").Of(ParamTypeBool)
boolCond := boolArray.Contains(true)
boolResult := gen.conditionToCUE(boolCond)
if boolResult != `list.Contains(parameter.flags, true)` {
t.Errorf("Bool contains = %q", boolResult)
}
}
ginkgo.It("should generate string NotEmpty with pattern combined", func() {
p := String("name").NotEmpty().Pattern(`^[a-z0-9.-]{3,63}$`)
func TestCombinedStringConstraints(t *testing.T) {
// Test all string constraints together
p := String("hostname").
Pattern("^[a-z][a-z0-9-]*$").
MinLen(3).
MaxLen(63)
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(`!=""`))
gomega.Expect(cue).To(gomega.ContainSubstring(`=~"^[a-z0-9.-]{3,63}$"`))
})
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
ginkgo.It("should generate MapParam closed struct", func() {
p := Object("governance").Closed().WithFields(
String("tenantName"),
String("departmentCode"),
)
gomega.Expect(p.IsClosed()).To(gomega.BeTrue())
// All constraints should be present
if !strings.Contains(cue, `=~"^[a-z][a-z0-9-]*$"`) {
t.Errorf("Should contain pattern, got:\n%s", cue)
}
if !strings.Contains(cue, "strings.MinRunes(3)") {
t.Errorf("Should contain MinRunes, got:\n%s", cue)
}
if !strings.Contains(cue, "strings.MaxRunes(63)") {
t.Errorf("Should contain MaxRunes, got:\n%s", cue)
}
// They should be combined with &
if !strings.Contains(cue, " & ") {
t.Errorf("Constraints should be combined with &, got:\n%s", cue)
}
}
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring("close({"))
gomega.Expect(cue).To(gomega.ContainSubstring("})"))
})
func TestStringConstraintsWithDefault(t *testing.T) {
p := String("env").
Pattern("^(dev|staging|prod)$").
Default("dev")
ginkgo.It("should generate ArrayParam OfEnum schema", func() {
p := Array("allowedMethods").OfEnum("GET", "PUT", "HEAD", "POST", "DELETE")
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(`[...("GET" | "PUT" | "HEAD" | "POST" | "DELETE")]`))
})
})
// Should have both default and pattern
if !strings.Contains(cue, `*"dev"`) {
t.Errorf("Should contain default, got:\n%s", cue)
}
if !strings.Contains(cue, `=~"^(dev|staging|prod)$"`) {
t.Errorf("Should contain pattern, got:\n%s", cue)
}
}
// --- Runtime Condition Tests ---
func TestIntConstraintsWithDefault(t *testing.T) {
p := Int("port").
Min(1).
Max(65535).
Default(8080)
ginkgo.Context("Runtime Conditions", func() {
ginkgo.It("should generate string Contains condition", func() {
p := String("name")
cueStr := gen.conditionToCUE(p.Contains("prod"))
gomega.Expect(cueStr).To(gomega.Equal(`strings.Contains(parameter.name, "prod")`))
})
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
ginkgo.It("should generate string Matches condition", func() {
p := String("name")
cueStr := gen.conditionToCUE(p.Matches("^prod-"))
gomega.Expect(cueStr).To(gomega.Equal(`parameter.name =~ "^prod-"`))
})
if !strings.Contains(cue, "*8080") {
t.Errorf("Should contain default, got:\n%s", cue)
}
if !strings.Contains(cue, ">=1") {
t.Errorf("Should contain min, got:\n%s", cue)
}
if !strings.Contains(cue, "<=65535") {
t.Errorf("Should contain max, got:\n%s", cue)
}
}
ginkgo.It("should generate string StartsWith condition", func() {
p := String("name")
cueStr := gen.conditionToCUE(p.StartsWith("prod-"))
gomega.Expect(cueStr).To(gomega.Equal(`strings.HasPrefix(parameter.name, "prod-")`))
})
func TestEdgeCaseZeroValues(t *testing.T) {
// Min of 0 should still be generated
p := Int("count").Min(0).Max(10)
ginkgo.It("should generate string EndsWith condition", func() {
p := String("name")
cueStr := gen.conditionToCUE(p.EndsWith("-prod"))
gomega.Expect(cueStr).To(gomega.Equal(`strings.HasSuffix(parameter.name, "-prod")`))
})
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
ginkgo.It("should generate string In condition", func() {
p := String("name")
cueStr := gen.conditionToCUE(p.In("api", "web", "worker"))
gomega.Expect(cueStr).To(gomega.ContainSubstring(`parameter.name == "api"`))
gomega.Expect(cueStr).To(gomega.ContainSubstring(`parameter.name == "web"`))
gomega.Expect(cueStr).To(gomega.ContainSubstring(" || "))
})
if !strings.Contains(cue, ">=0") {
t.Errorf("Should contain >=0, got:\n%s", cue)
}
}
ginkgo.It("should generate int In condition", func() {
p := Int("port")
cueStr := gen.conditionToCUE(p.In(80, 443, 8080))
gomega.Expect(cueStr).To(gomega.ContainSubstring("parameter.port == 80"))
gomega.Expect(cueStr).To(gomega.ContainSubstring("parameter.port == 443"))
})
func TestEdgeCaseEmptyStringConditions(t *testing.T) {
gen := NewCUEGenerator()
p := String("name")
ginkgo.It("should generate bool IsFalse condition", func() {
p := Bool("enabled")
cueStr := gen.conditionToCUE(p.IsFalse())
gomega.Expect(cueStr).To(gomega.Equal("!parameter.enabled"))
})
// Empty string checks should work
containsEmpty := p.Contains("")
result := gen.conditionToCUE(containsEmpty)
if result != `strings.Contains(parameter.name, "")` {
t.Errorf("Empty contains = %q", result)
}
ginkgo.It("should generate float In condition", func() {
p := Float("ratio")
cueStr := gen.conditionToCUE(p.In(0.5, 1.0, 2.0))
gomega.Expect(cueStr).To(gomega.ContainSubstring("parameter.ratio == 0.5"))
gomega.Expect(cueStr).To(gomega.ContainSubstring("parameter.ratio == 1"))
gomega.Expect(cueStr).To(gomega.ContainSubstring(" || "))
})
})
startsEmpty := p.StartsWith("")
result2 := gen.conditionToCUE(startsEmpty)
if result2 != `strings.HasPrefix(parameter.name, "")` {
t.Errorf("Empty startsWith = %q", result2)
}
}
ginkgo.Context("String Length Conditions", func() {
ginkgo.DescribeTable("should generate correct CUE for length conditions",
func(condFn func(*StringParam) Condition, expected string) {
p := String("name")
cueStr := gen.conditionToCUE(condFn(p))
gomega.Expect(cueStr).To(gomega.Equal(expected))
},
ginkgo.Entry("LenEq", func(p *StringParam) Condition { return p.LenEq(5) }, "len(parameter.name) == 5"),
ginkgo.Entry("LenGt", func(p *StringParam) Condition { return p.LenGt(5) }, "len(parameter.name) > 5"),
ginkgo.Entry("LenGte", func(p *StringParam) Condition { return p.LenGte(5) }, "len(parameter.name) >= 5"),
ginkgo.Entry("LenLt", func(p *StringParam) Condition { return p.LenLt(5) }, "len(parameter.name) < 5"),
ginkgo.Entry("LenLte", func(p *StringParam) Condition { return p.LenLte(5) }, "len(parameter.name) <= 5"),
)
})
func TestSingleValueIn(t *testing.T) {
gen := NewCUEGenerator()
p := String("status")
ginkgo.Context("Array Conditions", func() {
ginkgo.DescribeTable("should generate correct CUE for array conditions",
func(condFn func(*ArrayParam) Condition, expected string) {
p := Array("tags").Of(ParamTypeString)
cueStr := gen.conditionToCUE(condFn(p))
gomega.Expect(cueStr).To(gomega.Equal(expected))
},
ginkgo.Entry("LenEq", func(p *ArrayParam) Condition { return p.LenEq(5) }, "len(parameter.tags) == 5"),
ginkgo.Entry("LenGt", func(p *ArrayParam) Condition { return p.LenGt(0) }, "len(parameter.tags) > 0"),
ginkgo.Entry("IsEmpty", func(p *ArrayParam) Condition { return p.IsEmpty() }, "len(parameter.tags) == 0"),
ginkgo.Entry("IsNotEmpty", func(p *ArrayParam) Condition { return p.IsNotEmpty() }, "len(parameter.tags) > 0"),
ginkgo.Entry("Contains", func(p *ArrayParam) Condition { return p.Contains("gpu") }, `list.Contains(parameter.tags, "gpu")`),
)
// Single value In should work (even if it's equivalent to Eq)
cond := p.In("active")
result := gen.conditionToCUE(cond)
ginkgo.It("should generate array Contains with different element types", func() {
intArray := Array("ports").Of(ParamTypeInt)
gomega.Expect(gen.conditionToCUE(intArray.Contains(8080))).To(gomega.Equal(`list.Contains(parameter.ports, 8080)`))
if result != `parameter.status == "active"` {
t.Errorf("Single value In = %q, want parameter.status == \"active\"", result)
}
}
boolArray := Array("flags").Of(ParamTypeBool)
gomega.Expect(gen.conditionToCUE(boolArray.Contains(true))).To(gomega.Equal(`list.Contains(parameter.flags, true)`))
})
})
func TestSpecialCharactersInPattern(t *testing.T) {
// Patterns with special regex characters
p := String("email").Pattern(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
ginkgo.Context("Map Conditions", func() {
ginkgo.DescribeTable("should generate correct CUE for map conditions",
func(condFn func(*MapParam) Condition, expected string) {
p := Map("config")
cueStr := gen.conditionToCUE(condFn(p))
gomega.Expect(cueStr).To(gomega.Equal(expected))
},
ginkgo.Entry("HasKey", func(p *MapParam) Condition { return p.HasKey("debug") }, "parameter.config.debug != _|_"),
ginkgo.Entry("LenEq", func(p *MapParam) Condition { return p.LenEq(5) }, "len(parameter.config) == 5"),
ginkgo.Entry("LenGt", func(p *MapParam) Condition { return p.LenGt(0) }, "len(parameter.config) > 0"),
ginkgo.Entry("IsEmpty", func(p *MapParam) Condition { return p.IsEmpty() }, "len(parameter.config) == 0"),
ginkgo.Entry("IsNotEmpty", func(p *MapParam) Condition { return p.IsNotEmpty() }, "len(parameter.config) > 0"),
)
})
gen := NewCUEGenerator()
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
// --- Chaining Tests ---
// Pattern should be properly quoted
if !strings.Contains(cue, `=~"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"`) {
t.Errorf("Pattern with special chars should be escaped, got:\n%s", cue)
}
}
ginkgo.Context("Constraint Chaining", func() {
ginkgo.It("should preserve all chained string constraints", func() {
p := String("name").
Pattern("^[a-z]+$").
MinLen(3).
MaxLen(63).
Description("The name")
gomega.Expect(p.GetPattern()).To(gomega.Equal("^[a-z]+$"))
gomega.Expect(*p.GetMinLen()).To(gomega.Equal(3))
gomega.Expect(*p.GetMaxLen()).To(gomega.Equal(63))
gomega.Expect(p.GetDescription()).To(gomega.Equal("The name"))
})
ginkgo.It("should preserve all chained int constraints", func() {
p := Int("replicas").Min(1).Max(100).Default(3)
gomega.Expect(*p.GetMin()).To(gomega.Equal(1))
gomega.Expect(*p.GetMax()).To(gomega.Equal(100))
gomega.Expect(p.HasDefault()).To(gomega.BeTrue())
gomega.Expect(p.GetDefault()).To(gomega.Equal(3))
})
})
// --- Combined Schema + Runtime ---
ginkgo.Context("Combined Schema and Runtime", func() {
ginkgo.It("should generate both schema constraints and runtime conditions", func() {
replicas := Int("replicas").Min(1).Max(100).Default(3)
comp := NewComponent("test").Params(replicas)
schema := gen.GenerateParameterSchema(comp)
gomega.Expect(schema).To(gomega.ContainSubstring(">=1"))
gomega.Expect(schema).To(gomega.ContainSubstring("<=100"))
gomega.Expect(schema).To(gomega.ContainSubstring("*3"))
condStr := gen.conditionToCUE(replicas.Gt(5))
gomega.Expect(condStr).To(gomega.Equal("parameter.replicas > 5"))
})
ginkgo.It("should generate combined string constraints", func() {
p := String("hostname").
Pattern("^[a-z][a-z0-9-]*$").
MinLen(3).
MaxLen(63)
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(`=~"^[a-z][a-z0-9-]*$"`))
gomega.Expect(cue).To(gomega.ContainSubstring("strings.MinRunes(3)"))
gomega.Expect(cue).To(gomega.ContainSubstring("strings.MaxRunes(63)"))
gomega.Expect(cue).To(gomega.ContainSubstring(" & "))
})
ginkgo.It("should generate string constraints with default", func() {
p := String("env").
Pattern("^(dev|staging|prod)$").
Default("dev")
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(`*"dev"`))
gomega.Expect(cue).To(gomega.ContainSubstring(`=~"^(dev|staging|prod)$"`))
})
ginkgo.It("should generate int constraints with default", func() {
p := Int("port").Min(1).Max(65535).Default(8080)
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring("*8080"))
gomega.Expect(cue).To(gomega.ContainSubstring(">=1"))
gomega.Expect(cue).To(gomega.ContainSubstring("<=65535"))
})
})
// --- Integration with SetIf ---
ginkgo.Context("SetIf Integration", func() {
ginkgo.It("should generate CUE with various SetIf conditions", func() {
name := String("name")
replicas := Int("replicas").Min(1).Max(100)
tags := Array("tags").Of(ParamTypeString)
comp := NewComponent("test-app").
Params(name, replicas, tags).
Template(func(t *Template) {
deployment := NewResource("apps/v1", "Deployment").
Set("metadata.name", name).
Set("spec.replicas", replicas).
SetIf(name.StartsWith("prod-"), "metadata.labels.env", Lit("production")).
SetIf(name.Contains("canary"), "metadata.labels.deployment", Lit("canary")).
SetIf(replicas.Gt(5), "spec.strategy.type", Lit("RollingUpdate")).
SetIf(tags.IsNotEmpty(), "metadata.annotations.has-tags", Lit("true")).
SetIf(tags.Contains("gpu"), "spec.template.spec.nodeSelector.accelerator", Lit("nvidia"))
t.Output(deployment)
})
cue := comp.ToCue()
gomega.Expect(cue).To(gomega.ContainSubstring(`strings.HasPrefix(parameter.name, "prod-")`))
gomega.Expect(cue).To(gomega.ContainSubstring(`strings.Contains(parameter.name, "canary")`))
gomega.Expect(cue).To(gomega.ContainSubstring(`parameter.replicas > 5`))
gomega.Expect(cue).To(gomega.ContainSubstring(`len(parameter.tags) > 0`))
gomega.Expect(cue).To(gomega.ContainSubstring(`list.Contains(parameter.tags, "gpu")`))
})
})
// --- Edge Cases ---
ginkgo.Context("Edge Cases", func() {
ginkgo.It("should handle zero as min value", func() {
p := Int("count").Min(0).Max(10)
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(">=0"))
})
ginkgo.It("should handle empty string conditions", func() {
p := String("name")
gomega.Expect(gen.conditionToCUE(p.Contains(""))).To(gomega.Equal(`strings.Contains(parameter.name, "")`))
gomega.Expect(gen.conditionToCUE(p.StartsWith(""))).To(gomega.Equal(`strings.HasPrefix(parameter.name, "")`))
})
ginkgo.It("should handle single value In condition", func() {
p := String("status")
cueStr := gen.conditionToCUE(p.In("active"))
gomega.Expect(cueStr).To(gomega.Equal(`parameter.status == "active"`))
})
ginkgo.It("should escape special regex characters in pattern", func() {
p := String("email").Pattern(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
comp := NewComponent("test").Params(p)
cue := gen.GenerateParameterSchema(comp)
gomega.Expect(cue).To(gomega.ContainSubstring(`=~"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"`))
})
})
})
+163 -3
View File
@@ -617,9 +617,9 @@ var _ = Describe("Parameters", func() {
It("should create NotSet condition from param", func() {
replicas := defkit.Int("replicas")
cond := replicas.NotSet()
notCond, ok := cond.(*defkit.NotCondition)
Expect(ok).To(BeTrue(), "expected *NotCondition")
inner, ok := notCond.Inner().(*defkit.IsSetCondition)
notExpr, ok := cond.(*defkit.NotExpr)
Expect(ok).To(BeTrue(), "expected *NotExpr")
inner, ok := notExpr.Cond().(*defkit.IsSetCondition)
Expect(ok).To(BeTrue(), "expected inner *IsSetCondition")
Expect(inner.ParamName()).To(Equal("replicas"))
})
@@ -1065,6 +1065,166 @@ var _ = Describe("Parameters", func() {
})
})
Context("StringParam NotEmpty", func() {
It("should default to false", func() {
p := defkit.String("name")
Expect(p.GetNotEmpty()).To(BeFalse())
})
It("should set NotEmpty flag", func() {
p := defkit.String("name").NotEmpty()
Expect(p.GetNotEmpty()).To(BeTrue())
})
It("should chain with other constraints", func() {
p := defkit.String("name").NotEmpty().Pattern("^[a-z]+$").MinLen(3)
Expect(p.GetNotEmpty()).To(BeTrue())
Expect(p.GetPattern()).To(Equal("^[a-z]+$"))
Expect(p.GetMinLen()).NotTo(BeNil())
Expect(*p.GetMinLen()).To(Equal(3))
})
})
Context("ArrayParam NotEmpty elements", func() {
It("should default to false", func() {
p := defkit.StringList("tags")
Expect(p.HasNotEmpty()).To(BeFalse())
})
It("should set NotEmpty flag for elements", func() {
p := defkit.StringList("tags").NotEmpty()
Expect(p.HasNotEmpty()).To(BeTrue())
})
It("should chain with other methods", func() {
p := defkit.StringList("tags").NotEmpty().Optional()
Expect(p.HasNotEmpty()).To(BeTrue())
Expect(p.IsOptional()).To(BeTrue())
})
})
Context("ArrayParam OfEnum", func() {
It("should set schema for enum array", func() {
p := defkit.Array("methods").OfEnum("GET", "POST", "DELETE")
Expect(p.GetSchema()).To(ContainSubstring(`"GET"`))
Expect(p.GetSchema()).To(ContainSubstring(`"POST"`))
Expect(p.GetSchema()).To(ContainSubstring(`"DELETE"`))
Expect(p.GetSchema()).To(ContainSubstring("|"))
})
It("should chain with other methods", func() {
p := defkit.Array("methods").OfEnum("GET", "POST").Optional()
Expect(p.IsOptional()).To(BeTrue())
Expect(p.GetSchema()).To(ContainSubstring(`"GET"`))
})
})
Context("ArrayParam Validators", func() {
It("should store validators", func() {
v := defkit.Validate("check").WithName("_v")
p := defkit.Array("items").WithFields(
defkit.String("name"),
).Validators(v)
Expect(p.GetValidators()).To(HaveLen(1))
Expect(p.GetValidators()[0]).To(Equal(v))
})
It("should accumulate across calls", func() {
p := defkit.Array("items").WithFields(defkit.String("name")).
Validators(defkit.Validate("a").WithName("_a")).
Validators(defkit.Validate("b").WithName("_b"))
Expect(p.GetValidators()).To(HaveLen(2))
})
It("should return empty when not set", func() {
p := defkit.Array("items")
Expect(p.GetValidators()).To(BeEmpty())
})
})
Context("MapParam Closed", func() {
It("should default to false", func() {
p := defkit.Object("config")
Expect(p.IsClosed()).To(BeFalse())
})
It("should set closed flag", func() {
p := defkit.Object("config").Closed()
Expect(p.IsClosed()).To(BeTrue())
})
It("should chain with WithFields", func() {
p := defkit.Object("config").Closed().WithFields(
defkit.String("name"),
)
Expect(p.IsClosed()).To(BeTrue())
Expect(p.GetFields()).To(HaveLen(1))
})
})
Context("MapParam Validators", func() {
It("should store validators", func() {
v := defkit.Validate("check").WithName("_v")
p := defkit.Object("governance").WithFields(
defkit.String("name"),
).Validators(v)
Expect(p.GetValidators()).To(HaveLen(1))
Expect(p.GetValidators()[0]).To(Equal(v))
})
It("should accumulate across calls", func() {
p := defkit.Object("governance").
Validators(defkit.Validate("a").WithName("_a")).
Validators(defkit.Validate("b").WithName("_b"))
Expect(p.GetValidators()).To(HaveLen(2))
})
It("should return empty when not set", func() {
p := defkit.Object("config")
Expect(p.GetValidators()).To(BeEmpty())
})
})
Context("MapParam ConditionalFields", func() {
It("should store conditional field branches", func() {
branch := defkit.WhenParam(defkit.Bool("x").Eq(true)).
Params(defkit.String("secret").Required())
p := defkit.Object("config").ConditionalFields(branch)
Expect(p.GetConditionalFields()).To(HaveLen(1))
Expect(p.GetConditionalFields()[0]).To(Equal(branch))
})
It("should accumulate branches across calls", func() {
b1 := defkit.WhenParam(defkit.Bool("x").Eq(true)).Params(defkit.String("a"))
b2 := defkit.WhenParam(defkit.Bool("x").Eq(false)).Params(defkit.String("b"))
p := defkit.Object("config").
ConditionalFields(b1).
ConditionalFields(b2)
Expect(p.GetConditionalFields()).To(HaveLen(2))
})
It("should return empty when not set", func() {
p := defkit.Object("config")
Expect(p.GetConditionalFields()).To(BeEmpty())
})
It("should chain with other methods", func() {
p := defkit.Object("config").Optional().
WithFields(defkit.String("base")).
ConditionalFields(
defkit.WhenParam(defkit.Bool("x").Eq(true)).Params(defkit.String("extra")),
)
Expect(p.IsOptional()).To(BeTrue())
Expect(p.GetFields()).To(HaveLen(1))
Expect(p.GetConditionalFields()).To(HaveLen(1))
})
})
Context("ClosedStructOption", func() {
It("should create an empty closed struct", func() {
cs := defkit.ClosedStruct()
+3 -3
View File
@@ -482,15 +482,15 @@ func ParamIsSet(name string) *IsSetCondition {
// ParamNotSet creates a condition that checks if a parameter is NOT set.
// This generates CUE: parameter.name == _|_
//
// This is a convenience wrapper around NotCondition{inner: IsSetCondition}.
// This is a convenience wrapper around Not(IsSetCondition).
//
// Example:
//
// // Set default only if not explicitly specified
// tpl.Patch().
// SetIf(defkit.ParamNotSet("replicas"), "spec.replicas", defkit.Literal(1))
func ParamNotSet(name string) *NotCondition {
return &NotCondition{inner: &IsSetCondition{paramName: name}}
func ParamNotSet(name string) *NotExpr {
return Not(&IsSetCondition{paramName: name})
}
// --- ContextOutputExists condition ---
@@ -935,7 +935,7 @@ var _ = Describe("PatchContainer", func() {
It("ParamNotSet should store param name for CUE == _|_ generation", func() {
cond := defkit.ParamNotSet("defaults")
inner := cond.Inner()
inner := cond.Cond()
isSet, ok := inner.(*defkit.IsSetCondition)
Expect(ok).To(BeTrue())
Expect(isSet.ParamName()).To(Equal("defaults"))
+2 -10
View File
@@ -238,20 +238,14 @@ func evaluateCondition(cond Condition, ctx *TestRuntimeContext) bool {
switch c := cond.(type) {
case *IsSetCondition:
return ctx.IsParamSet(c.paramName)
case *CompareCondition:
left := resolveConditionValue(c.left, ctx)
right := resolveConditionValue(c.right, ctx)
return compareValues(left, right, c.op)
case *Comparison:
left := resolveConditionValue(c.Left(), ctx)
right := resolveConditionValue(c.Right(), ctx)
return compareValues(left, right, string(c.Op()))
case *AndCondition:
return evaluateCondition(c.left, ctx) && evaluateCondition(c.right, ctx)
case *OrCondition:
return evaluateCondition(c.left, ctx) || evaluateCondition(c.right, ctx)
case *NotCondition:
return !evaluateCondition(c.inner, ctx)
case *NotExpr:
return !evaluateCondition(c.Cond(), ctx)
case *LogicalExpr:
if c.Op() == OpAnd {
for _, sub := range c.Conditions() {
@@ -268,8 +262,6 @@ func evaluateCondition(cond Condition, ctx *TestRuntimeContext) bool {
}
return false
}
case *NotExpr:
return !evaluateCondition(c.Cond(), ctx)
case *HasExposedPortsCondition:
// Resolve the ports value and check if any have expose=true
portsValue := resolveValue(c.ports, ctx)
+44
View File
@@ -716,6 +716,50 @@ var _ = Describe("Render", func() {
})
})
Context("evaluateCondition with NotExpr", func() {
It("should negate IsSet via NotExpr (ParamNotSet)", func() {
comp := defkit.NewComponent("test").
Workload("v1", "ConfigMap").
Params(defkit.String("optionalField").Optional()).
Template(func(tpl *defkit.Template) {
tpl.Output(
defkit.NewResource("v1", "ConfigMap").
SetIf(defkit.ParamNotSet("optionalField"), "data.fallback", defkit.Lit("default")),
)
})
// Without the param set, ParamNotSet should evaluate to true
rendered := comp.Render(defkit.TestContext())
Expect(rendered.Get("data.fallback")).To(Equal("default"))
// With the param set, ParamNotSet should evaluate to false
rendered2 := comp.Render(defkit.TestContext().WithParam("optionalField", "value"))
Expect(rendered2.Get("data.fallback")).To(BeNil())
})
It("should negate via Not() wrapper with Comparison", func() {
enabled := defkit.Bool("enabled")
comp := defkit.NewComponent("test").
Workload("v1", "ConfigMap").
Params(enabled.Default(true)).
Template(func(tpl *defkit.Template) {
tpl.Output(
defkit.NewResource("v1", "ConfigMap").
SetIf(defkit.Not(defkit.Eq(enabled, defkit.Lit(true))), "data.disabled", defkit.Lit("yes")),
)
})
// enabled=true => Not(true==true) => Not(true) => false => field absent
rendered := comp.Render(defkit.TestContext().WithParam("enabled", true))
Expect(rendered.Get("data.disabled")).To(BeNil())
// enabled=false => Not(false==true) => Not(false) => true => field present
rendered2 := comp.Render(defkit.TestContext().WithParam("enabled", false))
Expect(rendered2.Get("data.disabled")).To(Equal("yes"))
})
})
Context("evaluateCondition with nil", func() {
It("should treat nil condition as true", func() {
// This is tested indirectly: a SetIf with no condition should apply
+85
View File
@@ -253,3 +253,88 @@ func (d *DirectiveOp) Path() string { return d.path }
// GetDirective returns the directive string.
func (d *DirectiveOp) GetDirective() string { return d.directive }
// ConditionalStructOp represents a conditional struct block emitted in the output.
// When the condition is true, the struct builder function is called to generate
// CUE fields at the given path.
//
// Example:
//
// output.ConditionalStruct(replConfig.IsSet(), "spec.replicationConfiguration", func(b *OutputStructBuilder) {
// b.Set("role", defkit.Ref("parameter.replicationConfiguration.role"))
// })
//
// Emits:
//
// if parameter["replicationConfiguration"] != _|_ {
// spec: replicationConfiguration: {
// role: parameter.replicationConfiguration.role
// }
// }
type ConditionalStructOp struct {
cond Condition
path string
builder func(b *OutputStructBuilder)
}
func (c *ConditionalStructOp) resourceOp() {}
// Cond returns the condition.
func (c *ConditionalStructOp) Cond() Condition { return c.cond }
// Path returns the output path.
func (c *ConditionalStructOp) Path() string { return c.path }
// Builder returns the struct builder function.
func (c *ConditionalStructOp) Builder() func(b *OutputStructBuilder) { return c.builder }
// ConditionalStruct records a conditional struct block in the output.
// When the condition is true, the fields built by fn are emitted at the given path.
func (r *Resource) ConditionalStruct(cond Condition, path string, fn func(b *OutputStructBuilder)) *Resource {
op := &ConditionalStructOp{cond: cond, path: path, builder: fn}
if r.currentIf != nil {
r.currentIf.ops = append(r.currentIf.ops, op)
} else {
r.ops = append(r.ops, op)
}
return r
}
// OutputStructBuilder collects field operations for building a conditional struct.
type OutputStructBuilder struct {
ops []structBuilderOp
}
type structBuilderOp interface {
structBuilderOp()
}
type structSetOp struct {
field string
value Value
}
func (s *structSetOp) structBuilderOp() {}
type structSetIfOp struct {
cond Condition
field string
value Value
}
func (s *structSetIfOp) structBuilderOp() {}
// Set adds an unconditional field assignment to the struct.
func (b *OutputStructBuilder) Set(field string, value Value) {
b.ops = append(b.ops, &structSetOp{field: field, value: value})
}
// SetIf adds a conditional field assignment to the struct.
func (b *OutputStructBuilder) SetIf(cond Condition, field string, value Value) {
b.ops = append(b.ops, &structSetIfOp{cond: cond, field: field, value: value})
}
// Ops returns all operations recorded in the struct builder.
func (b *OutputStructBuilder) Ops() []structBuilderOp {
return b.ops
}
+103
View File
@@ -203,6 +203,109 @@ var _ = Describe("Resource", func() {
})
})
Context("ConditionalStruct", func() {
It("should record a ConditionalStructOp", func() {
cond := defkit.Bool("flag").IsSet()
r := defkit.NewResource("v1", "ConfigMap").
ConditionalStruct(cond, "spec.config", func(b *defkit.OutputStructBuilder) {
b.Set("key", defkit.Lit("value"))
})
Expect(r.Ops()).To(HaveLen(1))
csOp, ok := r.Ops()[0].(*defkit.ConditionalStructOp)
Expect(ok).To(BeTrue())
Expect(csOp.Path()).To(Equal("spec.config"))
Expect(csOp.Cond()).To(Equal(cond))
Expect(csOp.Builder()).NotTo(BeNil())
})
It("should record ConditionalStructOp inside If block", func() {
flag := defkit.Bool("flag")
inner := defkit.Object("config").IsSet()
r := defkit.NewResource("v1", "ConfigMap").
If(flag.Eq(true)).
ConditionalStruct(inner, "spec.nested", func(b *defkit.OutputStructBuilder) {
b.Set("x", defkit.Lit(1))
}).
EndIf()
Expect(r.Ops()).To(HaveLen(1))
ifBlock, ok := r.Ops()[0].(*defkit.IfBlock)
Expect(ok).To(BeTrue())
Expect(ifBlock.Ops()).To(HaveLen(1))
_, isCS := ifBlock.Ops()[0].(*defkit.ConditionalStructOp)
Expect(isCS).To(BeTrue())
})
It("should invoke the builder function and record ops", func() {
cond := defkit.Bool("flag").IsSet()
var builderCalled bool
r := defkit.NewResource("v1", "ConfigMap").
ConditionalStruct(cond, "spec.data", func(b *defkit.OutputStructBuilder) {
builderCalled = true
b.Set("name", defkit.Lit("test"))
b.SetIf(defkit.Bool("debug").Eq(true), "debug", defkit.Lit(true))
})
// Builder is not called at record time, only at CUE generation time
Expect(builderCalled).To(BeFalse())
// Call builder manually to verify it works
csOp := r.Ops()[0].(*defkit.ConditionalStructOp)
builder := &defkit.OutputStructBuilder{}
csOp.Builder()(builder)
Expect(builderCalled).To(BeTrue())
Expect(builder.Ops()).To(HaveLen(2))
})
It("should coexist with Set operations", func() {
r := defkit.NewResource("v1", "ConfigMap").
Set("metadata.name", defkit.Lit("test")).
ConditionalStruct(defkit.Bool("x").IsSet(), "spec.x", func(b *defkit.OutputStructBuilder) {
b.Set("val", defkit.Lit(1))
}).
Set("spec.type", defkit.Lit("Opaque"))
Expect(r.Ops()).To(HaveLen(3))
_, isSet1 := r.Ops()[0].(*defkit.SetOp)
_, isCS := r.Ops()[1].(*defkit.ConditionalStructOp)
_, isSet2 := r.Ops()[2].(*defkit.SetOp)
Expect(isSet1).To(BeTrue())
Expect(isCS).To(BeTrue())
Expect(isSet2).To(BeTrue())
})
})
Context("OutputStructBuilder", func() {
It("should record Set operations", func() {
b := &defkit.OutputStructBuilder{}
b.Set("name", defkit.Lit("test"))
b.Set("count", defkit.Lit(5))
Expect(b.Ops()).To(HaveLen(2))
})
It("should record SetIf operations", func() {
b := &defkit.OutputStructBuilder{}
b.SetIf(defkit.Bool("flag").Eq(true), "enabled", defkit.Lit(true))
Expect(b.Ops()).To(HaveLen(1))
})
It("should record mixed Set and SetIf operations in order", func() {
b := &defkit.OutputStructBuilder{}
b.Set("name", defkit.Lit("a"))
b.SetIf(defkit.Bool("x").Eq(true), "x", defkit.Lit(1))
b.Set("type", defkit.Lit("b"))
Expect(b.Ops()).To(HaveLen(3))
})
It("should return empty ops when nothing recorded", func() {
b := &defkit.OutputStructBuilder{}
Expect(b.Ops()).To(BeEmpty())
})
})
Context("Directive", func() {
It("should record a Directive operation", func() {
r := defkit.NewResource("apps/v1", "DaemonSet").
+239
View File
@@ -0,0 +1,239 @@
/*
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
// Validator represents a CUE _validate* block pattern used for cross-field validation.
// Validators emit blocks like:
//
// _validateTenantName: {
// "tenantName must not end with a hyphen": true
// if tenantName =~ ".*-$" {
// "tenantName must not end with a hyphen": false
// }
// }
//
// Validators can be guarded (only active when a condition is true) and can be attached
// at different levels: top-level parameter, inside map/struct params, or inside array elements.
type Validator struct {
message string // the validation message (used as CUE field key)
failCond Condition // when this condition is true, validation fails
guardCond Condition // optional: validator only active when guard is true
name string // optional: override the CUE variable name (default: derived from message)
}
// Validate creates a new Validator with the given error message.
// The message is used both as the CUE field key and as the error shown on failure.
func Validate(message string) *Validator {
return &Validator{message: message}
}
// FailWhen sets the condition that causes validation to fail.
// When this condition evaluates to true, the validator emits false for the message key.
func (v *Validator) FailWhen(cond Condition) *Validator {
v.failCond = cond
return v
}
// OnlyWhen sets a guard condition — the validator is only active when this condition is true.
// If the guard condition is false, the entire validator block is skipped.
func (v *Validator) OnlyWhen(guard Condition) *Validator {
v.guardCond = guard
return v
}
// WithName overrides the CUE variable name for the validator block.
// By default, the name is derived from the message (e.g., "_validateTenantName").
func (v *Validator) WithName(name string) *Validator {
v.name = name
return v
}
// Message returns the validation message.
func (v *Validator) Message() string { return v.message }
// FailCondition returns the fail condition.
func (v *Validator) FailCondition() Condition { return v.failCond }
// GuardCondition returns the guard condition, or nil if not set.
func (v *Validator) GuardCondition() Condition { return v.guardCond }
// CUEName returns the CUE variable name for this validator.
func (v *Validator) CUEName() string { return v.name }
// LocalField creates a reference to a field in the current CUE scope (no "parameter." prefix).
// Used inside validators for condition-building on sibling fields.
// The name is emitted verbatim — supports dot-paths ("Principal.AWS")
// and array indexing ("expiration[0].date").
//
// Compare with defkit.String("name") / defkit.Bool("name") which reference
// top-level parameters and emit "parameter.name" in CUE.
//
// Example: LocalField("tenantName").Matches(".*-$")
func LocalField(name string) *LocalFieldRef {
return &LocalFieldRef{fieldName: name}
}
// LocalFieldRef represents a reference to a field within the current scope.
// It implements Value so it can be used with upstream Comparison, LenValueCondition, etc.
// It provides ergonomic condition builder methods: Matches(), Eq(), IsSet(), LenEq(), Gte(), etc.
type LocalFieldRef struct {
fieldName string
}
// Value/Expr interface implementation — allows LocalFieldRef to be used
// with upstream types like Comparison, LenValueCondition, etc.
func (s *LocalFieldRef) expr() {}
func (s *LocalFieldRef) value() {}
// Name returns the field name.
func (s *LocalFieldRef) Name() string { return s.fieldName }
// Matches creates a condition that checks if this field matches a regex pattern.
// Example: LocalField("tenantName").Matches(".*-$") generates: tenantName =~ ".*-$"
// Reuses upstream RegexMatchCondition.
func (s *LocalFieldRef) Matches(pattern string) Condition {
return RegexMatch(s, pattern)
}
// Eq creates a condition comparing this field to a value.
// Example: LocalField("type").Eq("aws") generates: type == "aws"
// Reuses upstream Comparison type.
func (s *LocalFieldRef) Eq(val any) Condition {
return Eq(s, Lit(val))
}
// Ne creates a condition checking this field is not equal to a value.
// Reuses upstream Comparison type.
func (s *LocalFieldRef) Ne(val any) Condition {
return Ne(s, Lit(val))
}
// IsSet creates a condition that checks if this field has a value (not bottom).
// Example: LocalField("role").IsSet() generates: role != _|_
// Reuses upstream PathExistsCondition.
func (s *LocalFieldRef) IsSet() Condition {
return PathExists(s.fieldName)
}
// NotSet creates a condition that checks if this field is not set (is bottom).
// Example: LocalField("role").NotSet() generates: role == _|_
func (s *LocalFieldRef) NotSet() Condition {
return Not(PathExists(s.fieldName))
}
// LenEq creates a condition that checks if this field's length equals n.
// Example: LocalField("Principal.AWS").LenEq(0) generates: len(Principal.AWS) == 0
// Reuses upstream LenValueCondition via LenEq().
func (s *LocalFieldRef) LenEq(n int) Condition {
return LenEq(s, n)
}
// LenGt creates a condition that checks if this field's length is greater than n.
// Reuses upstream LenValueCondition via LenGt().
func (s *LocalFieldRef) LenGt(n int) Condition {
return LenGt(s, n)
}
// IsEmpty creates a condition that checks if this field has length 0.
func (s *LocalFieldRef) IsEmpty() Condition {
return s.LenEq(0)
}
// Gte creates a condition comparing this field >= another local field.
// Example: LocalField("days").Gte(LocalField("expiration[0].days"))
// generates: days >= expiration[0].days
// Reuses upstream Comparison type via Ge().
func (s *LocalFieldRef) Gte(other *LocalFieldRef) Condition {
return Ge(s, other)
}
// LenOfExpr wraps a Value expression and provides comparison methods that produce Conditions.
// It emits len(<value>) in CUE. Reuses upstream LenValueCondition.
//
// Example: LenOf(Plus(Lit("tenant-"), Reference("parameter.governance.tenantName"), Lit("-"), name)).Gt(63)
// generates: len("tenant-" + parameter.governance.tenantName + "-" + parameter.name) > 63
type LenOfExpr struct {
inner Value
}
// LenOf creates a length expression wrapper around any Value.
func LenOf(v Value) *LenOfExpr {
return &LenOfExpr{inner: v}
}
// Gt creates a condition: len(inner) > n. Returns upstream *LenValueCondition.
func (l *LenOfExpr) Gt(n int) Condition {
return LenGt(l.inner, n)
}
// Gte creates a condition: len(inner) >= n. Returns upstream *LenValueCondition.
func (l *LenOfExpr) Gte(n int) Condition {
return LenGe(l.inner, n)
}
// Eq creates a condition: len(inner) == n. Returns upstream *LenValueCondition.
func (l *LenOfExpr) Eq(n int) Condition {
return LenEq(l.inner, n)
}
// TimeParse creates a Value representing time.Parse(layout, expr) in CUE.
// Used for date comparison validators.
//
// Example: TimeParse("2006-01-02T15:04:05Z", LocalField("date"))
// generates: time.Parse("2006-01-02T15:04:05Z", date)
func TimeParse(layout string, field *LocalFieldRef) *TimeParseExpr {
return &TimeParseExpr{layout: layout, fieldName: field.fieldName}
}
// TimeParseExpr represents a time.Parse() call in CUE.
type TimeParseExpr struct {
layout string
fieldName string
}
func (t *TimeParseExpr) expr() {}
func (t *TimeParseExpr) value() {}
// Layout returns the time layout string.
func (t *TimeParseExpr) Layout() string { return t.layout }
// FieldName returns the field name passed to time.Parse.
func (t *TimeParseExpr) FieldName() string { return t.fieldName }
// Gte creates a condition: time.Parse(layout, fieldA) >= time.Parse(layout, fieldB)
// Reuses upstream Comparison type via Ge().
func (t *TimeParseExpr) Gte(other *TimeParseExpr) Condition {
return Ge(t, other)
}
// RawCUECondition wraps a raw CUE expression string as a Condition.
// Use this for expressions too complex to model with the fluent API.
//
// Example: CUEExpr(`len("tenant-"+parameter.governance.tenantName+"-"+name) > 63`)
type RawCUECondition struct {
baseCondition
rawExpr string
}
// CUEExpr creates a condition from a raw CUE expression string.
// The expression is emitted verbatim in the generated CUE.
func CUEExpr(rawExpr string) *RawCUECondition {
return &RawCUECondition{rawExpr: rawExpr}
}
// Expr returns the raw CUE expression.
func (c *RawCUECondition) Expr() string { return c.rawExpr }
+627
View File
@@ -0,0 +1,627 @@
/*
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("Validator", func() {
var gen *defkit.CUEGenerator
BeforeEach(func() {
gen = defkit.NewCUEGenerator()
})
// --- Validator builder and accessors ---
Context("Builder and Accessors", func() {
It("should create a validator with message", func() {
v := defkit.Validate("field is required")
Expect(v.Message()).To(Equal("field is required"))
Expect(v.CUEName()).To(BeEmpty())
Expect(v.GuardCondition()).To(BeNil())
Expect(v.FailCondition()).To(BeNil())
})
It("should set and return WithName", func() {
v := defkit.Validate("msg").WithName("_validateFoo")
Expect(v.CUEName()).To(Equal("_validateFoo"))
})
It("should set and return FailWhen condition", func() {
cond := defkit.LocalField("x").Eq("")
v := defkit.Validate("msg").FailWhen(cond)
Expect(v.FailCondition()).To(Equal(cond))
})
It("should set and return OnlyWhen guard condition", func() {
guard := defkit.Bool("flag").Eq(true)
v := defkit.Validate("msg").OnlyWhen(guard)
Expect(v.GuardCondition()).To(Equal(guard))
})
It("should support full builder chain returning all accessors correctly", func() {
guard := defkit.Bool("x").Eq(true)
fail := defkit.LocalField("y").Eq("")
v := defkit.Validate("y is required").
WithName("_validateY").
OnlyWhen(guard).
FailWhen(fail)
Expect(v.Message()).To(Equal("y is required"))
Expect(v.CUEName()).To(Equal("_validateY"))
Expect(v.GuardCondition()).To(Equal(guard))
Expect(v.FailCondition()).To(Equal(fail))
})
})
// --- Validator CUE generation ---
Context("Unguarded Validator CUE Generation", func() {
It("should generate a basic unguarded validator block", func() {
v := defkit.Validate("tenantName must not end with a hyphen").
WithName("_validateTenantName").
FailWhen(defkit.LocalField("tenantName").Matches(".*-$"))
comp := defkit.NewComponent("test").
Params(defkit.String("tenantName")).
Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("_validateTenantName:"))
Expect(cue).To(ContainSubstring(`"tenantName must not end with a hyphen": true`))
Expect(cue).To(ContainSubstring(`tenantName =~ ".*-$"`))
Expect(cue).To(ContainSubstring(`"tenantName must not end with a hyphen": false`))
})
It("should use fallback _validate name when no name is set", func() {
v := defkit.Validate("something is wrong")
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("_validate:"))
})
It("should generate validator with no fail condition", func() {
v := defkit.Validate("always passes").WithName("_validateAlways")
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`_validateAlways:`))
Expect(cue).To(ContainSubstring(`"always passes": true`))
Expect(cue).NotTo(ContainSubstring(`"always passes": false`))
})
})
Context("Guarded Validator CUE Generation", func() {
It("should wrap validator in guard condition", func() {
replConfig := defkit.Object("replicationConfiguration").Optional()
objectLock := defkit.Object("objectLock").Optional()
versioningEnabled := defkit.Bool("versioningEnabled").Default(true)
v := defkit.Validate("Require versioningEnabled to be true when replication or object lock is configured").
WithName("_validateVersioning").
OnlyWhen(defkit.Or(replConfig.IsSet(), objectLock.IsSet())).
FailWhen(versioningEnabled.Eq(false))
comp := defkit.NewComponent("test").
Params(replConfig, objectLock, versioningEnabled).
Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`replicationConfiguration"] != _|_`))
Expect(cue).To(ContainSubstring(`objectLock"] != _|_`))
Expect(cue).To(ContainSubstring("parameter.versioningEnabled == false"))
Expect(cue).To(ContainSubstring("_validateVersioning:"))
})
})
Context("Validator inside MapParam", func() {
It("should emit validator inside struct", func() {
v := defkit.Validate("name is required").
WithName("_validateName").
FailWhen(defkit.LocalField("name").Eq(""))
mp := defkit.Object("governance").WithFields(
defkit.String("name"),
defkit.String("department"),
).Validators(v)
comp := defkit.NewComponent("test").Params(mp)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("governance: {"))
Expect(cue).To(ContainSubstring("_validateName:"))
Expect(cue).To(ContainSubstring(`name == ""`))
})
It("should emit mutual exclusion validator inside struct", func() {
v := defkit.Validate("Principal and NotPrincipal cannot be used together").
WithName("_validatePrincipal").
FailWhen(defkit.And(defkit.LocalField("Principal").IsSet(), defkit.LocalField("NotPrincipal").IsSet()))
comp := defkit.NewComponent("test").
Params(
defkit.Object("statement").WithFields(
defkit.String("Principal").Optional(),
defkit.String("NotPrincipal").Optional(),
).Validators(v),
)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("_validatePrincipal:"))
Expect(cue).To(ContainSubstring("Principal != _|_"))
Expect(cue).To(ContainSubstring("NotPrincipal != _|_"))
})
})
Context("Validator inside ArrayParam", func() {
It("should emit validator inside array element struct", func() {
v := defkit.Validate("action is required").
WithName("_validateAction").
FailWhen(defkit.LocalField("action").Eq(""))
arr := defkit.Array("rules").WithFields(
defkit.String("action"),
defkit.String("resource"),
).Validators(v)
comp := defkit.NewComponent("test").Params(arr)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("[...{"))
Expect(cue).To(ContainSubstring("_validateAction:"))
})
It("should emit non-empty length validator inside array element struct", func() {
arr := defkit.Array("corsRules").Optional().WithFields(
defkit.Array("allowedMethods").OfEnum("GET", "PUT", "HEAD", "POST", "DELETE"),
).Validators(
defkit.Validate("allowedMethods cannot be empty").
WithName("_validateAllowedMethods").
FailWhen(defkit.LocalField("allowedMethods").IsEmpty()),
)
comp := defkit.NewComponent("test").Params(arr)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(allowedMethods) == 0"))
})
})
// --- LocalFieldRef ---
Context("LocalFieldRef", func() {
It("should return the field name", func() {
ref := defkit.LocalField("tenantName")
Expect(ref.Name()).To(Equal("tenantName"))
})
It("should support dot-path names", func() {
ref := defkit.LocalField("Principal.AWS")
Expect(ref.Name()).To(Equal("Principal.AWS"))
})
It("should support array-indexed names", func() {
ref := defkit.LocalField("expiration[0].date")
Expect(ref.Name()).To(Equal("expiration[0].date"))
})
It("should implement Value interface", func() {
ref := defkit.LocalField("test")
var v defkit.Value = ref
Expect(v).NotTo(BeNil())
})
It("Matches should generate regex match CUE condition", func() {
v := defkit.Validate("bad pattern").
WithName("_v").
FailWhen(defkit.LocalField("name").Matches(".*-$"))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`name =~ ".*-$"`))
})
It("Eq should generate equality CUE condition", func() {
v := defkit.Validate("check").
WithName("_v").
FailWhen(defkit.LocalField("type").Eq("disabled"))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`type == "disabled"`))
})
It("Ne should generate inequality CUE condition", func() {
v := defkit.Validate("check").
WithName("_v").
FailWhen(defkit.LocalField("type").Ne("enabled"))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`type != "enabled"`))
})
It("IsSet should generate path-exists CUE condition", func() {
v := defkit.Validate("check").
WithName("_v").
FailWhen(defkit.Not(defkit.LocalField("role").IsSet()))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("role == _|_"))
})
It("NotSet should generate path-not-exists CUE condition", func() {
v := defkit.Validate("role must be set").
WithName("_v").
FailWhen(defkit.LocalField("role").NotSet())
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("role == _|_"))
})
It("LenEq should generate length equality CUE condition", func() {
v := defkit.Validate("check").
WithName("_v").
FailWhen(defkit.LocalField("Principal.AWS").LenEq(0))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(Principal.AWS) == 0"))
})
It("LenGt should generate length greater-than CUE condition", func() {
v := defkit.Validate("too many").
WithName("_v").
FailWhen(defkit.LocalField("items").LenGt(10))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(items) > 10"))
})
It("IsEmpty should generate length == 0 CUE condition", func() {
v := defkit.Validate("empty").
WithName("_v").
FailWhen(defkit.LocalField("list").IsEmpty())
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(list) == 0"))
})
It("Gte should generate >= comparison between two local fields", func() {
v := defkit.Validate("days must be >= minDays").
WithName("_v").
FailWhen(defkit.Not(defkit.LocalField("days").Gte(defkit.LocalField("minDays"))))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("days >= minDays"))
})
})
// --- LenOfExpr ---
Context("LenOfExpr", func() {
It("Gt should generate len(value) > n", func() {
v := defkit.Validate("too long").
WithName("_v").
FailWhen(defkit.LenOf(defkit.LocalField("name")).Gt(63))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(name) > 63"))
})
It("Gte should generate len(value) >= n", func() {
v := defkit.Validate("too long").
WithName("_v").
FailWhen(defkit.LenOf(defkit.LocalField("data")).Gte(100))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(data) >= 100"))
})
It("Eq should generate len(value) == n", func() {
v := defkit.Validate("wrong length").
WithName("_v").
FailWhen(defkit.Not(defkit.LenOf(defkit.LocalField("code")).Eq(3)))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(code) == 3"))
})
It("should work with complex expressions like Plus", func() {
v := defkit.Validate("combined name too long").
WithName("_v").
FailWhen(defkit.LenOf(defkit.Plus(
defkit.Lit("tenant-"),
defkit.Reference("parameter.name"),
)).Gt(63))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`len("tenant-" + parameter.name) > 63`))
})
})
// --- TimeParse ---
Context("TimeParse", func() {
It("should return correct accessors", func() {
tp := defkit.TimeParse("2006-01-02T15:04:05Z", defkit.LocalField("startDate"))
Expect(tp.Layout()).To(Equal("2006-01-02T15:04:05Z"))
Expect(tp.FieldName()).To(Equal("startDate"))
})
It("should generate time.Parse CUE expression in validator", func() {
v := defkit.Validate("start must be before end").
WithName("_v").
FailWhen(defkit.TimeParse("2006-01-02T15:04:05Z", defkit.LocalField("startDate")).
Gte(defkit.TimeParse("2006-01-02T15:04:05Z", defkit.LocalField("endDate"))))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`time.Parse("2006-01-02T15:04:05Z", startDate)`))
Expect(cue).To(ContainSubstring(`time.Parse("2006-01-02T15:04:05Z", endDate)`))
Expect(cue).To(ContainSubstring(">="))
})
It("should use different layout formats", func() {
v := defkit.Validate("check").
WithName("_v").
FailWhen(defkit.TimeParse("2006-01-02", defkit.LocalField("d1")).
Gte(defkit.TimeParse("2006-01-02", defkit.LocalField("d2"))))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`time.Parse("2006-01-02", d1)`))
Expect(cue).To(ContainSubstring(`time.Parse("2006-01-02", d2)`))
})
})
// --- RawCUECondition / CUEExpr ---
Context("CUEExpr", func() {
It("should return raw expression from accessor", func() {
c := defkit.CUEExpr(`len(x) > 5`)
Expect(c.Expr()).To(Equal(`len(x) > 5`))
})
It("should emit raw expression in guarded validator", func() {
existingResources := defkit.Bool("existingResources").Default(false)
v := defkit.Validate("Combined name must be less than 64 characters").
WithName("_validateNameLength").
OnlyWhen(existingResources.Eq(false)).
FailWhen(defkit.CUEExpr(`len("tenant-"+parameter.governance.tenantName+"-"+name) > 63`))
comp := defkit.NewComponent("test").
Params(existingResources).
Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring(`len("tenant-"+parameter.governance.tenantName+"-"+name) > 63`))
Expect(cue).To(ContainSubstring("if parameter.existingResources == false"))
})
It("should work inside And condition", func() {
v := defkit.Validate("complex check").
WithName("_validateComplex").
FailWhen(defkit.And(
defkit.CUEExpr(`len(parameter.name) > 10`),
defkit.CUEExpr(`parameter.name =~ "^test"`),
))
comp := defkit.NewComponent("test").Validators(v)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("len(parameter.name) > 10"))
Expect(cue).To(ContainSubstring(`parameter.name =~ "^test"`))
})
})
// --- ConditionalParams CUE generation ---
Context("ConditionalParams CUE Generation", func() {
It("should generate conditional parameter blocks", func() {
existingResources := defkit.Bool("existingResources").Default(false)
comp := defkit.NewComponent("test").
Params(existingResources).
ConditionalParams(defkit.ConditionalParams(
defkit.WhenParam(existingResources.Eq(false)).Params(
defkit.Bool("forceDestroy").Default(false),
defkit.String("sseAlgorithm").Default("AES256").Values("AES256", "aws:kms"),
),
defkit.WhenParam(existingResources.Eq(true)).Params(
defkit.Bool("forceDestroy").Optional(),
defkit.String("sseAlgorithm").Optional().Values("AES256", "aws:kms"),
),
))
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("if parameter.existingResources == false"))
Expect(cue).To(ContainSubstring("if parameter.existingResources == true"))
Expect(cue).To(ContainSubstring(`forceDestroy: *false | bool`))
Expect(cue).To(ContainSubstring("forceDestroy?: bool"))
})
It("should generate validators inside conditional blocks", func() {
existingResources := defkit.Bool("existingResources").Default(false)
kmsMasterKeyId := defkit.String("kmsMasterKeyId").Optional()
comp := defkit.NewComponent("test").
Params(existingResources, kmsMasterKeyId).
ConditionalParams(defkit.ConditionalParams(
defkit.WhenParam(existingResources.Eq(false)).Params(
defkit.String("sseAlgorithm").Default("AES256").Values("AES256", "aws:kms"),
).Validators(
defkit.Validate("kmsMasterKeyId can only be specified when sseAlgorithm is aws:kms").
WithName("_validateKms").
FailWhen(defkit.And(
defkit.LocalField("sseAlgorithm").Ne("aws:kms"),
kmsMasterKeyId.IsSet(),
)),
),
))
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("_validateKms:"))
Expect(cue).To(ContainSubstring(`sseAlgorithm != "aws:kms"`))
})
})
Context("ConditionalFields Inside MapParam CUE Generation", func() {
It("should generate conditional fields inside struct", func() {
existingResources := defkit.Bool("existingResources").Default(false)
objectLock := defkit.Object("objectLock").Optional().ConditionalFields(
defkit.WhenParam(existingResources.Eq(false)).Params(
defkit.Int("retentionDays").Optional().Default(45).Min(1),
defkit.String("retentionMode").Optional().Default("GOVERNANCE").Values("GOVERNANCE", "COMPLIANCE"),
),
defkit.WhenParam(existingResources.Eq(true)).Params(
defkit.Int("retentionDays").Min(1),
defkit.String("retentionMode").Values("GOVERNANCE", "COMPLIANCE"),
),
)
comp := defkit.NewComponent("test").Params(existingResources, objectLock)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("objectLock?: {"))
Expect(cue).To(ContainSubstring("if parameter.existingResources == false"))
Expect(cue).To(ContainSubstring(`retentionDays?: *45 | int & >=1`))
})
})
// --- ConditionalStruct on Resource CUE generation ---
Context("ConditionalStruct CUE Generation", func() {
It("should generate conditional struct block in output", func() {
replConfig := defkit.Object("replicationConfiguration").Optional()
comp := defkit.NewComponent("test").
Params(replConfig).
Workload("apps/v1", "Deployment").
Template(func(tpl *defkit.Template) {
output := tpl.Output(defkit.NewResource("v1", "ConfigMap"))
output.Set("metadata.name", defkit.Lit("test")).
ConditionalStruct(replConfig.IsSet(), "spec.replicationConfiguration", func(b *defkit.OutputStructBuilder) {
b.Set("role", defkit.Reference("parameter.replicationConfiguration.role"))
b.SetIf(replConfig.IsSet(), "enabled", defkit.Lit(true))
})
})
cue := gen.GenerateTemplate(comp)
Expect(cue).To(ContainSubstring(`if parameter["replicationConfiguration"] != _|_`))
Expect(cue).To(ContainSubstring("replicationConfiguration:"))
Expect(cue).To(ContainSubstring("role: parameter.replicationConfiguration.role"))
Expect(cue).To(ContainSubstring("enabled:"))
})
It("should generate conditional struct with SetIf inside", func() {
existingResources := defkit.Bool("existingResources").Default(false)
replConfig := defkit.Object("replicationConfiguration").Optional()
comp := defkit.NewComponent("test").
Params(existingResources, replConfig).
Workload("apps/v1", "Deployment").
Template(func(tpl *defkit.Template) {
output := tpl.Output(defkit.NewResource("v1", "ConfigMap"))
output.Set("metadata.name", defkit.Lit("test")).
ConditionalStruct(replConfig.IsSet(), "spec.replication", func(b *defkit.OutputStructBuilder) {
b.Set("role", defkit.Reference("parameter.replicationConfiguration.role"))
b.SetIf(existingResources.Eq(false), "destinationBucketName", defkit.Lit("replica-bucket"))
})
})
cue := gen.GenerateTemplate(comp)
Expect(cue).To(ContainSubstring("parameter.existingResources == false"))
Expect(cue).To(ContainSubstring("destinationBucketName:"))
})
})
// --- Integration: all features combined ---
Context("All Features Integrated", func() {
It("should combine validators, conditional params, closed structs, and CUE expressions", func() {
existingResources := defkit.Bool("existingResources").Default(false)
governance := defkit.Object("governance").Closed().WithFields(
defkit.String("tenantName").NotEmpty(),
defkit.String("departmentCode").NotEmpty(),
).Validators(
defkit.Validate("tenantName must not end with a hyphen").
WithName("_validateTenant").
FailWhen(defkit.LocalField("tenantName").Matches(".*-$")),
)
comp := defkit.NewComponent("s3-bucket").
Params(existingResources, governance).
ConditionalParams(defkit.ConditionalParams(
defkit.WhenParam(existingResources.Eq(false)).Params(
defkit.Bool("forceDestroy").Default(false),
),
defkit.WhenParam(existingResources.Eq(true)).Params(
defkit.Bool("forceDestroy").Optional(),
),
)).
Validators(
defkit.Validate("Combined name check").
WithName("_validateName").
OnlyWhen(existingResources.Eq(false)).
FailWhen(defkit.CUEExpr(`len(parameter.governance.tenantName) > 63`)),
)
cue := gen.GenerateParameterSchema(comp)
Expect(cue).To(ContainSubstring("existingResources: *false | bool"))
Expect(cue).To(ContainSubstring("governance: close({"))
Expect(cue).To(ContainSubstring(`!=""`))
// Validator uses Matches which emits =~ (positive match inside fail block)
Expect(cue).To(ContainSubstring(`tenantName =~ ".*-$"`))
Expect(cue).To(ContainSubstring("_validateTenant:"))
Expect(cue).To(ContainSubstring("if parameter.existingResources == false"))
Expect(cue).To(ContainSubstring("if parameter.existingResources == true"))
Expect(cue).To(ContainSubstring("forceDestroy: *false | bool"))
Expect(cue).To(ContainSubstring("forceDestroy?: bool"))
Expect(cue).To(ContainSubstring("_validateName:"))
Expect(cue).To(ContainSubstring(`len(parameter.governance.tenantName) > 63`))
})
})
})