Skip to content

Commit 6efa364

Browse files
authored
Merge pull request #78 from speakeasy-api/subomi/fix/fix-syncing-array
fix: correct array sync to preserve identity matching
2 parents 4215825 + 991c710 commit 6efa364

File tree

4 files changed

+137
-24
lines changed

4 files changed

+137
-24
lines changed

marshaller/syncer.go

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -306,30 +306,6 @@ func syncArraySlice(ctx context.Context, source any, target any, valueNode *yaml
306306
targetVal.Set(reflect.MakeSlice(targetVal.Type(), 0, 0))
307307
}
308308

309-
if targetVal.Len() > sourceVal.Len() {
310-
// Shorten the slice
311-
tempVal := reflect.MakeSlice(targetVal.Type(), sourceVal.Len(), sourceVal.Len())
312-
for i := 0; i < sourceVal.Len(); i++ {
313-
tempVal.Index(i).Set(targetVal.Index(i))
314-
}
315-
316-
targetVal.Set(tempVal)
317-
}
318-
319-
if targetVal.Len() < sourceVal.Len() {
320-
// Equalize the slice
321-
tempVal := reflect.MakeSlice(targetVal.Type(), sourceVal.Len(), sourceVal.Len())
322-
323-
for i := 0; i < targetVal.Len(); i++ {
324-
tempVal.Index(i).Set(targetVal.Index(i))
325-
}
326-
for i := targetVal.Len(); i < sourceVal.Len(); i++ {
327-
tempVal.Index(i).Set(reflect.Zero(targetVal.Type().Elem()))
328-
}
329-
330-
targetVal.Set(tempVal)
331-
}
332-
333309
// When arrays are reordered at the high level (e.g., moving workflows around),
334310
// we need to match source elements with their corresponding target core models
335311
// by identity (RootNode) rather than by array position to preserve elements.
@@ -363,6 +339,24 @@ func syncArraySlice(ctx context.Context, source any, target any, valueNode *yaml
363339
elements[i] = currentElementNode
364340
}
365341

342+
// Update targetVal to contain only the synced elements in the correct order
343+
// This ensures the target slice reflects the reordering and removals
344+
tempVal := reflect.MakeSlice(targetVal.Type(), sourceVal.Len(), sourceVal.Len())
345+
for i := 0; i < sourceVal.Len(); i++ {
346+
// reorderedTargets[i] contains pointers from .Addr().Interface()
347+
// Use dereferenceToLastPtr to handle chains of pointers (e.g., **T -> *T)
348+
targetPtr := dereferenceToLastPtr(reflect.ValueOf(reorderedTargets[i]))
349+
350+
if targetVal.Type().Elem().Kind() == reflect.Ptr {
351+
// Target slice expects pointers (*T), set the pointer directly
352+
tempVal.Index(i).Set(targetPtr)
353+
} else {
354+
// Target slice expects values (T), dereference the pointer
355+
tempVal.Index(i).Set(targetPtr.Elem())
356+
}
357+
}
358+
targetVal.Set(tempVal)
359+
366360
return yml.CreateOrUpdateSliceNode(ctx, elements, valueNode), nil
367361
}
368362

