From cf66f20cd960ef336208dc9e2e4b0ebf143b859c Mon Sep 17 00:00:00 2001 From: Jerrin Francis Date: Thu, 12 Mar 2026 22:09:34 +0530 Subject: [PATCH] Feat: introduce Mandatory() API and three-state CUE field markers in defkit (#7068) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(defkit): introduce Mandatory() API and three-state CUE field markers Add a new .Mandatory() fluent method to all defkit param types and StructField, giving callers explicit control over all three CUE field presence semantics: field?: type — optional, emitted by default (no method call) field: type — non-optional; must have a value, defaults/merging can satisfy (.Mandatory()) field!: type — required in input; user must explicitly provide it (.Required()) Previously, Required() emitted no marker (identical to a mandatory field), making it impossible to generate the CUE ! marker at all. Now Required() emits ! and Mandatory() emits no marker, matching their intuitive meanings. - Add `mandatory bool` field to baseParam and StructField - Add IsMandatory() accessor to baseParam, StructField, and Param interface - Add .Mandatory() builder method to all 10 concrete param types and StructField - Replace the `optional string` variable in cuegen.go with a typed `marker` variable backed by named constants (fieldMarkerOptional, fieldMarkerNone, fieldMarkerRequired) - Update IsOptional() to return !required && !mandatory - Update Optional() to clear mandatory instead of required - Update all tests to use .Mandatory() for non-optional fields Signed-off-by: Jerrin Francis * fix(defkit): correct doc comments after Required/Mandatory semantic swap Fix misleading comments that still described the pre-swap behaviour. Mandatory() comments incorrectly referenced the "!" marker, and Required()/IsRequired() accessors had stale descriptions. Aligned all doc comments and field annotations with the actual semantics: Required → "!" marker, Mandatory → non-optional (no ? marker). Signed-off-by: Jerrin Francis * feat(defkit): add BeMandatory matcher and fix BeOptional for three-state semantics BeOptional incorrectly returned true for mandatory params by checking !IsRequired(). Now uses IsOptional() which correctly excludes both required and mandatory params. Adds BeMandatory() matcher and updates paramAccessor interface with IsOptional/IsMandatory methods. Signed-off-by: Jerrin Francis * fix(defkit): clear required flag in Optional() for all param types The previous commit only fixed StringParam.Optional(). Apply the same fix to Int, Bool, Float, Array, Map, Struct, StructField, Enum, OneOf, and StringKeyMap so that Optional() consistently resets both mandatory and required flags across all parameter types. Signed-off-by: Jerrin Francis * fix(defkit): make Required() and Mandatory() mutually exclusive and fix missing optional markers in CUE generation Required() and Mandatory() only set their own flag without clearing the other, allowing both to be true simultaneously via chaining. Each method now clears the opposite flag so the last call wins, matching the reset semantics that Optional() already followed. Also fix CUE generation for int, bool, float, and enum params where the optional marker was dropped from format strings when a default value was present, causing those fields to always render without the field marker regardless of their optional/mandatory/required state. Signed-off-by: Jerrin Francis * Empty commit to re-trigger e2e Signed-off-by: Jerrin Francis --------- Signed-off-by: Jerrin Francis --- pkg/definition/defkit/array_builder_test.go | 18 +-- pkg/definition/defkit/collections_test.go | 2 +- pkg/definition/defkit/component_test.go | 10 +- pkg/definition/defkit/context_test.go | 2 +- pkg/definition/defkit/cuegen.go | 95 +++++++----- pkg/definition/defkit/cuegen_test.go | 89 +++++++++--- .../defkit/helper_definition_test.go | 6 +- pkg/definition/defkit/param.go | 137 ++++++++++++++++-- pkg/definition/defkit/patch_container_test.go | 2 +- .../defkit/placement_integration_test.go | 4 +- pkg/definition/defkit/resource_test.go | 2 +- .../defkit/testing/matchers/matchers_test.go | 29 +++- .../defkit/testing/matchers/param_matchers.go | 82 +++++++++-- pkg/definition/defkit/trait_test.go | 36 ++--- pkg/definition/defkit/typed_test.go | 2 +- pkg/definition/defkit/types.go | 6 +- pkg/definition/defkit/workflow_step_test.go | 2 +- 17 files changed, 391 insertions(+), 133 deletions(-) diff --git a/pkg/definition/defkit/array_builder_test.go b/pkg/definition/defkit/array_builder_test.go index 9a1f54856..39dff47a6 100644 --- a/pkg/definition/defkit/array_builder_test.go +++ b/pkg/definition/defkit/array_builder_test.go @@ -477,7 +477,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should generate CUE for ForEachWith with simple field assignments", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.String("name"), ) comp := defkit.NewComponent("test"). @@ -504,7 +504,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should generate CUE for ForEachWith with IfSet/IfNotSet conditionals", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.Int("containerPort"), ) comp := defkit.NewComponent("test"). @@ -536,7 +536,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should generate CUE for ForEachWith with let bindings and defaults", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), ) comp := defkit.NewComponent("test"). Workload("apps/v1", "Deployment"). @@ -562,7 +562,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should generate CUE for ForEachWithVar with custom variable name", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), ) comp := defkit.NewComponent("test"). Workload("apps/v1", "Deployment"). @@ -586,7 +586,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should generate CUE for ForEachWithGuardedFiltered with guard and filter", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.Bool("expose").Default(false), ) comp := defkit.NewComponent("test"). @@ -620,7 +620,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should generate CUE for ForEachWith with nested If conditions", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.String("protocol"), ) exposeType := defkit.String("exposeType") @@ -650,7 +650,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should auto-detect strconv import from ForEachWith ItemBuilder ops", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), ) comp := defkit.NewComponent("test"). Workload("apps/v1", "Deployment"). @@ -674,7 +674,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should auto-detect strings import from ForEachWith ItemBuilder ops", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.String("protocol"), ) comp := defkit.NewComponent("test"). @@ -699,7 +699,7 @@ var _ = Describe("ArrayBuilder CUE Generation", func() { It("should generate CUE for helper backed by FromArray with ArrayBuilder", func() { ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.Bool("expose").Default(false), ) comp := defkit.NewComponent("test"). diff --git a/pkg/definition/defkit/collections_test.go b/pkg/definition/defkit/collections_test.go index ab2ad5f92..d44dde9da 100644 --- a/pkg/definition/defkit/collections_test.go +++ b/pkg/definition/defkit/collections_test.go @@ -301,7 +301,7 @@ var _ = Describe("Collections", func() { Expect(compOpt).NotTo(BeNil()) // Verify it can be used in a FieldMap and the CUE generation picks it up ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.Int("nodePort"), ) col := defkit.Each(ports).Map(defkit.FieldMap{ diff --git a/pkg/definition/defkit/component_test.go b/pkg/definition/defkit/component_test.go index 863c314b0..a2602a095 100644 --- a/pkg/definition/defkit/component_test.go +++ b/pkg/definition/defkit/component_test.go @@ -50,7 +50,7 @@ var _ = Describe("ComponentDefinition", func() { }) It("should add parameters", func() { - image := defkit.String("image").Required() + image := defkit.String("image").Mandatory() replicas := defkit.Int("replicas").Default(1) c := defkit.NewComponent("webservice"). Params(image, replicas) @@ -519,7 +519,7 @@ var _ = Describe("ComponentDefinition", func() { c := defkit.NewComponent("webservice"). Description("Web service component"). Workload("apps/v1", "Deployment"). - Params(defkit.String("image").Required()) + Params(defkit.String("image").Mandatory()) yamlBytes, err := c.ToYAML() Expect(err).NotTo(HaveOccurred()) @@ -547,7 +547,7 @@ var _ = Describe("ComponentDefinition", func() { It("should generate parameter schema CUE", func() { c := defkit.NewComponent("test"). Params( - defkit.String("image").Required(), + defkit.String("image").Mandatory(), defkit.Int("replicas").Default(1), ) @@ -563,7 +563,7 @@ var _ = Describe("ComponentDefinition", func() { Description("Web service component"). Workload("apps/v1", "Deployment"). Params( - defkit.String("image").Required().Description("Container image"), + defkit.String("image").Mandatory().Description("Container image"), defkit.Int("replicas").Default(1), ) @@ -644,7 +644,7 @@ var _ = Describe("ComponentDefinition", func() { Context("Full Component Example", func() { It("should build a complete webservice component", func() { - image := defkit.String("image").Required().Description("Container image") + image := defkit.String("image").Mandatory().Description("Container image") replicas := defkit.Int("replicas").Default(1) port := defkit.Int("port").Default(80) diff --git a/pkg/definition/defkit/context_test.go b/pkg/definition/defkit/context_test.go index 68290ef7c..d5d3ca8be 100644 --- a/pkg/definition/defkit/context_test.go +++ b/pkg/definition/defkit/context_test.go @@ -256,7 +256,7 @@ var _ = Describe("TestContext", func() { ) BeforeEach(func() { - image = defkit.String("image").Required() + image = defkit.String("image").Mandatory() replicas = defkit.Int("replicas").Default(1) port = defkit.Int("port").Default(80) diff --git a/pkg/definition/defkit/cuegen.go b/pkg/definition/defkit/cuegen.go index 52b3da78b..d16e6f148 100644 --- a/pkg/definition/defkit/cuegen.go +++ b/pkg/definition/defkit/cuegen.go @@ -25,6 +25,13 @@ import ( // cueOpenStruct is the CUE literal for an open struct type. const cueOpenStruct = "{...}" +// CUE field marker constants express the three field presence semantics. +const ( + fieldMarkerOptional = "?" // field?: type — field may be absent from input + fieldMarkerNone = "" // field: type — field must have a value; defaults/merging can satisfy + fieldMarkerRequired = "!" // field!: type — field must be explicitly provided in user input +) + // cueLabel quotes a CUE field label if it contains characters that are not // valid in a CUE identifier (letters, digits, underscore, $). func cueLabel(name string) string { @@ -504,9 +511,11 @@ func (g *CUEGenerator) writeStructFieldForHelper(sb *strings.Builder, f *StructF } name := f.Name() - optional := "?" + marker := fieldMarkerOptional if f.IsRequired() { - optional = "" + marker = fieldMarkerRequired + } else if f.IsMandatory() { + marker = fieldMarkerNone } // Check if this field references another helper type @@ -517,13 +526,13 @@ func (g *CUEGenerator) writeStructFieldForHelper(sb *strings.Builder, f *StructF if f.HasDefault() { sb.WriteString(fmt.Sprintf("%s%s: *%v | [...#%s]\n", indent, name, formatCUEValue(f.GetDefault()), schemaRef)) } else { - sb.WriteString(fmt.Sprintf("%s%s%s: [...#%s]\n", indent, name, optional, schemaRef)) + sb.WriteString(fmt.Sprintf("%s%s%s: [...#%s]\n", indent, name, marker, schemaRef)) } } else { if f.HasDefault() { sb.WriteString(fmt.Sprintf("%s%s: *%v | #%s\n", indent, name, formatCUEValue(f.GetDefault()), schemaRef)) } else { - sb.WriteString(fmt.Sprintf("%s%s%s: #%s\n", indent, name, optional, schemaRef)) + sb.WriteString(fmt.Sprintf("%s%s%s: #%s\n", indent, name, marker, schemaRef)) } } return @@ -533,13 +542,13 @@ func (g *CUEGenerator) writeStructFieldForHelper(sb *strings.Builder, f *StructF if nested := f.GetNested(); nested != nil { if f.FieldType() == ParamTypeArray { // Array of structs: [...{fields}] - sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, optional)) + sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, marker)) for _, nestedField := range nested.GetFields() { g.writeStructFieldForHelper(sb, nestedField, depth+1) } sb.WriteString(fmt.Sprintf("%s}]\n", indent)) } else { - sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional)) + sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, marker)) for _, nestedField := range nested.GetFields() { g.writeStructFieldForHelper(sb, nestedField, depth+1) } @@ -574,16 +583,16 @@ func (g *CUEGenerator) writeStructFieldForHelper(sb *strings.Builder, f *StructF } case f.FieldType() == ParamTypeArray && f.GetElementType() != "": elemCUE := g.cueTypeForParamType(f.GetElementType()) - sb.WriteString(fmt.Sprintf("%s%s%s: [...%s]\n", indent, name, optional, elemCUE)) + sb.WriteString(fmt.Sprintf("%s%s%s: [...%s]\n", indent, name, marker, elemCUE)) case len(f.GetEnumValues()) > 0: // Enum without default: "value1" | "value2" var enumParts []string for _, v := range f.GetEnumValues() { enumParts = append(enumParts, fmt.Sprintf("%q", v)) } - sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, strings.Join(enumParts, " | "))) + sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, marker, strings.Join(enumParts, " | "))) default: - sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, fieldType)) + sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, marker, fieldType)) } } @@ -2753,42 +2762,50 @@ func (g *CUEGenerator) writeParam(sb *strings.Builder, param Param, depth int) { } name := param.Name() - optional := "?" + marker := fieldMarkerOptional forceOptional := false if bp, ok := param.(interface{ IsForceOptional() bool }); ok { forceOptional = bp.IsForceOptional() } - if param.IsRequired() || (param.HasDefault() && !forceOptional) { - // No ? for required fields or fields with defaults (defaults make them effectively present) - // Unless forceOptional is set, which keeps ? even with a default - optional = "" + if param.IsRequired() { + // "!" marker: user must explicitly provide this field in input + marker = fieldMarkerRequired + } else { + isMandatory := false + if bm, ok := param.(interface{ IsMandatory() bool }); ok { + isMandatory = bm.IsMandatory() + } + if isMandatory || (param.HasDefault() && !forceOptional) { + // No marker: field must have a value, but defaults/merging can satisfy it + marker = fieldMarkerNone + } } // Handle different parameter types switch p := param.(type) { case *StringParam: - g.writeStringParam(sb, p, indent, name, optional) + g.writeStringParam(sb, p, indent, name, marker) case *IntParam: - g.writeIntParam(sb, p, indent, name, optional) + g.writeIntParam(sb, p, indent, name, marker) case *BoolParam: - g.writeBoolParam(sb, p, indent, name, optional) + g.writeBoolParam(sb, p, indent, name, marker) case *FloatParam: - g.writeFloatParam(sb, p, indent, name, optional) + g.writeFloatParam(sb, p, indent, name, marker) case *ArrayParam: - g.writeArrayParam(sb, p, indent, name, optional, depth) + g.writeArrayParam(sb, p, indent, name, marker, depth) case *MapParam: - g.writeMapParam(sb, p, indent, name, optional, depth) + g.writeMapParam(sb, p, indent, name, marker, depth) case *StringKeyMapParam: - g.writeStringKeyMapParam(sb, p, indent, name, optional) + g.writeStringKeyMapParam(sb, p, indent, name, marker) case *StructParam: - g.writeStructParam(sb, p, indent, name, optional, depth) + g.writeStructParam(sb, p, indent, name, marker, depth) case *EnumParam: - g.writeEnumParam(sb, p, indent, name, optional) + g.writeEnumParam(sb, p, indent, name, marker) case *OneOfParam: - g.writeOneOfParam(sb, p, indent, name, optional, depth) + g.writeOneOfParam(sb, p, indent, name, marker, depth) default: // Generic fallback - sb.WriteString(fmt.Sprintf("%s%s%s: _\n", indent, name, optional)) + sb.WriteString(fmt.Sprintf("%s%s%s: _\n", indent, name, marker)) } } @@ -2877,9 +2894,9 @@ func (g *CUEGenerator) writeIntParam(sb *strings.Builder, p *IntParam, indent, n if p.HasDefault() { if len(constraints) > 0 { - sb.WriteString(fmt.Sprintf("%s%s: *%v | int & %s\n", indent, name, p.GetDefault(), strings.Join(constraints, " & "))) + sb.WriteString(fmt.Sprintf("%s%s%s: *%v | int & %s\n", indent, name, optional, p.GetDefault(), strings.Join(constraints, " & "))) } else { - sb.WriteString(fmt.Sprintf("%s%s: *%v | int\n", indent, name, p.GetDefault())) + sb.WriteString(fmt.Sprintf("%s%s%s: *%v | int\n", indent, name, optional, p.GetDefault())) } } else { if len(constraints) > 0 { @@ -2893,7 +2910,7 @@ func (g *CUEGenerator) writeIntParam(sb *strings.Builder, p *IntParam, indent, n // writeBoolParam writes a boolean parameter. func (g *CUEGenerator) writeBoolParam(sb *strings.Builder, p *BoolParam, indent, name, optional string) { if p.HasDefault() { - sb.WriteString(fmt.Sprintf("%s%s: *%v | bool\n", indent, name, p.GetDefault())) + sb.WriteString(fmt.Sprintf("%s%s%s: *%v | bool\n", indent, name, optional, p.GetDefault())) } else { sb.WriteString(fmt.Sprintf("%s%s%s: bool\n", indent, name, optional)) } @@ -2916,9 +2933,9 @@ func (g *CUEGenerator) writeFloatParam(sb *strings.Builder, p *FloatParam, inden if p.HasDefault() { if len(constraints) > 0 { - sb.WriteString(fmt.Sprintf("%s%s: *%v | number & %s\n", indent, name, p.GetDefault(), strings.Join(constraints, " & "))) + sb.WriteString(fmt.Sprintf("%s%s%s: *%v | number & %s\n", indent, name, optional, p.GetDefault(), strings.Join(constraints, " & "))) } else { - sb.WriteString(fmt.Sprintf("%s%s: *%v | float\n", indent, name, p.GetDefault())) + sb.WriteString(fmt.Sprintf("%s%s%s: *%v | float\n", indent, name, optional, p.GetDefault())) } } else { if len(constraints) > 0 { @@ -3044,9 +3061,11 @@ func (g *CUEGenerator) writeStructField(sb *strings.Builder, f *StructField, dep } name := f.Name() - optional := "?" + marker := fieldMarkerOptional if f.IsRequired() { - optional = "" + marker = fieldMarkerRequired + } else if f.IsMandatory() { + marker = fieldMarkerNone } fieldType := g.cueTypeForParamType(f.FieldType()) @@ -3055,13 +3074,13 @@ func (g *CUEGenerator) writeStructField(sb *strings.Builder, f *StructField, dep switch { case nested != nil: if f.FieldType() == ParamTypeArray { - sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, optional)) + sb.WriteString(fmt.Sprintf("%s%s%s: [...{\n", indent, name, marker)) for _, nestedField := range nested.GetFields() { g.writeStructField(sb, nestedField, depth+1) } sb.WriteString(fmt.Sprintf("%s}]\n", indent)) } else { - sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, optional)) + sb.WriteString(fmt.Sprintf("%s%s%s: {\n", indent, name, marker)) for _, nestedField := range nested.GetFields() { g.writeStructField(sb, nestedField, depth+1) } @@ -3089,16 +3108,16 @@ func (g *CUEGenerator) writeStructField(sb *strings.Builder, f *StructField, dep } case f.FieldType() == ParamTypeArray && f.GetElementType() != "": elemCUE := g.cueTypeForParamType(f.GetElementType()) - sb.WriteString(fmt.Sprintf("%s%s%s: [...%s]\n", indent, name, optional, elemCUE)) + sb.WriteString(fmt.Sprintf("%s%s%s: [...%s]\n", indent, name, marker, elemCUE)) case len(f.GetEnumValues()) > 0: // Enum without default: "value1" | "value2" var enumParts []string for _, v := range f.GetEnumValues() { enumParts = append(enumParts, fmt.Sprintf("%q", v)) } - sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, strings.Join(enumParts, " | "))) + sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, marker, strings.Join(enumParts, " | "))) default: - sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, fieldType)) + sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, marker, fieldType)) } } @@ -3121,7 +3140,7 @@ func (g *CUEGenerator) writeEnumParam(sb *strings.Builder, p *EnumParam, indent, enumParts = append(enumParts, fmt.Sprintf("%q", v)) } } - sb.WriteString(fmt.Sprintf("%s%s: %s\n", indent, name, strings.Join(enumParts, " | "))) + sb.WriteString(fmt.Sprintf("%s%s%s: %s\n", indent, name, optional, strings.Join(enumParts, " | "))) } else { // Build enum type without default: "value1" | "value2" | ... var enumParts []string diff --git a/pkg/definition/defkit/cuegen_test.go b/pkg/definition/defkit/cuegen_test.go index d6e8b28be..41a24e357 100644 --- a/pkg/definition/defkit/cuegen_test.go +++ b/pkg/definition/defkit/cuegen_test.go @@ -36,7 +36,7 @@ var _ = Describe("CUEGenerator", func() { It("should generate CUE for string parameters", func() { comp := defkit.NewComponent("test"). Params( - defkit.String("image").Required().Description("Container image"), + defkit.String("image").Mandatory().Description("Container image"), defkit.String("tag").Default("latest").Description("Image tag"), ) @@ -52,7 +52,7 @@ var _ = Describe("CUEGenerator", func() { comp := defkit.NewComponent("test"). Params( defkit.Int("replicas").Default(1).Description("Number of replicas"), - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), ) cue := gen.GenerateParameterSchema(comp) @@ -110,7 +110,7 @@ var _ = Describe("CUEGenerator", func() { Expect(cue).To(ContainSubstring("labels?: [string]: string")) }) - It("should mark required parameters without ?", func() { + It("should emit ! for required parameters", func() { comp := defkit.NewComponent("test"). Params( defkit.String("required").Required(), @@ -119,10 +119,61 @@ var _ = Describe("CUEGenerator", func() { cue := gen.GenerateParameterSchema(comp) - Expect(cue).To(ContainSubstring("required: string")) + Expect(cue).To(ContainSubstring("required!: string")) Expect(cue).To(ContainSubstring("optional?: string")) }) + It("should handle all three field presence markers", func() { + comp := defkit.NewComponent("test"). + Params( + defkit.String("accessKey").Required(), // ! — user must explicitly provide + defkit.Int("port").Mandatory(), // no marker — must have value, defaults can satisfy + defkit.Bool("enabled").Mandatory(), // no marker + defkit.String("tag"), // ? — fully optional + ) + + cue := gen.GenerateParameterSchema(comp) + + // Required fields get "!" marker + Expect(cue).To(ContainSubstring("accessKey!: string")) + // Mandatory fields get no marker (no ? and no !) + Expect(cue).To(ContainSubstring("port: int")) + Expect(cue).NotTo(ContainSubstring("port!:")) + Expect(cue).NotTo(ContainSubstring("port?:")) + Expect(cue).To(ContainSubstring("enabled: bool")) + // Plain optional gets ? + Expect(cue).To(ContainSubstring("tag?: string")) + }) + + It("should emit ! for required parameters with defaults", func() { + comp := defkit.NewComponent("test"). + Params( + defkit.String("accessKey").Required().Default("key123"), + ) + + cue := gen.GenerateParameterSchema(comp) + + // Required with default: "!" wins, default value is still in the CUE type + Expect(cue).To(ContainSubstring(`accessKey!: *"key123" | string`)) + }) + + It("should emit ! for required non-string parameters with defaults", func() { + comp := defkit.NewComponent("test"). + Params( + defkit.Int("port").Required().Default(8080), + defkit.Bool("enabled").Required().Default(true), + defkit.Float("ratio").Required().Default(0.5), + defkit.Enum("mode").Required().Default("fast").Values("fast", "slow"), + ) + + cue := gen.GenerateParameterSchema(comp) + + Expect(cue).To(ContainSubstring(`port!: *8080 | int`)) + Expect(cue).To(ContainSubstring(`enabled!: *true | bool`)) + Expect(cue).To(ContainSubstring(`ratio!: *0.5 | float`)) + Expect(cue).To(ContainSubstring(`mode!: *"fast" | "slow"`)) + }) + It("should keep ? for ForceOptional parameters even with defaults", func() { comp := defkit.NewComponent("test"). Params( @@ -167,7 +218,7 @@ var _ = Describe("CUEGenerator", func() { It("should generate // +short directive for params with short flags", func() { comp := defkit.NewComponent("test"). Params( - defkit.String("image").Required().Description("Container image").Short("i"), + defkit.String("image").Mandatory().Description("Container image").Short("i"), ) cue := gen.GenerateParameterSchema(comp) @@ -241,7 +292,7 @@ var _ = Describe("CUEGenerator", func() { comp := defkit.NewComponent("test"). Params( defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.String("name"), defkit.Enum("protocol").Values("TCP", "UDP").Default("TCP"), ), @@ -261,7 +312,7 @@ var _ = Describe("CUEGenerator", func() { defkit.Struct("selector").WithFields( defkit.Field("matchExpressions", defkit.ParamTypeArray). Nested(defkit.Struct("matchExpression").WithFields( - defkit.Field("key", defkit.ParamTypeString).Required(), + defkit.Field("key", defkit.ParamTypeString).Mandatory(), defkit.Field("operator", defkit.ParamTypeString), )), ), @@ -284,7 +335,7 @@ var _ = Describe("CUEGenerator", func() { Description("Volume type"). Variants( defkit.Variant("pvc").WithFields( - defkit.Field("claimName", defkit.ParamTypeString).Required(), + defkit.Field("claimName", defkit.ParamTypeString).Mandatory(), ), defkit.Variant("emptyDir").WithFields( defkit.Field("medium", defkit.ParamTypeString).Default("").Values("", "Memory"), @@ -308,10 +359,10 @@ var _ = Describe("CUEGenerator", func() { comp := defkit.NewComponent("test"). Params( defkit.List("volumes").WithFields( - defkit.String("name").Required(), + defkit.String("name").Mandatory(), defkit.OneOf("type").Default("emptyDir").Variants( defkit.Variant("pvc").WithFields( - defkit.Field("claimName", defkit.ParamTypeString).Required(), + defkit.Field("claimName", defkit.ParamTypeString).Mandatory(), ), defkit.Variant("emptyDir"), ), @@ -335,7 +386,7 @@ var _ = Describe("CUEGenerator", func() { Variants( defkit.Variant("simple"), // no fields defkit.Variant("complex").WithFields( - defkit.Field("config", defkit.ParamTypeString).Required(), + defkit.Field("config", defkit.ParamTypeString).Mandatory(), ), ), ) @@ -687,7 +738,7 @@ var _ = Describe("CUEGenerator", func() { Description("Web service component"). Workload("apps/v1", "Deployment"). Params( - defkit.String("image").Required(), + defkit.String("image").Mandatory(), ) cue := gen.GenerateFullDefinition(comp) @@ -866,7 +917,7 @@ var _ = Describe("CUEGenerator", func() { gen := defkit.NewCUEGenerator() ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.String("name"), ) comp := defkit.NewComponent("test"). @@ -930,7 +981,7 @@ var _ = Describe("CUEGenerator", func() { Values("ClusterIP", "NodePort", "LoadBalancer"). Default("ClusterIP") ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.String("name"), defkit.Int("nodePort"), defkit.Bool("expose").Default(false), @@ -966,7 +1017,7 @@ var _ = Describe("CUEGenerator", func() { Values("ClusterIP", "NodePort", "LoadBalancer"). Default("ClusterIP") ports := defkit.List("ports").WithFields( - defkit.Int("port").Required(), + defkit.Int("port").Mandatory(), defkit.Int("nodePort"), defkit.String("protocol"), defkit.Bool("expose").Default(false), @@ -1082,14 +1133,14 @@ var _ = Describe("CUEGenerator", func() { Expect(cue).NotTo(ContainSubstring("propagation?: string")) }) - It("should generate required enum without default on a helper struct field", func() { + It("should generate mandatory enum without default on a helper struct field", func() { rule := defkit.Struct("rule").WithFields( defkit.Field("mode", defkit.ParamTypeString). Values("strict", "permissive"). - Required(), + Mandatory(), ) - p := defkit.NewPolicy("test-enum-required"). + p := defkit.NewPolicy("test-enum-mandatory"). Description("Test"). Helper("Rule", rule) @@ -1110,7 +1161,7 @@ var _ = Describe("CUEGenerator", func() { Optional(), defkit.Field("mode", defkit.ParamTypeString). Values("fast", "safe"). - Required(), + Mandatory(), ), ). Template(func(tpl *defkit.Template) { diff --git a/pkg/definition/defkit/helper_definition_test.go b/pkg/definition/defkit/helper_definition_test.go index cf3dfcb13..5071096d5 100644 --- a/pkg/definition/defkit/helper_definition_test.go +++ b/pkg/definition/defkit/helper_definition_test.go @@ -102,7 +102,7 @@ var _ = Describe("HelperDefinition", func() { helper := defkit.Map("labelSelector").WithFields( defkit.StringKeyMap("matchLabels").Description("A map of {key,value} pairs"), defkit.Array("matchExpressions").Description("Label selector requirements").WithFields( - defkit.String("key").Required(), + defkit.String("key").Mandatory(), defkit.String("operator").Default("In").Values("In", "NotIn", "Exists", "DoesNotExist"), defkit.Array("values").Of(defkit.ParamTypeString), ), @@ -133,7 +133,7 @@ var _ = Describe("HelperDefinition", func() { It("should render Of(ParamTypeString) in Struct-based helper fields", func() { helper := defkit.Struct("nodeSelector").WithFields( - defkit.Field("key", defkit.ParamTypeString).Required(), + defkit.Field("key", defkit.ParamTypeString).Mandatory(), defkit.Field("operator", defkit.ParamTypeString).Default("In").Values("In", "NotIn"), defkit.Field("values", defkit.ParamTypeArray).Of(defkit.ParamTypeString), ) @@ -165,7 +165,7 @@ var _ = Describe("HelperDefinition", func() { affinityHelper := defkit.Struct("podAffinityTerm").WithFields( defkit.Field("labelSelector", defkit.ParamTypeStruct).WithSchemaRef("labelSelector"), defkit.Field("namespaces", defkit.ParamTypeArray).Of(defkit.ParamTypeString), - defkit.Field("topologyKey", defkit.ParamTypeString).Required(), + defkit.Field("topologyKey", defkit.ParamTypeString).Mandatory(), ) trait := defkit.NewTrait("schemaref-test"). diff --git a/pkg/definition/defkit/param.go b/pkg/definition/defkit/param.go index 010a44ef3..6c67f78a6 100644 --- a/pkg/definition/defkit/param.go +++ b/pkg/definition/defkit/param.go @@ -20,7 +20,8 @@ package defkit type baseParam struct { name string paramType ParamType - required bool + required bool // when true, emits "!" marker — user must explicitly provide this field in input + mandatory bool // when true, field is non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it defaultValue any description string forceOptional bool // when true, field stays optional even with a default value @@ -34,7 +35,8 @@ func (p *baseParam) condition() {} func (p *baseParam) Name() string { return p.name } func (p *baseParam) IsRequired() bool { return p.required } -func (p *baseParam) IsOptional() bool { return !p.required } +func (p *baseParam) IsOptional() bool { return !p.required && !p.mandatory } +func (p *baseParam) IsMandatory() bool { return p.mandatory } func (p *baseParam) HasDefault() bool { return p.defaultValue != nil } func (p *baseParam) GetDefault() any { return p.defaultValue } func (p *baseParam) GetDescription() string { return p.description } @@ -127,14 +129,26 @@ func String(name string) *StringParam { } } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. +// The user must explicitly provide this field — even if a default value exists. func (p *StringParam) Required() *StringParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker). +// The field must have a value, but defaults or CUE merging can satisfy it +// without the user explicitly providing it. +func (p *StringParam) Mandatory() *StringParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *StringParam) Optional() *StringParam { + p.mandatory = false p.required = false return p } @@ -327,14 +341,23 @@ func Int(name string) *IntParam { } } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *IntParam) Required() *IntParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *IntParam) Mandatory() *IntParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *IntParam) Optional() *IntParam { + p.mandatory = false p.required = false return p } @@ -443,14 +466,23 @@ func Bool(name string) *BoolParam { } } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *BoolParam) Required() *BoolParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *BoolParam) Mandatory() *BoolParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *BoolParam) Optional() *BoolParam { + p.mandatory = false p.required = false return p } @@ -518,14 +550,23 @@ func Float(name string) *FloatParam { } } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *FloatParam) Required() *FloatParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *FloatParam) Mandatory() *FloatParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *FloatParam) Optional() *FloatParam { + p.mandatory = false p.required = false return p } @@ -622,14 +663,23 @@ func (p *ArrayParam) Of(elemType ParamType) *ArrayParam { return p } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *ArrayParam) Required() *ArrayParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *ArrayParam) Mandatory() *ArrayParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *ArrayParam) Optional() *ArrayParam { + p.mandatory = false p.required = false return p } @@ -788,14 +838,23 @@ func (p *MapParam) Of(valueType ParamType) *MapParam { return p } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *MapParam) Required() *MapParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *MapParam) Mandatory() *MapParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *MapParam) Optional() *MapParam { + p.mandatory = false p.required = false return p } @@ -896,7 +955,8 @@ func (p *MapParam) IsNotEmpty() Condition { type StructField struct { name string fieldType ParamType - required bool + required bool // when true, emits "!" marker — user must explicitly provide this field in input + mandatory bool // when true, field is non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it defaultValue any description string nested *StructParam // for nested structs @@ -913,14 +973,22 @@ func Field(name string, fieldType ParamType) *StructField { } } -// Required marks the field as required. +// Required marks the field as required in input, emitting the "!" CUE marker. +// The user must explicitly provide this field — even if a default value exists. func (f *StructField) Required() *StructField { f.required = true return f } +// Mandatory marks the field as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (f *StructField) Mandatory() *StructField { + f.mandatory = true + return f +} + // Optional marks the field as optional (default behavior). func (f *StructField) Optional() *StructField { + f.mandatory = false f.required = false return f } @@ -959,9 +1027,12 @@ func (f *StructField) Name() string { return f.name } // FieldType returns the field type. func (f *StructField) FieldType() ParamType { return f.fieldType } -// IsRequired returns true if the field is required. +// IsRequired returns true if the field emits the "!" CUE marker — user must explicitly provide it. func (f *StructField) IsRequired() bool { return f.required } +// IsMandatory returns true if the field is non-optional (no ? marker) but defaults or CUE merging can satisfy it. +func (f *StructField) IsMandatory() bool { return f.mandatory } + // HasDefault returns true if the field has a default value. func (f *StructField) HasDefault() bool { return f.defaultValue != nil } @@ -1027,14 +1098,23 @@ func (p *StructParam) WithFields(fields ...*StructField) *StructParam { return p } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *StructParam) Required() *StructParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *StructParam) Mandatory() *StructParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *StructParam) Optional() *StructParam { + p.mandatory = false p.required = false return p } @@ -1100,14 +1180,23 @@ func (p *EnumParam) Values(values ...string) *EnumParam { return p } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *EnumParam) Required() *EnumParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *EnumParam) Mandatory() *EnumParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *EnumParam) Optional() *EnumParam { + p.mandatory = false p.required = false return p } @@ -1211,14 +1300,23 @@ func (p *OneOfParam) Default(value string) *OneOfParam { return p } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *OneOfParam) Required() *OneOfParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *OneOfParam) Mandatory() *OneOfParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *OneOfParam) Optional() *OneOfParam { + p.mandatory = false p.required = false return p } @@ -1289,14 +1387,23 @@ func StringKeyMap(name string) *StringKeyMapParam { } } -// Required marks the parameter as required. +// Required marks the parameter as required in input, emitting the "!" CUE marker. func (p *StringKeyMapParam) Required() *StringKeyMapParam { p.required = true + p.mandatory = false + return p +} + +// Mandatory marks the parameter as non-optional (no ? marker); must have a value but defaults or CUE merging can satisfy it. +func (p *StringKeyMapParam) Mandatory() *StringKeyMapParam { + p.mandatory = true + p.required = false return p } // Optional marks the parameter as optional (default behavior). func (p *StringKeyMapParam) Optional() *StringKeyMapParam { + p.mandatory = false p.required = false return p } diff --git a/pkg/definition/defkit/patch_container_test.go b/pkg/definition/defkit/patch_container_test.go index c095c1fea..3442e2684 100644 --- a/pkg/definition/defkit/patch_container_test.go +++ b/pkg/definition/defkit/patch_container_test.go @@ -608,7 +608,7 @@ var _ = Describe("PatchContainer", func() { trait := defkit.NewTrait("no-dup-param-test"). Description("Test no duplicate parameter block"). AppliesTo("deployments.apps"). - Params(defkit.String("image").Required()). + Params(defkit.String("image").Mandatory()). Template(func(tpl *defkit.Template) { tpl.UsePatchContainer(defkit.PatchContainerConfig{ ContainerNameParam: "containerName", diff --git a/pkg/definition/defkit/placement_integration_test.go b/pkg/definition/defkit/placement_integration_test.go index 8b2302662..3fe6bebb7 100644 --- a/pkg/definition/defkit/placement_integration_test.go +++ b/pkg/definition/defkit/placement_integration_test.go @@ -78,7 +78,7 @@ var _ = Describe("Placement Integration", func() { Description("A component with placement"). Workload("apps/v1", "Deployment"). RunOn(placement.Label("provider").Eq("aws")). - Params(defkit.String("image").Required()) + Params(defkit.String("image").Mandatory()) Expect(c.GetDescription()).To(Equal("A component with placement")) Expect(c.GetWorkload().Kind()).To(Equal("Deployment")) @@ -139,7 +139,7 @@ var _ = Describe("Placement Integration", func() { p := defkit.NewPolicy("full-policy"). Description("A policy with placement"). RunOn(placement.Label("provider").In("aws", "gcp")). - Params(defkit.String("target").Required()) + Params(defkit.String("target").Mandatory()) Expect(p.GetDescription()).To(Equal("A policy with placement")) Expect(p.HasPlacement()).To(BeTrue()) diff --git a/pkg/definition/defkit/resource_test.go b/pkg/definition/defkit/resource_test.go index 81a4efa94..aa97cea64 100644 --- a/pkg/definition/defkit/resource_test.go +++ b/pkg/definition/defkit/resource_test.go @@ -36,7 +36,7 @@ var _ = Describe("Resource", func() { Context("Set", func() { It("should record a Set operation", func() { - image := defkit.String("image").Required() + image := defkit.String("image").Mandatory() r := defkit.NewResource("apps/v1", "Deployment"). Set("spec.template.spec.containers[0].image", image) Expect(r.Ops()).To(HaveLen(1)) diff --git a/pkg/definition/defkit/testing/matchers/matchers_test.go b/pkg/definition/defkit/testing/matchers/matchers_test.go index a45785975..a7be53ea8 100644 --- a/pkg/definition/defkit/testing/matchers/matchers_test.go +++ b/pkg/definition/defkit/testing/matchers/matchers_test.go @@ -133,6 +133,11 @@ var _ = Describe("Parameter Matchers", func() { p := defkit.String("image") Expect(p).NotTo(BeRequired()) }) + + It("should not match a mandatory parameter", func() { + p := defkit.String("image").Mandatory() + Expect(p).NotTo(BeRequired()) + }) }) Describe("BeOptional", func() { @@ -145,6 +150,28 @@ var _ = Describe("Parameter Matchers", func() { p := defkit.Int("replicas").Required() Expect(p).NotTo(BeOptional()) }) + + It("should not match a mandatory parameter", func() { + p := defkit.String("image").Mandatory() + Expect(p).NotTo(BeOptional()) + }) + }) + + Describe("BeMandatory", func() { + It("should match a mandatory parameter", func() { + p := defkit.String("image").Mandatory() + Expect(p).To(BeMandatory()) + }) + + It("should not match an optional parameter", func() { + p := defkit.String("image") + Expect(p).NotTo(BeMandatory()) + }) + + It("should not match a required parameter", func() { + p := defkit.String("image").Required() + Expect(p).NotTo(BeMandatory()) + }) }) Describe("HaveDefaultValue", func() { @@ -190,7 +217,7 @@ var _ = Describe("Parameter Matchers", func() { It("should match when component has parameter", func() { c := defkit.NewComponent("webservice"). Params( - defkit.String("image").Required(), + defkit.String("image").Mandatory(), defkit.Int("replicas").Default(1), ) Expect(c).To(HaveParamNamed("image")) diff --git a/pkg/definition/defkit/testing/matchers/param_matchers.go b/pkg/definition/defkit/testing/matchers/param_matchers.go index 2c101b16c..9d6a0fd52 100644 --- a/pkg/definition/defkit/testing/matchers/param_matchers.go +++ b/pkg/definition/defkit/testing/matchers/param_matchers.go @@ -24,12 +24,39 @@ import ( "github.com/oam-dev/kubevela/pkg/definition/defkit" ) -// paramAccessor provides common access to parameter properties. -type paramAccessor interface { +// named is the minimal interface for extracting a parameter name (used in failure messages). +type named interface { Name() string +} + +// requiredParam is satisfied by any parameter that can report required status. +type requiredParam interface { + named IsRequired() bool +} + +// optionalParam is satisfied by any parameter that can report optional status. +type optionalParam interface { + named + IsOptional() bool +} + +// mandatoryParam is satisfied by any parameter that can report mandatory status. +type mandatoryParam interface { + named + IsMandatory() bool +} + +// defaultParam is satisfied by any parameter that can report its default value. +type defaultParam interface { + named HasDefault() bool GetDefault() any +} + +// describedParam is satisfied by any parameter that can report its description. +type describedParam interface { + named GetDescription() string } @@ -41,7 +68,7 @@ func BeRequired() types.GomegaMatcher { type requiredMatcher struct{} func (m *requiredMatcher) Match(actual interface{}) (bool, error) { - param, ok := actual.(paramAccessor) + param, ok := actual.(requiredParam) if !ok { return false, fmt.Errorf("BeRequired expects a parameter type, got %T", actual) } @@ -49,12 +76,12 @@ func (m *requiredMatcher) Match(actual interface{}) (bool, error) { } func (m *requiredMatcher) FailureMessage(actual interface{}) string { - param := actual.(paramAccessor) + param := actual.(requiredParam) return fmt.Sprintf("Expected parameter %q to be required", param.Name()) } func (m *requiredMatcher) NegatedFailureMessage(actual interface{}) string { - param := actual.(paramAccessor) + param := actual.(requiredParam) return fmt.Sprintf("Expected parameter %q not to be required", param.Name()) } @@ -66,23 +93,48 @@ func BeOptional() types.GomegaMatcher { type optionalMatcher struct{} func (m *optionalMatcher) Match(actual interface{}) (bool, error) { - param, ok := actual.(paramAccessor) + param, ok := actual.(optionalParam) if !ok { return false, fmt.Errorf("BeOptional expects a parameter type, got %T", actual) } - return !param.IsRequired(), nil + return param.IsOptional(), nil } func (m *optionalMatcher) FailureMessage(actual interface{}) string { - param := actual.(paramAccessor) + param := actual.(optionalParam) return fmt.Sprintf("Expected parameter %q to be optional", param.Name()) } func (m *optionalMatcher) NegatedFailureMessage(actual interface{}) string { - param := actual.(paramAccessor) + param := actual.(optionalParam) return fmt.Sprintf("Expected parameter %q not to be optional", param.Name()) } +// BeMandatory returns a matcher that checks if a parameter is mandatory (non-optional, no ? marker). +func BeMandatory() types.GomegaMatcher { + return &mandatoryMatcher{} +} + +type mandatoryMatcher struct{} + +func (m *mandatoryMatcher) Match(actual interface{}) (bool, error) { + param, ok := actual.(mandatoryParam) + if !ok { + return false, fmt.Errorf("BeMandatory expects a parameter type, got %T", actual) + } + return param.IsMandatory(), nil +} + +func (m *mandatoryMatcher) FailureMessage(actual interface{}) string { + param := actual.(mandatoryParam) + return fmt.Sprintf("Expected parameter %q to be mandatory", param.Name()) +} + +func (m *mandatoryMatcher) NegatedFailureMessage(actual interface{}) string { + param := actual.(mandatoryParam) + return fmt.Sprintf("Expected parameter %q not to be mandatory", param.Name()) +} + // HaveDefaultValue returns a matcher that checks if a parameter has the expected default value. func HaveDefaultValue(expected any) types.GomegaMatcher { return &defaultValueMatcher{expectedValue: expected} @@ -93,7 +145,7 @@ type defaultValueMatcher struct { } func (m *defaultValueMatcher) Match(actual interface{}) (bool, error) { - param, ok := actual.(paramAccessor) + param, ok := actual.(defaultParam) if !ok { return false, fmt.Errorf("HaveDefaultValue expects a parameter type, got %T", actual) } @@ -104,7 +156,7 @@ func (m *defaultValueMatcher) Match(actual interface{}) (bool, error) { } func (m *defaultValueMatcher) FailureMessage(actual interface{}) string { - param := actual.(paramAccessor) + param := actual.(defaultParam) if !param.HasDefault() { return fmt.Sprintf("Expected parameter %q to have default value %v, but it has no default", param.Name(), m.expectedValue) } @@ -112,7 +164,7 @@ func (m *defaultValueMatcher) FailureMessage(actual interface{}) string { } func (m *defaultValueMatcher) NegatedFailureMessage(actual interface{}) string { - param := actual.(paramAccessor) + param := actual.(defaultParam) return fmt.Sprintf("Expected parameter %q not to have default value %v", param.Name(), m.expectedValue) } @@ -126,7 +178,7 @@ type descriptionMatcher struct { } func (m *descriptionMatcher) Match(actual interface{}) (bool, error) { - param, ok := actual.(paramAccessor) + param, ok := actual.(describedParam) if !ok { return false, fmt.Errorf("HaveDescription expects a parameter type, got %T", actual) } @@ -134,12 +186,12 @@ func (m *descriptionMatcher) Match(actual interface{}) (bool, error) { } func (m *descriptionMatcher) FailureMessage(actual interface{}) string { - param := actual.(paramAccessor) + param := actual.(describedParam) return fmt.Sprintf("Expected parameter %q to have description %q, but got %q", param.Name(), m.expectedDesc, param.GetDescription()) } func (m *descriptionMatcher) NegatedFailureMessage(actual interface{}) string { - param := actual.(paramAccessor) + param := actual.(describedParam) return fmt.Sprintf("Expected parameter %q not to have description %q", param.Name(), m.expectedDesc) } diff --git a/pkg/definition/defkit/trait_test.go b/pkg/definition/defkit/trait_test.go index aef983424..f0d7df0e0 100644 --- a/pkg/definition/defkit/trait_test.go +++ b/pkg/definition/defkit/trait_test.go @@ -180,7 +180,7 @@ parameter: #PatchParams trait := defkit.NewTrait("scaler"). Description("Scale workloads"). AppliesTo("deployments.apps"). - Params(defkit.Int("replicas").Default(1).Required().Description("Number of replicas")) + Params(defkit.Int("replicas").Default(1).Mandatory().Description("Number of replicas")) cue := trait.ToCue() @@ -402,7 +402,7 @@ template: { AppliesTo("deployments.apps"). Params( defkit.Int("replicas").Default(1).Description("Number of replicas"), - defkit.String("image").Required().Description("Container image"), + defkit.String("image").Mandatory().Description("Container image"), ) cue := trait.ToCue() @@ -560,7 +560,7 @@ template: { It("should generate Optional field guards in Map comprehension", func() { items := defkit.Array("items").WithFields( - defkit.String("name").Required(), + defkit.String("name").Mandatory(), defkit.String("label"), defkit.Int("priority"), ) @@ -595,10 +595,10 @@ template: { It("should generate If/EndIf with SetIf using sub-field conditions", func() { parent := defkit.Map("parent").WithFields( defkit.Array("required").WithFields( - defkit.String("key").Required(), + defkit.String("key").Mandatory(), ), defkit.Array("preferred").WithFields( - defkit.Int("weight").Required(), + defkit.Int("weight").Mandatory(), ), ) trait := defkit.NewTrait("if-subfield-test"). @@ -701,9 +701,9 @@ template: { Context("PatchKey with ArrayParam (no array wrapping)", func() { It("should emit direct assignment when single element is an ArrayParam", func() { items := defkit.Array("items").WithFields( - defkit.String("name").Required(), - defkit.String("value").Required(), - ).Required() + defkit.String("name").Mandatory(), + defkit.String("value").Mandatory(), + ).Mandatory() trait := defkit.NewTrait("patchkey-array-test"). Description("Test PatchKey with ArrayParam"). @@ -837,7 +837,7 @@ template: { }) It("should emit patchStrategy annotation in CUE output for unconditional field", func() { - strategy := defkit.Struct("strategy").Required().WithFields( + strategy := defkit.Struct("strategy").Mandatory().WithFields( defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate"), ) @@ -861,7 +861,7 @@ template: { It("should emit patchStrategy annotation inside conditional block", func() { kind := defkit.String("kind").Default("Deployment").Values("Deployment", "StatefulSet") - strategy := defkit.Struct("strategy").Required().WithFields( + strategy := defkit.Struct("strategy").Mandatory().WithFields( defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate"), ) @@ -988,7 +988,7 @@ template: { It("should produce correct k8s-update-strategy-like pattern", func() { targetKind := defkit.String("targetKind").Default("Deployment").Values("Deployment", "StatefulSet", "DaemonSet") - strategy := defkit.Struct("strategy").Required().WithFields( + strategy := defkit.Struct("strategy").Mandatory().WithFields( defkit.Field("type", defkit.ParamTypeString).Default("RollingUpdate").Values("RollingUpdate", "Recreate", "OnDelete"), defkit.Field("rollingStrategy", defkit.ParamTypeStruct). Nested(defkit.Struct("rollingStrategy").WithFields( @@ -1069,7 +1069,7 @@ template: { Helper("Handler", defkit.Struct("Handler").WithFields( defkit.Field("exec", defkit.ParamTypeStruct). Nested(defkit.Struct("exec").WithFields( - defkit.Field("command", defkit.ParamTypeArray).Of(defkit.ParamTypeString).Required(), + defkit.Field("command", defkit.ParamTypeArray).Of(defkit.ParamTypeString).Mandatory(), )), )). Template(func(tpl *defkit.Template) { @@ -1201,7 +1201,7 @@ template: { }) It("should render unconditional SpreadAll with simple value", func() { - image := defkit.String("image").Required() + image := defkit.String("image").Mandatory() trait := defkit.NewTrait("spreadall-simple-test"). Description("Test SpreadAll with simple value"). @@ -1243,7 +1243,7 @@ template: { It("should render SpreadAll inside an IfBlock", func() { enabled := defkit.Bool("enabled").Default(false) - image := defkit.String("image").Required() + image := defkit.String("image").Mandatory() trait := defkit.NewTrait("spreadall-ifblock-test"). Description("Test SpreadAll inside IfBlock"). @@ -1338,8 +1338,8 @@ template: { Helper("Config", defkit.Struct("Config").WithFields( defkit.Field("headers", defkit.ParamTypeArray). Nested(defkit.Struct("headers").WithFields( - defkit.Field("name", defkit.ParamTypeString).Required(), - defkit.Field("value", defkit.ParamTypeString).Required(), + defkit.Field("name", defkit.ParamTypeString).Mandatory(), + defkit.Field("value", defkit.ParamTypeString).Mandatory(), )), )). Template(func(tpl *defkit.Template) { @@ -1360,7 +1360,7 @@ template: { AppliesTo("deployments.apps"). Helper("Port", defkit.Int("Port").Min(1).Max(65535)). Helper("Endpoint", defkit.Struct("Endpoint").WithFields( - defkit.Field("port", defkit.ParamTypeInt).WithSchemaRef("Port").Required(), + defkit.Field("port", defkit.ParamTypeInt).WithSchemaRef("Port").Mandatory(), defkit.Field("host", defkit.ParamTypeString), )). Template(func(tpl *defkit.Template) { @@ -1371,7 +1371,7 @@ template: { Expect(cue).To(ContainSubstring("#Port: int & >=1 & <=65535")) Expect(cue).To(ContainSubstring("#Endpoint: {")) - Expect(cue).To(ContainSubstring("port: #Port")) + Expect(cue).To(MatchRegexp(`port:\s+#Port`)) Expect(cue).To(ContainSubstring("host?: string")) }) }) diff --git a/pkg/definition/defkit/typed_test.go b/pkg/definition/defkit/typed_test.go index 512d3471c..52a509398 100644 --- a/pkg/definition/defkit/typed_test.go +++ b/pkg/definition/defkit/typed_test.go @@ -163,7 +163,7 @@ func TestFromTyped_ChainAdditionalOperations(t *testing.T) { } // Chain additional operations - image := defkit.String("image").Required() + image := defkit.String("image").Mandatory() r.Set("spec.template.spec.containers[0].image", image) ops := r.Ops() diff --git a/pkg/definition/defkit/types.go b/pkg/definition/defkit/types.go index 979cee6f9..72b93f5d0 100644 --- a/pkg/definition/defkit/types.go +++ b/pkg/definition/defkit/types.go @@ -57,10 +57,12 @@ type Param interface { Value // Name returns the parameter name Name() string - // IsRequired returns true if the parameter is required + // IsRequired returns true if the parameter emits the "!" CUE marker — user must explicitly provide it IsRequired() bool - // IsOptional returns true if the parameter is optional + // IsOptional returns true if the parameter is optional (emits "?" marker) IsOptional() bool + // IsMandatory returns true if the parameter is non-optional (no ? marker) but defaults or CUE merging can satisfy it + IsMandatory() bool // HasDefault returns true if the parameter has a default value HasDefault() bool // GetDefault returns the default value, or nil if none diff --git a/pkg/definition/defkit/workflow_step_test.go b/pkg/definition/defkit/workflow_step_test.go index c12f508ef..f98faee68 100644 --- a/pkg/definition/defkit/workflow_step_test.go +++ b/pkg/definition/defkit/workflow_step_test.go @@ -331,7 +331,7 @@ template: { step := defkit.NewWorkflowStep("check-metrics"). Description("Verify metrics"). WithImports("vela/metrics"). - Params(defkit.String("query").Required()). + Params(defkit.String("query").Mandatory()). TemplateBody("check: metrics.#PromCheck & {\n\t$params: query: parameter.query\n}") cue := step.ToCue()