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()