marshaller/syncing_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1143,3 +1143,94 @@ func TestSync_EmbeddedMapComparison_PointerVsValue_Success(t *testing.T) {
11431143
require.Equal(t, "shared_key: shared_value\n", string(ptrYAML))
11441144
})
11451145
}
1146+
1147+
func TestSync_ArraySubset_Success(t *testing.T) {
1148+
t.Parallel()
1149+
1150+
// Create high-level model with 3 items
1151+
itemOne := &tests.TestItemHighModel{
1152+
Name: "one",
1153+
Description: "First item",
1154+
}
1155+
itemFour := &tests.TestItemHighModel{
1156+
Name: "four",
1157+
Description: "Fourth item",
1158+
}
1159+
itemSix := &tests.TestItemHighModel{
1160+
Name: "six",
1161+
Description: "Sixth item",
1162+
}
1163+
1164+
highModel := tests.TestArrayOfObjectsHighModel{
1165+
Items: []*tests.TestItemHighModel{itemOne, itemFour, itemSix},
1166+
}
1167+
1168+
// Populate core model with 6 items from YAML
1169+
initialYAML := `items:
1170+
- name: three
1171+
description: Third item
1172+
- name: five
1173+
description: Fifth item
1174+
- name: one
1175+
description: First item
1176+
- name: four
1177+
description: Fourth item
1178+
- name: second
1179+
description: Second item
1180+
- name: six
1181+
description: Sixth item
1182+
`
1183+
var rootNode yaml.Node
1184+
err := yaml.Unmarshal([]byte(initialYAML), &rootNode)
1185+
require.NoError(t, err)
1186+
1187+
coreModel := highModel.GetCore()
1188+
coreModel.SetRootNode(rootNode.Content[0])
1189+
1190+
_, err = marshaller.UnmarshalModel(t.Context(), rootNode.Content[0], coreModel)
1191+
require.NoError(t, err)
1192+
1193+
// Link high-level items to their corresponding core items
1194+
for _, item := range coreModel.Items.Value {
1195+
switch item.Name.Value {
1196+
case "one":
1197+
itemOne.SetCore(item)
1198+
case "four":
1199+
itemFour.SetCore(item)
1200+
case "six":
1201+
itemSix.SetCore(item)
1202+
}
1203+
}
1204+
1205+
// Sync high model subset to core model
1206+
resultNode, err := marshaller.SyncValue(t.Context(), &highModel, highModel.GetCore(), highModel.GetRootNode(), false)
1207+
require.NoError(t, err)
1208+
require.NotNil(t, resultNode)
1209+
1210+
// Verify synced array matches high model subset
1211+
items := coreModel.Items.Value
1212+
require.Len(t, items, 3)
1213+
1214+
require.Equal(t, "one", items[0].Name.Value)
1215+
require.Equal(t, "First item", items[0].Description.Value)
1216+
1217+
require.Equal(t, "four", items[1].Name.Value)
1218+
require.Equal(t, "Fourth item", items[1].Description.Value)
1219+
1220+
require.Equal(t, "six", items[2].Name.Value)
1221+
require.Equal(t, "Sixth item", items[2].Description.Value)
1222+
1223+
// Verify YAML output
1224+
expectedYAML := `items:
1225+
- name: one
1226+
description: First item
1227+
- name: four
1228+
description: Fourth item
1229+
- name: six
1230+
description: Sixth item
1231+
`
1232+
1233+
actualYAML, err := yaml.Marshal(coreModel.GetRootNode())
1234+
require.NoError(t, err)
1235+
require.Equal(t, expectedYAML, string(actualYAML))
1236+
}

marshaller/tests/core/models.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,18 @@ type TestTypeConversionCoreModel struct {
183183
HTTPMethodField marshaller.Node[*string] `key:"httpMethodField"`
184184
Extensions core.Extensions `key:"extensions"`
185185
}
186+
187+
// TestItemModel represents an item with a name and description
188+
type TestItemModel struct {
189+
marshaller.CoreModel `model:"testItemModel"`
190+
191+
Name marshaller.Node[string] `key:"name"`
192+
Description marshaller.Node[string] `key:"description"`
193+
}
194+
195+
// TestArrayOfObjectsModel contains an array of items
196+
type TestArrayOfObjectsModel struct {
197+
marshaller.CoreModel `model:"testArrayOfObjectsModel"`
198+
199+
Items marshaller.Node[[]*TestItemModel] `key:"items"`
200+
}

marshaller/tests/models.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,16 @@ type TestEmbeddedMapWithFieldsPointerHighModel struct {
128128
NameField string
129129
Extensions *extensions.Extensions
130130
}
131+
132+
// TestItemHighModel represents an item with a name and description
133+
type TestItemHighModel struct {
134+
marshaller.Model[core.TestItemModel]
135+
Name string
136+
Description string
137+
}
138+
139+
// TestArrayOfObjectsHighModel contains an array of items
140+
type TestArrayOfObjectsHighModel struct {
141+
marshaller.Model[core.TestArrayOfObjectsModel]
142+
Items []*TestItemHighModel
143+
}

0 commit comments

Comments
 (0)