Files
Ayush KumarandGitHub 401ad98a37 Feat: add ClosedUnion and ClosedStruct support in defkit (#7069)
* feat: add ClosedUnion and ClosedStruct support in defkit

- Introduced ClosedUnionParam and ClosedStructOption to handle closed struct disjunctions in parameters.
- Implemented methods for creating closed structs and adding fields.
- Enhanced CUE generation to support closed unions, allowing for more complex parameter definitions.
- Added tests for ClosedUnion and ClosedStruct to ensure correct functionality and integration.
- Updated existing tests to cover new features and ensure backward compatibility.

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

* feat: enhance CUE generation with deduplication and inner braces for array elements

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

* feat: enhance WorkflowStep parameter handling in CUE generation

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

* feat: add WithDirectFields method to BuiltinActionBuilder for direct field rendering

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

* feat: streamline string formatting in CUE generation for improved readability

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

* feat: enhance CUE generation with deduplication support and improve test assertions

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

* feat: add support for ClosedUnion and condition rendering in CUE generation

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

* ci: retrigger checks

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

* feat: refactor value rendering in CUE generation to improve builder support

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

* feat: add GuardedBlockAction and corresponding CUE generation support

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

* feat: update writeOneOfParam and writeStructField to use marker instead of optional

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

* feat: add ClosedUnionParam support and update related tests for optionality

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

* feat: update parameter definitions to remove required constraints in example code

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

* feat: enhance CUE generation with multiline value indentation and improved block handling

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

* feat: add baseContainer definition to CUE generation for container patterns

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

* fix: update test to expect _baseContainer singular field in PatchContainer output

The _baseContainer: *_|_ | {...} field was intentionally added to CUE
generation but the test was still asserting it should not exist.

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

* ci: retrigger checks

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

---------

Signed-off-by: Ayush Kumar <ayushshyamkumar888@gmail.com>
2026-03-17 09:20:48 -07:00

799 lines
25 KiB
Go

/*
Copyright 2025 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package defkit_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/oam-dev/kubevela/pkg/definition/defkit"
)
var _ = Describe("Helper", func() {
Describe("HelperVar Attributes", func() {
var tpl *defkit.Template
BeforeEach(func() {
tpl = defkit.NewTemplate()
})
It("should have all attributes correctly set for a basic helper", func() {
ports := defkit.List("ports")
helper := tpl.Helper("portsArray").
From(ports).
Build()
// Test ALL attributes of HelperVar
Expect(helper.Name()).To(Equal("portsArray"))
Expect(helper.IsAfterOutput()).To(BeFalse())
Expect(helper.Guard()).To(BeNil())
Expect(helper.String()).To(Equal("Helper(portsArray)"))
// Verify collection is set and correct
col, ok := helper.Collection().(*defkit.CollectionOp)
Expect(ok).To(BeTrue(), "Collection should be a CollectionOp")
Expect(col.Source()).To(Equal(ports))
Expect(col.Operations()).To(BeEmpty())
})
It("should have all attributes correctly set for a fully configured helper", func() {
ports := defkit.List("ports")
guardCond := ports.IsSet()
helper := tpl.Helper("exposePorts").
From(ports).
Guard(guardCond).
Filter(defkit.FieldEquals("expose", true)).
Pick("name", "port").
AfterOutput().
Build()
// Verify ALL attributes
Expect(helper.Name()).To(Equal("exposePorts"))
Expect(helper.IsAfterOutput()).To(BeTrue())
Expect(helper.Guard()).To(Equal(guardCond))
Expect(helper.String()).To(Equal("Helper(exposePorts)"))
// Verify collection has correct operations
col, ok := helper.Collection().(*defkit.CollectionOp)
Expect(ok).To(BeTrue())
Expect(col.Source()).To(Equal(ports))
Expect(col.Operations()).To(HaveLen(2)) // Filter + Pick
})
It("should create LenNotZeroCondition with helper as source via NotEmpty()", func() {
ports := defkit.List("ports")
helper := tpl.Helper("portsArray").From(ports).Build()
cond := helper.NotEmpty()
lenCond, ok := cond.(*defkit.LenNotZeroCondition)
Expect(ok).To(BeTrue())
Expect(lenCond.Source()).To(Equal(helper))
})
})
Describe("HelperBuilder with From source - Attributes", func() {
var tpl *defkit.Template
BeforeEach(func() {
tpl = defkit.NewTemplate()
})
It("should create CollectionOp with correct source", func() {
ports := defkit.List("ports")
helper := tpl.Helper("portsArray").From(ports).Build()
col, ok := helper.Collection().(*defkit.CollectionOp)
Expect(ok).To(BeTrue())
Expect(col.Source()).To(Equal(ports))
Expect(col.Operations()).To(BeEmpty())
})
It("should track multiple operations in sequence", func() {
ports := defkit.List("ports")
helper := tpl.Helper("portsArray").
From(ports).
Filter(defkit.FieldEquals("expose", true)).
Pick("name", "port").
Rename("port", "containerPort").
DefaultField("protocol", defkit.LitField("TCP")).
Wrap("container").
Dedupe("name").
Build()
col := helper.Collection().(*defkit.CollectionOp)
// Filter + Pick + Rename + DefaultField + Wrap + Dedupe = 6 operations
Expect(col.Operations()).To(HaveLen(6))
})
})
Describe("HelperBuilder with FromFields source - Attributes", func() {
var tpl *defkit.Template
BeforeEach(func() {
tpl = defkit.NewTemplate()
})
It("should create MultiSource with all specified fields", func() {
volumeMounts := defkit.Object("volumeMounts")
helper := tpl.Helper("mountsArray").
FromFields(volumeMounts, "pvc", "configMap", "secret", "emptyDir", "hostPath").
Build()
ms, ok := helper.Collection().(*defkit.MultiSource)
Expect(ok).To(BeTrue())
Expect(ms.Source()).To(Equal(volumeMounts))
Expect(ms.Sources()).To(Equal([]string{"pvc", "configMap", "secret", "emptyDir", "hostPath"}))
Expect(ms.Operations()).To(BeEmpty())
Expect(ms.MapBySourceMappings()).To(BeEmpty())
})
It("should store MapBySource mappings correctly", func() {
volumeMounts := defkit.Object("volumeMounts")
pvcMapping := defkit.FieldMap{
"name": defkit.FieldRef("name"),
"mountPath": defkit.FieldRef("mountPath"),
"claimName": defkit.FieldRef("claimName"),
}
cmMapping := defkit.FieldMap{
"name": defkit.FieldRef("name"),
"mountPath": defkit.FieldRef("mountPath"),
"cmName": defkit.FieldRef("cmName"),
}
helper := tpl.Helper("volumesList").
FromFields(volumeMounts, "pvc", "configMap").
MapBySource(map[string]defkit.FieldMap{
"pvc": pvcMapping,
"configMap": cmMapping,
}).
Build()
ms := helper.Collection().(*defkit.MultiSource)
Expect(ms.MapBySourceMappings()).To(HaveLen(2))
Expect(ms.MapBySourceMappings()["pvc"]).To(Equal(pvcMapping))
Expect(ms.MapBySourceMappings()["configMap"]).To(Equal(cmMapping))
})
})
Describe("HelperBuilder with FromHelper source - Attributes", func() {
var tpl *defkit.Template
BeforeEach(func() {
tpl = defkit.NewTemplate()
})
It("should create CollectionOp referencing the source helper", func() {
ports := defkit.List("ports")
sourceHelper := tpl.Helper("portsArray").From(ports).Build()
derivedHelper := tpl.Helper("dedupedPorts").
FromHelper(sourceHelper).
Dedupe("name").
Build()
col, ok := derivedHelper.Collection().(*defkit.CollectionOp)
Expect(ok).To(BeTrue())
Expect(col.Source()).To(Equal(sourceHelper))
Expect(col.Operations()).To(HaveLen(1))
})
})
Describe("HelperBuilder without source - Attributes", func() {
It("should create CollectionOp with empty literal array", func() {
tpl := defkit.NewTemplate()
helper := tpl.Helper("emptyHelper").Build()
col, ok := helper.Collection().(*defkit.CollectionOp)
Expect(ok).To(BeTrue())
lit, ok := col.Source().(*defkit.Literal)
Expect(ok).To(BeTrue())
Expect(lit.Val()).To(Equal([]any{}))
})
})
Describe("Template Helper Registration - Behavior", func() {
var tpl *defkit.Template
BeforeEach(func() {
tpl = defkit.NewTemplate()
})
It("should register helpers in order and make them all retrievable", func() {
ports := defkit.List("ports")
helper1 := tpl.Helper("helper1").From(ports).Build()
helper2 := tpl.Helper("helper2").From(ports).Build()
helper3 := tpl.Helper("helper3").From(ports).Build()
helpers := tpl.GetHelpers()
Expect(helpers).To(HaveLen(3))
Expect(helpers[0]).To(Equal(helper1))
Expect(helpers[1]).To(Equal(helper2))
Expect(helpers[2]).To(Equal(helper3))
})
It("should correctly categorize helpers by placement", func() {
ports := defkit.List("ports")
before1 := tpl.Helper("before1").From(ports).Build()
after1 := tpl.Helper("after1").From(ports).AfterOutput().Build()
before2 := tpl.Helper("before2").From(ports).Build()
after2 := tpl.Helper("after2").From(ports).AfterOutput().Build()
beforeHelpers := tpl.GetHelpersBeforeOutput()
Expect(beforeHelpers).To(HaveLen(2))
Expect(beforeHelpers[0]).To(Equal(before1))
Expect(beforeHelpers[1]).To(Equal(before2))
afterHelpers := tpl.GetHelpersAfterOutput()
Expect(afterHelpers).To(HaveLen(2))
Expect(afterHelpers[0]).To(Equal(after1))
Expect(afterHelpers[1]).To(Equal(after2))
})
})
Describe("StructBuilder and StructFieldDef - Attributes", func() {
It("should create struct with all field attributes preserved", func() {
nameVal := defkit.Item().Get("name")
pathVal := defkit.Item().Get("mountPath")
subPathVal := defkit.Item().Get("subPath")
subPathCond := defkit.ItemFieldIsSet("subPath")
s := defkit.HelperStruct(
defkit.HelperField("name", nameVal),
defkit.HelperField("mountPath", pathVal),
defkit.HelperFieldIf(subPathCond, "subPath", subPathVal),
)
fields := s.Fields()
Expect(fields).To(HaveLen(3))
// First field - all attributes
Expect(fields[0].Name()).To(Equal("name"))
Expect(fields[0].Value()).To(Equal(nameVal))
Expect(fields[0].Cond()).To(BeNil())
// Second field - all attributes
Expect(fields[1].Name()).To(Equal("mountPath"))
Expect(fields[1].Value()).To(Equal(pathVal))
Expect(fields[1].Cond()).To(BeNil())
// Third field - all attributes including condition
Expect(fields[2].Name()).To(Equal("subPath"))
Expect(fields[2].Value()).To(Equal(subPathVal))
Expect(fields[2].Cond()).To(Equal(subPathCond))
})
})
Describe("ItemValue - Attributes", func() {
It("should create item reference with empty field", func() {
item := defkit.Item()
Expect(item.Field()).To(Equal(""))
})
It("should create field reference with correct field name", func() {
item := defkit.Item()
fieldRef := item.Get("name")
Expect(fieldRef.Field()).To(Equal("name"))
})
})
Describe("ItemFieldIsSet - Attributes", func() {
It("should create IsSetCondition with correct field name", func() {
cond := defkit.ItemFieldIsSet("protocol")
isSet, ok := cond.(*defkit.IsSetCondition)
Expect(ok).To(BeTrue())
Expect(isSet.ParamName()).To(Equal("protocol"))
})
})
Describe("StructArrayHelper - Attributes", func() {
var tpl *defkit.Template
BeforeEach(func() {
tpl = defkit.NewTemplate()
})
It("should create helper with all attributes correctly set", func() {
volumeMounts := defkit.Object("volumeMounts")
pvcMapping := defkit.FieldMap{
"name": defkit.FieldRef("name"),
"mountPath": defkit.FieldRef("mountPath"),
}
cmMapping := defkit.FieldMap{
"name": defkit.FieldRef("name"),
"mountPath": defkit.FieldRef("mountPath"),
"cmName": defkit.FieldRef("cmName"),
}
helper := tpl.StructArrayHelper("mountsArray", volumeMounts).
Field("pvc", pvcMapping).
Field("configMap", cmMapping).
Build()
// Verify ALL attributes
Expect(helper.HelperName()).To(Equal("mountsArray"))
Expect(helper.Source()).To(Equal(volumeMounts))
fields := helper.Fields()
Expect(fields).To(HaveLen(2))
// First field - all attributes
Expect(fields[0].Name).To(Equal("pvc"))
Expect(fields[0].Mappings).To(HaveLen(2))
Expect(fields[0].Mappings["name"]).To(Equal(defkit.FieldRef("name")))
Expect(fields[0].Mappings["mountPath"]).To(Equal(defkit.FieldRef("mountPath")))
// Second field - all attributes
Expect(fields[1].Name).To(Equal("configMap"))
Expect(fields[1].Mappings).To(HaveLen(3))
})
It("should register with template", func() {
volumeMounts := defkit.Object("volumeMounts")
helper := tpl.StructArrayHelper("mountsArray", volumeMounts).
Field("pvc", defkit.FieldMap{"name": defkit.FieldRef("name")}).
Build()
registered := tpl.GetStructArrayHelpers()
Expect(registered).To(HaveLen(1))
Expect(registered[0]).To(Equal(helper))
})
})
Describe("ConcatHelper - Attributes", func() {
var tpl *defkit.Template
var structHelper *defkit.StructArrayHelper
BeforeEach(func() {
tpl = defkit.NewTemplate()
volumeMounts := defkit.Object("volumeMounts")
structHelper = tpl.StructArrayHelper("mountsArray", volumeMounts).
Field("pvc", defkit.FieldMap{"name": defkit.FieldRef("name")}).
Field("configMap", defkit.FieldMap{"name": defkit.FieldRef("name")}).
Field("secret", defkit.FieldMap{"name": defkit.FieldRef("name")}).
Build()
})
It("should create helper with all attributes correctly set", func() {
concatHelper := tpl.ConcatHelper("volumesList", structHelper).
Fields("pvc", "configMap", "secret").
Build()
// Verify ALL attributes
Expect(concatHelper.HelperName()).To(Equal("volumesList"))
Expect(concatHelper.Source()).To(Equal(structHelper))
Expect(concatHelper.FieldRefs()).To(Equal([]string{"pvc", "configMap", "secret"}))
Expect(concatHelper.RequiredImports()).To(Equal([]string{"list"}))
})
It("should register with template", func() {
helper := tpl.ConcatHelper("volumesList", structHelper).
Fields("pvc").
Build()
registered := tpl.GetConcatHelpers()
Expect(registered).To(HaveLen(1))
Expect(registered[0]).To(Equal(helper))
})
})
Describe("DedupeHelper - Attributes", func() {
var tpl *defkit.Template
var concatHelper *defkit.ConcatHelper
BeforeEach(func() {
tpl = defkit.NewTemplate()
volumeMounts := defkit.Object("volumeMounts")
structHelper := tpl.StructArrayHelper("mountsArray", volumeMounts).
Field("pvc", defkit.FieldMap{"name": defkit.FieldRef("name")}).
Build()
concatHelper = tpl.ConcatHelper("volumesList", structHelper).
Fields("pvc").
Build()
})
It("should create helper with all attributes correctly set", func() {
dedupeHelper := tpl.DedupeHelper("deDupVolumes", concatHelper).
ByKey("name").
Build()
// Verify ALL attributes
Expect(dedupeHelper.HelperName()).To(Equal("deDupVolumes"))
Expect(dedupeHelper.Source()).To(Equal(concatHelper))
Expect(dedupeHelper.KeyField()).To(Equal("name"))
})
It("should register with template", func() {
helper := tpl.DedupeHelper("deDupVolumes", concatHelper).
ByKey("name").
Build()
registered := tpl.GetDedupeHelpers()
Expect(registered).To(HaveLen(1))
Expect(registered[0]).To(Equal(helper))
})
It("should accept HelperVar as source", func() {
ports := defkit.List("ports")
portsHelper := tpl.Helper("portsArray").From(ports).Build()
dedupeHelper := tpl.DedupeHelper("uniquePorts", portsHelper).
ByKey("id").
Build()
Expect(dedupeHelper.HelperName()).To(Equal("uniquePorts"))
Expect(dedupeHelper.Source()).To(Equal(portsHelper))
Expect(dedupeHelper.KeyField()).To(Equal("id"))
})
})
// Behavioral Tests - Test actual data transformations
Describe("Helper Operations - Behavioral Tests", func() {
Describe("Filter behavior via component render", func() {
It("should filter items based on predicate when rendered", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port"),
defkit.String("name"),
defkit.Bool("expose").Default(false),
)
comp := defkit.NewComponent("test").
Params(ports).
Template(func(tpl *defkit.Template) {
exposedPorts := defkit.Each(ports).
Filter(defkit.FieldEquals("expose", true)).
Map(defkit.FieldMap{
"port": defkit.FieldRef("port"),
"name": defkit.FieldRef("name"),
})
tpl.Output(
defkit.NewResource("v1", "Service").
Set("spec.ports", exposedPorts),
)
})
rendered := comp.Render(
defkit.TestContext().
WithParam("ports", []map[string]any{
{"port": 80, "name": "http", "expose": true},
{"port": 443, "name": "https", "expose": false},
{"port": 8080, "name": "admin", "expose": true},
}),
)
servicePorts := rendered.Get("spec.ports").([]any)
Expect(servicePorts).To(HaveLen(2))
Expect(servicePorts[0].(map[string]any)["port"]).To(Equal(80))
Expect(servicePorts[1].(map[string]any)["port"]).To(Equal(8080))
})
})
Describe("Pick behavior via component render", func() {
It("should only include picked fields in output", func() {
items := defkit.List("items").WithFields(
defkit.String("name"),
defkit.String("path"),
defkit.String("extra"),
defkit.Int("count"),
)
comp := defkit.NewComponent("test").
Params(items).
Template(func(tpl *defkit.Template) {
picked := defkit.Each(items).Pick("name", "path")
tpl.Output(
defkit.NewResource("v1", "ConfigMap").
Set("data.items", picked),
)
})
rendered := comp.Render(
defkit.TestContext().
WithParam("items", []map[string]any{
{"name": "item1", "path": "/path1", "extra": "ignored", "count": 10},
{"name": "item2", "path": "/path2", "extra": "also ignored", "count": 20},
}),
)
resultItems := rendered.Get("data.items").([]any)
Expect(resultItems).To(HaveLen(2))
// Verify only picked fields are present
item1 := resultItems[0].(map[string]any)
Expect(item1).To(HaveKey("name"))
Expect(item1).To(HaveKey("path"))
Expect(item1).NotTo(HaveKey("extra"))
Expect(item1).NotTo(HaveKey("count"))
Expect(item1["name"]).To(Equal("item1"))
Expect(item1["path"]).To(Equal("/path1"))
})
})
Describe("Rename behavior via component render", func() {
It("should rename field in all items", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port"),
defkit.String("name"),
)
comp := defkit.NewComponent("test").
Params(ports).
Template(func(tpl *defkit.Template) {
renamedPorts := defkit.Each(ports).Rename("port", "containerPort")
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.template.spec.containers[0].ports", renamedPorts),
)
})
rendered := comp.Render(
defkit.TestContext().
WithParam("ports", []map[string]any{
{"port": 80, "name": "http"},
{"port": 443, "name": "https"},
}),
)
containerPorts := rendered.Get("spec.template.spec.containers[0].ports").([]any)
Expect(containerPorts).To(HaveLen(2))
// Verify rename
port1 := containerPorts[0].(map[string]any)
Expect(port1).To(HaveKey("containerPort"))
Expect(port1).NotTo(HaveKey("port"))
Expect(port1["containerPort"]).To(Equal(80))
Expect(port1["name"]).To(Equal("http"))
})
})
Describe("Wrap behavior via component render", func() {
It("should wrap each item value under specified key", func() {
secrets := defkit.StringList("imagePullSecrets")
comp := defkit.NewComponent("test").
Params(secrets).
Template(func(tpl *defkit.Template) {
wrappedSecrets := defkit.Each(secrets).Wrap("name")
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.template.spec.imagePullSecrets", wrappedSecrets),
)
})
rendered := comp.Render(
defkit.TestContext().
WithParam("imagePullSecrets", []any{"docker-secret", "gcr-secret"}),
)
pullSecrets := rendered.Get("spec.template.spec.imagePullSecrets").([]any)
Expect(pullSecrets).To(HaveLen(2))
Expect(pullSecrets[0].(map[string]any)["name"]).To(Equal("docker-secret"))
Expect(pullSecrets[1].(map[string]any)["name"]).To(Equal("gcr-secret"))
})
})
Describe("DefaultField behavior via component render", func() {
It("should set default value for missing fields", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port"),
defkit.String("name"),
defkit.String("protocol"),
)
comp := defkit.NewComponent("test").
Params(ports).
Template(func(tpl *defkit.Template) {
portsWithDefaults := defkit.Each(ports).
DefaultField("protocol", defkit.LitField("TCP"))
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.template.spec.containers[0].ports", portsWithDefaults),
)
})
rendered := comp.Render(
defkit.TestContext().
WithParam("ports", []map[string]any{
{"port": 80, "name": "http"}, // no protocol - should get default
{"port": 443, "name": "https", "protocol": "TCP"}, // has protocol - keep it
{"port": 53, "name": "dns", "protocol": "UDP"}, // has different protocol - keep it
}),
)
containerPorts := rendered.Get("spec.template.spec.containers[0].ports").([]any)
Expect(containerPorts).To(HaveLen(3))
Expect(containerPorts[0].(map[string]any)["protocol"]).To(Equal("TCP"))
Expect(containerPorts[1].(map[string]any)["protocol"]).To(Equal("TCP"))
Expect(containerPorts[2].(map[string]any)["protocol"]).To(Equal("UDP"))
})
})
Describe("Map behavior via component render", func() {
It("should transform items according to field mappings", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port"),
defkit.String("name"),
defkit.String("protocol"),
)
comp := defkit.NewComponent("test").
Params(ports).
Template(func(tpl *defkit.Template) {
mappedPorts := defkit.Each(ports).Map(defkit.FieldMap{
"containerPort": defkit.FieldRef("port"),
"portName": defkit.FieldRef("name"),
"proto": defkit.FieldRef("protocol"),
})
tpl.Output(
defkit.NewResource("apps/v1", "Deployment").
Set("spec.template.spec.containers[0].ports", mappedPorts),
)
})
rendered := comp.Render(
defkit.TestContext().
WithParam("ports", []map[string]any{
{"port": 80, "name": "http", "protocol": "TCP"},
}),
)
containerPorts := rendered.Get("spec.template.spec.containers[0].ports").([]any)
Expect(containerPorts).To(HaveLen(1))
port := containerPorts[0].(map[string]any)
Expect(port["containerPort"]).To(Equal(80))
Expect(port["portName"]).To(Equal("http"))
Expect(port["proto"]).To(Equal("TCP"))
// Original keys should not be present
Expect(port).NotTo(HaveKey("port"))
Expect(port).NotTo(HaveKey("name"))
Expect(port).NotTo(HaveKey("protocol"))
})
})
Describe("Chained operations behavior via component render", func() {
It("should apply all operations in sequence correctly", func() {
ports := defkit.List("ports").WithFields(
defkit.Int("port"),
defkit.String("name"),
defkit.Bool("expose").Default(false),
)
comp := defkit.NewComponent("test").
Params(ports).
Template(func(tpl *defkit.Template) {
servicePorts := defkit.Each(ports).
Filter(defkit.FieldEquals("expose", true)).
Map(defkit.FieldMap{
"port": defkit.FieldRef("port"),
"targetPort": defkit.FieldRef("port"),
"name": defkit.FieldRef("name"),
})
tpl.Output(
defkit.NewResource("v1", "Service").
Set("spec.ports", servicePorts),
)
})
rendered := comp.Render(
defkit.TestContext().
WithParam("ports", []map[string]any{
{"port": 80, "name": "http", "expose": true},
{"port": 443, "name": "https", "expose": false},
{"port": 8080, "name": "admin", "expose": true},
}),
)
servicePorts := rendered.Get("spec.ports").([]any)
Expect(servicePorts).To(HaveLen(2)) // Only exposed ports
port1 := servicePorts[0].(map[string]any)
Expect(port1["port"]).To(Equal(80))
Expect(port1["targetPort"]).To(Equal(80))
Expect(port1["name"]).To(Equal("http"))
Expect(port1).NotTo(HaveKey("expose")) // Not in mapping
port2 := servicePorts[1].(map[string]any)
Expect(port2["port"]).To(Equal(8080))
Expect(port2["targetPort"]).To(Equal(8080))
Expect(port2["name"]).To(Equal("admin"))
})
})
})
Describe("HelperBuilder Filter and FilterCond on MultiSource", func() {
var tpl *defkit.Template
BeforeEach(func() {
tpl = defkit.NewTemplate()
})
It("should propagate Filter to MultiSource", func() {
volumeMounts := defkit.Object("volumeMounts")
helper := tpl.Helper("filteredMounts").
FromFields(volumeMounts, "pvc", "configMap").
Filter(defkit.FieldEquals("readOnly", false)).
Build()
ms, ok := helper.Collection().(*defkit.MultiSource)
Expect(ok).To(BeTrue())
Expect(ms.Operations()).To(HaveLen(1))
})
It("should propagate FilterCond to MultiSource", func() {
volumeMounts := defkit.Object("volumeMounts")
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
helper := tpl.Helper("filteredMounts").
FromFields(volumeMounts, "pvc", "configMap").
FilterCond(cond).
Build()
ms, ok := helper.Collection().(*defkit.MultiSource)
Expect(ok).To(BeTrue())
Expect(ms.Operations()).To(HaveLen(1))
})
It("should propagate FilterCond to CollectionOp via From", func() {
ports := defkit.List("ports")
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
helper := tpl.Helper("filteredPorts").
From(ports).
FilterCond(cond).
Build()
col, ok := helper.Collection().(*defkit.CollectionOp)
Expect(ok).To(BeTrue())
Expect(col.Operations()).To(HaveLen(1))
})
})
Describe("CollectionOp FilterCond", func() {
It("should add FilterCond operation to collection", func() {
ports := defkit.List("ports")
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
col := defkit.Each(ports).FilterCond(cond)
Expect(col.Operations()).To(HaveLen(1))
})
It("should compose with other operations", func() {
ports := defkit.List("ports")
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
col := defkit.Each(ports).
Filter(defkit.FieldEquals("expose", true)).
FilterCond(cond).
Pick("name", "port")
Expect(col.Operations()).To(HaveLen(3))
})
})
Describe("MultiSource Filter and FilterCond", func() {
It("should add Filter operation to multi-source", func() {
volumeMounts := defkit.Object("volumeMounts")
ms := defkit.FromFields(volumeMounts, "pvc", "configMap")
ms.Filter(defkit.FieldEquals("readOnly", false))
Expect(ms.Operations()).To(HaveLen(1))
})
It("should add FilterCond operation to multi-source", func() {
volumeMounts := defkit.Object("volumeMounts")
ms := defkit.FromFields(volumeMounts, "pvc", "configMap")
cond := defkit.Eq(defkit.Item().Get("enabled"), defkit.Lit(true))
ms.FilterCond(cond)
Expect(ms.Operations()).To(HaveLen(1))
})
})
})