mirror of
https://github.com/kubevela/kubevela.git
synced 2026-08-19 04:26:39 +00:00
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:
co-authored by
Ayush Kumar
parent
54197b4721
commit
012a134829
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,}$"`))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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 }
|
||||
@@ -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`))
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user