@@ -1406,3 +1406,195 @@ func TestNodeBuilder_RenderContext_FallbackToMarshalYAML_Slice(t *testing.T) {
14061406 data , _ := yaml .Marshal (node )
14071407 assert .Contains (t , string (data ), "basic-item1" )
14081408}
1409+
1410+ // nilLowRenderable implements GoesLowUntyped but returns nil for GoLowUntyped()
1411+ // This simulates a newly created high-level object without a low-level model
1412+ type nilLowRenderable struct {
1413+ Value string
1414+ }
1415+
1416+ func (n * nilLowRenderable ) MarshalYAML () (interface {}, error ) {
1417+ return utils .CreateStringNode ("nillow-" + n .Value ), nil
1418+ }
1419+
1420+ func (n * nilLowRenderable ) GoLowUntyped () any {
1421+ return nil // Simulates newly created high-level object without low-level model
1422+ }
1423+
1424+ type testWithMixedSlice struct {
1425+ Items []* valueReferenceStruct `yaml:"items,omitempty"`
1426+ }
1427+
1428+ type testWithNilLowSlice struct {
1429+ Items []* nilLowRenderable `yaml:"items,omitempty"`
1430+ }
1431+
1432+ // TestNodeBuilder_SliceRefFollowedByNilLow tests the bug fix for rendering slices
1433+ // where a $ref item is followed by a newly created item without a low-level model.
1434+ // Before the fix, the 'skip' variable was not reset between iterations, causing
1435+ // items following a $ref to be incorrectly skipped when they had no low-level model.
1436+ func TestNodeBuilder_SliceRefFollowedByNilLow (t * testing.T ) {
1437+ // Create a slice with a $ref followed by a new item without low-level model
1438+ refItem := & valueReferenceStruct {
1439+ ref : true ,
1440+ refStr : "#/components/parameters/RefId" ,
1441+ Value : "refValue" ,
1442+ }
1443+ newItem := & valueReferenceStruct {
1444+ ref : false , // Not a reference
1445+ Value : "newValue" ,
1446+ }
1447+
1448+ // The new item's GoLowUntyped returns a pointer to itself (not nil),
1449+ // but it's not a reference, so it should be rendered normally.
1450+ // However, to truly test the nil case, we need a different approach.
1451+
1452+ ty := []* valueReferenceStruct {refItem , newItem }
1453+ t1 := test1 {
1454+ Throg : ty ,
1455+ }
1456+
1457+ nb := NewNodeBuilder (& t1 , & t1 )
1458+ node := nb .Render ()
1459+
1460+ data , _ := yaml .Marshal (node )
1461+
1462+ // Both items should be rendered - the ref as $ref and the new item normally
1463+ assert .Contains (t , string (data ), "$ref: '#/components/parameters/RefId'" )
1464+ assert .Contains (t , string (data ), "pizza" ) // newItem renders as "pizza" via MarshalYAML
1465+ }
1466+
1467+ // testMixedItem can have either a reference low-level model or nil low-level model
1468+ type testMixedItem struct {
1469+ ref bool
1470+ refStr string
1471+ Value string
1472+ hasLowRef bool // if true, GoLowUntyped returns a reference; if false, returns nil
1473+ }
1474+
1475+ func (t * testMixedItem ) MarshalYAML () (interface {}, error ) {
1476+ return utils .CreateStringNode ("mixed-" + t .Value ), nil
1477+ }
1478+
1479+ func (t * testMixedItem ) GoLowUntyped () any {
1480+ if t .hasLowRef {
1481+ return t
1482+ }
1483+ return nil
1484+ }
1485+
1486+ func (t * testMixedItem ) IsReference () bool {
1487+ return t .ref
1488+ }
1489+
1490+ func (t * testMixedItem ) GetReference () string {
1491+ return t .refStr
1492+ }
1493+
1494+ func (t * testMixedItem ) SetReference (ref string , _ * yaml.Node ) {
1495+ t .refStr = ref
1496+ }
1497+
1498+ func (t * testMixedItem ) GetReferenceNode () * yaml.Node {
1499+ return nil
1500+ }
1501+
1502+ type testWithMixedItems struct {
1503+ Items []* testMixedItem `yaml:"items,omitempty"`
1504+ }
1505+
1506+ // TestNodeBuilder_SliceRefFollowedByNilLowItem tests the specific bug case:
1507+ // a $ref item followed by an item with nil GoLowUntyped() (newly created high-level object)
1508+ func TestNodeBuilder_SliceRefFollowedByNilLowItem (t * testing.T ) {
1509+ // Create a slice where:
1510+ // 1. First item IS a reference with a low-level model
1511+ // 2. Second item has NO low-level model (GoLowUntyped returns nil)
1512+ refItem := & testMixedItem {
1513+ ref : true ,
1514+ refStr : "#/components/parameters/RefId" ,
1515+ Value : "refValue" ,
1516+ hasLowRef : true , // Has a low-level reference
1517+ }
1518+ newItem := & testMixedItem {
1519+ ref : false ,
1520+ Value : "newValue" ,
1521+ hasLowRef : false , // NO low-level model (simulates newly created item)
1522+ }
1523+
1524+ ty := []* testMixedItem {refItem , newItem }
1525+ t1 := testWithMixedItems {
1526+ Items : ty ,
1527+ }
1528+
1529+ nb := NewNodeBuilder (& t1 , nil )
1530+ node := nb .Render ()
1531+
1532+ data , _ := yaml .Marshal (node )
1533+ result := strings .TrimSpace (string (data ))
1534+
1535+ // The bug was that 'newItem' would be skipped because 'skip' wasn't reset
1536+ // after processing the $ref item, and newItem's GoLowUntyped() returns nil.
1537+ // After the fix, both items should be rendered.
1538+ assert .Contains (t , result , "$ref: '#/components/parameters/RefId'" , "Reference item should be rendered" )
1539+ assert .Contains (t , result , "mixed-newValue" , "New item without low-level model should also be rendered" )
1540+ }
1541+
1542+ // TestNodeBuilder_SliceNilLowFollowedByRef tests the reverse case:
1543+ // an item with nil GoLowUntyped() followed by a $ref (should work in both cases)
1544+ func TestNodeBuilder_SliceNilLowFollowedByRef (t * testing.T ) {
1545+ // Create a slice where:
1546+ // 1. First item has NO low-level model
1547+ // 2. Second item IS a reference with a low-level model
1548+ newItem := & testMixedItem {
1549+ ref : false ,
1550+ Value : "newValue" ,
1551+ hasLowRef : false , // NO low-level model
1552+ }
1553+ refItem := & testMixedItem {
1554+ ref : true ,
1555+ refStr : "#/components/parameters/RefId" ,
1556+ Value : "refValue" ,
1557+ hasLowRef : true , // Has a low-level reference
1558+ }
1559+
1560+ ty := []* testMixedItem {newItem , refItem }
1561+ t1 := testWithMixedItems {
1562+ Items : ty ,
1563+ }
1564+
1565+ nb := NewNodeBuilder (& t1 , nil )
1566+ node := nb .Render ()
1567+
1568+ data , _ := yaml .Marshal (node )
1569+ result := strings .TrimSpace (string (data ))
1570+
1571+ // Both items should be rendered
1572+ assert .Contains (t , result , "mixed-newValue" , "New item without low-level model should be rendered" )
1573+ assert .Contains (t , result , "$ref: '#/components/parameters/RefId'" , "Reference item should be rendered" )
1574+ }
1575+
1576+ // TestNodeBuilder_SliceMultipleRefsAndNilLow tests multiple refs interspersed with nil-low items
1577+ func TestNodeBuilder_SliceMultipleRefsAndNilLow (t * testing.T ) {
1578+ items := []* testMixedItem {
1579+ {ref : true , refStr : "#/ref1" , Value : "ref1" , hasLowRef : true },
1580+ {ref : false , Value : "new1" , hasLowRef : false }, // nil low
1581+ {ref : true , refStr : "#/ref2" , Value : "ref2" , hasLowRef : true },
1582+ {ref : false , Value : "new2" , hasLowRef : false }, // nil low
1583+ }
1584+
1585+ t1 := testWithMixedItems {
1586+ Items : items ,
1587+ }
1588+
1589+ nb := NewNodeBuilder (& t1 , nil )
1590+ node := nb .Render ()
1591+
1592+ data , _ := yaml .Marshal (node )
1593+ result := strings .TrimSpace (string (data ))
1594+
1595+ // All items should be rendered
1596+ assert .Contains (t , result , "$ref: '#/ref1'" )
1597+ assert .Contains (t , result , "mixed-new1" )
1598+ assert .Contains (t , result , "$ref: '#/ref2'" )
1599+ assert .Contains (t , result , "mixed-new2" )
1600+ }
0 commit comments