Skip to content

Commit 2816fa5

Browse files
fix: fixed modification of extensions
1 parent aff7de4 commit 2816fa5

File tree

3 files changed

+159
-6
lines changed

3 files changed

+159
-6
lines changed

marshaller/extensions.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,27 @@ func syncExtensions(ctx context.Context, source any, target reflect.Value, mapNo
131131
targetMap.Set(key, node)
132132
}
133133

134-
for key := range targetMap.All() {
134+
// Collect keys to delete safely before deletion to avoid iterator corruption
135+
// This prevents issues when the target map contains stale entries from previous operations
136+
keysToDelete := []string{}
137+
for key, node := range targetMap.All() {
138+
// Skip corrupted entries that may have nil values
139+
if node.Value == nil {
140+
keysToDelete = append(keysToDelete, key)
141+
continue
142+
}
143+
135144
if !slices.Contains(presentKeys, key) {
136-
mapNode = yml.DeleteMapNodeElement(ctx, key, mapNode)
137-
targetMap.Delete(key)
145+
keysToDelete = append(keysToDelete, key)
138146
}
139147
}
140148

149+
// Now safely delete the collected keys
150+
for _, key := range keysToDelete {
151+
mapNode = yml.DeleteMapNodeElement(ctx, key, mapNode)
152+
targetMap.Delete(key)
153+
}
154+
141155
sUnderlying := getUnderlyingValue(reflect.ValueOf(source))
142156

143157
// Update the core of the source with the updated value

marshaller/sequencedmap.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,6 @@ func syncSequencedMapChanges(ctx context.Context, target sequencedMapInterface,
222222

223223
kn, vn, _ := yml.GetMapElementNodes(ctx, valueNode, keyStr)
224224

225-
// Recreate the original behavior: lv, _ := m.Get(key); syncFunc(ctx, v, &lv, vn, false); m.Set(key, lv)
226-
// The original lv was always the concrete type V (or zero value), and &lv was pointer to that type
227-
228225
valueType := target.GetValueType()
229226

230227
// Create a concrete typed variable (equivalent to original lv)

marshaller/syncing_test.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -771,3 +771,145 @@ x-sync: sync extension
771771
require.NoError(t, err)
772772
require.Equal(t, expectedYAML, string(actualYAML))
773773
}
774+
775+
func TestSync_ExtensionModification_Success(t *testing.T) {
776+
// Create a model with initial extensions
777+
highModel := tests.TestPrimitiveHighModel{
778+
StringField: "model with extensions",
779+
BoolField: true,
780+
IntField: 42,
781+
Float64Field: 3.14,
782+
}
783+
784+
// Initialize extensions
785+
highModel.Extensions = &extensions.Extensions{}
786+
highModel.Extensions.Init()
787+
highModel.Extensions.Set("x-version", testutils.CreateStringYamlNode("1.0", 1, 1))
788+
highModel.Extensions.Set("x-author", testutils.CreateStringYamlNode("developer", 1, 1))
789+
790+
// Perform initial sync
791+
resultNode, err := marshaller.SyncValue(context.Background(), &highModel, highModel.GetCore(), highModel.GetRootNode(), false)
792+
require.NoError(t, err)
793+
require.NotNil(t, resultNode)
794+
795+
// Verify initial extensions were synced
796+
coreModel := highModel.GetCore()
797+
require.NotNil(t, coreModel.Extensions)
798+
799+
versionExt, ok := coreModel.Extensions.Get("x-version")
800+
require.True(t, ok)
801+
require.Equal(t, "1.0", versionExt.Value.Value)
802+
803+
authorExt, ok := coreModel.Extensions.Get("x-author")
804+
require.True(t, ok)
805+
require.Equal(t, "developer", authorExt.Value.Value)
806+
807+
// Modify extensions: update existing, add new, remove one
808+
highModel.Extensions.Set("x-version", testutils.CreateStringYamlNode("2.0", 1, 1)) // Update
809+
highModel.Extensions.Set("x-status", testutils.CreateStringYamlNode("active", 1, 1)) // Add new
810+
highModel.Extensions.Delete("x-author") // Remove
811+
812+
// Sync the changes
813+
resultNode, err = marshaller.SyncValue(context.Background(), &highModel, highModel.GetCore(), highModel.GetRootNode(), false)
814+
require.NoError(t, err)
815+
require.NotNil(t, resultNode)
816+
817+
// Verify extensions were updated correctly
818+
updatedVersionExt, ok := coreModel.Extensions.Get("x-version")
819+
require.True(t, ok)
820+
require.Equal(t, "2.0", updatedVersionExt.Value.Value)
821+
822+
statusExt, ok := coreModel.Extensions.Get("x-status")
823+
require.True(t, ok)
824+
require.Equal(t, "active", statusExt.Value.Value)
825+
826+
// Verify removed extension is gone
827+
_, ok = coreModel.Extensions.Get("x-author")
828+
require.False(t, ok)
829+
830+
// Verify the core model's RootNode contains the correct YAML
831+
expectedYAML := `stringField: model with extensions
832+
boolField: true
833+
intField: 42
834+
float64Field: 3.14
835+
x-version: "2.0"
836+
x-status: active
837+
`
838+
839+
actualYAML, err := yaml.Marshal(coreModel.GetRootNode())
840+
require.NoError(t, err)
841+
require.Equal(t, expectedYAML, string(actualYAML))
842+
}
843+
844+
func TestSync_ExtensionReplacement_Success(t *testing.T) {
845+
// Create a model with extensions that will be completely replaced
846+
highModel := tests.TestPrimitiveHighModel{
847+
StringField: "model for replacement",
848+
BoolField: false,
849+
IntField: 100,
850+
Float64Field: 1.5,
851+
}
852+
853+
// Initialize with original extensions
854+
highModel.Extensions = &extensions.Extensions{}
855+
highModel.Extensions.Init()
856+
highModel.Extensions.Set("x-original-id", testutils.CreateStringYamlNode("orig-123", 1, 1))
857+
highModel.Extensions.Set("x-legacy-flag", testutils.CreateStringYamlNode("true", 1, 1))
858+
highModel.Extensions.Set("x-deprecated", testutils.CreateStringYamlNode("soon", 1, 1))
859+
860+
// Perform initial sync
861+
resultNode, err := marshaller.SyncValue(context.Background(), &highModel, highModel.GetCore(), highModel.GetRootNode(), false)
862+
require.NoError(t, err)
863+
require.NotNil(t, resultNode)
864+
865+
// Verify initial state
866+
coreModel := highModel.GetCore()
867+
require.NotNil(t, coreModel.Extensions)
868+
require.Equal(t, 3, coreModel.Extensions.Len())
869+
870+
// Replace with completely new extensions (simulating workflow replacement scenario)
871+
newExtensions := &extensions.Extensions{}
872+
newExtensions.Init()
873+
newExtensions.Set("x-new-id", testutils.CreateStringYamlNode("new-456", 1, 1))
874+
newExtensions.Set("x-modern-flag", testutils.CreateStringYamlNode("enabled", 1, 1))
875+
876+
// Replace the extensions entirely
877+
highModel.Extensions = newExtensions
878+
879+
// Sync the replacement
880+
resultNode, err = marshaller.SyncValue(context.Background(), &highModel, highModel.GetCore(), highModel.GetRootNode(), false)
881+
require.NoError(t, err)
882+
require.NotNil(t, resultNode)
883+
884+
// Verify all old extensions are gone and new ones are present
885+
require.Equal(t, 2, coreModel.Extensions.Len())
886+
887+
newIdExt, ok := coreModel.Extensions.Get("x-new-id")
888+
require.True(t, ok)
889+
require.Equal(t, "new-456", newIdExt.Value.Value)
890+
891+
modernFlagExt, ok := coreModel.Extensions.Get("x-modern-flag")
892+
require.True(t, ok)
893+
require.Equal(t, "enabled", modernFlagExt.Value.Value)
894+
895+
// Verify old extensions are completely removed
896+
_, ok = coreModel.Extensions.Get("x-original-id")
897+
require.False(t, ok)
898+
_, ok = coreModel.Extensions.Get("x-legacy-flag")
899+
require.False(t, ok)
900+
_, ok = coreModel.Extensions.Get("x-deprecated")
901+
require.False(t, ok)
902+
903+
// Verify the core model's RootNode contains only new extensions
904+
expectedYAML := `stringField: model for replacement
905+
boolField: false
906+
intField: 100
907+
float64Field: 1.5
908+
x-new-id: new-456
909+
x-modern-flag: enabled
910+
`
911+
912+
actualYAML, err := yaml.Marshal(coreModel.GetRootNode())
913+
require.NoError(t, err)
914+
require.Equal(t, expectedYAML, string(actualYAML))
915+
}

0 commit comments

Comments
 (0)